#include "main_window.hpp" #include "graph_widget.hpp" #include "sequencer_widget.hpp" #include "piano_roll.hpp" #include #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(bool prompt_unsaved) : Fl_Double_Window(1000, 680, XTC_PROJECT_NAME " — modular audio workstation"), prompt_unsaved_(prompt_unsaved) { xtc_project *initial = nullptr; xtc_result result = xtc_project_create(&initial); if (result != XTC_OK) throw std::runtime_error(xtc_result_string(result)); project_.reset(initial); begin(); auto *menu = new Fl_Menu_Bar(0, 0, 1000, 28); menu->add("File/New", FL_CTRL + 'n', file_callback, this); menu->add("File/Open", FL_CTRL + 'o', file_callback, this); menu->add("File/Save", FL_CTRL + 's', file_callback, this); menu->add("File/Save As", FL_CTRL + FL_SHIFT + 's', file_callback, this); menu->add("File/Import WAV", FL_CTRL + 'i', audio_file_callback, this); menu->add("File/Relink selected audio", 0, audio_file_callback, this); menu->add("Audio/Panic", FL_F + 9, panic_callback, this); menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this); menu->add("Audio/Record WAV", FL_SHIFT + FL_F + 5, audio_file_callback, this); 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). Play audio clips and the modular graph."); 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)."); record_ = new Fl_Button(306, 40, 90, 30, "Record"); record_->callback(audio_file_callback, this); record_->tooltip("Record JACK input to WAV; choose path, then Stop to finalize and add the take."); auto *panic = new Fl_Button(404, 40, 70, 30, "Panic"); panic->callback(panic_callback, this); panic->tooltip("Silence held instrument notes (F9)."); indicator_ = new Fl_Box(482, 40, 506, 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; Import WAV clips or record JACK input; connect external ports explicitly."); 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(); auto *add_track = new Fl_Button(20, 150, 100, 26, "Add track"); auto *add_region = new Fl_Button(128, 150, 110, 26, "Add region"); auto *delete_region = new Fl_Button(246, 150, 130, 26, "Delete region/track"); auto *zoom_in = new Fl_Button(384, 150, 80, 26, "Zoom +"); auto *zoom_out = new Fl_Button(472, 150, 80, 26, "Zoom -"); auto *add_midi = new Fl_Button(560, 150, 110, 26, "Add MIDI clip"); auto *piano = new Fl_Button(678, 150, 100, 26, "Piano roll"); piano->callback(piano_callback, this); auto *timeline_hint = new Fl_Box(20, 548, 956, 28, "Import WAV (Ctrl+I) or Record. Drag body to move, edges to trim; ruler seeks while stopped."); timeline_hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); timeline_hint->labelsize(12); auto *timeline_scroll = new Fl_Scroll(20, 188, 956, 350); sequencer_ = new SequencerWidget(20, 188, project_.get(), seek, this); timeline_scroll->end(); sequencer_->before_edit = before_arrangement_edit; sequencer_->callback([](Fl_Widget *, void *c) { static_cast(c)->mark_dirty(); }, this); add_track->callback([](Fl_Widget *, void *c) { static_cast(c)->add_track(); }, sequencer_); add_midi->callback([](Fl_Widget *, void *c) { static_cast(c)->add_midi_clip(); }, sequencer_); add_region->callback([](Fl_Widget *, void *c) { static_cast(c)->add_region(); }, sequencer_); delete_region->callback([](Fl_Widget *, void *c) { static_cast(c)->delete_selection(); }, sequencer_); zoom_in->callback([](Fl_Widget *, void *c) { static_cast(c)->zoom(1); }, sequencer_); zoom_out->callback([](Fl_Widget *, void *c) { static_cast(c)->zoom(-1); }, sequencer_); sequencer->resizable(timeline_scroll); 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, xtc_project_graph(project_.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 *audio = new Fl_Button(650, 150, 130, 26, "Add audio source"); audio->callback([](Fl_Widget *, void *context) { (void)static_cast(context)->add_node(XTC_NODE_AUDIO); }, graph_widget_); auto *instrument = new Fl_Button(788, 150, 130, 26, "Add instrument"); instrument->callback([](Fl_Widget *, void *c) { (void)static_cast(c)->add_node(XTC_NODE_INSTRUMENT); }, 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 and recording retained until process exit.\n", stderr); else if (recorder_) { (void)xtc_recorder_finish(recorder_); xtc_recorder_destroy(recorder_); } } void MainWindow::report_error(const char *operation, xtc_result result) { char text[384]; const char *hint = std::strstr(operation, "Project") != nullptr ? "Check the project path, file format and write permissions." : 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); if (window->recorder_) return XTC_ERR_INVALID_STATE; xtc_graph *candidate = nullptr; size_t added_id = SIZE_MAX; xtc_result result = xtc_graph_apply_edit(xtc_project_graph(window->project_.get()), &edit, &candidate, &added_id); if (result != XTC_OK) return result; xtc_project *replacement = nullptr; result = xtc_project_clone(window->project_.get(), &replacement); if (result == XTC_OK) result = xtc_project_set_graph(replacement, candidate); xtc_graph_destroy(candidate); if (result == XTC_OK && window->ready_) result = xtc_backend_submit(window->backend_, xtc_project_graph(replacement)); if (result != XTC_OK) { xtc_project_destroy(replacement); return result; } window->project_.reset(replacement); window->graph_widget_->set_graph(xtc_project_graph(replacement)); window->sequencer_->set_project(replacement, false); window->mark_dirty(); 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_, xtc_project_graph(project_.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_prepare_project(backend_, project_.get()); if (result == XTC_OK) result = xtc_backend_start(backend_); if (result != XTC_OK) report_error("Start engine failed", result); else { set_label(error_, ""); const char *issue = xtc_backend_asset_issue(backend_); if (issue[0]) { char message[640]; std::snprintf(message, sizeof(message), "Missing/unreadable audio (silent): %s. Select clip and Relink to repair.", issue); set_label(error_, message); } } 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_, ""); if (recorder_) finish_recording(); } refresh_status(); } void MainWindow::reconnect_engine() { if (recorder_) { report_error("Stop recording before reconnecting", XTC_ERR_INVALID_STATE); return; } 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, can_record = 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 { uint64_t tick = 0; if (xtc_project_sample_to_tick(project_.get(), status.sample_position, status.sample_rate, &tick) == XTC_OK) sequencer_->position(tick); running_ = status.running != 0; const bool fault = status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK; can_play = !running_ && !fault; can_record = !fault && recorder_ == nullptr; 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" : recorder_ ? "Audio: recording JACK input — Stop to finish" : 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 | %u asset issues | %u MIDI drops", status.sample_rate, status.buffer_frames, status.processed_blocks, status.xruns, xtc_result_string(status.process_result), status.asset_issues, status.midi_dropped); 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 (can_record) record_->activate(); else record_->deactivate(); if (ready_ && (running_ || recorder_)) stop_->activate(); else stop_->deactivate(); } bool MainWindow::close_window() { if (recorder_) { report_error("Stop recording before closing", XTC_ERR_INVALID_STATE); return false; } if (!discard_changes()) return false; 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); if (piano_window_) piano_window_->hide(); 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 7: MIDI input, sine instrument, persisted notes and piano roll.", XTC_PROJECT_NAME, XTC_VERSION_STRING); } void MainWindow::timer_callback(void *context) { auto *window = static_cast(context); if (window->recorder_) { xtc_result r = xtc_recorder_drain(window->recorder_); xtc_backend_status status{}; xtc_backend_get_status(window->backend_, &status); if (r != XTC_OK || status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK) window->stop_engine(); } window->refresh_status(); if (!window->closing_) Fl::repeat_timeout(0.1, timer_callback, context); } void MainWindow::mark_dirty() { graph_widget_->set_graph(xtc_project_graph(project_.get())); if (piano_) piano_->set_project(project_.get(), piano_->clip()); dirty_ = true; copy_label("xtc * — unsaved changes"); } bool MainWindow::discard_changes() { return !dirty_ || !prompt_unsaved_ || fl_choice("Discard unsaved project changes?", "Cancel", "Discard", nullptr) == 1; } xtc_result MainWindow::seek(void *context, uint64_t tick) { auto *window = static_cast(context); if (!window->ready_) return XTC_OK; xtc_backend_status status{}; xtc_result r = xtc_backend_get_status(window->backend_, &status); uint64_t sample = 0; if (r == XTC_OK) r = xtc_project_tick_to_sample(window->project_.get(), tick, status.sample_rate, &sample); return r == XTC_OK ? xtc_backend_seek(window->backend_, sample) : r; } xtc_result MainWindow::save_project(const char *path) { if (recorder_) return XTC_ERR_INVALID_STATE; if (path == nullptr) return XTC_ERR_INVALID_ARGUMENT; std::string saved_path(path); xtc_result r = xtc_project_save(project_.get(), path); if (r == XTC_OK) { path_ = saved_path; dirty_ = false; copy_label(path_.c_str()); } return r; } xtc_result MainWindow::install_project(xtc_project *candidate) { if (recorder_) return XTC_ERR_INVALID_STATE; xtc_result r = XTC_OK; if (ready_) { r = xtc_backend_stop(backend_); if (r == XTC_OK) r = xtc_backend_seek(backend_, 0); if (r == XTC_OK) r = xtc_backend_prepare_project(backend_, candidate); } xtc_audio_plan *offline = nullptr; if (r == XTC_OK && !ready_) r = xtc_audio_plan_create(candidate, 48000, &offline); xtc_midi_plan *offline_midi = nullptr; if (r == XTC_OK && !ready_) r = xtc_midi_plan_create(candidate, 48000, &offline_midi); xtc_midi_plan_destroy(offline_midi); if (r != XTC_OK) { xtc_audio_plan_destroy(offline); return r; } std::string issue = ready_ ? xtc_backend_asset_issue(backend_) : ""; if (offline) { const char *path = ""; xtc_audio_plan_issues(offline, &path); issue = path; xtc_audio_plan_destroy(offline); } if (piano_window_) piano_window_->hide(); if (piano_) piano_->set_project(candidate, UINT64_MAX); project_.reset(candidate); graph_widget_->reset_layout(); graph_widget_->set_graph(xtc_project_graph(candidate)); sequencer_->set_project(candidate); dirty_ = false; if (!issue.empty()) { char text[640]; std::snprintf(text, sizeof(text), "Missing/unreadable audio (silent): %s. Select clip and Relink to repair.", issue.c_str()); set_label(error_, text); } else set_label(error_, ""); refresh_status(); return XTC_OK; } xtc_result MainWindow::open_project(const char *path) { if (path == nullptr) return XTC_ERR_INVALID_ARGUMENT; std::string loaded_path(path); xtc_project *candidate = nullptr; xtc_result r = xtc_project_create(&candidate); if (r == XTC_OK) r = xtc_project_load(candidate, path); if (r == XTC_OK) r = install_project(candidate); if (r != XTC_OK) xtc_project_destroy(candidate); else { path_ = loaded_path; copy_label(path_.c_str()); } return r; } void MainWindow::file_callback(Fl_Widget *widget, void *context) { auto *window = static_cast(context); if (window->recorder_) { window->report_error("Stop recording before file operations", XTC_ERR_INVALID_STATE); return; } auto *menu = static_cast(widget); const char *action = menu->mvalue()->label(); xtc_result r = XTC_OK; if (std::strcmp(action, "New") == 0) { if (!window->discard_changes()) return; xtc_project *candidate = nullptr; r = xtc_project_create(&candidate); if (r == XTC_OK) r = window->install_project(candidate); if (r != XTC_OK) xtc_project_destroy(candidate); else { window->path_.clear(); window->copy_label("xtc — Untitled"); } } else { bool open = std::strcmp(action, "Open") == 0; if (open && !window->discard_changes()) return; if (!open && std::strcmp(action, "Save") == 0 && !window->path_.empty()) r = window->save_project(window->path_.c_str()); else { Fl_Native_File_Chooser chooser; chooser.title(open ? "Open project" : "Save project"); chooser.type(open ? Fl_Native_File_Chooser::BROWSE_FILE : Fl_Native_File_Chooser::BROWSE_SAVE_FILE); chooser.filter("xtc projects *.xtc"); if (!open) chooser.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM); int result = chooser.show(); if (result == 1) return; if (result < 0) r = XTC_ERR_IO; else r = open ? window->open_project(chooser.filename()) : window->save_project(chooser.filename()); } } if (r != XTC_OK) window->report_error("Project operation failed", r); } xtc_result MainWindow::before_arrangement_edit(void *context) { auto *window = static_cast(context); if (window->recorder_) return XTC_ERR_INVALID_STATE; if (!window->ready_) return XTC_OK; xtc_result r = xtc_backend_stop(window->backend_); window->refresh_status(); return r; } xtc_result MainWindow::import_audio(const char *path) { xtc_result r = before_arrangement_edit(this); if (r != XTC_OK) return r; xtc_project *candidate = nullptr; r = xtc_project_clone(project_.get(), &candidate); uint64_t track = sequencer_->selected_track(), clip = 0; if (r == XTC_OK && track == UINT64_MAX) r = xtc_project_add_track(candidate, "Audio", &track); if (r == XTC_OK) r = xtc_project_import_audio(candidate, track, path, sequencer_->position(), &clip); if (r == XTC_OK && ready_) r = xtc_backend_prepare_project(backend_, candidate); if (r != XTC_OK) { xtc_project_destroy(candidate); return r; } project_.reset(candidate); sequencer_->set_project(candidate, false); graph_widget_->set_graph(xtc_project_graph(candidate)); mark_dirty(); refresh_status(); return XTC_OK; } xtc_result MainWindow::record_audio(const char *path) { if (!ready_ || recorder_) return XTC_ERR_INVALID_STATE; xtc_result r = xtc_backend_stop(backend_); xtc_backend_status status{}; if (r == XTC_OK) r = xtc_backend_get_status(backend_, &status); if (r == XTC_OK) r = xtc_project_sample_to_tick(project_.get(), status.sample_position, status.sample_rate, &recording_tick_); if (r == XTC_OK) r = xtc_backend_prepare_project(backend_, project_.get()); if (r == XTC_OK) r = xtc_recorder_create(path, status.sample_rate, static_cast(status.sample_rate) * 2, &recorder_); if (r != XTC_OK) return r; recording_path_ = path; recording_track_ = sequencer_->selected_track(); r = xtc_backend_set_recorder(backend_, recorder_); if (r == XTC_OK) r = xtc_backend_start(backend_); if (r != XTC_OK) { (void)xtc_backend_set_recorder(backend_, nullptr); xtc_recorder_destroy(recorder_); recorder_ = nullptr; } else mark_dirty(); refresh_status(); return r; } void MainWindow::finish_recording() { // Called only after successful backend_stop: no producer can reference ring. (void)xtc_backend_set_recorder(backend_, nullptr); uint64_t frames = 0; (void)xtc_recorder_status(recorder_, &frames); xtc_result r = xtc_recorder_finish(recorder_); xtc_recorder_destroy(recorder_); recorder_ = nullptr; if (r != XTC_OK) { char text[768]; std::snprintf(text, sizeof(text), "Recording failed (%s). Partial take, if finalized: %s. It was not added to the project.", xtc_result_string(r), recording_path_.c_str()); set_label(error_, text); return; } if (!frames) { set_label(error_, "Empty recording saved; no clip added."); return; } xtc_project *candidate = nullptr; r = xtc_project_clone(project_.get(), &candidate); uint64_t clip = 0, track = recording_track_; if (r == XTC_OK && track == UINT64_MAX) r = xtc_project_add_track(candidate, "Recording", &track); if (r == XTC_OK) r = xtc_project_import_audio(candidate, track, recording_path_.c_str(), recording_tick_, &clip); if (r == XTC_OK) r = xtc_backend_prepare_project(backend_, candidate); if (r != XTC_OK) { xtc_project_destroy(candidate); char text[768]; std::snprintf(text, sizeof(text), "Take saved to %s; adding clip failed: %s. Import the WAV to recover.", recording_path_.c_str(), xtc_result_string(r)); set_label(error_, text); return; } project_.reset(candidate); sequencer_->set_project(candidate, false); graph_widget_->set_graph(xtc_project_graph(candidate)); mark_dirty(); } void MainWindow::audio_file_callback(Fl_Widget *widget, void *context) { auto *window = static_cast(context); auto *menu = dynamic_cast(widget); const char *action = menu ? menu->mvalue()->label() : "Record WAV"; bool recording = std::strcmp(action, "Record WAV") == 0; bool relink = std::strcmp(action, "Relink selected audio") == 0; if (window->recorder_) { window->report_error("Stop recording first", XTC_ERR_INVALID_STATE); return; } Fl_Native_File_Chooser chooser; chooser.title(recording ? "Record JACK input to WAV" : relink ? "Relink selected clip" : "Import WAV"); chooser.type(recording ? Fl_Native_File_Chooser::BROWSE_SAVE_FILE : Fl_Native_File_Chooser::BROWSE_FILE); chooser.filter("WAV audio\t*.wav"); if (recording) chooser.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM); int chosen = chooser.show(); if (chosen == 1) return; xtc_result r = chosen < 0 ? XTC_ERR_IO : XTC_OK; if (r == XTC_OK && relink) { r = before_arrangement_edit(window); if (r == XTC_OK) r = xtc_project_set_clip_asset(window->project_.get(), window->sequencer_->selected_clip(), chooser.filename()); if (r == XTC_OK) { window->mark_dirty(); window->sequencer_->redraw(); } } else if (r == XTC_OK) r = recording ? window->record_audio(chooser.filename()) : window->import_audio(chooser.filename()); if (r != XTC_OK) window->report_error("Project audio operation failed", r); } xtc_result MainWindow::open_piano_roll() { xtc_clip_info clip{}; if (xtc_project_find_clip(project_.get(), sequencer_->selected_clip(), &clip) != XTC_OK || !clip.midi) return XTC_ERR_INVALID_STATE; if (!piano_window_) { piano_window_ = std::make_unique(900, 620, "xtc — Piano roll"); piano_window_->begin(); auto *hint = new Fl_Box(12, 8, 876, 32, "Source ticks: click draws; drag body moves/pitches, right edge resizes; right-click/Delete removes. Snap 1/16."); hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); hint->labelsize(12); auto *scroll = new Fl_Scroll(12, 48, 876, 560); piano_ = new PianoRollWidget(12, 48, project_.get(), clip.id, before_arrangement_edit, this); piano_->callback([](Fl_Widget *, void *c) { static_cast(c)->mark_dirty(); }, this); scroll->end(); piano_window_->end(); piano_window_->resizable(scroll); scroll->scroll_to(0, 60 * 16); } else piano_->set_project(project_.get(), clip.id); piano_window_->show(); return XTC_OK; } void MainWindow::piano_callback(Fl_Widget *, void *context) { auto *window = static_cast(context); if (window->recorder_) { window->report_error("Stop recording before editing MIDI", XTC_ERR_INVALID_STATE); return; } xtc_result r = window->open_piano_roll(); if (r != XTC_OK) set_label(window->error_, "Select a MIDI clip, then open Piano roll."); } void MainWindow::panic_callback(Fl_Widget *, void *context) { auto *window = static_cast(context); if (!window->ready_) return; xtc_result r = xtc_backend_panic(window->backend_); if (r != XTC_OK) window->report_error("Panic failed", r); }