#include "main_window.hpp" #include "xtc/xtc_version.h" #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") { 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, "Engine controls only. Playback content and recording are not implemented; no ports are connected automatically."); 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(); new Fl_Box(24, 164, 940, 40, "Modular graph placeholder — visual graph editing is not implemented yet."); 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); } 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 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)); } 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(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 (silence — no playback content)" : "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); } 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 3: JACK engine controls; editors 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); }