#include "main_window.hpp" #include "graph_widget.hpp" #include "xtc/xtc_version.h" #include #include #include #include #include #include #include #include #include #include #include namespace { void set_label(Fl_Box *box, const char *text) { if (box->label() == nullptr || std::strcmp(box->label(), text) != 0) box->copy_label(text); } } MainWindow::MainWindow() : Fl_Double_Window(1000, 680, XTC_PROJECT_NAME " — modular audio workstation") { xtc_graph *initial_graph = nullptr; xtc_result graph_result = xtc_graph_create(64U, &initial_graph); if (graph_result != XTC_OK) throw std::runtime_error(xtc_result_string(graph_result)); graph_.reset(initial_graph); graph_result = xtc_graph_require_output_nodes(graph_.get(), 1); if (graph_result != XTC_OK) throw std::runtime_error(xtc_result_string(graph_result)); begin(); auto *menu = new Fl_Menu_Bar(0, 0, 1000, 28); menu->add("File/New (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE); menu->add("File/Open (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE); menu->add("File/Save (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE); menu->add("File/Quit", FL_CTRL + 'q', close_callback, this); menu->add("Audio/Start engine", FL_F + 5, play_callback, this); menu->add("Audio/Stop engine", FL_F + 8, stop_callback, this); menu->add("Audio/Reconnect", FL_F + 6, reconnect_callback, this); play_ = new Fl_Button(12, 40, 80, 30, "Play"); play_->callback(play_callback, this); play_->tooltip("Start the audio engine (F5 or Space). No sequencer content yet."); stop_ = new Fl_Button(100, 40, 80, 30, "Stop"); stop_->callback(stop_callback, this); stop_->tooltip("Stop the audio engine (F8 or Space)."); auto *reconnect = new Fl_Button(188, 40, 110, 30, "Reconnect"); reconnect->callback(reconnect_callback, this); reconnect->tooltip("Close and reopen JACK, leaving the engine stopped (F6)."); indicator_ = new Fl_Box(316, 40, 672, 30, "Audio: disconnected"); indicator_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); indicator_->labelfont(FL_BOLD); auto *hint = new Fl_Box(12, 78, 976, 24, "Edit signal flow in Modular Graph. No hardware ports are connected automatically; sequencer and recording are not implemented."); hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); hint->labelsize(12); tabs_ = new Fl_Tabs(8, 114, 984, 470); tabs_->begin(); auto *sequencer = new Fl_Group(8, 140, 984, 444, "Sequencer"); sequencer->begin(); new Fl_Box(24, 164, 940, 40, "Sequencer placeholder — timeline and clips are not implemented yet."); sequencer->end(); auto *mixer = new Fl_Group(8, 140, 984, 444, "Mixer"); mixer->begin(); new Fl_Box(24, 164, 940, 40, "Mixer placeholder — channels and meters are not implemented yet."); mixer->end(); auto *graph = new Fl_Group(8, 140, 984, 444, "Modular Graph"); graph->begin(); graph_state_ = new Fl_Box(20, 182, 956, 24, "Graph: offline"); graph_state_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); graph_state_->labelsize(12); auto *feedback = new Fl_Box(20, 548, 956, 28, "Add nodes. Drag output to input to connect; click a node or wire then Delete. Scroll to pan."); feedback->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP); feedback->labelsize(12); auto *scroll = new Fl_Scroll(20, 212, 956, 328); graph_widget_ = new GraphWidget(20, 212, 2000, 4000, graph_.get(), edit_graph, this, feedback); scroll->end(); auto *oscillator = new Fl_Button(20, 150, 130, 26, "Add oscillator"); oscillator->callback([](Fl_Widget *, void *context) { (void)static_cast(context)->add_node(XTC_NODE_OSCILLATOR); }, graph_widget_); auto *gain = new Fl_Button(158, 150, 110, 26, "Add gain"); gain->callback([](Fl_Widget *, void *context) { (void)static_cast(context)->add_node(XTC_NODE_GAIN); }, graph_widget_); auto *meter = new Fl_Button(276, 150, 110, 26, "Add meter"); meter->callback([](Fl_Widget *, void *context) { (void)static_cast(context)->add_node(XTC_NODE_METER); }, graph_widget_); auto *output = new Fl_Button(394, 150, 110, 26, "Add output"); output->callback([](Fl_Widget *, void *context) { (void)static_cast(context)->add_node(XTC_NODE_OUTPUT); }, graph_widget_); auto *remove = new Fl_Button(512, 150, 130, 26, "Delete selection"); remove->callback([](Fl_Widget *, void *context) { (void)static_cast(context)->delete_selected(); }, graph_widget_); graph->resizable(scroll); graph->end(); tabs_->end(); tabs_->value(sequencer); menu->add("View/Sequencer", FL_CTRL + '1', view_callback, sequencer); menu->add("View/Mixer", FL_CTRL + '2', view_callback, mixer); menu->add("View/Modular Graph", FL_CTRL + '3', view_callback, graph); menu->add("Help/About", 0, about_callback); status_ = new Fl_Box(12, 594, 976, 26, "JACK: not connected"); status_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); status_->labelsize(12); error_ = new Fl_Box(12, 624, 976, 44, ""); error_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP); error_->labelcolor(fl_rgb_color(255, 155, 120)); error_->labelsize(12); end(); resizable(tabs_); size_range(800, 540); callback(close_callback, this); open_backend(); Fl::add_timeout(0.1, timer_callback, this); } MainWindow::~MainWindow() { Fl::remove_timeout(timer_callback, this); // Normal window/menu close already synchronized callbacks. This fallback // covers exceptional event-loop exit. A failed close must retain its C // callback context until process exit, rather than free active audio state. if (backend_ != nullptr && xtc_backend_close(backend_) != XTC_OK) std::fputs("Failed to close JACK during GUI destruction; backend retained until process exit.\n", stderr); } void MainWindow::report_error(const char *operation, xtc_result result) { char text[384]; const char *hint = result == XTC_ERR_UNSUPPORTED ? "Rebuild with JACK development support." : "Check the JACK-compatible server and use Reconnect to retry."; std::snprintf(text, sizeof(text), "%s: %s. %s", operation, xtc_result_string(result), hint); set_label(error_, text); } xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id) { auto *window = static_cast(context); xtc_graph *candidate = nullptr; size_t added_id = SIZE_MAX; xtc_result result = xtc_graph_apply_edit(window->graph_.get(), &edit, &candidate, &added_id); if (result != XTC_OK) return result; if (window->ready_) result = xtc_backend_submit(window->backend_, candidate); if (result != XTC_OK) { xtc_graph_destroy(candidate); return result; } window->graph_.reset(candidate); window->graph_widget_->set_graph(candidate); if (out_added_id != nullptr && edit.kind == XTC_EDIT_ADD_NODE) *out_added_id = added_id; window->refresh_status(); return XTC_OK; } void MainWindow::open_backend() { xtc_backend_config config = {XTC_BACKEND_JACK, XTC_PROJECT_NAME, 65536U, 1U}; xtc_result result = xtc_backend_open(&config, &backend_); ready_ = result == XTC_OK; running_ = false; if (result != XTC_OK) report_error("Open JACK failed", result); else { result = xtc_backend_submit(backend_, graph_.get()); if (result != XTC_OK) { ready_ = false; // Handle is retained for close/reconnect; graph was not installed. report_error("Submit graph failed", result); } else set_label(error_, ""); } refresh_status(); } void MainWindow::start_engine() { if (!ready_) { report_error("Audio is disconnected; use Reconnect", XTC_ERR_INVALID_STATE); return; } if (running_) return; xtc_result result = xtc_backend_start(backend_); if (result != XTC_OK) report_error("Start engine failed", result); else set_label(error_, ""); refresh_status(); } void MainWindow::stop_engine() { if (!ready_) return; xtc_result result = xtc_backend_stop(backend_); if (result != XTC_OK) report_error("Stop engine failed", result); else set_label(error_, ""); refresh_status(); } void MainWindow::reconnect_engine() { xtc_result result = xtc_backend_close(backend_); if (result != XTC_OK) { report_error("Reconnect could not close JACK", result); return; // Preserve the existing handle for a later retry. } backend_ = nullptr; ready_ = false; running_ = false; open_backend(); } void MainWindow::refresh_status() { bool can_play = false; if (!ready_) { set_label(indicator_, "Audio: disconnected"); set_label(status_, "JACK: not connected — use Reconnect after starting the server"); indicator_->labelcolor(fl_rgb_color(255, 155, 120)); set_label(graph_state_, "Graph: offline — edits stored locally; Reconnect to play"); } else { xtc_backend_status status = {}; xtc_result result = xtc_backend_get_status(backend_, &status); if (result != XTC_OK) { report_error("Read engine status failed", result); set_label(indicator_, "Audio: status unavailable"); set_label(graph_state_, "Graph: engine status unavailable"); set_label(status_, "Unable to read JACK status — use Reconnect"); indicator_->labelcolor(fl_rgb_color(255, 155, 120)); } else { running_ = status.running != 0; const bool fault = status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK; can_play = !running_ && !fault; const char *state = status.server_lost ? "Audio: server lost — Reconnect required" : status.sample_rate_changed ? "Audio: sample rate changed — Reconnect required" : status.process_result != XTC_OK ? "Audio: processing error" : running_ ? "Audio: running — modular graph" : "Audio: ready / stopped"; set_label(indicator_, state); indicator_->labelcolor(fault ? fl_rgb_color(255, 155, 120) : running_ ? fl_rgb_color(125, 225, 160) : fl_rgb_color(215, 215, 215)); char text[256]; std::snprintf(text, sizeof(text), "JACK | %u Hz | %u frames | mono | %u blocks | %u xruns | %s", status.sample_rate, status.buffer_frames, status.processed_blocks, status.xruns, xtc_result_string(status.process_result)); set_label(status_, text); if (fault) set_label(graph_state_, "Graph: engine unavailable — Reconnect required"); else if (status.submitted_revision != status.active_revision) set_label(graph_state_, running_ ? "Graph: queued — engine still processing previous graph" : "Graph: prepared — starts on Play"); else set_label(graph_state_, running_ ? "Graph: active in engine" : "Graph: stopped"); } xtc_backend_collect(backend_); } if (can_play) play_->activate(); else play_->deactivate(); if (ready_ && running_) stop_->activate(); else stop_->deactivate(); } bool MainWindow::close_window() { xtc_result result = xtc_backend_close(backend_); if (result != XTC_OK) { report_error("Close JACK failed; window kept open for retry", result); return false; } backend_ = nullptr; ready_ = false; running_ = false; closing_ = true; Fl::remove_timeout(timer_callback, this); hide(); return true; } int MainWindow::handle(int event) { if (event == FL_SHORTCUT && Fl::event_key() == ' ' && (Fl::event_state() & (FL_CTRL | FL_ALT | FL_META)) == 0) { if (running_) stop_engine(); else start_engine(); return 1; } return Fl_Double_Window::handle(event); } void MainWindow::play_callback(Fl_Widget *, void *context) { static_cast(context)->start_engine(); } void MainWindow::stop_callback(Fl_Widget *, void *context) { static_cast(context)->stop_engine(); } void MainWindow::reconnect_callback(Fl_Widget *, void *context) { static_cast(context)->reconnect_engine(); } void MainWindow::close_callback(Fl_Widget *, void *context) { (void)static_cast(context)->close_window(); } void MainWindow::view_callback(Fl_Widget *widget, void *view) { auto *window = static_cast(widget->window()); window->tabs_->value(static_cast(view)); window->tabs_->redraw(); } void MainWindow::about_callback(Fl_Widget *, void *) { fl_message("%s %s\nModular audio workstation\nPhase 4: modular graph editor; sequencer and mixer remain placeholders.", XTC_PROJECT_NAME, XTC_VERSION_STRING); } void MainWindow::timer_callback(void *context) { auto *window = static_cast(context); window->refresh_status(); if (!window->closing_) Fl::repeat_timeout(0.1, timer_callback, context); }