Line data Source code
1 : #include "common.h"
2 : #include "audio/engine/audio_engine.h"
3 : #ifndef AMPLITRON_HEADLESS
4 : #include "gui/gui_manager.h"
5 : #include "gui/state/gui_graph_state.h"
6 : // New include for Recovery
7 : #include "gui/crash_recovery_ui.h"
8 : #endif
9 : #include "preset_manager.h"
10 : #include "cli.h"
11 : #include "gui/commands/command_graph.h"
12 : #include "gui/commands/command_history.h"
13 :
14 : #include "audio/effects/noise_gate.h"
15 : #include "audio/effects/compressor.h"
16 : #include "audio/effects/overdrive.h"
17 : #include "audio/effects/distortion.h"
18 : #include "audio/effects/equalizer.h"
19 : #include "audio/effects/chorus.h"
20 : #include "audio/effects/delay.h"
21 : #include "audio/effects/reverb.h"
22 : #include "audio/effects/cabinet_sim.h"
23 : #include "audio/effects/amp_simulator.h"
24 :
25 : #include <iostream>
26 : #include <csignal>
27 : #include <atomic>
28 : #include <filesystem>
29 : #include <thread>
30 : #include <chrono>
31 : #include <mutex>
32 : #include <vector>
33 :
34 : // New include for Autosave
35 : #include "session_manager.h"
36 :
37 : #ifdef __EMSCRIPTEN__
38 : #include <emscripten.h>
39 : #endif
40 :
41 : #ifdef __ANDROID__
42 : #include <SDL_main.h>
43 : #elif defined(__APPLE__)
44 : #include <TargetConditionals.h>
45 : #if TARGET_OS_IOS
46 : #include <SDL_main.h>
47 : #endif
48 : #endif
49 :
50 : static std::atomic<bool> g_running{true};
51 :
52 : #ifdef __EMSCRIPTEN__
53 : static Amplitron::GuiManager* g_gui = nullptr;
54 :
55 : static void em_main_loop() {
56 : if (!g_gui || !g_gui->run_frame()) {
57 : g_running = false;
58 : emscripten_cancel_main_loop();
59 : }
60 : }
61 :
62 : extern "C" EMSCRIPTEN_KEEPALIVE void on_midi_cc(int channel, int cc, int value) {
63 : // Validate MIDI ranges before processing
64 : if (!g_gui) {
65 : emscripten_log(EM_LOG_WARN, "[MIDI] GUI not initialized, dropping event");
66 : return;
67 : }
68 :
69 : // Range validation (standard MIDI)
70 : if (channel < 0 || channel > 15) {
71 : emscripten_log(EM_LOG_DEBUG, "[MIDI] Invalid channel: %d", channel);
72 : return;
73 : }
74 : if (cc < 0 || cc > 127) {
75 : emscripten_log(EM_LOG_DEBUG, "[MIDI] Invalid CC number: %d", cc);
76 : return;
77 : }
78 : if (value < 0 || value > 127) {
79 : emscripten_log(EM_LOG_DEBUG, "[MIDI] Invalid CC value: %d", value);
80 : return;
81 : }
82 :
83 : // Create MIDI event
84 : Amplitron::MidiEvent event;
85 : event.status = static_cast<uint8_t>(0xB0 | (channel & 0x0F)); // CC message
86 : event.data1 = static_cast<uint8_t>(cc);
87 : event.data2 = static_cast<uint8_t>(value);
88 : event.pad = 0;
89 :
90 : // Inject into MIDI queue
91 : g_gui->midi_manager().inject_event(event);
92 :
93 : emscripten_log(EM_LOG_DEBUG, "[MIDI] CC %d = %d on channel %d", cc, value, channel);
94 : }
95 :
96 : extern "C" EMSCRIPTEN_KEEPALIVE void on_midi_device_connected(const char* device_name) {
97 : if (!g_gui || !device_name) return;
98 :
99 : emscripten_log(EM_LOG_INFO, "[MIDI] Device connected: %s", device_name);
100 : }
101 :
102 : extern "C" EMSCRIPTEN_KEEPALIVE void on_canvas_touch_gesture(float dx, float dy, float dscale, float local_x, float local_y) {
103 : auto& ui = Amplitron::GuiGraphState::get_instance();
104 : if (dscale != 0.0f) {
105 : float factor = 1.0f + dscale;
106 : float old_zoom = ui.target_zoom;
107 : float new_zoom = old_zoom * factor;
108 : if (new_zoom < 0.2f) new_zoom = 0.2f;
109 : if (new_zoom > 5.0f) new_zoom = 5.0f;
110 : float actual_factor = new_zoom / old_zoom;
111 : ui.target_scrolling.x = local_x - (local_x - ui.target_scrolling.x) * actual_factor;
112 : ui.target_scrolling.y = local_y - (local_y - ui.target_scrolling.y) * actual_factor;
113 : ui.target_zoom = new_zoom;
114 : }
115 : ui.target_scrolling.x += dx;
116 : ui.target_scrolling.y += dy;
117 : }
118 :
119 : extern "C" EMSCRIPTEN_KEEPALIVE void on_canvas_touch_down(float x, float y) {
120 : auto& io = ImGui::GetIO();
121 : io.AddMousePosEvent(x, y);
122 : io.AddMouseButtonEvent(0, true);
123 : }
124 :
125 : extern "C" EMSCRIPTEN_KEEPALIVE void on_canvas_touch_move(float x, float y) {
126 : ImGui::GetIO().AddMousePosEvent(x, y);
127 : }
128 :
129 : extern "C" EMSCRIPTEN_KEEPALIVE void on_canvas_touch_up(float x, float y) {
130 : auto& io = ImGui::GetIO();
131 : io.AddMousePosEvent(x, y);
132 : io.AddMouseButtonEvent(0, false);
133 : }
134 :
135 : extern "C" EMSCRIPTEN_KEEPALIVE void on_canvas_touch_cancel() {
136 : auto& io = ImGui::GetIO();
137 : io.AddMouseButtonEvent(0, false);
138 : }
139 :
140 : extern "C" EMSCRIPTEN_KEEPALIVE bool is_canvas_hovered() {
141 : return Amplitron::GuiGraphState::get_instance().canvas_hovered;
142 : }
143 :
144 : extern "C" EMSCRIPTEN_KEEPALIVE float get_canvas_zoom() {
145 : return Amplitron::GuiGraphState::get_instance().target_zoom;
146 : }
147 :
148 : extern "C" EMSCRIPTEN_KEEPALIVE float get_canvas_scroll_x() {
149 : return Amplitron::GuiGraphState::get_instance().target_scrolling.x;
150 : }
151 :
152 : extern "C" EMSCRIPTEN_KEEPALIVE float get_canvas_scroll_y() {
153 : return Amplitron::GuiGraphState::get_instance().target_scrolling.y;
154 : }
155 :
156 : extern "C" EMSCRIPTEN_KEEPALIVE int get_node_count() {
157 : if (!g_gui) return 0;
158 : return static_cast<int>(g_gui->audio_engine().graph().get_nodes().size());
159 : }
160 :
161 : extern "C" EMSCRIPTEN_KEEPALIVE int get_link_count() {
162 : if (!g_gui) return 0;
163 : return static_cast<int>(g_gui->audio_engine().graph().get_links().size());
164 : }
165 :
166 : extern "C" EMSCRIPTEN_KEEPALIVE bool has_node_of_type(int routing_type) {
167 : if (!g_gui) return false;
168 : for (const auto& n : g_gui->audio_engine().graph().get_nodes()) {
169 : if (static_cast<int>(n.routing_type) == routing_type) return true;
170 : }
171 : return false;
172 : }
173 :
174 : extern "C" EMSCRIPTEN_KEEPALIVE int trigger_add_splitter_node() {
175 : if (!g_gui) return -1;
176 : auto cmd = std::make_unique<Amplitron::AddGraphNodeCommand>(
177 : g_gui->audio_engine(), "Splitter", Amplitron::NodeRoutingType::Splitter, nullptr, ImVec2(0, 0));
178 : auto* raw = cmd.get();
179 : g_gui->command_history().execute(std::move(cmd));
180 : return (raw->node_id != -1) ? raw->node_id : -1;
181 : }
182 :
183 : extern "C" EMSCRIPTEN_KEEPALIVE int trigger_add_link(int src_pin, int dst_pin) {
184 : if (!g_gui) return -1;
185 : auto cmd = std::make_unique<Amplitron::AddGraphLinkCommand>(g_gui->audio_engine(), src_pin, dst_pin);
186 : auto* raw = cmd.get();
187 : g_gui->command_history().execute(std::move(cmd));
188 : return raw->was_successful ? raw->link.id : -1;
189 : }
190 :
191 : extern "C" EMSCRIPTEN_KEEPALIVE int get_node_output_pin_by_index(int node_index, int pin_index) {
192 : if (!g_gui) return -1;
193 : const auto& nodes = g_gui->audio_engine().graph().get_nodes();
194 : if (node_index < 0 || node_index >= static_cast<int>(nodes.size())) return -1;
195 : const auto& node = nodes[node_index];
196 : if (pin_index < 0 || pin_index >= static_cast<int>(node.output_pin_ids.size())) return -1;
197 : return node.output_pin_ids[pin_index];
198 : }
199 :
200 : extern "C" EMSCRIPTEN_KEEPALIVE int get_node_input_pin_by_index(int node_index, int pin_index) {
201 : if (!g_gui) return -1;
202 : const auto& nodes = g_gui->audio_engine().graph().get_nodes();
203 : if (node_index < 0 || node_index >= static_cast<int>(nodes.size())) return -1;
204 : const auto& node = nodes[node_index];
205 : if (pin_index < 0 || pin_index >= static_cast<int>(node.input_pin_ids.size())) return -1;
206 : return node.input_pin_ids[pin_index];
207 : }
208 :
209 : extern "C" EMSCRIPTEN_KEEPALIVE bool trigger_delete_last_node() {
210 : if (!g_gui) return false;
211 : auto& graph = g_gui->audio_engine().graph();
212 : const auto& nodes = graph.get_nodes();
213 : // Walk backwards to find the last deletable node (mirrors GUI rules: Input and Amp Sim are protected)
214 : for (int i = static_cast<int>(nodes.size()) - 1; i >= 0; --i) {
215 : const auto& node = nodes[i];
216 : if (node.name == "Input" || node.name == "Amp Sim") continue;
217 :
218 : auto& ui_positions = Amplitron::GuiGraphState::get_instance().node_positions;
219 : ImVec2 pos(0, 0);
220 : auto pos_it = ui_positions.find(node.id);
221 : if (pos_it != ui_positions.end()) pos = pos_it->second.position;
222 :
223 : g_gui->command_history().execute(
224 : std::make_unique<Amplitron::RemoveGraphNodeCommand>(
225 : g_gui->audio_engine(), node.id, node.routing_type, pos
226 : )
227 : );
228 : // Note: node_positions.erase is handled inside RemoveGraphNodeCommand::execute()
229 : return true;
230 : }
231 : return false; // No deletable node found
232 : }
233 :
234 : #endif
235 :
236 6 : void signal_handler(int /*signal*/) {
237 5 : g_running = false;
238 4 : }
239 :
240 9 : int main(int argc, char* argv[]) {
241 : //breaks global iostream lock
242 9 : std::cin.tie(nullptr);
243 : //CLI argument parsing
244 9 : Amplitron::CliOptions cli_opts = Amplitron::handle_cli_args(argc, argv);
245 :
246 : #ifdef AMPLITRON_HEADLESS
247 6 : cli_opts.is_headless = true;
248 :
249 : // If the parser didn't already trigger an exit (like --help) validate the preset
250 6 : if (!cli_opts.exit_early && cli_opts.preset_path.empty()) {
251 0 : std::cerr << "Error: Strict headless build requires a --preset <path> argument." << std::endl;
252 0 : return 1; // Return non-zero failure code
253 : }
254 : #endif
255 :
256 9 : if(cli_opts.exit_early){
257 12 : std::cout << "[CLI]Application exited early :" << cli_opts.exit_reason << std::endl;
258 9 : return cli_opts.exit_code;
259 : }
260 :
261 0 : std::signal(SIGINT, signal_handler);
262 0 : std::signal(SIGTERM, signal_handler);
263 :
264 : // Initialize Session Manager
265 0 : Amplitron::SessionManager sessionManager("SudipMondal", "Amplitron");
266 :
267 0 : std::cout << "=== Amplitron v1.0 - Guitar Amp Simulator ===" << std::endl;
268 0 : std::cout << "Starting up..." << std::endl;
269 :
270 : // Initialize audio engine
271 0 : Amplitron::AudioEngine engine;
272 0 : if (!engine.initialize()) {
273 0 : std::cerr << "Failed to initialize audio engine!" << std::endl;
274 0 : return 1;
275 : }
276 :
277 : #ifndef AMPLITRON_HEADLESS
278 0 : std::unique_ptr<Amplitron::GuiManager> gui = nullptr;
279 : #endif
280 :
281 0 : if(cli_opts.is_headless){
282 0 : std::cout << "=== HEADLESS MODE ===" << std::endl;
283 0 : std::cout << "Loading preset: " << cli_opts.preset_path << std::endl;
284 :
285 : //Safe preset injection
286 0 : if (!Amplitron::PresetManager::load_preset(cli_opts.preset_path, engine, nullptr)){
287 0 : std::cerr << "Fatal Error: Could not load preset for headless mode." << std::endl;
288 0 : engine.shutdown();
289 0 : return 1;
290 : }
291 : //Hardware routing(i/p)
292 0 : if(!cli_opts.input_device.empty()){
293 0 : auto devices = engine.get_input_devices();
294 0 : std::vector<int> match_indices;
295 :
296 : //Searching and storing all matching devices
297 0 : for(size_t i = 0; i < devices.size(); ++i){
298 0 : if (devices[i].name.find(cli_opts.input_device) != std::string::npos){
299 0 : match_indices.push_back(i);
300 0 : }
301 0 : }
302 :
303 0 : if(match_indices.empty()){
304 0 : std::cerr << "Warning: Could not find requested input device: '" << cli_opts.input_device << "'" << std::endl;
305 0 : } else if (match_indices.size() == 1){
306 0 : engine.set_input_device(devices[match_indices[0]].index);
307 0 : std::cout << "Input routed to: " << devices[match_indices[0]].name << std::endl;
308 0 : } else {
309 0 : std::cerr << "Warning: Ambiguous input name '" << cli_opts.input_device << "'. Multiple matches found:" << std::endl;
310 0 : for(int idx : match_indices){
311 0 : std::cerr << " -- " << devices[idx].name << std::endl;
312 : }
313 0 : std::cerr << "Auto-selecting the first match: " << devices[match_indices[0]].name << std::endl;
314 0 : engine.set_input_device(devices[match_indices[0]].index);
315 : }
316 0 : }
317 : //hardware routing(o/p)
318 0 : if(!cli_opts.output_device.empty()) {
319 0 : auto devices = engine.get_output_devices();
320 0 : std::vector<int> match_indices;
321 :
322 0 : for(size_t i = 0;i <devices.size(); ++i){
323 0 : if(devices[i].name.find(cli_opts.output_device) != std::string::npos) {
324 0 : match_indices.push_back(i);
325 0 : }
326 0 : }
327 :
328 0 : if(match_indices.empty()){
329 0 : std::cerr << "Warning: Could not find requested output device: '" << cli_opts.output_device << "'" << std::endl;
330 0 : } else if(match_indices.size() == 1){
331 0 : engine.set_output_device(devices[match_indices[0]].index);
332 0 : std::cout<< "Output routed to: " << devices[match_indices[0]].name << std::endl;
333 0 : } else{
334 0 : std::cerr << "Warning: Ambiguous output name '" << cli_opts.output_device << "'. Multiple matches found:" <<std::endl;
335 0 : for(int idx : match_indices){
336 0 : std::cerr << " -- " << devices[idx].name << std::endl;
337 : }
338 0 : std::cerr << "Auto-selecting the first match: " << devices[match_indices[0]].name << std::endl;
339 0 : engine.set_output_device(devices[match_indices[0]].index);
340 : }
341 0 : }
342 :
343 0 : }
344 : #ifndef AMPLITRON_HEADLESS
345 : else {
346 : // GUI bootup
347 0 : gui = std::make_unique<Amplitron::GuiManager>(engine);
348 : // Create a small, automatically wired, and highly playable circuit
349 0 : auto cabinet = std::make_shared<Amplitron::CabinetSim>();
350 0 : cabinet->set_enabled(true);
351 :
352 0 : auto amp = std::make_shared<Amplitron::AmpSimulator>();
353 0 : amp->set_enabled(true);
354 :
355 0 : engine.add_initial_effects({cabinet, amp});
356 :
357 0 : engine.set_input_gain(0.7f);
358 :
359 0 : if (sessionManager.hasUnsavedSession()) {
360 0 : if (promptRestoreSession()) {
361 0 : try {
362 0 : nlohmann::json savedState = sessionManager.loadSession();
363 0 : engine.deserialize(savedState);
364 0 : } catch (const nlohmann::json::parse_error& e) {
365 0 : std::cerr << "Autosave file corrupted. Discarding." << std::endl;
366 0 : sessionManager.clearSession();
367 0 : }
368 0 : } else {
369 0 : sessionManager.clearSession();
370 : }
371 0 : }
372 :
373 0 : if (std::filesystem::exists("presets")) {
374 0 : Amplitron::PresetManager::set_presets_dir("presets");
375 0 : }
376 :
377 0 : if (!gui->initialize(1280, 720)) {
378 0 : std::cerr << "Failed to initialize GUI!" << std::endl;
379 0 : engine.shutdown();
380 0 : return 1;
381 : }
382 0 : }
383 : #endif
384 :
385 :
386 0 : if (!engine.start()) {
387 0 : std::cerr << "Warning: Could not start audio stream." << std::endl;
388 0 : }
389 :
390 0 : std::cout << "Amplitron is ready. Let's play!" << std::endl;
391 : #ifdef __EMSCRIPTEN__
392 : g_gui = gui.get();
393 : emscripten_set_main_loop(em_main_loop, 0, 1);
394 : #else
395 0 : std::atomic<bool> show_telemetry{true};
396 :
397 0 : if (cli_opts.is_headless){
398 0 : std::cout << "Audio Engine is running in the background." << std::endl;
399 0 : std::cout << "Commands: chain, gain <val>, bypass <idx>, enable <idx>, telemetry <on/off>" << std::endl;
400 0 : std::cout << "Press Ctrl+C to shut down." << std::endl;
401 :
402 0 : std::mutex cli_mutex;
403 0 : std::vector<std::string> cli_commands;
404 :
405 : //stdin thread to listen commands
406 0 : std::thread stdin_listener([&cli_mutex, &cli_commands](){
407 0 : std::string line;
408 0 : while(std::getline(std::cin, line)){
409 0 : if(line.empty()) continue;
410 0 : std::lock_guard<std::mutex> lock(cli_mutex);
411 0 : cli_commands.push_back(line);
412 0 : }
413 0 : });
414 0 : stdin_listener.detach();
415 0 : int loop_counter=0;
416 :
417 : //headless loop
418 0 : while(g_running){
419 0 : std::this_thread::sleep_for(std::chrono::milliseconds(100));
420 0 : std::vector<std::string> pending_commands;
421 0 : {
422 0 : std::lock_guard<std::mutex> lock(cli_mutex);
423 0 : std::swap(pending_commands,cli_commands);
424 0 : }
425 :
426 0 : for(const std::string& line : pending_commands){
427 0 : if(line.find("gain ") == 0){
428 0 : try{
429 0 : float val = std::stof(line.substr(5));
430 0 : engine.set_output_gain(val);
431 0 : std::cout << ">> Output gain set to " << val << std::endl;
432 0 : } catch (...){
433 0 : std::cout << ">> Invalid gain" << std::endl;
434 0 : }
435 0 : } else if (line.find("bypass ") == 0){
436 0 : try {
437 0 : int idx = std::stoi(line.substr(7));
438 : //push_effect_enabled expects a float(>0.5 is enabled, <0.5 is bypassed)
439 0 : engine.push_effect_enabled(idx, 0.0f);
440 0 : std::cout << ">> Effect " << idx << " bypassed." << std::endl;
441 0 : } catch(...){
442 0 : std::cout << ">> Invalid index." << std::endl;
443 0 : }
444 0 : } else if (line.find("enable ") == 0){
445 0 : try{
446 0 : int idx = std::stoi(line.substr(7));
447 0 : engine.push_effect_enabled(idx, 1.0f);
448 0 : std::cout << ">> Effect " << idx << " enabled." << std::endl;
449 0 : } catch(...){
450 0 : std::cout << ">> Invalid index. Try: enable 0" << std::endl;
451 0 : }
452 0 : } else if (line == "telemetry off"){
453 0 : show_telemetry.store(false, std::memory_order_relaxed);
454 0 : std::cout << ">> Telemetry muted. Type 'telemetry on' to resume." << std::endl;
455 0 : }
456 0 : else if (line == "telemetry on"){
457 0 : show_telemetry.store(true, std::memory_order_relaxed);
458 0 : std::cout << ">> Telemetry resumed." << std::endl;
459 0 : } else if (line == "chain"){
460 0 : std::string chain_str = "\n=== ACTIVE SIGNAL CHAIN ===\n";
461 :
462 0 : const auto& nodes = engine.graph().get_nodes();
463 0 : int print_index = 0;
464 :
465 0 : for (const auto& node : nodes) {
466 0 : if (node.pedal) { // Only print actual effects
467 0 : chain_str += "[" + std::to_string(print_index) + "] " +
468 0 : node.pedal->get_display_name() +
469 0 : (node.pedal->is_enabled() ? " (ON)\n" : " (BYPASSED)\n");
470 0 : print_index++;
471 0 : }
472 : }
473 :
474 : // If no actual pedals were found in the graph
475 0 : if (print_index == 0) {
476 0 : chain_str += "(Chain is empty)\n";
477 0 : }
478 :
479 0 : chain_str += "===========================";
480 0 : std::cout << chain_str << std::endl;
481 0 : }
482 : else {
483 0 : std::cout << ">> Unknown command. Available: gain <val>, bypass <idx>, enable <idx>, telemetry <on/off>" << std::endl;
484 : }
485 : }
486 : //telemetry logic(activates every 10s)
487 0 : if(++loop_counter >= 100){
488 0 : if(show_telemetry.load(std::memory_order_relaxed)) {
489 0 : float dsp_load = engine.get_cpu_load() * 100.0f;
490 0 : float in_peak = engine.get_input_level();
491 0 : float out_peak = engine.get_output_level();
492 0 : std::string dashboard = "\n========================================\n";
493 0 : dashboard += "Active Buffer Size: " + std::to_string(engine.get_buffer_size()) +
494 0 : " samples @ " + std::to_string(engine.get_sample_rate()) + "Hz\n";
495 0 : dashboard += "DSP Load: " + std::to_string(dsp_load) + "%\n";
496 0 : dashboard += "Peak I/O : IN " + std::to_string(in_peak) + " | OUT " + std::to_string(out_peak) + "\n";
497 0 : dashboard += "========================================\n";
498 :
499 0 : std::cout << dashboard << std::flush;
500 0 : }
501 0 : loop_counter = 0;//reset timer
502 0 : }
503 0 : }
504 0 : }
505 : #ifndef AMPLITRON_HEADLESS
506 : else{
507 : //GUI loop
508 0 : while(g_running && gui->run_frame()){
509 0 : if (sessionManager.shouldSave()){
510 0 : sessionManager.saveSession(engine.serialize());
511 0 : }
512 : }
513 : }
514 : #endif
515 : #endif
516 :
517 : // Cleanup
518 0 : std::cout << "Shutting down..." << std::endl;
519 :
520 : #ifdef __EMSCRIPTEN__
521 : g_gui = nullptr;
522 : #endif
523 :
524 : #ifndef AMPLITRON_HEADLESS
525 0 : if(!cli_opts.is_headless){
526 0 : gui->shutdown();
527 0 : }
528 : #endif
529 :
530 0 : if(!cli_opts.is_headless) {
531 0 : sessionManager.clearSession();
532 0 : }
533 0 : engine.shutdown();
534 0 : std::cout << "Goodbye!" << std::endl;
535 :
536 0 : return 0;
537 9 : }
|