2026-10-26 22:51:25 +01:00
# include "main_window.hpp"
2026-10-26 23:55:35 +01:00
# include "graph_widget.hpp"
2026-10-27 00:10:45 +01:00
# include "sequencer_widget.hpp"
2026-10-27 00:53:41 +01:00
# include "piano_roll.hpp"
2026-10-27 01:06:57 +01:00
# include "mixer_widget.hpp"
2026-10-27 16:24:21 +01:00
# include "plugin_dialog.hpp"
2026-10-27 18:04:39 +01:00
# include "theme.hpp"
2026-10-28 10:08:13 +01:00
# include "snap.hpp"
# include "velocity_lane.hpp"
2026-10-28 10:37:01 +01:00
# include "preferences_dialog.hpp"
2026-10-28 17:16:01 +01:00
# include "command_palette.hpp"
# include <FL/Fl_Value_Input.H>
2026-10-28 10:37:01 +01:00
# include "plugin_library.hpp"
# include <FL/Fl_Check_Button.H>
2026-10-28 10:08:13 +01:00
# include <FL/Fl_Choice.H>
# include <FL/Fl_Value_Slider.H>
2026-10-27 00:10:45 +01:00
# include <FL/Fl_Native_File_Chooser.H>
2026-10-26 23:40:28 +01:00
# include "xtc/xtc_version.h"
2026-10-26 22:51:25 +01:00
2026-10-26 23:40:28 +01:00
# include <FL/Fl.H>
2026-10-26 22:51:25 +01:00
# include <FL/Fl_Box.H>
2026-10-26 23:40:28 +01:00
# include <FL/Fl_Button.H>
# include <FL/Fl_Group.H>
2026-10-26 23:55:35 +01:00
# include <FL/Fl_Scroll.H>
2026-10-26 22:51:25 +01:00
# include <FL/Fl_Menu_Bar.H>
2026-10-27 16:49:30 +01:00
# include <FL/Fl_Input_.H>
2026-10-26 22:51:25 +01:00
# include <FL/Fl_Tabs.H>
2026-10-26 23:40:28 +01:00
# include <FL/fl_ask.H>
# include <cstdio>
# include <cstring>
2026-10-26 23:55:35 +01:00
# include <stdexcept>
2026-10-27 04:00:27 +01:00
# include <cstdlib>
# include <cmath>
# include <cerrno>
2026-11-05 23:57:56 +01:00
# include <algorithm>
2026-10-26 22:51:25 +01:00
2026-10-26 23:40:28 +01:00
namespace {
2026-10-30 12:39:47 +01:00
class PanelWindow : public Fl_Double_Window {
MainWindow * owner_ ;
public :
PanelWindow ( int width , int height , const char * title , MainWindow * owner )
: Fl_Double_Window ( width , height , title ) , owner_ ( owner ) { }
2026-11-05 23:57:56 +01:00
void resize ( int x , int y , int width , int height ) override {
Fl_Double_Window : : resize ( x , y , width , height ) ;
if ( owner_ & & owner_ - > transport_window ( ) = = this ) owner_ - > layout_transport ( ) ;
}
2026-10-30 12:39:47 +01:00
int handle ( int event ) override {
if ( event = = FL_SHORTCUT & & owner_ - > panel_shortcut ( event ) ) return 1 ;
return Fl_Double_Window : : handle ( event ) ;
}
} ;
2026-11-05 23:57:56 +01:00
void restore_panel ( Fl_Window * window , int x , int y , int width , int height , int min_width , int min_height )
{
int sx , sy , sw , sh ; Fl : : screen_work_area ( sx , sy , sw , sh , Fl : : screen_num ( x , y , width , height ) ) ;
width = std : : min ( std : : max ( width , min_width ) , sw ) ; height = std : : min ( std : : max ( height , min_height ) , sh ) ;
x = std : : clamp ( x , sx , sx + sw - width ) ; y = std : : clamp ( y , sy , sy + sh - height ) ;
window - > resize ( x , y , width , height ) ;
}
2026-10-28 17:16:01 +01:00
bool editing_number ( Fl_Value_Input * input )
{
return Fl : : focus ( ) = = input | | Fl : : focus ( ) = = & input - > input | | Fl : : pushed ( ) = = input | |
Fl : : pushed ( ) = = & input - > input ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Updates a box label only when the text differs .
*
* Avoids redundant relayout and redraw when status text is refreshed
* repeatedly . The box takes a copy of the text .
*
* @ param box Box whose label is updated ; must not be NULL .
* @ param text New label text ; must not be NULL .
*/
2026-10-26 23:40:28 +01:00
void set_label ( Fl_Box * box , const char * text )
{
if ( box - > label ( ) = = nullptr | | std : : strcmp ( box - > label ( ) , text ) ! = 0 )
box - > copy_label ( text ) ;
}
2026-10-27 16:49:30 +01:00
/* Paint progress without dispatching input/reentering edit commands. */
class BusyOperation {
Fl_Window & window_ ;
public :
BusyOperation ( Fl_Window & window , Fl_Box * status , const char * message ) : window_ ( window ) {
set_label ( status , message ) ; window_ . cursor ( FL_CURSOR_WAIT ) ; Fl : : flush ( ) ;
}
~ BusyOperation ( ) { window_ . cursor ( FL_CURSOR_DEFAULT ) ; }
} ;
2026-10-27 01:37:21 +01:00
} // namespace
2026-10-26 22:51:25 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Constructs the window , its widgets , and the initial project .
*
* Creates an empty project and undo history , opens the audio backend , and
* schedules the periodic status timer . Throws std : : runtime_error when the
* initial project or history cannot be created .
*
* @ param prompt_unsaved Whether open , new , and close actions ask before
* discarding unsaved changes .
*/
2026-10-28 10:37:01 +01:00
MainWindow : : MainWindow ( bool prompt_unsaved , const char * configuration )
2026-10-27 18:04:39 +01:00
: Fl_Double_Window ( 1280 , 800 , XTC_PROJECT_NAME " — modular audio workstation " ) , prompt_unsaved_ ( prompt_unsaved )
2026-10-26 22:51:25 +01:00
{
2026-10-27 18:04:39 +01:00
xtc_ui : : initialize ( ) ;
2026-10-28 10:37:01 +01:00
xtc_settings_defaults ( & settings_ ) ;
char config_path [ 4096 ] ;
if ( configuration ) configuration_ = configuration ;
else if ( xtc_settings_path ( config_path , sizeof ( config_path ) ) = = XTC_OK ) configuration_ = config_path ;
xtc_result config_result = configuration_ . empty ( ) ? XTC_ERR_NOT_FOUND : xtc_settings_load ( & settings_ , configuration_ . c_str ( ) ) ;
2026-10-27 00:10:45 +01:00
xtc_project * initial = nullptr ;
xtc_result result = xtc_project_create ( & initial ) ;
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK )
throw std : : runtime_error ( xtc_result_string ( result ) ) ;
2026-10-28 10:37:01 +01:00
project_defaults ( initial ) ;
2026-10-27 00:10:45 +01:00
project_ . reset ( initial ) ;
2026-10-27 01:06:57 +01:00
xtc_history * history = nullptr ;
result = xtc_history_create ( initial , & history ) ;
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK )
throw std : : runtime_error ( xtc_result_string ( result ) ) ;
2026-10-27 01:06:57 +01:00
history_ . reset ( history ) ;
2026-10-26 22:51:25 +01:00
begin ( ) ;
2026-10-27 18:04:39 +01:00
auto * menu = new Fl_Menu_Bar ( 0 , 0 , 1280 , 28 ) ;
2026-10-27 16:49:30 +01:00
menu_ = menu ;
2026-10-27 00:10:45 +01:00
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 ) ;
2026-11-05 23:55:56 +01:00
menu - > add ( " File/Export WAV " , 0 , [ ] ( Fl_Widget * , void * context ) { static_cast < MainWindow * > ( context ) - > export_audio ( ) ; } , this ) ;
2026-10-27 00:10:45 +01:00
menu - > add ( " File/Save As " , FL_CTRL + FL_SHIFT + ' s ' , file_callback , this ) ;
2026-10-27 00:31:03 +01:00
menu - > add ( " File/Import WAV " , FL_CTRL + ' i ' , audio_file_callback , this ) ;
menu - > add ( " File/Relink selected audio " , 0 , audio_file_callback , this ) ;
2026-10-28 10:37:01 +01:00
menu - > add ( " Edit/Preferences " , FL_CTRL + ' , ' , [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > show_preferences ( ) ; } , this ) ;
2026-10-27 18:04:39 +01:00
menu - > add ( " Edit/Undo " , FL_CTRL + ' z ' , history_callback , this ) ;
menu - > add ( " Edit/Redo " , FL_CTRL + FL_SHIFT + ' z ' , history_callback , this ) ;
menu - > add ( " Edit/Redo alternate " , FL_CTRL + ' y ' , history_callback , this ) ;
2026-10-27 16:37:59 +01:00
menu - > add ( " Audio/MIDI controller mapping " , 0 , control_map_callback , this ) ;
2026-10-27 00:53:41 +01:00
menu - > add ( " Audio/Panic " , FL_F + 9 , panic_callback , this ) ;
2026-10-28 17:16:01 +01:00
menu - > add ( " View/Commands " , FL_CTRL + ' p ' , [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > show_commands ( ) ; } , this ) ;
menu - > add ( " View/Fit song " , FL_CTRL + ' 0 ' , [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > fit_song ( ) ; } , this ) ;
2026-10-27 00:53:41 +01:00
menu - > add ( " View/Piano roll " , FL_CTRL + ' 4 ' , piano_callback , this ) ;
2026-10-27 00:31:03 +01:00
menu - > add ( " Audio/Record WAV " , FL_SHIFT + FL_F + 5 , audio_file_callback , this ) ;
2026-10-27 17:51:48 +01:00
menu - > add ( " Track/Load instrument plugin " , 0 , instrument_callback , this ) ;
menu - > add ( " Track/Use built-in instrument " , 0 , instrument_callback , this ) ;
menu - > add ( " Track/Live MIDI to selected instrument " , 0 , instrument_callback , this ) ;
menu - > add ( " Track/Live MIDI to built-in sine " , 0 , instrument_callback , this ) ;
2026-11-05 23:47:54 +01:00
menu - > add ( " Edit/Selected instrument controls " , 0 , instrument_callback , this ) ;
menu - > add ( " Edit/Selected instrument automation " , 0 , instrument_callback , this ) ;
2026-11-05 23:50:14 +01:00
menu - > add ( " File/Load sampler WAV " , 0 , instrument_callback , this ) ;
2026-11-05 23:47:54 +01:00
menu - > add ( " File/Save instrument preset " , 0 , instrument_callback , this ) ;
menu - > add ( " File/Load instrument preset " , 0 , instrument_callback , this ) ;
2026-10-28 17:16:01 +01:00
menu - > add ( " Edit/Insert internal delay on wire " , 0 , plugin_callback , this ) ;
menu - > add ( " Edit/Insert internal reverb on wire " , 0 , plugin_callback , this ) ;
2026-11-05 23:53:03 +01:00
menu - > add ( " Edit/Insert stereo delay on wire " , 0 , plugin_callback , this ) ;
menu - > add ( " Edit/Insert stereo reverb on wire " , 0 , plugin_callback , this ) ;
menu - > add ( " File/Add stereo delay " , 0 , plugin_callback , this ) ;
menu - > add ( " File/Add stereo reverb " , 0 , plugin_callback , this ) ;
2026-10-28 17:16:01 +01:00
menu - > add ( " File/Add internal delay " , 0 , plugin_callback , this ) ;
menu - > add ( " File/Add internal reverb " , 0 , plugin_callback , this ) ;
2026-10-27 17:34:46 +01:00
menu - > add ( " File/Load plugin " , 0 , plugin_callback , this ) ;
2026-10-27 16:24:21 +01:00
menu - > add ( " Edit/Replace selected plugin " , 0 , plugin_callback , this ) ;
2026-10-27 17:34:46 +01:00
menu - > add ( " Edit/Insert plugin on selected wire " , 0 , plugin_callback , this ) ;
2026-10-27 16:24:21 +01:00
menu - > add ( " Edit/Reset selected plugin " , 0 , plugin_callback , this ) ;
2026-10-27 06:25:30 +01:00
menu - > add ( " Edit/Selected plugin automation " , 0 , plugin_callback , this ) ;
2026-10-27 04:00:27 +01:00
menu - > add ( " Edit/Selected plugin control " , 0 , plugin_callback , this ) ;
2026-10-26 23:40:28 +01:00
menu - > add ( " File/Quit " , FL_CTRL + ' q ' , close_callback , this ) ;
2026-10-27 18:04:39 +01:00
2026-10-26 23:40:28 +01:00
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 ) ;
2026-10-26 22:51:25 +01:00
2026-10-27 03:25:04 +01:00
menu - > add ( " Audio/Pause " , FL_F + 7 , [ ] ( Fl_Widget * , void * c ) {
( void ) static_cast < MainWindow * > ( c ) - > pause_playback ( ) ;
} , this ) ;
menu - > add ( " Audio/Return to start " , FL_Home , [ ] ( Fl_Widget * , void * c ) {
( void ) static_cast < MainWindow * > ( c ) - > return_to_start ( ) ;
} , this ) ;
2026-10-30 12:39:47 +01:00
menu - > add ( " View/Detach transport " , 0 , [ ] ( Fl_Widget * , void * c ) {
auto * w = static_cast < MainWindow * > ( c ) ; w - > detach_transport ( ! w - > transport_detached_ ) ;
} , this , FL_MENU_TOGGLE ) ;
menu - > add ( " View/Detach mixer " , 0 , [ ] ( Fl_Widget * , void * c ) {
auto * w = static_cast < MainWindow * > ( c ) ; w - > detach_mixer ( ! w - > mixer_detached_ ) ;
} , this , FL_MENU_TOGGLE ) ;
menu - > add ( " View/Dock all panels " , 0 , [ ] ( Fl_Widget * , void * c ) {
auto * w = static_cast < MainWindow * > ( c ) ; w - > detach_transport ( false ) ; w - > detach_mixer ( false ) ;
} , this ) ;
transport_group_ = new Fl_Group ( 0 , 28 , 1280 , 90 ) ;
transport_group_ - > begin ( ) ;
2026-10-26 23:40:28 +01:00
play_ = new Fl_Button ( 12 , 40 , 80 , 30 , " Play " ) ;
play_ - > callback ( play_callback , this ) ;
2026-10-27 00:31:03 +01:00
play_ - > tooltip ( " Start the audio engine (F5 or Space). Play audio clips and the modular graph. " ) ;
2026-10-26 23:40:28 +01:00
stop_ = new Fl_Button ( 100 , 40 , 80 , 30 , " Stop " ) ;
stop_ - > callback ( stop_callback , this ) ;
2026-10-27 03:25:04 +01:00
stop_ - > tooltip ( " Stop playback at its current position; finalize recording (F8 or Space). " ) ;
2026-10-26 23:40:28 +01:00
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). " ) ;
2026-10-27 00:31:03 +01:00
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. " ) ;
2026-10-27 00:53:41 +01:00
auto * panic = new Fl_Button ( 404 , 40 , 70 , 30 , " Panic " ) ;
panic - > callback ( panic_callback , this ) ;
panic - > tooltip ( " Silence held instrument notes (F9). " ) ;
2026-10-27 03:25:04 +01:00
pause_ = new Fl_Button ( 482 , 40 , 70 , 30 , " Pause " ) ;
pause_ - > callback ( [ ] ( Fl_Widget * , void * c ) {
( void ) static_cast < MainWindow * > ( c ) - > pause_playback ( ) ;
} , this ) ;
pause_ - > tooltip ( " Pause at the current sample position (F7). Play resumes; recording uses Stop. " ) ;
home_ = new Fl_Button ( 560 , 40 , 100 , 30 , " To start " ) ;
home_ - > callback ( [ ] ( Fl_Widget * , void * c ) {
( void ) static_cast < MainWindow * > ( c ) - > return_to_start ( ) ;
} , this ) ;
home_ - > tooltip ( " Stop and return to the project beginning (Home). Unavailable while recording. " ) ;
2026-10-27 18:04:39 +01:00
indicator_ = new Fl_Box ( 1000 , 40 , 268 , 30 , " Audio: disconnected " ) ;
2026-10-26 23:40:28 +01:00
indicator_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
indicator_ - > labelfont ( FL_BOLD ) ;
2026-10-27 03:25:04 +01:00
indicator_ - > labelsize ( 12 ) ;
2026-10-27 18:04:39 +01:00
transport_position_ = new Fl_Box ( 680 , 40 , 190 , 30 , " 001 : 1 : 000 " ) ;
transport_position_ - > tooltip ( " Musical position: bar : beat : tick (4/4, 960 ticks per beat) " ) ;
2026-10-28 17:16:01 +01:00
tempo_display_ = new Fl_Value_Input ( 880 , 40 , 78 , 30 , " BPM " ) ;
tempo_display_ - > align ( FL_ALIGN_RIGHT ) ;
tempo_display_ - > bounds ( 20 , 300 ) ;
tempo_display_ - > step ( .1 ) ;
tempo_display_ - > when ( FL_WHEN_RELEASE | FL_WHEN_ENTER_KEY ) ;
tempo_display_ - > tooltip ( " Project tempo in BPM. Enter or release to apply; undoable. Stops playback. " ) ;
tempo_display_ - > callback (
[ ] ( Fl_Widget * , void * c ) {
auto * w = static_cast < MainWindow * > ( c ) ;
double bpm = w - > tempo_display_ - > value ( ) ;
xtc_result r = ( ! std : : isfinite ( bpm ) | | bpm < 20 | | bpm > 300 )
? XTC_ERR_INVALID_ARGUMENT
: w - > set_tempo ( static_cast < unsigned int > ( std : : lround ( bpm * 1000 ) ) ) ;
if ( r ! = XTC_OK )
w - > report_error ( " Tempo unchanged " , r ) ;
w - > tempo_display_ - > value ( xtc_project_tempo ( w - > project_ . get ( ) ) / 1000.0 ) ;
w - > refresh_status ( ) ;
} ,
this ) ;
auto * hint = new Fl_Box ( 12 , 82 , 160 , 28 , " xtc / SESSION " ) ;
hint - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
session_info_ = new Fl_Box ( 180 , 82 , 280 , 28 ) ;
session_info_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
auto * commands = new Fl_Button ( 480 , 82 , 120 , 28 , " Commands " ) ;
commands - > tooltip ( " Search actions (Ctrl+P) " ) ;
commands - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > show_commands ( ) ; } , this ) ;
follow_ = new Fl_Check_Button ( 620 , 82 , 154 , 28 , " Follow playhead " ) ;
follow_ - > tooltip ( " Keep playback visible in the arrangement; pauses while dragging. " ) ;
follow_ - > callback (
[ ] ( Fl_Widget * widget , void * c ) {
static_cast < MainWindow * > ( c ) - > settings_ . follow_playhead = static_cast < Fl_Check_Button * > ( widget ) - > value ( ) ;
} ,
this ) ;
auto * fit = new Fl_Button ( 790 , 82 , 100 , 28 , " Fit song " ) ;
fit - > tooltip ( " Choose the largest supported zoom that fits the song (Ctrl+0) " ) ;
fit - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > fit_song ( ) ; } , this ) ;
loop_button_ = new Fl_Check_Button ( 1000 , 82 , 100 , 28 , " Loop " ) ;
2026-10-28 10:37:01 +01:00
loop_button_ - > callback ( [ ] ( Fl_Widget * widget , void * c ) { auto * w = static_cast < MainWindow * > ( c ) ;
xtc_result r = w - > set_loop ( static_cast < uint64_t > ( w - > settings_ . loop_start ) , static_cast < uint64_t > ( w - > settings_ . loop_end ) , static_cast < Fl_Check_Button * > ( widget ) - > value ( ) ! = 0 ) ;
if ( r ! = XTC_OK ) w - > report_error ( " Loop unchanged " , r ) ;
w - > refresh_status ( ) ; } , this ) ;
auto * range = new Fl_Button ( 1110 , 82 , 150 , 28 , " Loop range... " ) ;
range - > tooltip ( " Shift-drag the sequencer ruler to select a loop, or enter start/end ticks here. " ) ;
range - > callback ( [ ] ( Fl_Widget * , void * c ) { auto * w = static_cast < MainWindow * > ( c ) ; char initial [ 100 ] ;
std : : snprintf ( initial , sizeof ( initial ) , " %lld %lld " , static_cast < long long > ( w - > settings_ . loop_start ) , static_cast < long long > ( w - > settings_ . loop_end ) ) ;
const char * text = fl_input ( " Loop start and end ticks (end excluded) " , initial ) ; if ( ! text ) return ;
unsigned long long start , end ; char extra ; xtc_result r = XTC_ERR_INVALID_ARGUMENT ;
if ( std : : sscanf ( text , " %llu %llu %c " , & start , & end , & extra ) = = 2 ) r = w - > set_loop ( start , end , w - > settings_ . loop_enabled ! = 0 ) ;
if ( r ! = XTC_OK ) w - > report_error ( " Loop unchanged " , r ) ; } , this ) ;
2026-10-26 22:51:25 +01:00
2026-10-30 12:39:47 +01:00
transport_group_ - > end ( ) ;
2026-11-05 23:57:56 +01:00
for ( int i = 0 ; i < transport_group_ - > children ( ) ; + + i ) {
auto * item = transport_group_ - > child ( i ) ;
transport_items_ . push_back ( { item , item - > x ( ) , item - > y ( ) - 28 , item - > w ( ) , item - > h ( ) } ) ;
}
2026-10-30 12:39:47 +01:00
2026-10-27 18:04:39 +01:00
tabs_ = new Fl_Tabs ( 216 , 126 , 1056 , 578 ) ;
2026-10-26 23:40:28 +01:00
tabs_ - > begin ( ) ;
2026-10-27 18:04:39 +01:00
auto * sequencer = new Fl_Group ( 216 , 158 , 1056 , 546 , " Sequencer " ) ;
2026-10-26 22:51:25 +01:00
sequencer - > begin ( ) ;
2026-10-27 18:04:39 +01:00
auto * add_track = new Fl_Button ( 228 , 168 , 100 , 26 , " Add track " ) ;
auto * add_region = new Fl_Button ( 336 , 168 , 110 , 26 , " Add region " ) ;
auto * delete_region = new Fl_Button ( 454 , 168 , 130 , 26 , " Delete region/track " ) ;
auto * zoom_in = new Fl_Button ( 592 , 168 , 80 , 26 , " Zoom + " ) ;
auto * zoom_out = new Fl_Button ( 680 , 168 , 80 , 26 , " Zoom - " ) ;
auto * add_midi = new Fl_Button ( 768 , 168 , 110 , 26 , " Add MIDI clip " ) ;
auto * piano = new Fl_Button ( 886 , 168 , 100 , 26 , " Piano roll " ) ;
2026-10-27 00:53:41 +01:00
piano - > callback ( piano_callback , this ) ;
2026-10-27 01:37:21 +01:00
auto * timeline_hint =
2026-10-27 18:04:39 +01:00
new Fl_Box ( 228 , 666 , 1032 , 28 ,
2026-10-27 03:25:04 +01:00
" Drag clips to move, edges to trim. Click the ruler while stopped to seek; Home returns to start. " ) ;
2026-10-27 01:37:21 +01:00
timeline_hint - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
timeline_hint - > labelsize ( 12 ) ;
2026-10-27 18:04:39 +01:00
auto * timeline_scroll = new Fl_Scroll ( 228 , 206 , 1032 , 452 ) ;
2026-10-28 17:16:01 +01:00
timeline_scroll_ = timeline_scroll ;
2026-10-27 18:04:39 +01:00
sequencer_ = new SequencerWidget ( 228 , 206 , project_ . get ( ) , seek , this ) ;
2026-10-27 00:10:45 +01:00
timeline_scroll - > end ( ) ;
2026-10-27 00:31:03 +01:00
sequencer_ - > before_edit = before_arrangement_edit ;
2026-10-28 10:37:01 +01:00
sequencer_ - > loop_select = [ ] ( void * c , uint64_t start , uint64_t end ) { auto * w = static_cast < MainWindow * > ( c ) ; return w - > set_loop ( start , end , w - > settings_ . loop_enabled ! = 0 ) ; } ;
2026-10-28 10:08:13 +01:00
sequencer_ - > open_clip = [ ] ( void * c ) { piano_callback ( nullptr , c ) ; } ;
sequencer_ - > instrument_select = [ ] ( void * c , uint64_t track , const xtc_plugin_spec * spec , bool browse ) {
auto * w = static_cast < MainWindow * > ( c ) ;
w - > sequencer_ - > select ( track ) ;
if ( browse ) { instrument_callback ( nullptr , c ) ; return ; }
xtc_result r = w - > assign_instrument ( track , spec ? spec - > path : nullptr , spec ? spec - > index : 0 ) ;
if ( r ! = XTC_OK ) w - > report_error ( " Track instrument unchanged " , r ) ;
} ;
// Keep this in the editor group rather than the scrollable timeline.
auto * parent = Fl_Group : : current ( ) ; Fl_Group : : current ( sequencer ) ;
snap_ = new Fl_Choice ( 1040 , 168 , 210 , 26 , " Snap " ) ;
for ( size_t i = 0 ; i < xtc_ui : : snap_count ; + + i ) { int item = snap_ - > add ( " Snap " ) ; snap_ - > replace ( item , xtc_ui : : snap_labels [ i ] ) ; }
snap_ - > value ( xtc_ui : : snap_index ( sequencer_ - > grid ( ) ) ) ;
snap_ - > callback ( [ ] ( Fl_Widget * widget , void * c ) {
auto * choice = static_cast < Fl_Choice * > ( widget ) ; int value = choice - > value ( ) ;
if ( value > = 0 & & static_cast < size_t > ( value ) < xtc_ui : : snap_count )
( void ) static_cast < MainWindow * > ( c ) - > sequencer_ - > grid ( xtc_ui : : snap_ticks [ value ] ) ;
} , this ) ;
Fl_Group : : current ( parent ) ;
2026-10-27 06:28:25 +01:00
menu - > add ( " Edit/Duplicate clip " , FL_CTRL + ' d ' , [ ] ( Fl_Widget * , void * c ) { ( void ) static_cast < SequencerWidget * > ( c ) - > duplicate_selection ( ) ; } , sequencer_ ) ;
2026-10-27 15:46:41 +01:00
menu - > add ( " Edit/Split audio at cursor " , 0 , [ ] ( Fl_Widget * , void * c ) { ( void ) static_cast < SequencerWidget * > ( c ) - > split_selection ( ) ; } , sequencer_ ) ;
menu - > add ( " Edit/Audio clip gain and fades " , 0 , [ ] ( Fl_Widget * , void * c ) { static_cast < SequencerWidget * > ( c ) - > audio_properties_dialog ( ) ; } , sequencer_ ) ;
2026-10-27 06:28:25 +01:00
menu - > add ( " Edit/Rename clip or track " , FL_F + 2 , [ ] ( Fl_Widget * , void * c ) {
const char * name = fl_input ( " Name (1..63 UTF-8 bytes) " , " " ) ; if ( name ) ( void ) static_cast < SequencerWidget * > ( c ) - > rename_selection ( name ) ;
} , sequencer_ ) ;
2026-10-28 10:08:13 +01:00
for ( const char * label : { " View/Snap/1 tick " , " View/Snap/120 ticks " , " View/Snap/160 ticks " , " View/Snap/240 ticks " , " View/Snap/320 ticks " , " View/Snap/480 ticks " , " View/Snap/960 ticks " , " View/Snap/1920 ticks " , " View/Snap/3840 ticks " } )
2026-10-27 06:28:25 +01:00
menu - > add ( label , 0 , [ ] ( Fl_Widget * widget , void * c ) {
auto * m = static_cast < Fl_Menu_Bar * > ( widget ) ;
( void ) static_cast < SequencerWidget * > ( c ) - > grid ( std : : strtoull ( m - > mvalue ( ) - > label ( ) , nullptr , 10 ) ) ;
} , sequencer_ ) ;
2026-10-27 01:37:21 +01:00
sequencer_ - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < MainWindow * > ( c ) - > mark_dirty ( ) ;
} ,
this ) ;
add_track - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < SequencerWidget * > ( c ) - > add_track ( ) ;
} ,
sequencer_ ) ;
add_midi - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < SequencerWidget * > ( c ) - > add_midi_clip ( ) ;
} ,
sequencer_ ) ;
add_region - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < SequencerWidget * > ( c ) - > add_region ( ) ;
} ,
sequencer_ ) ;
delete_region - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < SequencerWidget * > ( c ) - > delete_selection ( ) ;
} ,
sequencer_ ) ;
zoom_in - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < SequencerWidget * > ( c ) - > zoom ( 1 ) ;
} ,
sequencer_ ) ;
zoom_out - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < SequencerWidget * > ( c ) - > zoom ( - 1 ) ;
} ,
sequencer_ ) ;
2026-10-27 00:10:45 +01:00
sequencer - > resizable ( timeline_scroll ) ;
2026-10-26 22:51:25 +01:00
sequencer - > end ( ) ;
2026-10-27 18:04:39 +01:00
auto * mixer = new Fl_Group ( 216 , 158 , 1056 , 546 , " Mixer " ) ;
2026-10-30 12:39:47 +01:00
mixer_page_ = mixer ;
2026-10-26 22:51:25 +01:00
mixer - > begin ( ) ;
2026-10-27 18:04:39 +01:00
mixer_ = new MixerWidget ( 228 , 168 , 1032 , 522 , project_ . get ( ) , before_arrangement_edit , this ) ;
2026-10-27 01:37:21 +01:00
mixer_ - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < MainWindow * > ( c ) - > mark_dirty ( ) ;
} ,
this ) ;
2026-10-30 12:39:47 +01:00
mixer_placeholder_ = new Fl_Group ( 228 , 210 , 1032 , 120 ) ;
auto * detached_hint = new Fl_Box ( 240 , 220 , 600 , 30 , " The mixer is open in a separate window. " ) ;
detached_hint - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
auto * show_mixer = new Fl_Button ( 240 , 260 , 170 , 30 , " Show mixer window " ) ;
show_mixer - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > mixer_window_ - > show ( ) ; } , this ) ;
auto * dock_mixer = new Fl_Button ( 424 , 260 , 140 , 30 , " Dock mixer " ) ;
dock_mixer - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > detach_mixer ( false ) ; } , this ) ;
mixer_placeholder_ - > end ( ) ; mixer_placeholder_ - > hide ( ) ;
2026-10-27 01:06:57 +01:00
mixer - > resizable ( mixer_ ) ;
2026-10-26 22:51:25 +01:00
mixer - > end ( ) ;
2026-10-27 18:04:39 +01:00
auto * graph = new Fl_Group ( 216 , 158 , 1056 , 546 , " Modular Graph " ) ;
2026-10-26 22:51:25 +01:00
graph - > begin ( ) ;
2026-10-27 18:04:39 +01:00
graph_state_ = new Fl_Box ( 228 , 200 , 956 , 24 , " Graph: offline " ) ;
2026-10-26 23:55:35 +01:00
graph_state_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
graph_state_ - > labelsize ( 12 ) ;
2026-10-27 01:37:21 +01:00
auto * feedback =
2026-10-27 18:04:39 +01:00
new Fl_Box ( 228 , 666 , 1032 , 28 ,
2026-10-27 01:37:21 +01:00
" Add nodes. Drag output to input to connect; click a node or wire then Delete. Scroll to pan. " ) ;
2026-10-26 23:55:35 +01:00
feedback - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP ) ;
feedback - > labelsize ( 12 ) ;
2026-10-27 18:04:39 +01:00
auto * scroll = new Fl_Scroll ( 228 , 230 , 1032 , 430 ) ;
graph_widget_ = new GraphWidget ( 228 , 230 , 2000 , 4000 , xtc_project_graph ( project_ . get ( ) ) , edit_graph , this , feedback ) ;
2026-10-26 23:55:35 +01:00
scroll - > end ( ) ;
2026-10-27 16:00:29 +01:00
menu - > add ( " View/Arrange graph by signal flow " , 0 , [ ] ( Fl_Widget * , void * c ) { ( void ) static_cast < GraphWidget * > ( c ) - > auto_layout ( ) ; } , graph_widget_ ) ;
menu - > add ( " Edit/Insert unity gain in selected wire " , 0 , [ ] ( Fl_Widget * , void * c ) { ( void ) static_cast < GraphWidget * > ( c ) - > insert_gain ( ) ; } , graph_widget_ ) ;
2026-10-27 18:04:39 +01:00
auto * oscillator = new Fl_Button ( 228 , 168 , 130 , 26 , " Add oscillator " ) ;
2026-10-27 01:37:21 +01:00
oscillator - > callback (
[ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_OSCILLATOR ) ;
} ,
graph_widget_ ) ;
2026-10-27 18:04:39 +01:00
auto * gain = new Fl_Button ( 366 , 168 , 110 , 26 , " Add gain " ) ;
2026-10-27 01:37:21 +01:00
gain - > callback (
[ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_GAIN ) ;
} ,
graph_widget_ ) ;
2026-10-27 18:04:39 +01:00
auto * meter = new Fl_Button ( 484 , 168 , 110 , 26 , " Add meter " ) ;
2026-10-27 01:37:21 +01:00
meter - > callback (
[ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_METER ) ;
} ,
graph_widget_ ) ;
2026-10-27 18:04:39 +01:00
auto * output = new Fl_Button ( 602 , 168 , 110 , 26 , " Add output " ) ;
2026-10-27 01:37:21 +01:00
output - > callback (
[ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_OUTPUT ) ;
} ,
graph_widget_ ) ;
2026-10-27 18:04:39 +01:00
auto * audio = new Fl_Button ( 858 , 168 , 130 , 26 , " Add audio source " ) ;
2026-10-27 01:37:21 +01:00
audio - > callback (
[ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_AUDIO ) ;
} ,
graph_widget_ ) ;
2026-10-27 18:04:39 +01:00
auto * instrument = new Fl_Button ( 996 , 168 , 130 , 26 , " Add instrument " ) ;
2026-10-27 01:37:21 +01:00
instrument - > callback (
[ ] ( Fl_Widget * , void * c ) {
( void ) static_cast < GraphWidget * > ( c ) - > add_node ( XTC_NODE_INSTRUMENT ) ;
} ,
graph_widget_ ) ;
2026-10-27 18:04:39 +01:00
auto * remove = new Fl_Button ( 720 , 168 , 130 , 26 , " Delete selection " ) ;
2026-10-27 01:37:21 +01:00
remove - > callback (
[ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > delete_selected ( ) ;
} ,
graph_widget_ ) ;
2026-10-26 23:55:35 +01:00
graph - > resizable ( scroll ) ;
2026-10-26 22:51:25 +01:00
graph - > end ( ) ;
2026-10-26 23:40:28 +01:00
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 ) ;
2026-10-26 22:51:25 +01:00
2026-10-27 18:04:39 +01:00
auto * sidebar = new Fl_Group ( 12 , 126 , 192 , 578 ) ;
2026-10-30 12:39:47 +01:00
sidebar_ = sidebar ;
2026-10-27 18:04:39 +01:00
sidebar - > box ( FL_BORDER_BOX ) ;
auto * create_title = new Fl_Box ( 24 , 140 , 168 , 24 , " CREATE TRACKS " ) ;
create_title - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
auto * midi_track = new Fl_Button ( 24 , 176 , 168 , 34 , " New MIDI track " ) ;
midi_track - > tooltip ( " Create a track and one-bar MIDI clip at the cursor; undo as one edit. " ) ;
midi_track - > callback ( [ ] ( Fl_Widget * , void * c ) { auto * w = static_cast < MainWindow * > ( c ) ;
xtc_result r = w - > create_track ( true ) ; if ( r ! = XTC_OK ) w - > report_error ( " Create MIDI track " , r ) ; } , this ) ;
auto * audio_track = new Fl_Button ( 24 , 218 , 168 , 34 , " New audio track " ) ;
audio_track - > tooltip ( " Create an empty audio track, ready for WAV import or recording. " ) ;
audio_track - > callback ( [ ] ( Fl_Widget * , void * c ) { auto * w = static_cast < MainWindow * > ( c ) ;
xtc_result r = w - > create_track ( false ) ; if ( r ! = XTC_OK ) w - > report_error ( " Create audio track " , r ) ; } , this ) ;
menu - > add ( " Track/New MIDI track " , FL_CTRL + FL_SHIFT + ' m ' , midi_track - > callback ( ) , this ) ;
menu - > add ( " Track/New audio track " , FL_CTRL + FL_SHIFT + ' a ' , audio_track - > callback ( ) , this ) ;
auto * import = new Fl_Button ( 24 , 260 , 168 , 34 , " Import WAV " ) ;
import - > callback ( audio_file_callback , this ) ;
auto * load = new Fl_Button ( 24 , 302 , 168 , 34 , " Load instrument plugin " ) ;
load - > callback ( instrument_callback , this ) ;
auto * editor = new Fl_Button ( 24 , 344 , 168 , 34 , " Edit MIDI notes " ) ;
editor - > callback ( piano_callback , this ) ;
2026-10-28 17:16:01 +01:00
auto * selection_title = new Fl_Box ( 24 , 404 , 168 , 24 , " TRACK INSPECTOR " ) ;
2026-10-27 18:04:39 +01:00
selection_title - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
2026-10-28 17:16:01 +01:00
selection_info_ = new Fl_Box ( 24 , 438 , 168 , 82 , " Select a track to inspect it. " ) ;
2026-10-27 18:04:39 +01:00
selection_info_ - > align ( FL_ALIGN_TOP_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP ) ;
2026-10-28 17:16:01 +01:00
inspector_gain_ = new Fl_Value_Input ( 78 , 528 , 114 , 26 , " Gain " ) ;
inspector_gain_ - > bounds ( 0 , 4 ) ;
inspector_gain_ - > step ( .01 ) ;
inspector_pan_ = new Fl_Value_Input ( 78 , 562 , 114 , 26 , " Pan " ) ;
inspector_pan_ - > bounds ( - 1 , 1 ) ;
inspector_pan_ - > step ( .01 ) ;
inspector_mute_ = new Fl_Check_Button ( 24 , 596 , 78 , 24 , " Mute " ) ;
inspector_solo_ = new Fl_Check_Button ( 108 , 596 , 84 , 24 , " Solo " ) ;
for ( auto * control : { static_cast < Fl_Widget * > ( inspector_gain_ ) , static_cast < Fl_Widget * > ( inspector_pan_ ) ,
static_cast < Fl_Widget * > ( inspector_mute_ ) , static_cast < Fl_Widget * > ( inspector_solo_ ) } ) {
control - > when ( FL_WHEN_RELEASE ) ;
control - > callback ( inspector_callback , this ) ;
}
inspector_gain_ - > tooltip (
" Selected track linear gain: 0 silence, 1 unity, 4 maximum. Automation overrides static gain. " ) ;
inspector_pan_ - > tooltip ( " Selected track pan: -1 left, 0 center, +1 right. " ) ;
auto * tip = new Fl_Box ( 24 , 632 , 168 , 52 , " Ctrl+P Commands \n Ctrl+1/2/3 Views \n Ctrl+0 Fit song " ) ;
2026-10-27 18:04:39 +01:00
tip - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
sidebar - > end ( ) ;
status_ = new Fl_Box ( 12 , 714 , 1256 , 26 , " JACK: not connected " ) ;
2026-10-26 23:40:28 +01:00
status_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
2026-10-27 18:04:39 +01:00
error_ = new Fl_Box ( 12 , 744 , 1256 , 44 , " " ) ;
2026-10-26 23:40:28 +01:00
error_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP ) ;
2026-10-27 18:04:39 +01:00
xtc_ui : : style ( this ) ;
color ( xtc_ui : : background ( ) ) ;
create_title - > labelcolor ( xtc_ui : : muted ( ) ) ; selection_title - > labelcolor ( xtc_ui : : muted ( ) ) ;
tip - > labelcolor ( xtc_ui : : muted ( ) ) ; hint - > labelcolor ( xtc_ui : : muted ( ) ) ;
transport_position_ - > box ( xtc_ui : : flat ) ; transport_position_ - > color ( xtc_ui : : background ( ) ) ;
transport_position_ - > labelfont ( FL_COURIER_BOLD ) ; transport_position_ - > labelsize ( 18 ) ;
tempo_display_ - > labelcolor ( xtc_ui : : muted ( ) ) ;
midi_track - > color ( fl_rgb_color ( 45 , 91 , 86 ) ) ;
play_ - > labelcolor ( xtc_ui : : accent ( ) ) ; record_ - > labelcolor ( fl_rgb_color ( 245 , 132 , 142 ) ) ;
error_ - > labelcolor ( fl_rgb_color ( 255 , 166 , 139 ) ) ;
2026-10-26 22:51:25 +01:00
end ( ) ;
2026-10-26 23:40:28 +01:00
resizable ( tabs_ ) ;
2026-10-27 18:04:39 +01:00
size_range ( 1220 , 720 ) ;
2026-10-26 23:40:28 +01:00
callback ( close_callback , this ) ;
2026-10-28 10:37:01 +01:00
apply_ui_preferences ( ) ;
2026-10-26 23:40:28 +01:00
open_backend ( ) ;
2026-10-28 10:37:01 +01:00
if ( config_result ! = XTC_OK & & config_result ! = XTC_ERR_NOT_FOUND ) report_error ( " Preferences file invalid; using defaults " , config_result ) ;
Fl : : add_timeout ( static_cast < double > ( settings_ . refresh_ms ) / 1000 , timer_callback , this ) ;
2026-10-26 22:51:25 +01:00
}
2026-11-05 23:57:56 +01:00
void MainWindow : : resize ( int x , int y , int width , int height )
{
Fl_Double_Window : : resize ( x , y , width , height ) ;
if ( transport_group_ ) layout_transport ( ) ;
}
void MainWindow : : layout_transport ( )
{
if ( layout_transport_busy_ | | ! transport_group_ | | transport_items_ . empty ( ) ) return ;
layout_transport_busy_ = true ;
int width = transport_detached_ & & transport_window_ ? transport_window_ - > w ( ) : w ( ) ;
int origin = transport_detached_ ? 32 : 28 ;
bool compact = transport_detached_ & & width < 1220 ;
int left = 12 , top = 12 ;
transport_group_ - > resizable ( nullptr ) ;
for ( const auto & item : transport_items_ ) {
int x , y ;
if ( compact ) {
if ( left + item . w + ( item . widget = = tempo_display_ ? 36 : 0 ) > width - 12 ) { left = 12 ; top + = 40 ; }
x = left ; y = origin + top ; left + = item . w + ( item . widget = = tempo_display_ ? 36 : 0 ) + 8 ;
} else { x = item . x * width / 1280 ; y = origin + item . y ; }
item . widget - > resize ( x , y , item . w , item . h ) ;
}
int height = compact ? top + 48 : 90 ;
transport_group_ - > resize ( 0 , origin , width , height ) ;
// Group resize can transform children: restore exact layout after it.
left = 12 ; top = 12 ;
for ( const auto & item : transport_items_ ) {
int x , y ; if ( compact ) { if ( left + item . w + ( item . widget = = tempo_display_ ? 36 : 0 ) > width - 12 ) { left = 12 ; top + = 40 ; } x = left ; y = origin + top ; left + = item . w + ( item . widget = = tempo_display_ ? 36 : 0 ) + 8 ; }
else { x = item . x * width / 1280 ; y = origin + item . y ; }
item . widget - > resize ( x , y , item . w , item . h ) ;
}
transport_group_ - > init_sizes ( ) ;
if ( transport_detached_ & & transport_window_ & & transport_window_ - > h ( ) < origin + height + 8 )
transport_window_ - > resize ( transport_window_ - > x ( ) , transport_window_ - > y ( ) , width , origin + height + 8 ) ;
layout_transport_busy_ = false ;
}
2026-10-30 12:39:47 +01:00
void MainWindow : : detach_transport ( bool detached )
{
if ( detached = = transport_detached_ | | ! transport_group_ ) return ;
if ( detached & & ! transport_window_ ) {
auto * current = Fl_Group : : current ( ) ; Fl_Group : : current ( nullptr ) ;
2026-11-05 23:57:56 +01:00
transport_window_ . reset ( new PanelWindow ( static_cast < int > ( settings_ . transport_width ) , static_cast < int > ( settings_ . transport_height ) , " xtc — Transport (close to dock) " , this ) ) ;
2026-10-30 12:39:47 +01:00
auto * dock = new Fl_Button ( 12 , 4 , 140 , 24 , " Dock transport " ) ;
dock - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > detach_transport ( false ) ; } , this ) ;
xtc_ui : : style ( transport_window_ . get ( ) ) ;
2026-11-05 23:57:56 +01:00
transport_window_ - > end ( ) ; transport_window_ - > size_range ( 400 , 130 ) ;
2026-10-30 12:39:47 +01:00
transport_window_ - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > detach_transport ( false ) ; } , this ) ;
2026-11-05 23:57:56 +01:00
restore_panel ( transport_window_ . get ( ) , static_cast < int > ( settings_ . transport_x ) , static_cast < int > ( settings_ . transport_y ) , static_cast < int > ( settings_ . transport_width ) , static_cast < int > ( settings_ . transport_height ) , 400 , 130 ) ;
2026-10-30 12:39:47 +01:00
Fl_Group : : current ( current ) ;
}
2026-11-05 23:57:56 +01:00
transport_detached_ = detached ; settings_ . transport_detached = detached ;
2026-10-30 12:39:47 +01:00
transport_group_ - > parent ( ) - > remove ( transport_group_ ) ;
if ( detached ) {
transport_window_ - > add ( transport_group_ ) ;
transport_group_ - > resize ( 0 , 32 , transport_window_ - > w ( ) , 90 ) ;
transport_window_ - > resizable ( transport_group_ ) ;
} else {
transport_window_ - > resizable ( nullptr ) ;
add ( transport_group_ ) ; transport_group_ - > resize ( 0 , 28 , w ( ) , 90 ) ;
transport_window_ - > hide ( ) ;
}
int delta = detached ? - 90 : 90 ;
tabs_ - > resize ( tabs_ - > x ( ) , tabs_ - > y ( ) + delta , tabs_ - > w ( ) , tabs_ - > h ( ) - delta ) ;
sidebar_ - > resize ( sidebar_ - > x ( ) , sidebar_ - > y ( ) + delta , sidebar_ - > w ( ) , sidebar_ - > h ( ) - delta ) ;
init_sizes ( ) ;
auto * item = const_cast < Fl_Menu_Item * > ( menu_ - > find_item ( " View/Detach transport " ) ) ;
if ( detached ) item - > set ( ) ; else item - > clear ( ) ;
2026-11-05 23:57:56 +01:00
layout_transport ( ) ;
2026-10-30 12:39:47 +01:00
if ( detached ) transport_window_ - > show ( ) ;
redraw ( ) ;
}
void MainWindow : : detach_mixer ( bool detached )
{
if ( detached = = mixer_detached_ | | ! mixer_ ) return ;
if ( detached & & ! mixer_window_ ) {
auto * current = Fl_Group : : current ( ) ; Fl_Group : : current ( nullptr ) ;
mixer_window_ . reset ( new PanelWindow ( 1056 , 600 , " xtc — Mixer (close to dock) " , this ) ) ;
auto * dock = new Fl_Button ( 12 , 6 , 140 , 28 , " Dock mixer " ) ;
dock - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > detach_mixer ( false ) ; } , this ) ;
xtc_ui : : style ( mixer_window_ . get ( ) ) ;
mixer_window_ - > end ( ) ; mixer_window_ - > size_range ( 600 , 420 ) ;
mixer_window_ - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > detach_mixer ( false ) ; } , this ) ;
2026-11-05 23:57:56 +01:00
restore_panel ( mixer_window_ . get ( ) , static_cast < int > ( settings_ . mixer_x ) , static_cast < int > ( settings_ . mixer_y ) , static_cast < int > ( settings_ . mixer_width ) , static_cast < int > ( settings_ . mixer_height ) , 600 , 420 ) ;
2026-10-30 12:39:47 +01:00
Fl_Group : : current ( current ) ;
}
2026-11-05 23:57:56 +01:00
mixer_detached_ = detached ; settings_ . mixer_detached = detached ;
2026-10-30 12:39:47 +01:00
mixer_ - > parent ( ) - > remove ( mixer_ ) ;
if ( detached ) {
mixer_page_ - > resizable ( nullptr ) ;
mixer_window_ - > add ( mixer_ ) ; mixer_ - > resize ( 12 , 44 , mixer_window_ - > w ( ) - 24 , mixer_window_ - > h ( ) - 56 ) ;
mixer_window_ - > resizable ( mixer_ ) ; mixer_placeholder_ - > show ( ) ;
} else {
mixer_window_ - > resizable ( nullptr ) ;
mixer_page_ - > add ( mixer_ ) ;
mixer_ - > resize ( mixer_page_ - > x ( ) + 12 , mixer_page_ - > y ( ) + 10 , mixer_page_ - > w ( ) - 24 , mixer_page_ - > h ( ) - 24 ) ;
mixer_page_ - > resizable ( mixer_ ) ; mixer_placeholder_ - > hide ( ) ; mixer_window_ - > hide ( ) ;
}
mixer_ - > show ( ) ; mixer_page_ - > init_sizes ( ) ;
auto * item = const_cast < Fl_Menu_Item * > ( menu_ - > find_item ( " View/Detach mixer " ) ) ;
if ( detached ) item - > set ( ) ; else item - > clear ( ) ;
if ( detached ) mixer_window_ - > show ( ) ;
redraw ( ) ;
}
2026-10-27 18:04:39 +01:00
void MainWindow : : draw ( )
{
// FLTK may reload the platform scheme when a window first becomes visible.
// Keep built-in arrows, popups and choice fields flat as well as our boxes.
xtc_ui : : configure_boxes ( ) ;
Fl_Double_Window : : draw ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Removes the status timer and releases backend and recording state .
*
* A backend that fails to close is intentionally retained with its recorder
* until process exit so active callbacks never observe freed state .
*/
2026-10-26 23:40:28 +01:00
MainWindow : : ~ MainWindow ( )
2026-10-26 22:51:25 +01:00
{
2026-10-26 23:40:28 +01:00
Fl : : remove_timeout ( timer_callback , this ) ;
2026-10-30 12:39:47 +01:00
// Return ownership before member windows and the main widget tree destruct.
detach_transport ( false ) ;
detach_mixer ( false ) ;
2026-10-26 23:40:28 +01:00
// 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 )
2026-10-27 01:37:21 +01:00
std : : fputs ( " Failed to close JACK during GUI destruction; backend and recording retained until process exit. \n " ,
stderr ) ;
2026-10-27 00:31:03 +01:00
else if ( recorder_ ) {
2026-10-27 03:46:09 +01:00
xtc_result result = xtc_recorder_finish ( recorder_ ) ;
std : : fprintf ( stderr , " Recording finalized during shutdown (%s): %s \n " , xtc_result_string ( result ) ,
xtc_recorder_recovery_path ( recorder_ ) ) ;
2026-10-27 00:31:03 +01:00
xtc_recorder_destroy ( recorder_ ) ;
}
2026-10-26 23:40:28 +01:00
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Shows an operation failure and a context - sensitive hint .
*
* @ param operation Human - readable operation name ; names containing
* " Project " select a file and permission hint .
* @ param result Failing result code whose name is included in the message .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : report_error ( const char * operation , xtc_result result )
{
char text [ 384 ] ;
2026-10-27 01:37:21 +01:00
const char * hint = std : : strstr ( operation , " Project " ) ! = nullptr
? " Check the project path, file format and write permissions. "
2026-10-27 16:49:30 +01:00
: result = = XTC_ERR_OUT_OF_MEMORY ? " Insufficient memory; the command was not accepted. "
: result = = XTC_ERR_CAPACITY ? " A project capacity limit was reached. "
: result = = XTC_ERR_NOT_FOUND | | result = = XTC_ERR_INVALID_ARGUMENT ? " Check the selected target and entered values. "
: result = = XTC_ERR_UNSUPPORTED ? " Check optional build support and the requested format or plugin layout. "
: " Check the operation details; for audio failures use Reconnect to retry. " ;
2026-10-26 23:40:28 +01:00
std : : snprintf ( text , sizeof ( text ) , " %s: %s. %s " , operation , xtc_result_string ( result ) , hint ) ;
set_label ( error_ , text ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Edit handler passed to the modular graph widget .
*
* Applies the edit to a candidate graph , clones the project around it , and
* submits the new graph to the engine when connected . The live project is
* replaced only after every step succeeds .
*
* @ param context MainWindow * passed as the graph widget owner context .
* @ param edit Graph edit to apply .
* @ param out_added_id Optional output receiving the new node ID when the edit
* adds a node ; untouched otherwise or when nullptr .
* @ return XTC_OK on success , XTC_ERR_INVALID_STATE while recording , or an
* error from the graph , project , or backend operation .
*/
2026-10-26 23:55:35 +01:00
xtc_result MainWindow : : edit_graph ( void * context , const xtc_graph_edit & edit , size_t * out_added_id )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 01:37:21 +01:00
if ( window - > recorder_ )
return XTC_ERR_INVALID_STATE ;
2026-10-26 23:55:35 +01:00
xtc_graph * candidate = nullptr ;
size_t added_id = SIZE_MAX ;
2026-10-27 00:10:45 +01:00
xtc_result result = xtc_graph_apply_edit ( xtc_project_graph ( window - > project_ . get ( ) ) , & edit , & candidate , & added_id ) ;
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK )
return result ;
2026-10-27 00:10:45 +01:00
xtc_project * replacement = nullptr ;
result = xtc_project_clone ( window - > project_ . get ( ) , & replacement ) ;
2026-10-27 01:37:21 +01:00
if ( result = = XTC_OK )
result = xtc_project_set_graph ( replacement , candidate ) ;
2026-10-27 00:10:45 +01:00
xtc_graph_destroy ( candidate ) ;
2026-10-27 16:49:30 +01:00
if ( result = = XTC_OK ) result = xtc_history_reserve ( window - > history_ . get ( ) ) ;
2026-10-27 00:10:45 +01:00
if ( result = = XTC_OK & & window - > ready_ )
result = xtc_backend_submit ( window - > backend_ , xtc_project_graph ( replacement ) ) ;
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK ) {
xtc_project_destroy ( replacement ) ;
return result ;
}
2026-10-27 00:10:45 +01:00
window - > project_ . reset ( replacement ) ;
window - > graph_widget_ - > set_graph ( xtc_project_graph ( replacement ) ) ;
window - > sequencer_ - > set_project ( replacement , false ) ;
window - > mark_dirty ( ) ;
2026-10-27 16:00:29 +01:00
if ( out_added_id ! = nullptr & & ( edit . kind = = XTC_EDIT_ADD_NODE | | edit . kind = = XTC_EDIT_INSERT_GAIN ) )
2026-10-27 01:37:21 +01:00
* out_added_id = added_id ;
2026-10-26 23:55:35 +01:00
window - > refresh_status ( ) ;
return XTC_OK ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Opens the JACK backend and submits the current graph .
*
* Uses the default backend configuration and leaves the engine stopped .
* Failure is reported and leaves the backend disconnected ; a handle that
* opened but failed to accept the graph is retained for reconnect .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : open_backend ( )
{
2026-10-28 10:37:01 +01:00
xtc_backend_config config = { XTC_BACKEND_JACK , settings_ . client_name , static_cast < size_t > ( settings_ . max_frames ) , static_cast < size_t > ( settings_ . channels ) } ;
2026-10-26 23:40:28 +01:00
xtc_result result = xtc_backend_open ( & config , & backend_ ) ;
ready_ = result = = XTC_OK ;
running_ = false ;
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK )
report_error ( " Open JACK failed " , result ) ;
2026-10-26 23:55:35 +01:00
else {
2026-10-27 03:37:19 +01:00
xtc_backend_status format { } ;
result = xtc_backend_get_status ( backend_ , & format ) ;
if ( result = = XTC_OK )
2026-10-28 10:37:01 +01:00
result = xtc_backend_set_transition_frames ( backend_ , static_cast < size_t > ( format . sample_rate ) * static_cast < size_t > ( settings_ . transition_ms ) / 1000U ) ;
2026-10-27 03:37:19 +01:00
if ( result = = XTC_OK )
result = xtc_backend_submit ( backend_ , xtc_project_graph ( project_ . get ( ) ) ) ;
2026-10-26 23:55:35 +01:00
if ( result ! = XTC_OK ) {
ready_ = false ; // Handle is retained for close/reconnect; graph was not installed.
report_error ( " Submit graph failed " , result ) ;
2026-10-27 01:37:21 +01:00
} else
set_label ( error_ , " " ) ;
2026-10-26 23:55:35 +01:00
}
2026-10-26 23:40:28 +01:00
refresh_status ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Starts the audio engine with the current project .
*
* Does nothing when already running . Reports an error when disconnected .
* Otherwise prepares the project and starts the engine . Missing or
* unreadable audio is reported as a warning after a successful start .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : start_engine ( )
{
if ( ! ready_ ) {
report_error ( " Audio is disconnected; use Reconnect " , XTC_ERR_INVALID_STATE ) ;
return ;
}
2026-10-27 01:37:21 +01:00
if ( running_ )
return ;
2026-10-28 10:37:01 +01:00
xtc_result result = configure_loop ( project_ . get ( ) , settings_ ) ;
if ( result = = XTC_OK ) result = xtc_backend_prepare_project ( backend_ , project_ . get ( ) ) ;
2026-10-27 01:37:21 +01:00
if ( result = = XTC_OK )
result = xtc_backend_start ( backend_ ) ;
if ( result ! = XTC_OK )
report_error ( " Start engine failed " , result ) ;
2026-10-27 00:31:03 +01:00
else {
set_label ( error_ , " " ) ;
const char * issue = xtc_backend_asset_issue ( backend_ ) ;
if ( issue [ 0 ] ) {
2026-10-27 01:37:21 +01:00
char message [ 640 ] ;
std : : snprintf ( message , sizeof ( message ) ,
2026-11-05 23:50:14 +01:00
" Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair. " , issue ) ;
2026-10-27 00:31:03 +01:00
set_label ( error_ , message ) ;
}
}
2026-10-26 23:40:28 +01:00
refresh_status ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
2026-10-27 03:37:19 +01:00
* @ brief Requests a graceful playback stop , or immediately finalizes recording .
2026-10-27 01:58:27 +01:00
*
* Does nothing when disconnected . Failures are reported in the window .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : stop_engine ( )
{
2026-10-27 01:37:21 +01:00
if ( ! ready_ )
return ;
2026-10-27 03:37:19 +01:00
xtc_result result = recorder_ ? xtc_backend_stop ( backend_ ) : xtc_backend_request_stop ( backend_ ) ;
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK )
report_error ( " Stop engine failed " , result ) ;
2026-10-27 00:31:03 +01:00
else {
set_label ( error_ , " " ) ;
2026-10-27 01:37:21 +01:00
if ( recorder_ )
finish_recording ( ) ;
2026-10-27 00:31:03 +01:00
}
2026-10-26 23:40:28 +01:00
refresh_status ( ) ;
}
2026-10-27 03:25:04 +01:00
xtc_result MainWindow : : pause_playback ( )
{
2026-10-27 03:37:19 +01:00
xtc_result result = recorder_ | | ! ready_ ? XTC_ERR_INVALID_STATE : xtc_backend_request_stop ( backend_ ) ;
2026-10-27 03:25:04 +01:00
if ( result ! = XTC_OK )
report_error ( " Pause failed; use Stop to finish recording or Reconnect audio " , result ) ;
else
set_label ( error_ , " " ) ;
refresh_status ( ) ;
return result ;
}
xtc_result MainWindow : : return_to_start ( )
{
if ( recorder_ ) {
report_error ( " Stop recording before returning to start " , XTC_ERR_INVALID_STATE ) ;
return XTC_ERR_INVALID_STATE ;
}
xtc_result result = XTC_OK ;
if ( ready_ ) {
result = xtc_backend_stop ( backend_ ) ;
if ( result = = XTC_OK )
result = xtc_backend_seek ( backend_ , 0 ) ;
}
if ( result ! = XTC_OK )
report_error ( " Return to start failed " , result ) ;
else {
sequencer_ - > position ( 0 ) ;
set_label ( error_ , " " ) ;
}
refresh_status ( ) ;
return result ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Closes and reopens the JACK backend , leaving the engine stopped .
*
* Refuses to run while recording . If closing fails the existing handle is
* retained for a later retry and the failure is reported .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : reconnect_engine ( )
{
2026-10-27 01:37:21 +01:00
if ( recorder_ ) {
report_error ( " Stop recording before reconnecting " , XTC_ERR_INVALID_STATE ) ;
return ;
}
2026-10-26 23:40:28 +01:00
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 ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Polls the backend and updates labels , meters , and button states .
*
* When disconnected , shows offline text and disables transport controls .
* Otherwise updates the sequencer position , mixer peaks , status line , and
* error indicators from the latest engine status , then collects backend
* work .
*/
2026-10-27 16:49:30 +01:00
void MainWindow : : update_history_controls ( )
{
size_t undo_count = 0 , redo_count = 0 ;
if ( ! menu_ | | xtc_history_counts ( history_ . get ( ) , & undo_count , & redo_count ) ! = XTC_OK ) return ;
for ( const char * path : { " Edit/Undo " , " Edit/Redo " , " Edit/Redo alternate " } ) {
int index = menu_ - > find_index ( path ) ;
if ( index > = 0 ) menu_ - > mode ( index , ( ! recorder_ & & ( std : : strcmp ( path , " Edit/Undo " ) = = 0 ? undo_count : redo_count ) ) ? 0 : FL_MENU_INACTIVE ) ;
}
}
2026-10-26 23:40:28 +01:00
void MainWindow : : refresh_status ( )
{
2026-10-27 18:04:39 +01:00
refresh_workspace ( ) ;
2026-10-27 16:49:30 +01:00
update_history_controls ( ) ;
2026-10-28 11:23:25 +01:00
if ( mixer_ & & ! ready_ ) mixer_ - > meters ( xtc_meter_snapshot { } ) ;
2026-10-27 00:31:03 +01:00
bool can_play = false , can_record = false ;
2026-10-26 23:40:28 +01:00
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 ) ) ;
2026-10-26 23:55:35 +01:00
set_label ( graph_state_ , " Graph: offline — edits stored locally; Reconnect to play " ) ;
2026-10-26 23:40:28 +01:00
} else {
2026-10-27 03:37:19 +01:00
xtc_backend_collect ( backend_ ) ; // Completes an acknowledged graceful stop before reading status.
2026-10-26 23:40:28 +01:00
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 " ) ;
2026-10-26 23:55:35 +01:00
set_label ( graph_state_ , " Graph: engine status unavailable " ) ;
2026-10-26 23:40:28 +01:00
set_label ( status_ , " Unable to read JACK status — use Reconnect " ) ;
indicator_ - > labelcolor ( fl_rgb_color ( 255 , 155 , 120 ) ) ;
} else {
2026-10-27 00:10:45 +01:00
uint64_t tick = 0 ;
if ( xtc_project_sample_to_tick ( project_ . get ( ) , status . sample_position , status . sample_rate , & tick ) = = XTC_OK )
sequencer_ - > position ( tick ) ;
2026-10-26 23:40:28 +01:00
running_ = status . running ! = 0 ;
const bool fault = status . server_lost | | status . sample_rate_changed | | status . process_result ! = XTC_OK ;
can_play = ! running_ & & ! fault ;
2026-10-27 00:31:03 +01:00
can_record = ! fault & & recorder_ = = nullptr ;
2026-10-27 01:37:21 +01:00
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 "
2026-10-27 03:37:19 +01:00
: status . stopping ? " Audio: stopping — fading to silence "
2026-10-27 01:37:21 +01:00
: recorder_ ? " Audio: recording JACK input — Stop to finish "
: running_ ? " Audio: running — modular graph "
: " Audio: ready / stopped " ;
2026-10-26 23:40:28 +01:00
set_label ( indicator_ , state ) ;
2026-10-27 01:37:21 +01:00
indicator_ - > labelcolor ( fault ? fl_rgb_color ( 255 , 155 , 120 )
: running_ ? fl_rgb_color ( 125 , 225 , 160 )
: fl_rgb_color ( 215 , 215 , 215 ) ) ;
2026-10-27 16:15:34 +01:00
xtc_meter_snapshot meters { } ;
2026-10-28 11:23:25 +01:00
if ( xtc_backend_read_meters ( backend_ , & meters ) = = XTC_OK ) mixer_ - > meters ( meters ) ;
else { meters . master_left = status . peak_left ; meters . master_right = status . peak_right ; mixer_ - > meters ( meters ) ; }
2026-10-26 23:40:28 +01:00
char text [ 256 ] ;
2026-10-27 01:37:21 +01:00
std : : snprintf (
text , sizeof ( text ) ,
" JACK | %u Hz | %u frames | stereo | %u blocks | %u xruns | %s | %u asset issues | %u MIDI drops " ,
2026-10-26 23:40:28 +01:00
status . sample_rate , status . buffer_frames , status . processed_blocks , status . xruns ,
2026-10-27 00:53:41 +01:00
xtc_result_string ( status . process_result ) , status . asset_issues , status . midi_dropped ) ;
2026-10-26 23:40:28 +01:00
set_label ( status_ , text ) ;
2026-10-27 01:37:21 +01:00
if ( fault )
set_label ( graph_state_ , " Graph: engine unavailable — Reconnect required " ) ;
2026-10-26 23:55:35 +01:00
else if ( status . submitted_revision ! = status . active_revision )
2026-10-27 01:37:21 +01:00
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 " ) ;
2026-10-26 23:40:28 +01:00
}
}
2026-10-27 01:37:21 +01:00
if ( can_play )
play_ - > activate ( ) ;
else
play_ - > deactivate ( ) ;
2026-10-27 03:25:04 +01:00
if ( ready_ & & running_ & & ! recorder_ )
pause_ - > activate ( ) ;
else
pause_ - > deactivate ( ) ;
if ( ! recorder_ )
home_ - > activate ( ) ;
else
home_ - > deactivate ( ) ;
2026-10-27 01:37:21 +01:00
if ( can_record )
record_ - > activate ( ) ;
else
record_ - > deactivate ( ) ;
if ( ready_ & & ( running_ | | recorder_ ) )
stop_ - > activate ( ) ;
else
stop_ - > deactivate ( ) ;
2026-10-26 23:40:28 +01:00
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Attempts to close the window after discarding unsaved changes .
*
* Refuses while recording . On success closes the backend , removes the status
* timer , hides the piano - roll window , and hides this window .
*
* @ return True when the window closed ; false when recording is active , the
* user cancels discarding , or the backend cannot close .
*/
2026-10-26 23:40:28 +01:00
bool MainWindow : : close_window ( )
{
2026-10-27 01:37:21 +01:00
if ( recorder_ ) {
report_error ( " Stop recording before closing " , XTC_ERR_INVALID_STATE ) ;
return false ;
}
if ( ! discard_changes ( ) )
return false ;
2026-10-26 23:40:28 +01:00
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 ) ;
2026-10-27 01:37:21 +01:00
if ( piano_window_ )
piano_window_ - > hide ( ) ;
2026-10-30 12:39:47 +01:00
if ( transport_window_ ) transport_window_ - > hide ( ) ;
if ( mixer_window_ ) mixer_window_ - > hide ( ) ;
2026-10-26 23:40:28 +01:00
hide ( ) ;
return true ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Handles FLTK events for the window .
*
* Space without Ctrl / Alt / Meta toggles the audio engine and is consumed .
*
* @ param event FLTK event code , such as FL_SHORTCUT .
* @ return 1 when Space was consumed ; otherwise the Fl_Double_Window result .
*/
2026-10-30 12:39:47 +01:00
int MainWindow : : panel_shortcut ( int event )
2026-10-26 23:40:28 +01:00
{
2026-10-27 16:49:30 +01:00
if ( event = = FL_SHORTCUT & & dynamic_cast < Fl_Input_ * > ( Fl : : focus ( ) ) & &
( Fl : : event_key ( ) = = ' ' | | ( Fl : : event_state ( FL_CTRL ) & &
( Fl : : event_key ( ) = = ' z ' | | Fl : : event_key ( ) = = ' y ' | | Fl : : event_key ( ) = = ' d ' ) ) ) ) return 0 ;
2026-10-27 01:37:21 +01:00
if ( event = = FL_SHORTCUT & & Fl : : event_key ( ) = = ' ' & & ( Fl : : event_state ( ) & ( FL_CTRL | FL_ALT | FL_META ) ) = = 0 ) {
if ( running_ )
stop_engine ( ) ;
else
start_engine ( ) ;
2026-10-26 23:40:28 +01:00
return 1 ;
}
2026-10-30 12:39:47 +01:00
if ( event = = FL_SHORTCUT ) return menu_ - > test_shortcut ( ) ! = nullptr ;
return 0 ;
}
int MainWindow : : handle ( int event )
{
if ( panel_shortcut ( event ) ) return 1 ;
2026-10-26 23:40:28 +01:00
return Fl_Double_Window : : handle ( event ) ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK callback for the Play menu item and button .
*
* @ param context MainWindow * whose engine is started .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : play_callback ( Fl_Widget * , void * context )
2026-10-27 01:37:21 +01:00
{
static_cast < MainWindow * > ( context ) - > start_engine ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK callback for the Stop menu item and button .
*
* @ param context MainWindow * whose engine is stopped .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : stop_callback ( Fl_Widget * , void * context )
2026-10-27 01:37:21 +01:00
{
static_cast < MainWindow * > ( context ) - > stop_engine ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK callback for the Reconnect menu item and button .
*
* @ param context MainWindow * whose backend is reopened .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : reconnect_callback ( Fl_Widget * , void * context )
2026-10-27 01:37:21 +01:00
{
static_cast < MainWindow * > ( context ) - > reconnect_engine ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK callback for the window close box , File / Quit , and close events .
*
* @ param context MainWindow * whose close is requested ; a refused close
* simply leaves the window open .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : close_callback ( Fl_Widget * , void * context )
2026-10-27 01:37:21 +01:00
{
( void ) static_cast < MainWindow * > ( context ) - > close_window ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK menu callback that selects a tab page in the main window .
*
* @ param widget Triggering menu item ; window ( ) must return the MainWindow .
* @ param view Fl_Widget * tab page to make current , not the MainWindow .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : view_callback ( Fl_Widget * widget , void * view )
{
auto * window = static_cast < MainWindow * > ( widget - > window ( ) ) ;
window - > tabs_ - > value ( static_cast < Fl_Widget * > ( view ) ) ;
window - > tabs_ - > redraw ( ) ;
2026-10-30 12:39:47 +01:00
if ( view = = window - > mixer_page_ & & window - > mixer_detached_ ) window - > mixer_window_ - > show ( ) ;
2026-10-26 23:40:28 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK help callback that shows the application name and version .
*
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : about_callback ( Fl_Widget * , void * )
{
2026-10-27 03:37:19 +01:00
fl_message ( " %s %s \n Modular audio workstation \n Phase 12: timing, transitions and audio quality. " ,
2026-10-27 01:37:21 +01:00
XTC_PROJECT_NAME , XTC_VERSION_STRING ) ;
2026-10-26 23:40:28 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK timeout callback that refreshes status and supervises recording .
*
* While recording , drains the recorder and stops the engine if draining
* fails or the backend reports a lost server , sample - rate change , or
* processing error . Reschedules itself every 0.1 seconds until closing .
*
* @ param context MainWindow * registered with Fl : : add_timeout ( ) .
*/
2026-10-26 23:40:28 +01:00
void MainWindow : : timer_callback ( void * context )
{
2026-10-27 17:34:46 +01:00
xtc_plugin_idle ( ) ;
2026-10-26 23:40:28 +01:00
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 00:31:03 +01:00
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 ( ) ;
}
2026-11-05 23:55:56 +01:00
if ( window - > settings_ . stop_after_tail & & ! window - > settings_ . loop_enabled & & ! window - > recorder_ ) {
xtc_backend_status status { } ; uint64_t end = 0 ;
if ( xtc_backend_get_status ( window - > backend_ , & status ) = = XTC_OK & & status . running & &
xtc_project_render_length ( window - > project_ . get ( ) , status . sample_rate , static_cast < unsigned int > ( window - > settings_ . tail_ms ) , & end ) = = XTC_OK & & status . sample_position > = end )
window - > stop_engine ( ) ;
}
2026-10-26 23:40:28 +01:00
window - > refresh_status ( ) ;
2026-10-28 17:16:01 +01:00
window - > follow_playhead ( ) ;
2026-10-27 01:37:21 +01:00
if ( ! window - > closing_ )
2026-10-28 10:37:01 +01:00
Fl : : repeat_timeout ( static_cast < double > ( window - > settings_ . refresh_ms ) / 1000 , timer_callback , context ) ;
2026-10-26 22:51:25 +01:00
}
2026-10-27 00:10:45 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Records the current project in history and marks the window dirty .
*
* Dependent widgets are refreshed with the current project and graph , and
* the title gains the unsaved marker . A history failure is reported .
*/
2026-10-27 00:10:45 +01:00
void MainWindow : : mark_dirty ( )
2026-10-27 00:53:41 +01:00
{
2026-10-27 01:06:57 +01:00
xtc_result history_result = xtc_history_record ( history_ . get ( ) , project_ . get ( ) ) ;
2026-10-27 01:37:21 +01:00
if ( history_result ! = XTC_OK )
report_error ( " Undo history could not retain edit " , history_result ) ;
2026-10-27 01:06:57 +01:00
mixer_ - > set_project ( project_ . get ( ) ) ;
2026-10-27 00:53:41 +01:00
graph_widget_ - > set_graph ( xtc_project_graph ( project_ . get ( ) ) ) ;
2026-10-27 01:37:21 +01:00
if ( piano_ )
piano_ - > set_project ( project_ . get ( ) , piano_ - > clip ( ) ) ;
dirty_ = true ;
2026-10-27 16:49:30 +01:00
update_history_controls ( ) ;
2026-10-27 01:37:21 +01:00
copy_label ( " xtc * — unsaved changes " ) ;
2026-10-27 00:53:41 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Asks whether unsaved changes may be discarded .
*
* @ return True when the project is clean , prompting is disabled , or the user
* chose Discard ; false when the user cancels .
*/
2026-10-27 00:10:45 +01:00
bool MainWindow : : discard_changes ( )
{
return ! dirty_ | | ! prompt_unsaved_ | |
2026-10-27 01:37:21 +01:00
fl_choice ( " Discard unsaved project changes? " , " Cancel " , " Discard " , nullptr ) = = 1 ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Hook used by the sequencer to seek the audio engine .
*
* Does nothing when the backend is disconnected . Otherwise reads the engine
* sample rate , converts the tick position to samples , and seeks the backend .
*
* @ param context MainWindow * passed as the sequencer owner context .
* @ param tick Target timeline position in project ticks .
* @ return XTC_OK when disconnected or the seek succeeded , or the status ,
* conversion , or backend seek error .
*/
2026-10-27 00:10:45 +01:00
xtc_result MainWindow : : seek ( void * context , uint64_t tick )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 01:37:21 +01:00
if ( ! window - > ready_ )
return XTC_OK ;
2026-10-27 00:10:45 +01:00
xtc_backend_status status { } ;
xtc_result r = xtc_backend_get_status ( window - > backend_ , & status ) ;
uint64_t sample = 0 ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
r = xtc_project_tick_to_sample ( window - > project_ . get ( ) , tick , status . sample_rate , & sample ) ;
2026-10-27 00:10:45 +01:00
return r = = XTC_OK ? xtc_backend_seek ( window - > backend_ , sample ) : r ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Saves the current project to a path and updates the window title .
*
* On success the path is remembered and the unsaved marker is cleared .
*
* @ param path Destination project path ; nullptr is invalid .
* @ return XTC_OK on success , XTC_ERR_INVALID_ARGUMENT for nullptr ,
* XTC_ERR_INVALID_STATE while recording , or the save error .
*/
2026-10-27 00:10:45 +01:00
xtc_result MainWindow : : save_project ( const char * path )
{
2026-10-27 01:37:21 +01:00
if ( recorder_ )
return XTC_ERR_INVALID_STATE ;
if ( path = = nullptr )
return XTC_ERR_INVALID_ARGUMENT ;
2026-10-27 00:10:45 +01:00
std : : string saved_path ( path ) ;
xtc_result r = xtc_project_save ( project_ . get ( ) , path ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK ) {
path_ = saved_path ;
dirty_ = false ;
copy_label ( path_ . c_str ( ) ) ;
}
2026-10-27 00:10:45 +01:00
return r ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Installs a candidate project as the active project .
*
* Rejected while recording . Optionally builds fresh undo history , stops and
* prepares the connected backend , or validates offline audio and MIDI plans
* when disconnected . On success the window takes ownership of candidate ,
* rebinds widgets , resets the graph layout , clears the dirty flag , and shows
* any missing - asset warning . On failure ownership stays with the caller .
*
* @ param candidate Project to install ; must be non - null .
* @ param reset_history True to discard existing undo / redo history and start
* from candidate .
* @ return XTC_OK on success , XTC_ERR_INVALID_STATE while recording , or an
* error from history creation , the backend , or offline planning .
*/
2026-10-27 01:06:57 +01:00
xtc_result MainWindow : : install_project ( xtc_project * candidate , bool reset_history )
2026-10-27 00:10:45 +01:00
{
2026-10-27 01:37:21 +01:00
if ( recorder_ )
return XTC_ERR_INVALID_STATE ;
2026-10-27 01:06:57 +01:00
std : : unique_ptr < xtc_history , decltype ( & xtc_history_destroy ) > new_history ( nullptr , xtc_history_destroy ) ;
if ( reset_history ) {
xtc_history * h = nullptr ;
xtc_result result = xtc_history_create ( candidate , & h ) ;
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK )
return result ;
2026-10-27 01:06:57 +01:00
new_history . reset ( h ) ;
}
2026-10-27 00:10:45 +01:00
xtc_result r = XTC_OK ;
if ( ready_ ) {
r = xtc_backend_stop ( backend_ ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
r = xtc_backend_seek ( backend_ , 0 ) ;
2026-10-28 10:37:01 +01:00
if ( r = = XTC_OK ) r = configure_loop ( candidate , settings_ ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
r = xtc_backend_prepare_project ( backend_ , candidate ) ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 00:31:03 +01:00
xtc_audio_plan * offline = nullptr ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK & & ! ready_ )
2026-10-28 10:37:01 +01:00
r = xtc_audio_plan_create ( candidate , static_cast < unsigned int > ( settings_ . offline_rate ) , & offline ) ;
2026-10-27 00:53:41 +01:00
xtc_midi_plan * offline_midi = nullptr ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK & & ! ready_ )
2026-10-28 10:37:01 +01:00
r = xtc_midi_plan_create ( candidate , static_cast < unsigned int > ( settings_ . offline_rate ) , & offline_midi ) ;
2026-11-05 23:50:14 +01:00
size_t sample_issues = xtc_midi_plan_asset_issues ( offline_midi ) ;
2026-10-27 00:53:41 +01:00
xtc_midi_plan_destroy ( offline_midi ) ;
2026-10-27 04:00:27 +01:00
if ( r = = XTC_OK & & ! ready_ ) {
xtc_plan * graph = nullptr ;
2026-10-28 10:37:01 +01:00
r = xtc_graph_compile ( xtc_project_graph ( candidate ) , static_cast < double > ( settings_ . offline_rate ) , 256 , & graph ) ;
2026-10-27 04:00:27 +01:00
xtc_plan_destroy ( graph ) ;
}
2026-10-27 01:37:21 +01:00
if ( r ! = XTC_OK ) {
xtc_audio_plan_destroy ( offline ) ;
2026-10-27 03:25:04 +01:00
refresh_status ( ) ; // A failed install may have stopped playback.
2026-10-27 01:37:21 +01:00
return r ;
}
2026-10-27 00:31:03 +01:00
std : : string issue = ready_ ? xtc_backend_asset_issue ( backend_ ) : " " ;
if ( offline ) {
const char * path = " " ;
2026-10-27 01:37:21 +01:00
xtc_audio_plan_issues ( offline , & path ) ;
issue = path ;
2026-10-27 00:31:03 +01:00
xtc_audio_plan_destroy ( offline ) ;
}
2026-11-05 23:50:14 +01:00
if ( sample_issues ) issue = " Sampler WAV missing/invalid; File/Load sampler WAV relinks the selected track " ;
2026-10-27 01:37:21 +01:00
if ( piano_window_ )
piano_window_ - > hide ( ) ;
if ( piano_ )
piano_ - > set_project ( candidate , UINT64_MAX ) ;
2026-10-27 00:10:45 +01:00
project_ . reset ( candidate ) ;
2026-10-27 01:06:57 +01:00
mixer_ - > set_project ( candidate ) ;
2026-10-27 01:37:21 +01:00
if ( reset_history )
history_ . swap ( new_history ) ;
2026-10-27 00:10:45 +01:00
graph_widget_ - > reset_layout ( ) ;
graph_widget_ - > set_graph ( xtc_project_graph ( candidate ) ) ;
sequencer_ - > set_project ( candidate ) ;
2026-10-27 03:25:04 +01:00
if ( reset_history )
tabs_ - > value ( tabs_ - > child ( 0 ) ) ;
2026-10-27 00:10:45 +01:00
dirty_ = false ;
2026-10-27 00:31:03 +01:00
if ( ! issue . empty ( ) ) {
2026-10-27 01:37:21 +01:00
char text [ 640 ] ;
2026-11-05 23:50:14 +01:00
std : : snprintf ( text , sizeof ( text ) , " Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair. " ,
2026-10-27 01:37:21 +01:00
issue . c_str ( ) ) ;
2026-10-27 00:31:03 +01:00
set_label ( error_ , text ) ;
2026-10-27 01:37:21 +01:00
} else
set_label ( error_ , " " ) ;
2026-10-27 00:10:45 +01:00
refresh_status ( ) ;
return XTC_OK ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Loads a project from a path and makes it the current project .
*
* The file is loaded into a candidate project and installed only on success .
*
* @ param path Source project path ; nullptr is invalid .
* @ return XTC_OK on success , XTC_ERR_INVALID_ARGUMENT for nullptr , or an
* error from loading or installing the project .
*/
2026-10-27 00:10:45 +01:00
xtc_result MainWindow : : open_project ( const char * path )
{
2026-10-27 16:49:30 +01:00
BusyOperation busy ( * this , status_ , " Loading and preparing project… " ) ;
2026-10-27 01:37:21 +01:00
if ( path = = nullptr )
return XTC_ERR_INVALID_ARGUMENT ;
2026-10-27 04:00:27 +01:00
project_issue_ . clear ( ) ;
2026-10-27 00:10:45 +01:00
std : : string loaded_path ( path ) ;
xtc_project * candidate = nullptr ;
xtc_result r = xtc_project_create ( & candidate ) ;
2026-10-27 16:58:26 +01:00
xtc_project_diagnostic diagnostic { } ;
if ( r = = XTC_OK ) {
r = xtc_project_load_diagnostic ( candidate , path , & diagnostic ) ;
if ( r ! = XTC_OK ) {
project_issue_ = " Project load line " + std : : to_string ( diagnostic . line ) + " ( " + diagnostic . record + " ): " + diagnostic . message ;
report_error ( project_issue_ . c_str ( ) , r ) ;
}
}
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
r = install_project ( candidate ) ;
2026-10-27 04:00:27 +01:00
if ( r ! = XTC_OK ) {
if ( candidate ) {
const xtc_graph * graph = xtc_project_graph ( candidate ) ;
size_t nodes = 0 , edges = 0 ;
xtc_graph_get_counts ( graph , & nodes , & edges ) ;
for ( size_t i = 0 ; i < nodes ; + + i ) {
xtc_node_info node { } ; xtc_plugin_spec spec { } ;
if ( xtc_graph_get_node ( graph , i , & node ) ! = XTC_OK | | node . type ! = XTC_NODE_PLUGIN | |
xtc_graph_get_plugin ( graph , node . id , & spec ) ! = XTC_OK ) continue ;
2026-10-28 10:37:01 +01:00
double rate = sample_rate ( ) ; xtc_backend_status status { } ;
2026-10-27 04:00:27 +01:00
if ( ready_ & & xtc_backend_get_status ( backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
xtc_plugin * check = nullptr ;
xtc_result plugin_result = xtc_plugin_create ( & spec , rate , 256 , & check ) ;
xtc_plugin_destroy ( check ) ;
if ( plugin_result ! = XTC_OK ) {
project_issue_ = std : : string ( " Missing/incompatible plugin " ) + spec . label + " at " + spec . path +
" ; restore a compatible module and retry Open. Saved document retained " ;
report_error ( project_issue_ . c_str ( ) , plugin_result ) ;
break ;
}
}
}
2026-10-27 01:37:21 +01:00
xtc_project_destroy ( candidate ) ;
2026-10-27 04:00:27 +01:00
} else {
2026-10-27 01:37:21 +01:00
path_ = loaded_path ;
copy_label ( path_ . c_str ( ) ) ;
}
2026-10-27 00:10:45 +01:00
return r ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 03:25:04 +01:00
xtc_result MainWindow : : new_project ( )
{
if ( recorder_ )
return XTC_ERR_INVALID_STATE ;
xtc_project * candidate = nullptr ;
xtc_result result = xtc_project_create ( & candidate ) ;
2026-10-28 10:37:01 +01:00
if ( result = = XTC_OK ) project_defaults ( candidate ) ;
2026-10-27 03:25:04 +01:00
if ( result = = XTC_OK )
result = install_project ( candidate ) ;
if ( result ! = XTC_OK )
xtc_project_destroy ( candidate ) ;
else {
path_ . clear ( ) ;
copy_label ( " xtc — Untitled " ) ;
}
return result ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK menu callback for the File / New , Open , Save , and Save As items .
*
* Reads the action from the selected menu label , confirms discarding unsaved
* changes for New and Open , and uses a native file chooser unless Save can
* reuse the remembered path . Refuses file operations while recording and
* reports failures in the window .
*
* @ param widget Triggering Fl_Menu_Bar whose selected label names the action .
* @ param context MainWindow * owning the project and file path .
*/
2026-10-27 00:10:45 +01:00
void MainWindow : : file_callback ( Fl_Widget * widget , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 01:37:21 +01:00
if ( window - > recorder_ ) {
window - > report_error ( " Stop recording before file operations " , XTC_ERR_INVALID_STATE ) ;
return ;
}
2026-10-27 00:10:45 +01:00
auto * menu = static_cast < Fl_Menu_Bar * > ( widget ) ;
const char * action = menu - > mvalue ( ) - > label ( ) ;
xtc_result r = XTC_OK ;
2026-10-27 04:00:27 +01:00
window - > project_issue_ . clear ( ) ;
2026-10-27 00:10:45 +01:00
if ( std : : strcmp ( action , " New " ) = = 0 ) {
2026-10-27 01:37:21 +01:00
if ( ! window - > discard_changes ( ) )
return ;
2026-10-27 03:25:04 +01:00
r = window - > new_project ( ) ;
2026-10-27 00:10:45 +01:00
} else {
bool open = std : : strcmp ( action , " Open " ) = = 0 ;
2026-10-27 01:37:21 +01:00
if ( open & & ! window - > discard_changes ( ) )
return ;
2026-10-27 00:10:45 +01:00
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 " ) ;
2026-10-27 01:37:21 +01:00
if ( ! open )
chooser . options ( Fl_Native_File_Chooser : : SAVEAS_CONFIRM ) ;
2026-10-27 00:10:45 +01:00
int result = chooser . show ( ) ;
2026-10-27 01:37:21 +01:00
if ( result = = 1 )
return ;
if ( result < 0 )
r = XTC_ERR_IO ;
else
r = open ? window - > open_project ( chooser . filename ( ) ) : window - > save_project ( chooser . filename ( ) ) ;
2026-10-27 00:10:45 +01:00
}
}
2026-10-27 01:37:21 +01:00
if ( r ! = XTC_OK )
2026-10-27 04:00:27 +01:00
window - > report_error ( window - > project_issue_ . empty ( ) ? " Project operation failed " : window - > project_issue_ . c_str ( ) , r ) ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 00:31:03 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Hook called by arrangement widgets before mutating the project .
*
* Refuses edits while recording . When connected , stops the engine so the
* mutation cannot race the audio thread , then refreshes the status display .
*
* @ param context MainWindow * passed as the widget owner context .
* @ return XTC_OK when disconnected or the stop succeeded ,
* XTC_ERR_INVALID_STATE while recording , or the backend stop error .
*/
2026-10-27 00:31:03 +01:00
xtc_result MainWindow : : before_arrangement_edit ( void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 16:49:30 +01:00
if ( window - > recorder_ ) return XTC_ERR_INVALID_STATE ;
xtc_result r = xtc_history_reserve ( window - > history_ . get ( ) ) ;
if ( r = = XTC_OK & & window - > ready_ ) r = xtc_backend_stop ( window - > backend_ ) ;
2026-10-27 00:31:03 +01:00
window - > refresh_status ( ) ;
return r ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Imports a WAV file as an audio clip in a cloned project .
*
* Stops the engine first , creates an " Audio " track when none is selected ,
* and places the clip at the sequencer cursor . The clone replaces the live
* project only after every step succeeds .
*
* @ param path Path to the WAV file to import .
* @ return XTC_OK on success , or an error from the engine stop , clone , import ,
* or backend preparation .
*/
2026-10-27 00:31:03 +01:00
xtc_result MainWindow : : import_audio ( const char * path )
{
2026-10-27 16:49:30 +01:00
BusyOperation busy ( * this , status_ , " Importing audio and preparing playback… " ) ;
2026-10-27 00:31:03 +01:00
xtc_result r = before_arrangement_edit ( this ) ;
2026-10-27 01:37:21 +01:00
if ( r ! = XTC_OK )
return r ;
2026-10-27 00:31:03 +01:00
xtc_project * candidate = nullptr ;
r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
uint64_t track = sequencer_ - > selected_track ( ) , clip = 0 ;
2026-10-27 01:37:21 +01:00
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 ;
}
2026-10-27 00:31:03 +01:00
project_ . reset ( candidate ) ;
sequencer_ - > set_project ( candidate , false ) ;
graph_widget_ - > set_graph ( xtc_project_graph ( candidate ) ) ;
2026-10-27 01:37:21 +01:00
mark_dirty ( ) ;
refresh_status ( ) ;
return XTC_OK ;
2026-10-27 00:31:03 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Starts recording JACK input to a WAV file .
*
* Requires a connected backend and no active recording . The engine is
* stopped , the current position and selected track are remembered , and a
* stereo recorder at the engine sample rate is attached before starting .
* Failure leaves the window without a recorder .
*
* @ param path Destination WAV path chosen by the user .
* @ return XTC_OK when recording started , XTC_ERR_INVALID_STATE when
* disconnected or already recording , or another backend or recorder
* error .
*/
2026-10-27 00:31:03 +01:00
xtc_result MainWindow : : record_audio ( const char * path )
{
2026-10-28 10:37:01 +01:00
if ( ! ready_ | | recorder_ | | settings_ . loop_enabled )
2026-10-27 01:37:21 +01:00
return XTC_ERR_INVALID_STATE ;
2026-10-27 00:31:03 +01:00
xtc_result r = xtc_backend_stop ( backend_ ) ;
xtc_backend_status status { } ;
2026-10-27 01:37:21 +01:00
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 )
2026-10-28 10:37:01 +01:00
r = xtc_recorder_create ( path , status . sample_rate , static_cast < size_t > ( status . sample_rate ) * static_cast < size_t > ( settings_ . recording_seconds ) , & recorder_ ) ;
2026-10-27 01:37:21 +01:00
if ( r ! = XTC_OK )
return r ;
2026-10-27 00:31:03 +01:00
recording_path_ = path ;
recording_track_ = sequencer_ - > selected_track ( ) ;
r = xtc_backend_set_recorder ( backend_ , recorder_ ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
r = xtc_backend_start ( backend_ ) ;
2026-10-27 00:31:03 +01:00
if ( r ! = XTC_OK ) {
( void ) xtc_backend_set_recorder ( backend_ , nullptr ) ;
2026-10-27 01:37:21 +01:00
xtc_recorder_destroy ( recorder_ ) ;
recorder_ = nullptr ;
2026-10-27 16:49:30 +01:00
}
2026-10-27 01:37:21 +01:00
refresh_status ( ) ;
return r ;
2026-10-27 00:31:03 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Finalizes the active recording and adds the take to the project .
*
* Must run only after the engine has stopped , so no audio callback can
* reference the recorder ring . The recorder is detached , finished , and
* destroyed . A failed finalize or empty take is reported and not added . If
* the subsequent import fails , the saved WAV path is reported for recovery .
*/
2026-10-27 00:31:03 +01:00
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_ ) ;
2026-10-27 03:46:09 +01:00
std : : string recovery_path = xtc_recorder_recovery_path ( recorder_ ) ;
2026-10-27 01:37:21 +01:00
xtc_recorder_destroy ( recorder_ ) ;
recorder_ = nullptr ;
2026-10-27 00:31:03 +01:00
if ( r ! = XTC_OK ) {
2026-10-27 01:37:21 +01:00
char text [ 768 ] ;
std : : snprintf ( text , sizeof ( text ) ,
2026-10-27 03:46:09 +01:00
" Recording failed (%s). Retained take/temporary: %s. Recover with xtc-check --recover-recording. Not added. " ,
xtc_result_string ( r ) , recovery_path . c_str ( ) ) ;
2026-10-27 01:37:21 +01:00
set_label ( error_ , text ) ;
return ;
}
if ( ! frames ) {
set_label ( error_ , " Empty recording saved; no clip added. " ) ;
return ;
2026-10-27 00:31:03 +01:00
}
xtc_project * candidate = nullptr ;
r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
uint64_t clip = 0 , track = recording_track_ ;
2026-10-27 01:37:21 +01:00
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 ) ;
2026-10-27 16:49:30 +01:00
if ( r = = XTC_OK ) r = xtc_history_reserve ( history_ . get ( ) ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
r = xtc_backend_prepare_project ( backend_ , candidate ) ;
2026-10-27 00:31:03 +01:00
if ( r ! = XTC_OK ) {
xtc_project_destroy ( candidate ) ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:31:03 +01:00
}
2026-10-27 01:37:21 +01:00
project_ . reset ( candidate ) ;
sequencer_ - > set_project ( candidate , false ) ;
graph_widget_ - > set_graph ( xtc_project_graph ( candidate ) ) ;
mark_dirty ( ) ;
2026-10-27 00:31:03 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK callback for the audio import , record , and relink commands .
*
* The triggering widget supplies the menu label ; the Record button uses the
* " Record WAV " default . A native file chooser selects the file , after which
* the selected clip is relinked , recording starts , or a WAV is imported .
* Failures are reported in the window .
*
* @ param widget Triggering Fl_Menu_Bar or Fl_Button ; menu labels select the
* action .
* @ param context MainWindow * owning the project and sequencer .
*/
2026-10-27 00:31:03 +01:00
void MainWindow : : audio_file_callback ( Fl_Widget * widget , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
auto * menu = dynamic_cast < Fl_Menu_Bar * > ( widget ) ;
2026-10-27 18:04:39 +01:00
const char * action = menu ? menu - > mvalue ( ) - > label ( ) :
( widget & & widget - > label ( ) & & ! std : : strcmp ( widget - > label ( ) , " Import WAV " ) ? " Import WAV " : " Record WAV " ) ;
2026-10-27 00:31:03 +01:00
bool recording = std : : strcmp ( action , " Record WAV " ) = = 0 ;
bool relink = std : : strcmp ( action , " Relink selected audio " ) = = 0 ;
2026-10-27 01:37:21 +01:00
if ( window - > recorder_ ) {
window - > report_error ( " Stop recording first " , XTC_ERR_INVALID_STATE ) ;
return ;
}
2026-10-27 00:31:03 +01:00
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 " ) ;
2026-10-27 03:46:09 +01:00
// Recordings require a new destination; existing files are never replaced.
2026-10-27 00:31:03 +01:00
int chosen = chooser . show ( ) ;
2026-10-27 01:37:21 +01:00
if ( chosen = = 1 )
return ;
2026-10-27 00:31:03 +01:00
xtc_result r = chosen < 0 ? XTC_ERR_IO : XTC_OK ;
if ( r = = XTC_OK & & relink ) {
r = before_arrangement_edit ( window ) ;
2026-10-27 01:37:21 +01:00
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 )
2026-10-27 03:46:09 +01:00
window - > report_error ( recording ? " Project recording failed; choose a new WAV path in a writable directory "
: " Project audio operation failed " , r ) ;
2026-10-27 00:31:03 +01:00
}
2026-10-27 00:53:41 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Shows the piano roll for the selected MIDI clip .
*
* The piano - roll window is created on first use and reused afterwards .
*
* @ return XTC_OK on success , or XTC_ERR_INVALID_STATE when the selected clip
* does not exist or is not MIDI .
*/
2026-10-27 00:53:41 +01:00
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 < Fl_Double_Window > ( 900 , 620 , " xtc — Piano roll " ) ;
piano_window_ - > begin ( ) ;
2026-10-27 15:34:54 +01:00
auto * menu = new Fl_Menu_Bar ( 12 , 8 , 876 , 30 ) ;
const char * commands [ ] = { " Notes/Grid (G) " , " Notes/Filter (F) " , " Notes/Velocity and channel (V) " ,
" Notes/Quantize (Q) " , " Notes/Transpose (T) " , " Notes/Repeat (Ctrl+D) " ,
" Notes/Select filtered (Ctrl+A) " , " Notes/Delete " } ;
for ( int i = 0 ; i < 8 ; + + i )
menu - > add ( commands [ i ] , 0 , [ ] ( Fl_Widget * widget , void * data ) {
auto * owner = static_cast < MainWindow * > ( data ) ;
auto * bar = static_cast < Fl_Menu_Bar * > ( widget ) ;
owner - > piano_ - > command ( static_cast < int > ( bar - > mvalue ( ) - bar - > menu ( ) ) - 1 ) ;
} , this ) ;
2026-10-27 16:49:30 +01:00
menu - > add ( " Edit/Undo " , FL_CTRL + ' z ' , history_callback , this ) ;
menu - > add ( " Edit/Redo " , FL_CTRL + FL_SHIFT + ' z ' , history_callback , this ) ;
menu - > add ( " Edit/Redo alternate " , FL_CTRL + ' y ' , history_callback , this ) ;
2026-10-28 10:08:13 +01:00
piano_snap_ = new Fl_Choice ( 60 , 46 , 150 , 26 , " Snap " ) ;
for ( size_t i = 0 ; i < xtc_ui : : snap_count ; + + i ) { int item = piano_snap_ - > add ( " Snap " ) ; piano_snap_ - > replace ( item , xtc_ui : : snap_labels [ i ] ) ; }
piano_snap_ - > callback ( [ ] ( Fl_Widget * widget , void * c ) {
auto * w = static_cast < MainWindow * > ( c ) ; int value = static_cast < Fl_Choice * > ( widget ) - > value ( ) ;
if ( value > = 0 & & static_cast < size_t > ( value ) < xtc_ui : : snap_count ) ( void ) w - > piano_ - > set_grid ( xtc_ui : : snap_ticks [ value ] ) ;
} , this ) ;
note_velocity_ = new Fl_Value_Slider ( 340 , 46 , 300 , 26 , " Note velocity " ) ;
note_velocity_ - > align ( FL_ALIGN_LEFT ) ; note_velocity_ - > value ( 100 ) ;
note_velocity_ - > tooltip ( " Selected note MIDI velocity, 1..127. Instrument patches determine the response. " ) ;
note_velocity_ - > type ( FL_HOR_FILL_SLIDER ) ; note_velocity_ - > bounds ( 1 , 127 ) ; note_velocity_ - > step ( 1 ) ;
note_velocity_ - > when ( FL_WHEN_RELEASE ) ;
note_velocity_ - > callback ( [ ] ( Fl_Widget * widget , void * c ) {
auto * w = static_cast < MainWindow * > ( c ) ;
( void ) w - > piano_ - > set_velocity ( w - > piano_ - > selected_note ( ) , static_cast < unsigned int > ( static_cast < Fl_Value_Slider * > ( widget ) - > value ( ) ) ) ;
} , this ) ;
auto * scroll = new Fl_Scroll ( 12 , 84 , 876 , 404 ) ;
piano_ = new PianoRollWidget ( 12 , 84 , project_ . get ( ) , clip . id , before_arrangement_edit , this ) ;
2026-10-27 01:37:21 +01:00
piano_ - > callback (
[ ] ( Fl_Widget * , void * c ) {
static_cast < MainWindow * > ( c ) - > mark_dirty ( ) ;
} ,
this ) ;
scroll - > end ( ) ;
2026-10-28 10:08:13 +01:00
velocity_lane_ = new VelocityLane ( 12 , 500 , 876 , 108 , * piano_ ) ;
2026-10-27 01:37:21 +01:00
piano_window_ - > end ( ) ;
2026-10-28 10:08:13 +01:00
piano_window_ - > size_range ( 800 , 520 ) ;
2026-10-27 18:04:39 +01:00
xtc_ui : : style ( piano_window_ . get ( ) ) ;
2026-10-27 01:37:21 +01:00
piano_window_ - > resizable ( scroll ) ;
2026-10-27 00:53:41 +01:00
scroll - > scroll_to ( 0 , 60 * 16 ) ;
2026-11-05 23:57:56 +01:00
settings_ . transport_detached = transport_detached_ ; settings_ . mixer_detached = mixer_detached_ ;
if ( transport_window_ ) { settings_ . transport_x = transport_window_ - > x ( ) ; settings_ . transport_y = transport_window_ - > y ( ) ; settings_ . transport_width = transport_window_ - > w ( ) ; settings_ . transport_height = transport_window_ - > h ( ) ; }
if ( mixer_window_ ) { settings_ . mixer_x = mixer_window_ - > x ( ) ; settings_ . mixer_y = mixer_window_ - > y ( ) ; settings_ . mixer_width = mixer_window_ - > w ( ) ; settings_ . mixer_height = mixer_window_ - > h ( ) ; }
settings_ . width = w ( ) ; settings_ . height = h ( ) ;
2026-10-28 10:37:01 +01:00
settings_ . sequencer_snap = static_cast < int64_t > ( sequencer_ - > grid ( ) ) ; settings_ . sequencer_zoom = sequencer_ - > zoom_level ( ) ;
apply_ui_preferences ( ) ;
2026-10-27 01:37:21 +01:00
} else
piano_ - > set_project ( project_ . get ( ) , clip . id ) ;
2026-10-28 10:08:13 +01:00
refresh_workspace ( ) ;
2026-10-27 01:37:21 +01:00
piano_window_ - > show ( ) ;
return XTC_OK ;
2026-10-27 00:53:41 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK callback for the Piano roll menu item and button .
*
* Refuses to open the editor while recording and asks the user to select a
* MIDI clip when open_piano_roll ( ) fails .
*
* @ param context MainWindow * owning the piano roll .
*/
2026-10-27 00:53:41 +01:00
void MainWindow : : piano_callback ( Fl_Widget * , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 01:37:21 +01:00
if ( window - > recorder_ ) {
window - > report_error ( " Stop recording before editing MIDI " , XTC_ERR_INVALID_STATE ) ;
return ;
}
2026-10-27 00:53:41 +01:00
xtc_result r = window - > open_piano_roll ( ) ;
2026-10-27 01:37:21 +01:00
if ( r ! = XTC_OK )
set_label ( window - > error_ , " Select a MIDI clip, then open Piano roll. " ) ;
2026-10-27 00:53:41 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK callback for the Panic menu item and button .
*
* When the backend is connected , sends a panic request that silences held
* instrument notes and reports failures .
*
* @ param context MainWindow * owning the backend .
*/
2026-10-27 00:53:41 +01:00
void MainWindow : : panic_callback ( Fl_Widget * , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 01:37:21 +01:00
if ( ! window - > ready_ )
return ;
2026-10-27 00:53:41 +01:00
xtc_result r = xtc_backend_panic ( window - > backend_ ) ;
2026-10-27 01:37:21 +01:00
if ( r ! = XTC_OK )
window - > report_error ( " Panic failed " , r ) ;
2026-10-27 00:53:41 +01:00
}
2026-10-27 01:06:57 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Applies one undo or redo step from the project history .
*
* The selected snapshot is installed without resetting history and then
* accepted , leaving the window marked dirty .
*
* @ param redo True to redo , false to undo .
* @ return XTC_OK on success , XTC_ERR_INVALID_STATE while recording ,
* XTC_ERR_NOT_FOUND when no step is available , or another error from
* checkout , install , or accept .
*/
2026-10-27 01:06:57 +01:00
xtc_result MainWindow : : undo ( bool redo )
{
2026-10-27 01:37:21 +01:00
if ( recorder_ )
return XTC_ERR_INVALID_STATE ;
2026-10-27 16:49:30 +01:00
const uint64_t track = sequencer_ - > selected_track ( ) , clip = sequencer_ - > selected_clip ( ) ;
const uint64_t mixer_track = mixer_ - > selected_track ( ) ;
const size_t node = graph_widget_ - > selected_node ( ) ;
const uint64_t piano_clip = piano_ ? piano_ - > clip ( ) : UINT64_MAX ;
const uint64_t note = piano_ ? piano_ - > selected_note ( ) : UINT64_MAX ;
const bool piano_visible = piano_window_ & & piano_window_ - > shown ( ) ;
2026-10-27 01:06:57 +01:00
xtc_project * candidate = nullptr ;
xtc_result r = xtc_history_checkout ( history_ . get ( ) , redo ? 1 : 0 , & candidate ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
r = install_project ( candidate , false ) ;
if ( r ! = XTC_OK ) {
xtc_project_destroy ( candidate ) ;
return r ;
}
2026-10-27 16:49:30 +01:00
xtc_clip_info selected_clip { } ;
if ( xtc_project_find_clip ( project_ . get ( ) , clip , & selected_clip ) = = XTC_OK )
sequencer_ - > select ( selected_clip . track_id , clip ) ;
else sequencer_ - > select ( track ) ;
mixer_ - > select_track ( mixer_track ) ;
graph_widget_ - > select_node ( node ) ;
xtc_clip_info info { } ;
if ( piano_ & & xtc_project_find_clip ( project_ . get ( ) , piano_clip , & info ) = = XTC_OK & & info . midi ) {
piano_ - > set_project ( project_ . get ( ) , piano_clip ) ; piano_ - > select_note ( note ) ;
if ( piano_visible ) piano_window_ - > show ( ) ;
}
2026-10-27 01:06:57 +01:00
r = xtc_history_accept ( history_ . get ( ) , redo ? 1 : 0 ) ;
2026-10-27 01:37:21 +01:00
dirty_ = true ;
2026-10-27 16:49:30 +01:00
update_history_controls ( ) ;
2026-10-27 01:37:21 +01:00
copy_label ( " xtc * — unsaved changes " ) ;
2026-10-27 01:06:57 +01:00
return r ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief FLTK menu callback for the Undo and Redo menu items .
*
* The selected menu label starting with " Redo " chooses redo ; all other
* labels undo . Failures other than XTC_ERR_NOT_FOUND are shown in the window .
*
* @ param widget Fl_Menu_Bar whose selected item triggered the callback .
* @ param context MainWindow * owning the menu and history .
*/
2026-10-27 01:06:57 +01:00
void MainWindow : : history_callback ( Fl_Widget * widget , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
auto * menu = static_cast < Fl_Menu_Bar * > ( widget ) ;
bool redo = std : : strncmp ( menu - > mvalue ( ) - > label ( ) , " Redo " , 4 ) = = 0 ;
xtc_result r = window - > undo ( redo ) ;
2026-10-27 01:37:21 +01:00
if ( r ! = XTC_OK & & r ! = XTC_ERR_NOT_FOUND )
window - > report_error ( " Undo/redo failed " , r ) ;
2026-10-27 01:06:57 +01:00
}
2026-10-27 04:00:27 +01:00
2026-10-27 18:04:39 +01:00
xtc_result MainWindow : : create_track ( bool midi )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
const uint64_t position = sequencer_ - > position ( ) ;
xtc_project * candidate = nullptr ;
uint64_t track = UINT64_MAX , clip = UINT64_MAX ;
xtc_result r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
if ( r = = XTC_OK ) r = xtc_project_add_track ( candidate , midi ? " MIDI track " : " Audio track " , & track ) ;
2026-10-28 10:37:01 +01:00
if ( r = = XTC_OK & & midi ) r = xtc_project_add_midi_clip ( candidate , track , position , static_cast < uint64_t > ( settings_ . clip_ticks ) , & clip ) ;
2026-10-27 18:04:39 +01:00
if ( r = = XTC_OK ) r = xtc_history_reserve ( history_ . get ( ) ) ;
if ( r = = XTC_OK ) r = install_project ( candidate , false ) ;
if ( r ! = XTC_OK ) xtc_project_destroy ( candidate ) ;
else {
sequencer_ - > select ( track , clip ) ;
mixer_ - > select_track ( track ) ; tabs_ - > value ( tabs_ - > child ( 0 ) ) ; mark_dirty ( ) ;
}
return r ;
}
void MainWindow : : refresh_workspace ( )
{
2026-10-28 17:16:01 +01:00
if ( loop_button_ ) {
loop_button_ - > value ( settings_ . loop_enabled ! = 0 ) ;
if ( recorder_ )
loop_button_ - > deactivate ( ) ;
else
loop_button_ - > activate ( ) ;
}
if ( snap_ )
snap_ - > value ( xtc_ui : : snap_index ( sequencer_ - > grid ( ) ) ) ;
2026-10-28 10:08:13 +01:00
if ( piano_ ) {
piano_snap_ - > value ( xtc_ui : : snap_index ( piano_ - > grid ( ) ) ) ;
2026-10-28 17:16:01 +01:00
size_t count = 0 ;
bool selected = false ;
xtc_project_note_count ( project_ . get ( ) , & count ) ;
2026-10-28 10:08:13 +01:00
for ( size_t i = 0 ; i < count ; + + i ) {
2026-10-28 17:16:01 +01:00
xtc_note_info note { } ;
xtc_project_get_note ( project_ . get ( ) , i , & note ) ;
2026-10-28 10:08:13 +01:00
if ( note . id = = piano_ - > selected_note ( ) & & piano_ - > note_visible ( note ) ) {
2026-10-28 17:16:01 +01:00
selected = true ;
if ( Fl : : pushed ( ) ! = note_velocity_ )
note_velocity_ - > value ( note . velocity ) ;
break ;
2026-10-28 10:08:13 +01:00
}
}
2026-10-28 17:16:01 +01:00
if ( selected )
note_velocity_ - > activate ( ) ;
else
note_velocity_ - > deactivate ( ) ;
2026-10-28 10:08:13 +01:00
velocity_lane_ - > redraw ( ) ;
}
2026-10-28 17:16:01 +01:00
char position [ 64 ] , info [ 512 ] ;
2026-10-27 18:04:39 +01:00
uint64_t tick = sequencer_ - > position ( ) ;
2026-10-28 17:16:01 +01:00
std : : snprintf ( position , sizeof ( position ) , " %03llu : %u : %03u " , static_cast < unsigned long long > ( tick / 3840 + 1 ) ,
static_cast < unsigned int > ( ( tick / 960 ) % 4 + 1 ) , static_cast < unsigned int > ( tick % 960 ) ) ;
set_label ( transport_position_ , position ) ;
if ( ! editing_number ( tempo_display_ ) )
tempo_display_ - > value ( xtc_project_tempo ( project_ . get ( ) ) / 1000.0 ) ;
if ( recorder_ )
tempo_display_ - > deactivate ( ) ;
else
tempo_display_ - > activate ( ) ;
size_t tracks = 0 , clips = 0 ;
xtc_project_get_counts ( project_ . get ( ) , & tracks , & clips ) ;
uint64_t end = 0 ;
for ( size_t i = 0 ; i < clips ; + + i ) {
xtc_clip_info clip { } ;
xtc_project_get_clip ( project_ . get ( ) , i , & clip ) ;
end = std : : max ( end , clip . start_tick + clip . length_tick ) ;
2026-10-27 18:04:39 +01:00
}
2026-10-28 17:16:01 +01:00
std : : snprintf ( info , sizeof ( info ) , " %zu tracks / %zu clips / %llu bars " , tracks , clips ,
static_cast < unsigned long long > ( ( end + 3839 ) / 3840 ) ) ;
set_label ( session_info_ , info ) ;
2026-10-30 12:39:47 +01:00
bool mixer_context = tabs_ - > value ( ) = = mixer_page_ | |
( mixer_detached_ & & Fl : : focus ( ) & & Fl : : focus ( ) - > window ( ) = = mixer_window_ . get ( ) ) ;
uint64_t target = mixer_context ? mixer_ - > selected_track ( ) : sequencer_ - > selected_track ( ) ;
2026-10-28 17:16:01 +01:00
bool selection_changed = target ! = inspected_track_ ;
inspected_track_ = UINT64_MAX ;
for ( size_t i = 0 ; i < tracks ; + + i ) {
xtc_track_info track { } ;
xtc_project_get_track ( project_ . get ( ) , i , & track ) ;
if ( track . id ! = target )
continue ;
inspected_track_ = track . id ;
for ( auto * control : { static_cast < Fl_Widget * > ( inspector_gain_ ) , static_cast < Fl_Widget * > ( inspector_pan_ ) ,
static_cast < Fl_Widget * > ( inspector_mute_ ) , static_cast < Fl_Widget * > ( inspector_solo_ ) } ) {
if ( recorder_ )
control - > deactivate ( ) ;
else
control - > activate ( ) ;
}
if ( selection_changed | | ! editing_number ( inspector_gain_ ) )
inspector_gain_ - > value ( track . gain ) ;
if ( selection_changed | | ! editing_number ( inspector_pan_ ) )
inspector_pan_ - > value ( track . pan ) ;
if ( Fl : : pushed ( ) ! = inspector_mute_ )
inspector_mute_ - > value ( track . mute ) ;
if ( Fl : : pushed ( ) ! = inspector_solo_ )
inspector_solo_ - > value ( track . solo ) ;
xtc_plugin_spec spec { } ;
bool plugin = xtc_project_get_instrument ( project_ . get ( ) , track . id , & spec ) = = XTC_OK ;
std : : snprintf ( info , sizeof ( info ) , " %s \n %s %s%s \n %s " , track . name , track . is_bus ? " BUS " : " TRACK " ,
track . mute ? " Muted " : " " , track . solo ? " Solo " : " " ,
track . is_bus ? " Bus output "
: plugin ? spec . label
: " Built-in MIDI sine " ) ;
set_label ( selection_info_ , info ) ;
return ;
}
for ( auto * control : { static_cast < Fl_Widget * > ( inspector_gain_ ) , static_cast < Fl_Widget * > ( inspector_pan_ ) ,
static_cast < Fl_Widget * > ( inspector_mute_ ) , static_cast < Fl_Widget * > ( inspector_solo_ ) } )
control - > deactivate ( ) ;
std : : snprintf ( info , sizeof ( info ) , " No track selected \n Create a MIDI track or import a WAV to begin. " ) ;
set_label ( selection_info_ , info ) ;
2026-10-27 18:04:39 +01:00
}
2026-10-27 17:51:48 +01:00
xtc_result MainWindow : : assign_instrument ( uint64_t track , const char * path , unsigned int index )
{
2026-10-28 10:08:13 +01:00
uint64_t clip = sequencer_ - > selected_track ( ) = = track ? sequencer_ - > selected_clip ( ) : UINT64_MAX ;
2026-10-27 17:51:48 +01:00
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
xtc_project * candidate = nullptr ;
xtc_plugin_spec spec { } ;
2026-10-28 10:37:01 +01:00
xtc_result r = path ? xtc_plugin_probe ( path , index , sample_rate ( ) , & spec ) : XTC_OK ;
2026-10-27 17:51:48 +01:00
if ( r = = XTC_OK & & path & & ! spec . instrument ) return XTC_ERR_UNSUPPORTED ;
if ( r = = XTC_OK ) r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
if ( r = = XTC_OK ) r = xtc_project_set_instrument ( candidate , track , path ? & spec : nullptr ) ;
if ( r = = XTC_OK ) r = xtc_history_reserve ( history_ . get ( ) ) ;
if ( r = = XTC_OK ) r = install_project ( candidate , false ) ;
2026-10-28 10:08:13 +01:00
if ( r ! = XTC_OK ) xtc_project_destroy ( candidate ) ; else { sequencer_ - > select ( track , clip ) ; mixer_ - > select_track ( track ) ; mark_dirty ( ) ; }
2026-10-27 17:51:48 +01:00
return r ;
}
2026-11-05 23:55:56 +01:00
void MainWindow : : export_audio ( )
{
if ( recorder_ ) { report_error ( " Stop recording before export " , XTC_ERR_INVALID_STATE ) ; return ; }
const char * answer = fl_input ( " Export stereo WAV to a new absolute path (tail configured in Preferences) " , " " ) ;
if ( ! answer ) return ;
std : : string path = answer ; xtc_export * job = nullptr ;
xtc_result r = xtc_export_create ( project_ . get ( ) , path . c_str ( ) , static_cast < unsigned int > ( settings_ . offline_rate ) , static_cast < unsigned int > ( settings_ . tail_ms ) , & job ) ;
if ( r = = XTC_OK ) {
Fl_Double_Window dialog ( 460 , 130 , " Export WAV " ) ; dialog . set_modal ( ) ; dialog . begin ( ) ;
Fl_Box progress ( 12 , 12 , 436 , 45 , " Preparing export " ) ; Fl_Button cancel ( 330 , 80 , 110 , 30 , " Cancel export " ) ;
cancel . callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < Fl_Window * > ( c ) - > hide ( ) ; } , & dialog ) ;
dialog . callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < Fl_Window * > ( c ) - > hide ( ) ; } , & dialog ) ; dialog . end ( ) ; xtc_ui : : style ( & dialog ) ; dialog . show ( ) ;
int done = 0 ;
while ( dialog . shown ( ) & & ! done & & r = = XTC_OK ) {
r = xtc_export_step ( job , & done ) ; uint64_t position = 0 , total = 0 ; xtc_export_progress ( job , & position , & total ) ;
std : : string text = std : : to_string ( position ) + " / " + std : : to_string ( total ) + " frames " ; progress . copy_label ( text . c_str ( ) ) ; Fl : : check ( ) ;
}
if ( r = = XTC_OK & & ! done ) set_label ( status_ , " Export cancelled; destination retained " ) ;
else if ( r = = XTC_OK ) set_label ( status_ , " Export complete " ) ;
dialog . hide ( ) ;
}
xtc_export_destroy ( job ) ;
if ( r ! = XTC_OK ) report_error ( " Export failed; destination must be new and media available " , r ) ;
}
2026-11-05 23:50:14 +01:00
xtc_result MainWindow : : load_sampler ( uint64_t track , const char * path )
{
if ( ! path | | path [ 0 ] ! = ' / ' | | std : : strlen ( path ) > = sizeof ( xtc_plugin_spec { } . sample_path ) ) return XTC_ERR_INVALID_ARGUMENT ;
xtc_plugin_spec spec { } ; xtc_result r = xtc_project_get_instrument ( project_ . get ( ) , track , & spec ) ;
if ( r ! = XTC_OK | | spec . format ! = XTC_PLUGIN_BUILTIN | | spec . index ! = 4 ) r = xtc_plugin_probe ( XTC_BUILTIN_PATH , 4 , sample_rate ( ) , & spec ) ;
if ( r ! = XTC_OK ) return r ;
std : : strcpy ( spec . sample_path , path ) ; return set_instrument_spec ( track , spec ) ;
}
2026-11-05 23:47:54 +01:00
xtc_result MainWindow : : set_instrument_spec ( uint64_t track , const xtc_plugin_spec & spec )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
uint64_t clip = sequencer_ - > selected_clip ( ) ; xtc_project * candidate = nullptr ;
xtc_result r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
if ( r = = XTC_OK ) r = xtc_project_set_instrument ( candidate , track , & spec ) ;
if ( r = = XTC_OK ) r = xtc_history_reserve ( history_ . get ( ) ) ;
if ( r = = XTC_OK ) r = install_project ( candidate , false ) ;
if ( r ! = XTC_OK ) xtc_project_destroy ( candidate ) ;
else { sequencer_ - > select ( track , clip ) ; mixer_ - > select_track ( track ) ; mark_dirty ( ) ; }
return r ;
}
2026-10-27 17:51:48 +01:00
xtc_result MainWindow : : select_live_instrument ( uint64_t track )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
xtc_project * candidate = nullptr ;
xtc_result r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
if ( r = = XTC_OK ) r = xtc_project_set_live_instrument ( candidate , track ) ;
if ( r = = XTC_OK ) r = xtc_history_reserve ( history_ . get ( ) ) ;
if ( r = = XTC_OK ) r = install_project ( candidate , false ) ;
if ( r ! = XTC_OK ) xtc_project_destroy ( candidate ) ; else mark_dirty ( ) ;
return r ;
}
void MainWindow : : instrument_callback ( Fl_Widget * widget , void * context )
{
auto * w = static_cast < MainWindow * > ( context ) ;
2026-10-28 10:08:13 +01:00
if ( w - > recorder_ ) { w - > report_error ( " Stop recording before instrument changes " , XTC_ERR_INVALID_STATE ) ; return ; }
2026-10-27 18:04:39 +01:00
auto * menu = dynamic_cast < Fl_Menu_Bar * > ( widget ) ;
const char * label = menu ? menu - > mvalue ( ) - > label ( ) : " Load instrument plugin " ;
2026-10-27 17:51:48 +01:00
uint64_t track = w - > sequencer_ - > selected_track ( ) ;
xtc_result r ;
2026-11-05 23:50:14 +01:00
if ( std : : strcmp ( label , " Load sampler WAV " ) = = 0 ) {
const char * path = fl_input ( " Sampler WAV path (absolute; replaces/relinks selected track sample) " , " " ) ;
if ( path ) { r = w - > load_sampler ( track , path ) ; if ( r ! = XTC_OK ) w - > report_error ( " Sampler preparation failed " , r ) ; }
return ;
}
2026-11-05 23:47:54 +01:00
if ( std : : strcmp ( label , " Selected instrument controls " ) = = 0 | | std : : strcmp ( label , " Selected instrument automation " ) = = 0 | |
std : : strcmp ( label , " Save instrument preset " ) = = 0 | | std : : strcmp ( label , " Load instrument preset " ) = = 0 ) {
xtc_plugin_spec spec { } ;
if ( std : : strcmp ( label , " Load instrument preset " ) = = 0 ) {
const char * path = fl_input ( " Instrument preset path (absolute) " , " " ) ; if ( ! path ) return ;
r = xtc_plugin_preset_load ( path , & spec ) ;
if ( r = = XTC_OK ) r = w - > set_instrument_spec ( track , spec ) ;
} else {
r = xtc_project_get_instrument ( w - > project_ . get ( ) , track , & spec ) ;
if ( r = = XTC_OK & & spec . format = = XTC_PLUGIN_BUILTIN & & ! spec . control_count )
r = xtc_plugin_probe ( XTC_BUILTIN_PATH , spec . index , w - > sample_rate ( ) , & spec ) ;
if ( r = = XTC_OK & & std : : strcmp ( label , " Save instrument preset " ) = = 0 ) {
const char * path = fl_input ( " Save instrument preset path (absolute) " , " " ) ; if ( ! path ) return ;
r = xtc_plugin_preset_save ( & spec , path ) ;
} else if ( r = = XTC_OK & & std : : strcmp ( label , " Selected instrument controls " ) = = 0 ) {
xtc_plugin_details details { } ; xtc_plugin_spec edited { } ;
r = xtc_plugin_describe ( & spec , w - > sample_rate ( ) , & details ) ;
if ( r = = XTC_OK & & xtc_edit_plugin ( spec , details , edited ) ) r = w - > set_instrument_spec ( track , edited ) ;
} else if ( r = = XTC_OK ) {
xtc_parameter_info info [ XTC_PLUGIN_CONTROLS ] { } ; r = xtc_plugin_parameters ( & spec , w - > sample_rate ( ) , info ) ;
std : : string prompt = " Instrument automation: port tick mode value (mode -1 removes) \n " ;
for ( unsigned int i = 0 ; i < spec . control_count ; + + i ) prompt + = std : : to_string ( i ) + " : " + info [ i ] . name + " \n " ;
if ( r = = XTC_OK ) {
const char * answer = fl_input ( " %s " , " " , prompt . c_str ( ) ) ; if ( ! answer ) return ;
unsigned int port ; unsigned long long tick ; int mode ; float value ; char extra ;
if ( std : : sscanf ( answer , " %u %llu %d %f %c " , & port , & tick , & mode , & value , & extra ) ! = 4 | | mode < - 1 | | mode > 1 ) r = XTC_ERR_INVALID_ARGUMENT ;
else {
unsigned int i = 0 ; for ( ; i < spec . point_count ; + + i ) if ( spec . points [ i ] . port = = port & & spec . points [ i ] . tick = = tick ) break ;
if ( mode = = - 1 ) { if ( i = = spec . point_count ) r = XTC_ERR_NOT_FOUND ; else { for ( unsigned int j = i + 1 ; j < spec . point_count ; + + j ) spec . points [ j - 1 ] = spec . points [ j ] ; - - spec . point_count ; } }
else if ( i = = XTC_PLUGIN_POINTS ) r = XTC_ERR_CAPACITY ;
else { if ( i = = spec . point_count ) + + spec . point_count ; spec . points [ i ] = { static_cast < uint64_t > ( tick ) , port , static_cast < unsigned int > ( mode ) , value } ; }
if ( r = = XTC_OK ) r = w - > set_instrument_spec ( track , spec ) ;
}
}
}
}
if ( r ! = XTC_OK ) w - > report_error ( " Instrument edit failed; check track, preset and control ranges " , r ) ;
return ;
}
2026-10-27 17:51:48 +01:00
if ( std : : strcmp ( label , " Use built-in instrument " ) = = 0 ) r = w - > assign_instrument ( track , nullptr , 0 ) ;
else if ( std : : strcmp ( label , " Live MIDI to selected instrument " ) = = 0 ) r = w - > select_live_instrument ( track ) ;
else if ( std : : strcmp ( label , " Live MIDI to built-in sine " ) = = 0 ) r = w - > select_live_instrument ( UINT64_MAX ) ;
else {
2026-10-28 10:37:01 +01:00
std : : string module ; if ( ! xtc_choose_plugin_file ( w - > settings_ , module ) ) return ;
unsigned int index = 0 ;
if ( ! xtc_choose_plugin ( module . c_str ( ) , w - > sample_rate ( ) , index , true ) ) return ;
r = w - > assign_instrument ( track , module . c_str ( ) , index ) ;
2026-10-27 17:51:48 +01:00
}
if ( r ! = XTC_OK ) w - > report_error ( " Instrument assignment failed; select a non-bus track and compatible mono instrument " , r ) ;
}
2026-10-27 16:37:59 +01:00
xtc_result MainWindow : : set_control_map ( unsigned int channel , const unsigned int controllers [ 3 ] )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
xtc_project * candidate = nullptr ;
xtc_result r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
if ( r = = XTC_OK ) r = xtc_project_set_control_map ( candidate , channel , controllers ) ;
2026-10-27 16:49:30 +01:00
if ( r = = XTC_OK ) r = xtc_history_reserve ( history_ . get ( ) ) ;
2026-10-27 16:37:59 +01:00
if ( r = = XTC_OK ) r = install_project ( candidate , false ) ;
if ( r ! = XTC_OK ) xtc_project_destroy ( candidate ) ; else mark_dirty ( ) ;
return r ;
}
void MainWindow : : control_map_callback ( Fl_Widget * , void * context )
{
auto * w = static_cast < MainWindow * > ( context ) ;
if ( w - > recorder_ ) { w - > report_error ( " Stop recording before controller mapping " , XTC_ERR_INVALID_STATE ) ; return ; }
unsigned int controllers [ 3 ] { } ;
std : : string prompt = " Live sine MIDI bindings (channels 0..15; 128 disables). CC120/123 reserved. \n " ;
for ( unsigned int channel = 0 ; channel < 16 ; + + channel ) {
xtc_project_get_control_map ( w - > project_ . get ( ) , channel , controllers ) ;
prompt + = std : : to_string ( channel ) + " : volume " + std : : to_string ( controllers [ 0 ] ) +
" , pan " + std : : to_string ( controllers [ 1 ] ) + " , sustain " + std : : to_string ( controllers [ 2 ] ) + " \n " ;
}
prompt + = " Enter channel volume_CC pan_CC sustain_CC (distinct enabled CCs): " ;
const char * answer = fl_input ( " %s " , " 0 7 10 64 " , prompt . c_str ( ) ) ;
if ( ! answer ) return ;
unsigned int channel ; char extra ;
xtc_result r = std : : sscanf ( answer , " %u %u %u %u %c " , & channel , controllers , controllers + 1 , controllers + 2 , & extra ) = = 4 ?
w - > set_control_map ( channel , controllers ) : XTC_ERR_INVALID_ARGUMENT ;
if ( r ! = XTC_OK ) w - > report_error ( " Controller mapping rejected: check channel, unique CCs and reserved panic controllers " , r ) ;
}
2026-10-27 04:00:27 +01:00
xtc_result MainWindow : : install_plugin_graph ( xtc_graph * graph )
{
xtc_project * candidate = nullptr ;
xtc_result r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
if ( r = = XTC_OK ) r = xtc_project_set_graph ( candidate , graph ) ;
2026-10-27 16:49:30 +01:00
if ( r = = XTC_OK ) r = xtc_history_reserve ( history_ . get ( ) ) ;
2026-10-27 04:00:27 +01:00
if ( r = = XTC_OK ) r = install_project ( candidate , false ) ;
if ( r ! = XTC_OK ) xtc_project_destroy ( candidate ) ;
else mark_dirty ( ) ;
return r ;
}
xtc_result MainWindow : : add_plugin ( const char * path , unsigned int index , size_t * id )
{
2026-10-27 16:49:30 +01:00
BusyOperation busy ( * this , status_ , " Loading plugin and preparing playback… " ) ;
2026-10-27 04:00:27 +01:00
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
2026-10-28 10:37:01 +01:00
double rate = sample_rate ( ) ;
2026-10-27 04:00:27 +01:00
xtc_backend_status status { } ;
if ( ready_ & & xtc_backend_get_status ( backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
xtc_plugin_spec spec { } ;
xtc_result r = xtc_plugin_probe ( path , index , rate , & spec ) ;
xtc_graph * graph = nullptr ; size_t added = SIZE_MAX ;
if ( r = = XTC_OK ) r = xtc_graph_clone ( xtc_project_graph ( project_ . get ( ) ) , & graph ) ;
if ( r = = XTC_OK ) r = xtc_graph_add_plugin ( graph , & spec , & added ) ;
2026-10-27 16:24:21 +01:00
xtc_plan * check = nullptr ;
if ( r = = XTC_OK ) r = xtc_graph_compile ( graph , rate , 256 , & check ) ;
xtc_plan_destroy ( check ) ;
2026-10-27 04:00:27 +01:00
if ( r = = XTC_OK ) r = install_plugin_graph ( graph ) ;
xtc_graph_destroy ( graph ) ;
2026-10-27 16:24:21 +01:00
if ( r = = XTC_OK ) graph_widget_ - > select_node ( added ) ;
2026-10-27 04:00:27 +01:00
if ( r = = XTC_OK & & id ) * id = added ;
return r ;
}
2026-10-27 16:24:21 +01:00
xtc_result MainWindow : : replace_plugin ( size_t id , const char * path , unsigned int index )
{
2026-10-27 16:49:30 +01:00
BusyOperation busy ( * this , status_ , " Replacing plugin and preparing playback… " ) ;
2026-10-27 16:24:21 +01:00
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
2026-10-28 10:37:01 +01:00
double rate = sample_rate ( ) ; xtc_backend_status status { } ;
2026-10-27 16:24:21 +01:00
if ( ready_ & & xtc_backend_get_status ( backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
xtc_plugin_spec spec { } ;
xtc_result r = xtc_plugin_probe ( path , index , rate , & spec ) ;
if ( r = = XTC_OK ) r = apply_plugin_spec ( id , spec , true ) ;
if ( r = = XTC_OK ) graph_widget_ - > select_node ( id ) ;
return r ;
}
xtc_result MainWindow : : reset_plugin ( size_t id , unsigned int port )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
2026-10-28 10:37:01 +01:00
double rate = sample_rate ( ) ; xtc_backend_status status { } ;
2026-10-27 16:24:21 +01:00
if ( ready_ & & xtc_backend_get_status ( backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
xtc_plugin_spec spec { } ;
xtc_result r = xtc_graph_get_plugin ( xtc_project_graph ( project_ . get ( ) ) , id , & spec ) ;
if ( r = = XTC_OK ) r = xtc_plugin_reset ( & spec , rate , port ) ;
return r = = XTC_OK ? apply_plugin_spec ( id , spec ) : r ;
}
xtc_result MainWindow : : insert_plugin ( const xtc_connection_info & wire , const char * path , unsigned int index , size_t * id )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
2026-10-28 10:37:01 +01:00
double rate = sample_rate ( ) ; xtc_backend_status status { } ;
2026-10-27 16:24:21 +01:00
if ( ready_ & & xtc_backend_get_status ( backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
xtc_plugin_spec spec { } ;
xtc_result r = xtc_plugin_probe ( path , index , rate , & spec ) ;
if ( r = = XTC_OK & & ! spec . inputs ) r = XTC_ERR_UNSUPPORTED ;
xtc_graph * graph = nullptr ; size_t added = SIZE_MAX ;
xtc_graph_edit edit { } ; edit . kind = XTC_EDIT_DISCONNECT ; edit . data . connection = wire ;
if ( r = = XTC_OK ) r = xtc_graph_apply_edit ( xtc_project_graph ( project_ . get ( ) ) , & edit , & graph , nullptr ) ;
if ( r = = XTC_OK ) r = xtc_graph_add_plugin ( graph , & spec , & added ) ;
if ( r = = XTC_OK ) r = xtc_graph_connect_ports ( graph , wire . source_node , wire . source_port , added , 0 ) ;
if ( r = = XTC_OK ) r = xtc_graph_connect_ports ( graph , added , 1 , wire . destination_node , wire . destination_port ) ;
xtc_plan * check = nullptr ;
if ( r = = XTC_OK ) r = xtc_graph_compile ( graph , rate , 256 , & check ) ;
xtc_plan_destroy ( check ) ;
if ( r = = XTC_OK ) r = install_plugin_graph ( graph ) ;
xtc_graph_destroy ( graph ) ;
if ( r = = XTC_OK ) { graph_widget_ - > select_node ( added ) ; if ( id ) * id = added ; }
return r ;
}
2026-10-27 04:00:27 +01:00
xtc_result MainWindow : : set_plugin_control ( size_t id , unsigned int port , float value )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
xtc_plugin_spec spec { } ;
xtc_result r = xtc_graph_get_plugin ( xtc_project_graph ( project_ . get ( ) ) , id , & spec ) ;
if ( r ! = XTC_OK ) return r ;
size_t i ;
for ( i = 0 ; i < spec . control_count & & spec . ports [ i ] ! = port ; + + i ) { }
if ( i = = spec . control_count ) return XTC_ERR_NOT_FOUND ;
spec . values [ i ] = value ;
2026-10-27 06:25:30 +01:00
return apply_plugin_spec ( id , spec ) ;
}
xtc_result MainWindow : : set_plugin_automation ( size_t id , const xtc_plugin_point & point , bool remove )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
2026-10-28 10:37:01 +01:00
double rate = sample_rate ( ) ; xtc_backend_status status { } ;
2026-10-27 16:37:59 +01:00
if ( ready_ & & xtc_backend_get_status ( backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
xtc_project * candidate = nullptr ;
xtc_result r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
xtc_parameter_key key { XTC_PARAMETER_PLUGIN , static_cast < uint64_t > ( id ) , point . port } ;
if ( r = = XTC_OK ) r = xtc_project_automate ( candidate , key , rate , point . tick , point . step , point . value , remove ? 1 : 0 ) ;
2026-10-27 06:25:30 +01:00
xtc_plugin_spec spec { } ;
2026-10-27 16:37:59 +01:00
if ( r = = XTC_OK ) r = xtc_graph_get_plugin ( xtc_project_graph ( candidate ) , id , & spec ) ;
xtc_project_destroy ( candidate ) ;
return r = = XTC_OK ? apply_plugin_spec ( id , spec ) : r ;
2026-10-27 06:25:30 +01:00
}
2026-10-27 16:24:21 +01:00
xtc_result MainWindow : : apply_plugin_spec ( size_t id , const xtc_plugin_spec & spec , bool replace )
2026-10-27 06:25:30 +01:00
{
2026-10-27 04:00:27 +01:00
xtc_graph * graph = nullptr ;
2026-10-27 06:25:30 +01:00
xtc_result r = xtc_graph_clone ( xtc_project_graph ( project_ . get ( ) ) , & graph ) ;
2026-10-27 16:24:21 +01:00
if ( r = = XTC_OK ) r = replace ? xtc_graph_replace_plugin ( graph , id , & spec ) : xtc_graph_set_plugin ( graph , id , & spec ) ;
2026-10-27 04:00:27 +01:00
/* Compile before adoption even with a mocked/disconnected backend. */
xtc_plan * check = nullptr ;
2026-10-28 10:37:01 +01:00
xtc_backend_status status { } ; double rate = sample_rate ( ) ;
2026-10-27 04:00:27 +01:00
if ( ready_ & & xtc_backend_get_status ( backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
if ( r = = XTC_OK ) r = xtc_graph_compile ( graph , rate , 256 , & check ) ;
xtc_plan_destroy ( check ) ;
if ( r = = XTC_OK ) r = install_plugin_graph ( graph ) ;
xtc_graph_destroy ( graph ) ; return r ;
}
void MainWindow : : plugin_callback ( Fl_Widget * widget , void * context )
{
auto * w = static_cast < MainWindow * > ( context ) ;
if ( w - > recorder_ ) { w - > report_error ( " Stop recording before plugin changes " , XTC_ERR_INVALID_STATE ) ; return ; }
auto * menu = static_cast < Fl_Menu_Bar * > ( widget ) ;
const char * label = menu - > mvalue ( ) - > label ( ) ;
xtc_result r ;
2026-11-05 23:53:03 +01:00
if ( std : : strcmp ( label , " Insert stereo delay on wire " ) = = 0 | | std : : strcmp ( label , " Insert stereo reverb on wire " ) = = 0 ) {
xtc_connection_info wire { } ;
r = w - > graph_widget_ - > selected_connection ( wire ) ? w - > insert_plugin ( wire , XTC_BUILTIN_PATH , std : : strcmp ( label , " Insert stereo delay on wire " ) = = 0 ? 5U : 6U ) : XTC_ERR_INVALID_STATE ;
} else if ( std : : strcmp ( label , " Add stereo delay " ) = = 0 | | std : : strcmp ( label , " Add stereo reverb " ) = = 0 ) {
r = w - > add_plugin ( XTC_BUILTIN_PATH , std : : strcmp ( label , " Add stereo delay " ) = = 0 ? 5U : 6U ) ;
} else if ( std : : strcmp ( label , " Insert internal delay on wire " ) = = 0 | | std : : strcmp ( label , " Insert internal reverb on wire " ) = = 0 ) {
2026-10-28 17:16:01 +01:00
xtc_connection_info wire { } ;
r = w - > graph_widget_ - > selected_connection ( wire ) ? w - > insert_plugin ( wire , XTC_BUILTIN_PATH , std : : strcmp ( label , " Insert internal delay on wire " ) = = 0 ? 2U : 3U ) : XTC_ERR_INVALID_STATE ;
} else if ( std : : strcmp ( label , " Add internal delay " ) = = 0 | | std : : strcmp ( label , " Add internal reverb " ) = = 0 ) {
size_t id = 0 ; r = w - > add_plugin ( XTC_BUILTIN_PATH , std : : strcmp ( label , " Add internal delay " ) = = 0 ? 2U : 3U , & id ) ;
} else if ( std : : strcmp ( label , " Reset selected plugin " ) = = 0 ) {
2026-10-27 16:24:21 +01:00
r = w - > reset_plugin ( w - > graph_widget_ - > selected_node ( ) ) ;
2026-10-27 17:34:46 +01:00
} else if ( std : : strcmp ( label , " Load plugin " ) = = 0 | |
2026-10-27 16:24:21 +01:00
std : : strcmp ( label , " Replace selected plugin " ) = = 0 | |
2026-10-27 17:34:46 +01:00
std : : strcmp ( label , " Insert plugin on selected wire " ) = = 0 ) {
2026-10-27 16:24:21 +01:00
const size_t selected = w - > graph_widget_ - > selected_node ( ) ;
xtc_connection_info wire { } ;
2026-10-27 17:34:46 +01:00
const bool insert = std : : strcmp ( label , " Insert plugin on selected wire " ) = = 0 ;
2026-10-27 16:24:21 +01:00
const bool replace = std : : strcmp ( label , " Replace selected plugin " ) = = 0 ;
if ( insert & & ! w - > graph_widget_ - > selected_connection ( wire ) ) {
w - > report_error ( " Select an existing wire before inserting an effect " , XTC_ERR_INVALID_STATE ) ; return ;
}
if ( replace ) {
xtc_plugin_spec old { } ;
r = xtc_graph_get_plugin ( xtc_project_graph ( w - > project_ . get ( ) ) , selected , & old ) ;
2026-10-27 17:34:46 +01:00
if ( r ! = XTC_OK ) { w - > report_error ( " Select a plugin node to replace " , r ) ; return ; }
2026-10-27 16:24:21 +01:00
}
2026-10-28 10:37:01 +01:00
std : : string module ; if ( ! xtc_choose_plugin_file ( w - > settings_ , module ) ) return ;
double rate = w - > sample_rate ( ) ; xtc_backend_status status { } ;
2026-10-27 04:00:27 +01:00
if ( w - > ready_ & & xtc_backend_get_status ( w - > backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
2026-10-27 16:24:21 +01:00
unsigned int index = 0 ;
xtc_plugin_spec probe { } ;
2026-10-28 10:37:01 +01:00
r = xtc_plugin_probe ( module . c_str ( ) , 0 , rate , & probe ) ;
2026-10-27 16:24:21 +01:00
if ( r ! = XTC_OK & & r ! = XTC_ERR_UNSUPPORTED ) {
2026-10-27 17:34:46 +01:00
w - > report_error ( " Cannot discover plugin; check binary path, format, mono layout and dependencies " , r ) ; return ;
2026-10-27 04:00:27 +01:00
}
2026-10-28 10:37:01 +01:00
if ( ! xtc_choose_plugin ( module . c_str ( ) , rate , index ) ) return ;
if ( replace ) r = w - > replace_plugin ( selected , module . c_str ( ) , index ) ;
else if ( insert ) r = w - > insert_plugin ( wire , module . c_str ( ) , index ) ;
else r = w - > add_plugin ( module . c_str ( ) , index ) ;
2026-10-27 04:00:27 +01:00
} else {
size_t id = w - > graph_widget_ - > selected_node ( ) ; xtc_plugin_spec spec { } ;
r = xtc_graph_get_plugin ( xtc_project_graph ( w - > project_ . get ( ) ) , id , & spec ) ;
if ( r = = XTC_OK ) {
std : : string prompt = std : : string ( spec . label ) + " control ports and values: \n " ;
2026-10-27 16:24:21 +01:00
if ( std : : strcmp ( label , " Selected plugin control " ) = = 0 ) {
2026-10-28 10:37:01 +01:00
double rate = w - > sample_rate ( ) ; xtc_backend_status status { } ;
2026-10-27 16:24:21 +01:00
if ( w - > ready_ & & xtc_backend_get_status ( w - > backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
xtc_plugin_details details { } ; xtc_plugin_spec edited { } ;
r = xtc_plugin_describe ( & spec , rate , & details ) ;
if ( r = = XTC_OK & & xtc_edit_plugin ( spec , details , edited ) ) r = w - > apply_plugin_spec ( id , edited ) ;
if ( r ! = XTC_OK ) w - > report_error ( " Plugin editor failed; configuration retained. Check module identity and control ranges " , r ) ;
return ;
}
2026-10-27 04:00:27 +01:00
xtc_parameter_info info [ XTC_PLUGIN_CONTROLS ] { } ;
2026-10-28 10:37:01 +01:00
double rate = w - > sample_rate ( ) ; xtc_backend_status status { } ;
2026-10-27 04:00:27 +01:00
if ( w - > ready_ & & xtc_backend_get_status ( w - > backend_ , & status ) = = XTC_OK ) rate = status . sample_rate ;
r = xtc_plugin_parameters ( & spec , rate , info ) ;
if ( r ! = XTC_OK ) { w - > report_error ( " Read plugin controls failed " , r ) ; return ; }
for ( unsigned i = 0 ; i < spec . control_count ; + + i )
prompt + = std : : to_string ( spec . ports [ i ] ) + " : " + info [ i ] . name + " = " + std : : to_string ( spec . values [ i ] ) +
" [ " + std : : to_string ( info [ i ] . minimum ) + " , " + std : : to_string ( info [ i ] . maximum ) + " ] \n " ;
2026-10-27 06:25:30 +01:00
if ( std : : strcmp ( label , " Selected plugin automation " ) = = 0 ) {
prompt + = " Enter port tick mode value; mode 0 linear, 1 step, -1 remove. \n Current tick: " +
std : : to_string ( w - > sequencer_ - > position ( ) ) ;
const char * answer = fl_input ( " %s " , " " , prompt . c_str ( ) ) ;
if ( ! answer ) return ;
unsigned port ; unsigned long long tick ; int mode ; float value ; char extra ;
if ( std : : sscanf ( answer , " %u %llu %d %f %c " , & port , & tick , & mode , & value , & extra ) ! = 4 | | mode < - 1 | | mode > 1 )
r = XTC_ERR_INVALID_ARGUMENT ;
else {
xtc_plugin_point point { static_cast < uint64_t > ( tick ) , port , mode = = 1 ? 1U : 0U , value } ;
r = w - > set_plugin_automation ( id , point , mode = = - 1 ) ;
}
if ( r ! = XTC_OK ) w - > report_error ( " Plugin automation rejected: check tick, port, range and discrete mode " , r ) ;
return ;
}
2026-10-27 04:00:27 +01:00
}
}
if ( r ! = XTC_OK ) w - > report_error ( " Plugin change failed: check module, mono layout, control port/range and JACK format " , r ) ;
}
2026-10-28 10:37:01 +01:00
void MainWindow : : project_defaults ( xtc_project * project )
{
( void ) xtc_project_set_tempo ( project , static_cast < unsigned int > ( settings_ . tempo ) ) ;
unsigned int cc [ 3 ] = { static_cast < unsigned int > ( settings_ . volume_cc ) , static_cast < unsigned int > ( settings_ . pan_cc ) , static_cast < unsigned int > ( settings_ . sustain_cc ) } ;
for ( unsigned int channel = 0 ; channel < 16 ; + + channel ) ( void ) xtc_project_set_control_map ( project , channel , cc ) ;
}
double MainWindow : : sample_rate ( ) const
{
xtc_backend_status status { } ;
return ready_ & & xtc_backend_get_status ( backend_ , & status ) = = XTC_OK ? status . sample_rate : static_cast < double > ( settings_ . offline_rate ) ;
}
xtc_result MainWindow : : configure_loop ( const xtc_project * project , const xtc_settings & settings )
{
if ( ! ready_ ) return XTC_OK ;
uint64_t start , end ; unsigned int rate = static_cast < unsigned int > ( sample_rate ( ) ) ;
xtc_result r = xtc_project_tick_to_sample ( project , static_cast < uint64_t > ( settings . loop_start ) , rate , & start ) ;
if ( r = = XTC_OK ) r = xtc_project_tick_to_sample ( project , static_cast < uint64_t > ( settings . loop_end ) , rate , & end ) ;
if ( r = = XTC_OK ) r = xtc_backend_set_loop ( backend_ , start , end , settings . loop_enabled ! = 0 ) ;
return r ;
}
xtc_result MainWindow : : set_loop ( uint64_t start , uint64_t end , bool enabled )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
if ( start > = end | | end > XTC_PROJECT_MAX_TICK ) return XTC_ERR_INVALID_ARGUMENT ;
xtc_settings candidate = settings_ ; candidate . loop_start = static_cast < int64_t > ( start ) ; candidate . loop_end = static_cast < int64_t > ( end ) ; candidate . loop_enabled = enabled ;
bool resume = running_ ; xtc_result r = ready_ ? xtc_backend_stop ( backend_ ) : XTC_OK ;
if ( r = = XTC_OK ) r = configure_loop ( project_ . get ( ) , candidate ) ;
if ( r = = XTC_OK ) { settings_ = candidate ; sequencer_ - > loop_region ( start , end , enabled ) ; }
if ( ready_ & & resume & & r = = XTC_OK ) { xtc_result restarted = xtc_backend_start ( backend_ ) ; if ( r = = XTC_OK ) r = restarted ; }
refresh_status ( ) ; return r ;
}
void MainWindow : : collect_preferences ( )
{
2026-11-05 23:57:56 +01:00
settings_ . transport_detached = transport_detached_ ; settings_ . mixer_detached = mixer_detached_ ;
if ( transport_window_ ) { settings_ . transport_x = transport_window_ - > x ( ) ; settings_ . transport_y = transport_window_ - > y ( ) ; settings_ . transport_width = transport_window_ - > w ( ) ; settings_ . transport_height = transport_window_ - > h ( ) ; }
if ( mixer_window_ ) { settings_ . mixer_x = mixer_window_ - > x ( ) ; settings_ . mixer_y = mixer_window_ - > y ( ) ; settings_ . mixer_width = mixer_window_ - > w ( ) ; settings_ . mixer_height = mixer_window_ - > h ( ) ; }
2026-10-28 10:37:01 +01:00
settings_ . width = w ( ) ; settings_ . height = h ( ) ; settings_ . sequencer_snap = static_cast < int64_t > ( sequencer_ - > grid ( ) ) ; settings_ . sequencer_zoom = sequencer_ - > zoom_level ( ) ;
if ( piano_ ) {
settings_ . piano_width = piano_window_ - > w ( ) ; settings_ . piano_height = piano_window_ - > h ( ) ; settings_ . piano_snap = static_cast < int64_t > ( piano_ - > grid ( ) ) ;
settings_ . filter_channel = piano_ - > filter_channel ( ) ; settings_ . filter_low = piano_ - > filter_low ( ) ; settings_ . filter_high = piano_ - > filter_high ( ) ;
}
}
void MainWindow : : apply_ui_preferences ( )
{
2026-11-05 23:57:56 +01:00
bool floating_transport = settings_ . transport_detached ! = 0 , floating_mixer = settings_ . mixer_detached ! = 0 ;
detach_transport ( floating_transport ) ; detach_mixer ( floating_mixer ) ;
if ( transport_window_ ) restore_panel ( transport_window_ . get ( ) , static_cast < int > ( settings_ . transport_x ) , static_cast < int > ( settings_ . transport_y ) , static_cast < int > ( settings_ . transport_width ) , static_cast < int > ( settings_ . transport_height ) , 400 , 130 ) ;
if ( mixer_window_ ) restore_panel ( mixer_window_ . get ( ) , static_cast < int > ( settings_ . mixer_x ) , static_cast < int > ( settings_ . mixer_y ) , static_cast < int > ( settings_ . mixer_width ) , static_cast < int > ( settings_ . mixer_height ) , 600 , 420 ) ;
2026-10-28 17:16:01 +01:00
follow_ - > value ( static_cast < int > ( settings_ . follow_playhead ) ) ;
2026-10-28 10:37:01 +01:00
resize ( x ( ) , y ( ) , static_cast < int > ( settings_ . width ) , static_cast < int > ( settings_ . height ) ) ;
sequencer_ - > grid ( static_cast < uint64_t > ( settings_ . sequencer_snap ) ) ; sequencer_ - > set_zoom ( static_cast < int > ( settings_ . sequencer_zoom ) ) ;
sequencer_ - > new_clip_ticks = static_cast < uint64_t > ( settings_ . clip_ticks ) ;
sequencer_ - > loop_region ( static_cast < uint64_t > ( settings_ . loop_start ) , static_cast < uint64_t > ( settings_ . loop_end ) , settings_ . loop_enabled ! = 0 ) ;
if ( piano_ ) {
piano_window_ - > resize ( piano_window_ - > x ( ) , piano_window_ - > y ( ) , static_cast < int > ( settings_ . piano_width ) , static_cast < int > ( settings_ . piano_height ) ) ;
piano_ - > set_grid ( static_cast < uint64_t > ( settings_ . piano_snap ) ) ; piano_ - > set_filter ( static_cast < int > ( settings_ . filter_channel ) , static_cast < unsigned int > ( settings_ . filter_low ) , static_cast < unsigned int > ( settings_ . filter_high ) ) ;
piano_ - > new_note_ticks = static_cast < uint64_t > ( settings_ . note_ticks ) ; piano_ - > new_note_velocity = static_cast < unsigned int > ( settings_ . note_velocity ) ; piano_ - > new_note_channel = static_cast < unsigned int > ( settings_ . note_channel ) ;
}
}
xtc_result MainWindow : : apply_preferences ( const xtc_settings & candidate )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
xtc_result r = xtc_settings_validate ( & candidate ) ; if ( r ! = XTC_OK ) return r ;
if ( configuration_ . empty ( ) ) return XTC_ERR_INVALID_STATE ;
if ( ready_ ) r = xtc_backend_stop ( backend_ ) ;
if ( r = = XTC_OK ) r = configure_loop ( project_ . get ( ) , candidate ) ;
if ( r = = XTC_OK ) r = xtc_settings_save ( & candidate , configuration_ . c_str ( ) ) ;
if ( r ! = XTC_OK ) { if ( ready_ ) ( void ) configure_loop ( project_ . get ( ) , settings_ ) ; refresh_status ( ) ; return r ; }
settings_ = candidate ; apply_ui_preferences ( ) ; refresh_status ( ) ; return XTC_OK ;
}
void MainWindow : : show_preferences ( )
{
if ( recorder_ ) { report_error ( " Stop recording before preferences " , XTC_ERR_INVALID_STATE ) ; return ; }
collect_preferences ( ) ; PreferencesDialog dialog ( settings_ , configuration_ , [ this ] ( const xtc_settings & s ) { return apply_preferences ( s ) ; } ) ; dialog . run ( ) ;
}
2026-10-28 17:16:01 +01:00
xtc_result MainWindow : : set_tempo ( unsigned int milli_bpm )
{
if ( recorder_ )
return XTC_ERR_INVALID_STATE ;
if ( milli_bpm < 20000 | | milli_bpm > 300000 )
return XTC_ERR_INVALID_ARGUMENT ;
if ( milli_bpm = = xtc_project_tempo ( project_ . get ( ) ) )
return XTC_OK ;
uint64_t track = sequencer_ - > selected_track ( ) , clip = sequencer_ - > selected_clip ( ) , mix = mixer_ - > selected_track ( ) ;
uint64_t piano_clip = piano_ ? piano_ - > clip ( ) : UINT64_MAX , note = piano_ ? piano_ - > selected_note ( ) : UINT64_MAX ;
bool piano_visible = piano_window_ & & piano_window_ - > shown ( ) ;
xtc_project * candidate = nullptr ;
xtc_result r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
if ( r = = XTC_OK )
r = xtc_project_set_tempo ( candidate , milli_bpm ) ;
if ( r = = XTC_OK )
r = xtc_history_reserve ( history_ . get ( ) ) ;
if ( r = = XTC_OK )
r = install_project ( candidate , false ) ;
if ( r ! = XTC_OK )
xtc_project_destroy ( candidate ) ;
else {
sequencer_ - > select ( track , clip ) ;
mixer_ - > select_track ( mix ) ;
xtc_clip_info info { } ;
if ( piano_ & & xtc_project_find_clip ( project_ . get ( ) , piano_clip , & info ) = = XTC_OK ) {
piano_ - > set_project ( project_ . get ( ) , piano_clip ) ;
piano_ - > select_note ( note ) ;
if ( piano_visible )
piano_window_ - > show ( ) ;
}
mark_dirty ( ) ;
}
return r ;
}
void MainWindow : : inspector_callback ( Fl_Widget * widget , void * context )
{
auto * w = static_cast < MainWindow * > ( context ) ;
const uint64_t id = w - > inspected_track_ ;
float gain = static_cast < float > ( w - > inspector_gain_ - > value ( ) ) , pan = static_cast < float > ( w - > inspector_pan_ - > value ( ) ) ;
int mute = w - > inspector_mute_ - > value ( ) , solo = w - > inspector_solo_ - > value ( ) ;
// Editing a single field must not commit another field's unfinished input.
size_t count = 0 , clips = 0 ;
xtc_project_get_counts ( w - > project_ . get ( ) , & count , & clips ) ;
for ( size_t i = 0 ; i < count ; + + i ) {
xtc_track_info track { } ;
xtc_project_get_track ( w - > project_ . get ( ) , i , & track ) ;
if ( track . id ! = id )
continue ;
if ( widget ! = w - > inspector_gain_ )
gain = track . gain ;
if ( widget ! = w - > inspector_pan_ )
pan = track . pan ;
if ( widget ! = w - > inspector_mute_ )
mute = track . mute ;
if ( widget ! = w - > inspector_solo_ )
solo = track . solo ;
if ( gain ! = track . gain | | pan ! = track . pan | | mute ! = track . mute | | solo ! = track . solo )
if ( w - > mixer_ - > select_track ( id ) )
w - > mixer_ - > mix ( gain , pan , mute , solo ) ;
break ;
}
w - > inspected_track_ = UINT64_MAX ;
w - > refresh_status ( ) ;
}
void MainWindow : : show_commands ( )
{
std : : vector < std : : string > commands ;
for ( int i = 0 ; i < menu_ - > size ( ) ; + + i ) {
const auto & item = menu_ - > menu ( ) [ i ] ;
if ( ! item . label ( ) | | item . submenu ( ) | | ! item . active ( ) )
continue ;
char path [ 512 ] ;
if ( menu_ - > item_pathname ( path , sizeof ( path ) , & item ) = = 0 )
commands . emplace_back ( path ) ;
}
std : : string selected ;
{
CommandPalette palette ( std : : move ( commands ) ) ;
selected = palette . run ( ) ;
}
if ( ! selected . empty ( ) ) {
auto * item = menu_ - > find_item ( selected . c_str ( ) ) ;
if ( item & & item - > active ( ) )
menu_ - > picked ( item ) ;
}
}
void MainWindow : : fit_song ( )
{
size_t tracks = 0 , clips = 0 ;
uint64_t end = 3840 ;
xtc_project_get_counts ( project_ . get ( ) , & tracks , & clips ) ;
for ( size_t i = 0 ; i < clips ; + + i ) {
xtc_clip_info clip { } ;
xtc_project_get_clip ( project_ . get ( ) , i , & clip ) ;
end = std : : max ( end , clip . start_tick + clip . length_tick + 960 ) ;
}
int scale = 24 ;
for ( int candidate : { 24 , 48 , 96 , 192 , 384 } )
if ( end * static_cast < unsigned int > ( candidate ) / 960 < =
static_cast < uint64_t > ( std : : max ( 1 , timeline_scroll_ - > w ( ) - 204 ) ) )
scale = candidate ;
sequencer_ - > set_zoom ( scale ) ;
timeline_scroll_ - > scroll_to ( 0 , 0 ) ;
timeline_scroll_ - > redraw ( ) ;
}
void MainWindow : : follow_playhead ( )
{
if ( ! running_ | | ! follow_ - > value ( ) | | tabs_ - > value ( ) ! = tabs_ - > child ( 0 ) | | Fl : : pushed ( ) )
return ;
int pixel = static_cast < int > ( sequencer_ - > position ( ) * static_cast < unsigned int > ( sequencer_ - > zoom_level ( ) ) / 960 ) ;
int screen = sequencer_ - > x ( ) + 180 + pixel ;
if ( screen > timeline_scroll_ - > x ( ) + timeline_scroll_ - > w ( ) - 48 | | screen < timeline_scroll_ - > x ( ) + 180 )
timeline_scroll_ - > scroll_to ( std : : max ( 0 , pixel - 160 ) , timeline_scroll_ - > yposition ( ) ) ;
}