2026-11-06 00:17:23 +01:00
# include <sstream>
2026-10-27 00:10:45 +01:00
# include "sequencer_widget.hpp"
2026-10-27 18:04:39 +01:00
# include "theme.hpp"
2026-10-28 10:08:13 +01:00
# include "snap.hpp"
# include <cstring>
2026-10-27 00:53:41 +01:00
# include "xtc/xtc_midi.h"
2026-10-27 00:10:45 +01:00
# include <FL/Fl.H>
# include <FL/Fl_Scroll.H>
# include <FL/fl_draw.H>
# include <FL/fl_ask.H>
2026-10-27 06:28:25 +01:00
# include <FL/Fl_Menu_Item.H>
2026-10-27 00:10:45 +01:00
# include <algorithm>
# include <cstdio>
2026-10-27 15:46:41 +01:00
# include <sstream>
# include <new>
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Constructs the timeline widget with a borrowed project and seek callback .
*
* @ param left Left coordinate in pixels .
* @ param top Top coordinate in pixels .
* @ param project Borrowed project ; must outlive the widget ' s use of it .
* @ param seek Callback used to request a transport seek .
* @ param context Opaque callback context .
*/
2026-10-27 00:10:45 +01:00
SequencerWidget : : SequencerWidget ( int left , int top , xtc_project * project , Seek seek , void * context )
2026-10-28 10:08:13 +01:00
: Fl_Group ( left , top , 6300 , 3700 ) , project_ ( project ) , seek_ ( seek ) , context_ ( context )
2026-10-27 01:37:21 +01:00
{
2026-10-28 10:08:13 +01:00
resizable ( nullptr ) ;
2026-10-27 01:37:21 +01:00
extent ( ) ;
2026-10-28 10:08:13 +01:00
end ( ) ;
2026-10-27 01:37:21 +01:00
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Replaces the borrowed project and refreshes the widget ' s displayed state .
*
* @ param project Borrowed project ; must outlive the widget ' s use of it .
* @ param reset Whether to clear selection , cursor , and drag state .
*/
2026-10-27 16:49:30 +01:00
bool SequencerWidget : : select ( uint64_t track , uint64_t clip )
{
size_t tracks = 0 , clips = 0 ;
xtc_project_get_counts ( project_ , & tracks , & clips ) ;
bool found = false ;
for ( size_t i = 0 ; i < tracks ; + + i ) {
xtc_track_info info { } ;
if ( xtc_project_get_track ( project_ , i , & info ) = = XTC_OK & & info . id = = track ) found = true ;
}
if ( ! found ) return false ;
if ( clip ! = UINT64_MAX ) {
xtc_clip_info info { } ;
if ( xtc_project_find_clip ( project_ , clip , & info ) ! = XTC_OK | | info . track_id ! = track ) return false ;
}
2026-11-06 00:12:37 +01:00
selection_ . clear ( ) ;
2026-10-27 16:49:30 +01:00
track_ = track ; clip_ = clip ; mode_ = 0 ; redraw ( ) ;
return true ;
}
2026-10-27 00:10:45 +01:00
void SequencerWidget : : set_project ( xtc_project * project , bool reset )
{
project_ = project ;
2026-10-27 01:37:21 +01:00
if ( reset ) {
2026-10-27 15:46:41 +01:00
waves_ . clear ( ) ;
2026-11-06 00:12:37 +01:00
selection_ . clear ( ) ;
2026-10-27 01:37:21 +01:00
track_ = clip_ = UINT64_MAX ;
cursor_ = 0 ;
mode_ = 0 ;
}
2026-10-27 15:46:41 +01:00
refresh_waveforms ( ) ;
2026-10-27 01:37:21 +01:00
extent ( ) ;
redraw ( ) ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
2026-10-28 10:08:13 +01:00
* @ brief Converts a screen x coordinate to a bounded timeline tick snapped to the selected grid .
2026-10-27 01:58:27 +01:00
*
* @ param px Screen x coordinate in pixels .
* @ return Snapped tick within the supported project range .
*/
2026-11-06 00:06:56 +01:00
int SequencerWidget : : header_left ( ) const
{
auto * scroll = dynamic_cast < Fl_Scroll * > ( parent ( ) ) ; return scroll ? std : : max ( x ( ) , scroll - > x ( ) ) : x ( ) ;
}
void SequencerWidget : : header_width ( int width )
{
width = std : : clamp ( width , 180 , 360 ) ; if ( width = = header_width_ ) return ;
auto * scroll = dynamic_cast < Fl_Scroll * > ( parent ( ) ) ; int sx = scroll ? scroll - > xposition ( ) : 0 , sy = scroll ? scroll - > yposition ( ) : 0 ;
header_width_ = width ; extent ( ) ; if ( scroll ) scroll - > scroll_to ( sx , sy ) ; redraw ( ) ;
}
2026-10-27 00:10:45 +01:00
uint64_t SequencerWidget : : tick ( int px ) const
{
2026-11-06 00:06:56 +01:00
int64_t raw = static_cast < int64_t > ( std : : max ( 0 , px - x ( ) - header_width_ ) ) * 960 / scale_ ;
2026-10-28 10:08:13 +01:00
return std : : min ( XTC_PROJECT_MAX_TICK , static_cast < uint64_t > ( xtc_ui : : snap_position ( raw , grid_ ) ) ) ;
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 Resizes the timeline ' s scrollable area to fit its tracks and clip ends .
*/
2026-10-27 00:10:45 +01:00
void SequencerWidget : : extent ( )
{
size_t tracks , clips ;
xtc_project_get_counts ( project_ , & tracks , & clips ) ;
uint64_t end = 32U * 3840U ;
for ( size_t i = 0 ; i < clips ; + + i ) {
2026-10-27 01:37:21 +01:00
xtc_clip_info c { } ;
xtc_project_get_clip ( project_ , i , & c ) ;
2026-10-27 00:10:45 +01:00
end = std : : max ( end , c . start_tick + c . length_tick + 3840U ) ;
}
2026-11-06 00:06:56 +01:00
size ( static_cast < int > ( end * static_cast < unsigned int > ( scale_ ) / 960U + static_cast < unsigned int > ( header_width_ + 20 ) ) ,
2026-10-27 00:10:45 +01:00
std : : max ( 400 , static_cast < int > ( tracks ) * 56 + 100 ) ) ;
2026-10-28 10:08:13 +01:00
refresh_instruments ( ) ;
2026-10-27 01:37:21 +01:00
if ( parent ( ) )
parent ( ) - > redraw ( ) ;
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 Reports an edit result , refreshes the widget , and notifies listeners on success .
*
* @ param result Result of the attempted operation .
*/
2026-10-27 00:10:45 +01:00
void SequencerWidget : : changed ( xtc_result result )
{
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK )
fl_alert ( " %s " , xtc_result_string ( result ) ) ;
else {
2026-10-27 15:46:41 +01:00
refresh_waveforms ( ) ;
2026-10-27 01:37:21 +01:00
extent ( ) ;
do_callback ( ) ;
}
2026-10-27 00:10:45 +01:00
redraw ( ) ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Creates and selects a track through the arrangement edit hook .
* @ return XTC_OK on success , or an error code describing the failure .
*/
2026-10-27 00:10:45 +01:00
xtc_result SequencerWidget : : add_track ( )
{
2026-10-27 01:37:21 +01:00
if ( before_edit ) {
xtc_result r = before_edit ( context_ ) ;
if ( r ! = XTC_OK ) {
changed ( r ) ;
return r ;
}
}
uint64_t id ;
xtc_result r = xtc_project_add_track ( project_ , " Track " , & id ) ;
if ( r = = XTC_OK ) {
track_ = id ;
clip_ = UINT64_MAX ;
}
changed ( r ) ;
return r ;
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 Creates and selects a one - bar silent region at the timeline cursor .
* @ return XTC_OK on success , or an error code describing the failure .
*/
2026-10-27 00:10:45 +01:00
xtc_result SequencerWidget : : add_region ( )
{
2026-10-27 01:37:21 +01:00
if ( before_edit ) {
xtc_result r = before_edit ( context_ ) ;
if ( r ! = XTC_OK ) {
changed ( r ) ;
return r ;
}
}
size_t tracks , clips ;
xtc_project_get_counts ( project_ , & tracks , & clips ) ;
2026-10-27 00:10:45 +01:00
if ( track_ = = UINT64_MAX & & tracks ) {
2026-10-27 01:37:21 +01:00
xtc_track_info t { } ;
xtc_project_get_track ( project_ , 0 , & t ) ;
track_ = t . id ;
2026-10-27 00:10:45 +01:00
}
uint64_t id ;
2026-10-28 10:37:01 +01:00
xtc_result r = xtc_project_add_clip ( project_ , track_ , " Region (silent) " , cursor_ , new_clip_ticks , & id ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
clip_ = id ;
changed ( r ) ;
return r ;
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 Creates and selects a one - bar MIDI clip at the timeline cursor .
* @ return XTC_OK on success , or an error code describing the failure .
*/
2026-10-27 00:53:41 +01:00
xtc_result SequencerWidget : : add_midi_clip ( )
{
xtc_result r = before_edit ? before_edit ( context_ ) : XTC_OK ;
uint64_t id ;
if ( r = = XTC_OK ) {
if ( track_ = = UINT64_MAX ) {
xtc_track_info t { } ;
2026-10-27 01:37:21 +01:00
r = xtc_project_get_track ( project_ , 0 , & t ) ;
track_ = r = = XTC_OK ? t . id : UINT64_MAX ;
2026-10-27 00:53:41 +01:00
}
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
2026-10-28 10:37:01 +01:00
r = xtc_project_add_midi_clip ( project_ , track_ , cursor_ , new_clip_ticks , & id ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
clip_ = id ;
2026-10-27 00:53:41 +01:00
}
2026-10-27 01:37:21 +01:00
changed ( r ) ;
return r ;
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 Removes the selected clip , or the selected track when no clip is selected .
* @ return XTC_OK on success , or an error code describing the failure .
*/
2026-10-27 00:10:45 +01:00
xtc_result SequencerWidget : : delete_selection ( )
{
2026-11-06 00:13:48 +01:00
if ( ! selected_clips ( ) . empty ( ) ) return transform_selection ( XTC_CLIPS_DELETE , 0 ) ;
xtc_result r = before_edit ? before_edit ( context_ ) : XTC_OK ;
if ( r = = XTC_OK ) r = xtc_project_remove_track ( project_ , track_ ) ;
if ( r = = XTC_OK ) track_ = clip_ = UINT64_MAX ;
changed ( r ) ; return r ;
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 Adjusts timeline zoom within its supported bounds and updates the scroll extent .
*
* @ param direction Zoom direction ; positive zooms in , nonpositive zooms out .
*/
2026-10-27 00:10:45 +01:00
void SequencerWidget : : zoom ( int direction )
2026-10-27 01:37:21 +01:00
{
scale_ = std : : clamp ( direction > 0 ? scale_ * 2 : scale_ / 2 , 24 , 384 ) ;
extent ( ) ;
redraw ( ) ;
}
2026-10-27 01:58:27 +01:00
/**
* @ brief Draws the widget ' s current model , selection , and interaction feedback .
*/
2026-10-27 00:10:45 +01:00
void SequencerWidget : : draw ( )
{
2026-10-27 18:04:39 +01:00
fl_push_clip ( x ( ) , y ( ) , w ( ) , h ( ) ) ;
2026-10-27 01:37:21 +01:00
int cx , cy , cw , ch ;
2026-10-27 18:04:39 +01:00
fl_clip_box ( x ( ) , y ( ) , w ( ) , h ( ) , cx , cy , cw , ch ) ;
fl_color ( xtc_ui : : background ( ) ) ; fl_rectf ( cx , cy , cw , ch ) ;
size_t tracks = 0 , clips = 0 , notes = 0 ;
xtc_project_get_counts ( project_ , & tracks , & clips ) ; xtc_project_note_count ( project_ , & notes ) ;
2026-10-27 00:10:45 +01:00
for ( size_t i = 0 ; i < tracks ; + + i ) {
2026-10-27 18:04:39 +01:00
int py = y ( ) + 28 + static_cast < int > ( i ) * 56 ;
if ( py + 56 < cy | | py > cy + ch ) continue ;
fl_color ( i % 2 ? fl_rgb_color ( 30 , 34 , 40 ) : fl_rgb_color ( 33 , 37 , 43 ) ) ;
2026-11-06 00:06:56 +01:00
fl_rectf ( x ( ) + header_width_ , py , w ( ) - header_width_ , 55 ) ;
2026-10-27 18:04:39 +01:00
}
fl_font ( FL_HELVETICA , 11 ) ;
2026-10-28 10:08:13 +01:00
uint64_t visual = grid_ = = 1 ? 960 : grid_ ;
while ( visual * static_cast < unsigned int > ( scale_ ) / 960 < 6 ) visual * = 2 ;
int spacing = static_cast < int > ( visual * static_cast < unsigned int > ( scale_ ) / 960 ) ;
2026-11-06 00:06:56 +01:00
int first = std : : max ( 0 , ( cx - x ( ) - header_width_ ) / spacing ) ;
int last = ( cx + cw - x ( ) - header_width_ ) / spacing + 1 ;
2026-10-28 10:08:13 +01:00
for ( int step = first ; step < = last ; + + step ) {
uint64_t tick = static_cast < uint64_t > ( step ) * visual ;
2026-11-06 00:06:56 +01:00
int px = x ( ) + header_width_ + static_cast < int > ( tick * static_cast < unsigned int > ( scale_ ) / 960 ) ;
2026-10-28 10:08:13 +01:00
fl_color ( tick % 3840 = = 0 ? fl_rgb_color ( 76 , 84 , 96 ) : tick % 960 = = 0 ? fl_rgb_color ( 56 , 63 , 73 ) : fl_rgb_color ( 42 , 48 , 56 ) ) ;
2026-10-27 18:04:39 +01:00
fl_line ( px , y ( ) + 28 , px , y ( ) + h ( ) ) ;
2026-10-28 10:08:13 +01:00
}
2026-11-06 00:06:56 +01:00
first = std : : max ( 0 , ( cx - x ( ) - header_width_ ) / ( scale_ * 4 ) ) ;
last = ( cx + cw - x ( ) - header_width_ ) / ( scale_ * 4 ) + 1 ;
2026-10-28 10:08:13 +01:00
for ( int bar = first ; bar < = last ; + + bar ) {
char label [ 32 ] ; std : : snprintf ( label , sizeof ( label ) , " %d " , bar + 1 ) ;
2026-11-06 00:06:56 +01:00
fl_color ( xtc_ui : : muted ( ) ) ; fl_draw ( label , x ( ) + header_width_ + bar * scale_ * 4 + 6 , y ( ) + 19 ) ;
2026-10-27 18:04:39 +01:00
}
for ( size_t i = 0 ; i < tracks ; + + i ) {
xtc_track_info t { } ; xtc_project_get_track ( project_ , i , & t ) ;
int py = y ( ) + 28 + static_cast < int > ( i ) * 56 ;
if ( py + 56 < cy | | py > cy + ch ) continue ;
Fl_Color color = xtc_ui : : track ( t . id ) ;
2026-10-27 00:10:45 +01:00
for ( size_t j = 0 ; j < clips ; + + j ) {
2026-10-27 18:04:39 +01:00
xtc_clip_info c { } ; xtc_project_get_clip ( project_ , j , & c ) ;
if ( c . track_id ! = t . id ) continue ;
2026-11-06 00:06:56 +01:00
int left = x ( ) + header_width_ + static_cast < int > ( c . start_tick * static_cast < unsigned int > ( scale_ ) / 960U ) ;
2026-10-27 18:04:39 +01:00
int right = left + std : : max ( 4 , static_cast < int > ( c . length_tick * static_cast < unsigned int > ( scale_ ) / 960U ) ) ;
int a = std : : max ( left , cx ) , b = std : : min ( right , cx + cw ) ;
if ( b < = a ) continue ;
2026-11-06 00:12:37 +01:00
fl_color ( fl_color_average ( color , xtc_ui : : background ( ) , clip_selected ( c . id ) ? .75f : .45f ) ) ;
2026-10-27 18:04:39 +01:00
fl_rectf ( a , py + 5 , b - a , 46 ) ;
2026-11-06 00:12:37 +01:00
fl_color ( clip_selected ( c . id ) ? xtc_ui : : text ( ) : color ) ; fl_rect ( a , py + 5 , b - a , 46 ) ;
2026-10-27 18:04:39 +01:00
fl_push_clip ( a + 1 , py + 6 , std : : max ( 0 , b - a - 2 ) , 44 ) ;
if ( c . midi ) {
/* Real note positions, pitch and duration; no placeholder waveform. */
unsigned int low = 127 , high = 0 ;
for ( size_t n = 0 ; n < notes ; + + n ) {
xtc_note_info note { } ; xtc_project_get_note ( project_ , n , & note ) ;
if ( note . clip_id = = c . id ) { low = std : : min ( low , note . pitch ) ; high = std : : max ( high , note . pitch ) ; }
}
unsigned int range = high > = low ? std : : max ( 12U , high - low ) : 12U ;
fl_color ( fl_color_average ( color , FL_WHITE , .55f ) ) ;
for ( size_t n = 0 ; n < notes ; + + n ) {
xtc_note_info note { } ; xtc_project_get_note ( project_ , n , & note ) ;
if ( note . clip_id ! = c . id ) continue ;
uint64_t begin = std : : max ( note . start_tick , c . offset_tick ) ;
uint64_t end = std : : min ( note . start_tick + note . length_tick , c . offset_tick + c . length_tick ) ;
if ( begin > = end ) continue ;
int nx = left + static_cast < int > ( ( begin - c . offset_tick ) * static_cast < unsigned int > ( scale_ ) / 960U ) ;
int nw = std : : max ( 2 , static_cast < int > ( ( end - begin ) * static_cast < unsigned int > ( scale_ ) / 960U ) ) ;
fl_rectf ( nx , py + 26 + static_cast < int > ( ( high - note . pitch ) * 18 / range ) , nw , 2 ) ;
}
} else draw_waveform ( c , left , py ) ;
fl_color ( fl_color_average ( color , xtc_ui : : background ( ) , .35f ) ) ; fl_rectf ( a + 1 , py + 6 , std : : max ( 0 , b - a - 2 ) , 15 ) ;
fl_font ( FL_HELVETICA , 11 ) ; fl_color ( xtc_ui : : text ( ) ) ;
fl_draw ( c . name , left + 7 , py + 18 ) ;
2026-10-27 01:37:21 +01:00
fl_pop_clip ( ) ;
2026-10-27 00:10:45 +01:00
}
}
2026-10-27 18:04:39 +01:00
if ( ! tracks ) {
fl_font ( FL_HELVETICA_BOLD , 18 ) ; fl_color ( xtc_ui : : text ( ) ) ;
fl_draw ( " Your next track starts here " , x ( ) + 220 , y ( ) + 105 ) ;
fl_font ( FL_HELVETICA , 13 ) ; fl_color ( xtc_ui : : muted ( ) ) ;
fl_draw ( " Create a MIDI track or import audio from the panel on the left. " , x ( ) + 220 , y ( ) + 135 ) ;
fl_draw ( " Arrange clips here. Open the piano roll to draw and edit MIDI notes. " , x ( ) + 220 , y ( ) + 161 ) ;
}
2026-10-28 10:37:01 +01:00
{
uint64_t begin = mode_ = = 4 ? std : : min ( loop_anchor_ , loop_preview_ ) : loop_begin_ ;
uint64_t end = mode_ = = 4 ? std : : max ( loop_anchor_ , loop_preview_ ) : loop_end_ ;
2026-11-06 00:06:56 +01:00
int left = x ( ) + header_width_ + static_cast < int > ( begin * static_cast < unsigned int > ( scale_ ) / 960 ) ;
int right = x ( ) + header_width_ + static_cast < int > ( end * static_cast < unsigned int > ( scale_ ) / 960 ) ;
2026-10-28 10:37:01 +01:00
fl_color ( loop_enabled_ ? xtc_ui : : accent ( ) : xtc_ui : : muted ( ) ) ;
fl_rectf ( left , y ( ) + 23 , std : : max ( 0 , right - left ) , 4 ) ;
}
2026-11-06 00:06:56 +01:00
int playhead = x ( ) + header_width_ + static_cast < int > ( cursor_ * static_cast < unsigned int > ( scale_ ) / 960U ) ;
2026-10-27 18:04:39 +01:00
if ( playhead > = cx & & playhead < = cx + cw ) {
fl_color ( xtc_ui : : accent ( ) ) ; fl_line ( playhead , y ( ) , playhead , y ( ) + h ( ) ) ;
fl_polygon ( playhead - 4 , y ( ) , playhead + 4 , y ( ) , playhead , y ( ) + 6 ) ;
2026-10-27 00:10:45 +01:00
}
2026-11-06 00:06:56 +01:00
int pinned = header_left ( ) ;
fl_color ( xtc_ui : : background ( ) ) ; fl_rectf ( pinned , y ( ) , header_width_ , 28 ) ;
fl_color ( xtc_ui : : muted ( ) ) ; fl_draw ( " TRACKS / drag edge to resize " , pinned + 10 , y ( ) + 19 ) ;
for ( size_t i = 0 ; i < tracks ; + + i ) {
xtc_track_info t { } ; xtc_project_get_track ( project_ , i , & t ) ; int py = y ( ) + 28 + static_cast < int > ( i ) * 56 ;
if ( py + 56 < cy | | py > cy + ch ) continue ;
Fl_Color color = xtc_ui : : track ( t . id ) ;
fl_color ( t . id = = track_ ? fl_rgb_color ( 49 , 62 , 70 ) : xtc_ui : : panel ( ) ) ;
fl_rectf ( pinned , py , header_width_ - 2 , 55 ) ;
fl_color ( color ) ; fl_rectf ( pinned , py , 3 , 55 ) ;
fl_push_clip ( pinned + 9 , py , header_width_ - 18 , 55 ) ;
fl_color ( xtc_ui : : text ( ) ) ; fl_font ( FL_HELVETICA_BOLD , 12 ) ;
fl_draw ( t . name , pinned + 12 , py + 18 ) ;
char detail [ 64 ] ;
std : : snprintf ( detail , sizeof ( detail ) , " %02zu %s%s%s " , i + 1 , t . is_bus ? " BUS " : " TRACK " ,
t . mute ? " MUTE " : " " , t . solo ? " SOLO " : " " ) ;
if ( t . is_bus ) { fl_font ( FL_HELVETICA , 10 ) ; fl_color ( xtc_ui : : muted ( ) ) ; fl_draw ( detail , pinned + 12 , py + 42 ) ; }
if ( t . mute | | t . solo ) { fl_font ( FL_HELVETICA , 10 ) ; fl_color ( xtc_ui : : muted ( ) ) ; fl_draw ( t . mute ? " M " : " S " , pinned + header_width_ - 16 , py + 18 ) ; }
fl_pop_clip ( ) ;
}
fl_color ( xtc_ui : : muted ( ) ) ; fl_rectf ( pinned + header_width_ - 3 , cy , 3 , ch ) ;
for ( size_t i = 0 ; i < track_choices_ . size ( ) ; + + i ) if ( track_choices_ [ i ] . widget )
track_choices_ [ i ] . widget - > resize ( pinned + 10 , y ( ) + 58 + static_cast < int > ( i ) * 56 , header_width_ - 22 , 20 ) ;
2026-10-28 10:08:13 +01:00
draw_children ( ) ;
2026-10-27 00:10:45 +01:00
fl_pop_clip ( ) ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ brief Handles FLTK input events and delegates unhandled events to the base widget .
*
* @ param event FLTK event code .
* @ return Nonzero if the event was handled , zero otherwise .
*/
2026-10-27 00:10:45 +01:00
int SequencerWidget : : handle ( int event )
{
2026-11-06 00:06:56 +01:00
if ( event = = FL_PUSH & & Fl : : event_button ( ) = = FL_LEFT_MOUSE & & Fl : : event_x ( ) > = header_left ( ) + header_width_ - 5 & & Fl : : event_x ( ) < header_left ( ) + header_width_ ) {
mode_ = 5 ; header_drag_x_ = Fl : : event_x ( ) ; header_drag_original_ = header_width_ ; return 1 ;
}
if ( mode_ = = 5 & & ( event = = FL_DRAG | | event = = FL_RELEASE ) ) {
header_width ( header_drag_original_ + Fl : : event_x ( ) - header_drag_x_ ) ; if ( event = = FL_RELEASE ) mode_ = 0 ; return 1 ;
}
if ( mode_ = = 5 & & event = = FL_KEYDOWN & & Fl : : event_key ( ) = = FL_Escape ) { header_width ( header_drag_original_ ) ; mode_ = 0 ; return 1 ; }
2026-10-28 10:08:13 +01:00
for ( const auto & choice : track_choices_ ) {
if ( ! choice . widget | | ! choice . widget - > visible ( ) ) continue ;
if ( ( event = = FL_PUSH & & Fl : : event_inside ( choice . widget ) ) | |
( ( event = = FL_DRAG | | event = = FL_RELEASE ) & & Fl : : pushed ( ) = = choice . widget ) | |
( event = = FL_KEYDOWN & & Fl : : focus ( ) = = choice . widget ) )
return Fl_Group : : handle ( event ) ;
}
2026-10-27 06:28:25 +01:00
if ( event = = FL_PUSH & & ( Fl : : event_button ( ) = = FL_LEFT_MOUSE | | Fl : : event_button ( ) = = FL_RIGHT_MOUSE ) ) {
2026-10-27 01:37:21 +01:00
take_focus ( ) ;
mode_ = 0 ;
2026-11-06 00:06:56 +01:00
if ( Fl : : event_y ( ) < y ( ) + 28 & & Fl : : event_x ( ) > = header_left ( ) + header_width_ & &
2026-10-28 10:37:01 +01:00
Fl : : event_button ( ) = = FL_LEFT_MOUSE & & Fl : : event_state ( FL_SHIFT ) ) {
loop_anchor_ = loop_preview_ = tick ( Fl : : event_x ( ) ) ; mode_ = 4 ; redraw ( ) ; return 1 ;
}
2026-10-27 00:10:45 +01:00
if ( Fl : : event_y ( ) < y ( ) + 28 ) {
2026-11-06 00:06:56 +01:00
if ( Fl : : event_x ( ) < header_left ( ) + header_width_ ) return 1 ;
2026-10-27 00:10:45 +01:00
uint64_t target = tick ( Fl : : event_x ( ) ) ;
xtc_result r = seek_ ( context_ , target ) ;
2026-10-27 01:37:21 +01:00
if ( r = = XTC_OK )
cursor_ = target ;
else
fl_alert ( " Stop playback before seeking: %s " , xtc_result_string ( r ) ) ;
redraw ( ) ;
return 1 ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
size_t tracks , clips ;
xtc_project_get_counts ( project_ , & tracks , & clips ) ;
2026-10-27 00:10:45 +01:00
int row = ( Fl : : event_y ( ) - y ( ) - 28 ) / 56 ;
2026-10-27 01:37:21 +01:00
if ( row < 0 | | static_cast < size_t > ( row ) > = tracks )
return 1 ;
xtc_track_info t { } ;
xtc_project_get_track ( project_ , static_cast < size_t > ( row ) , & t ) ;
2026-11-06 00:12:37 +01:00
uint64_t previous_clip = clip_ ;
2026-10-27 01:37:21 +01:00
track_ = t . id ;
2026-10-27 00:10:45 +01:00
clip_ = UINT64_MAX ;
2026-11-06 00:06:56 +01:00
for ( size_t j = clips ; j > 0 & & Fl : : event_x ( ) > = header_left ( ) + header_width_ ; - - j ) {
2026-10-27 01:37:21 +01:00
xtc_clip_info c { } ;
xtc_project_get_clip ( project_ , j - 1 , & c ) ;
2026-11-06 00:06:56 +01:00
int left = x ( ) + header_width_ + static_cast < int > ( c . start_tick * static_cast < unsigned int > ( scale_ ) / 960U ) ;
2026-10-27 15:46:41 +01:00
int right = left + std : : max ( 4 , static_cast < int > ( c . length_tick * static_cast < unsigned int > ( scale_ ) / 960U ) ) ;
2026-10-27 00:10:45 +01:00
if ( c . track_id = = track_ & & Fl : : event_x ( ) > = left & & Fl : : event_x ( ) < right ) {
2026-10-27 01:37:21 +01:00
clip_ = c . id ;
original_ = c ;
drag_x_ = Fl : : event_x ( ) ;
mode_ = Fl : : event_x ( ) - left < 7 ? 2 : right - Fl : : event_x ( ) < 7 ? 3 : 1 ;
break ;
2026-10-27 00:10:45 +01:00
}
}
2026-11-06 00:12:37 +01:00
if ( Fl : : event_state ( FL_SHIFT ) & & clip_ ! = UINT64_MAX ) {
uint64_t hit = clip_ ; clip_ = previous_clip ; toggle_clip ( hit ) ; mode_ = 0 ; return 1 ;
}
selection_ . clear ( ) ;
2026-10-28 10:08:13 +01:00
if ( Fl : : event_button ( ) = = FL_LEFT_MOUSE & & Fl : : event_clicks ( ) & & clip_ ! = UINT64_MAX & & original_ . midi ) {
mode_ = 0 ;
if ( open_clip ) open_clip ( context_ ) ;
return 1 ;
}
2026-10-27 06:28:25 +01:00
if ( Fl : : event_button ( ) = = FL_RIGHT_MOUSE ) {
mode_ = 0 ;
2026-10-27 15:46:41 +01:00
Fl_Menu_Item items [ 6 ] { } ;
2026-10-27 06:28:25 +01:00
items [ 0 ] . text = " Duplicate clip (Ctrl+D) " ; items [ 1 ] . text = " Rename (F2) " ; items [ 2 ] . text = " Delete (undoable) " ;
2026-10-27 15:46:41 +01:00
items [ 3 ] . text = " Split audio at cursor (S) " ; items [ 4 ] . text = " Audio gain and fades (A) " ;
2026-10-27 06:28:25 +01:00
const Fl_Menu_Item * picked = items - > popup ( Fl : : event_x ( ) , Fl : : event_y ( ) ) ;
if ( picked = = items ) duplicate_selection ( ) ;
else if ( picked = = items + 1 ) { const char * name = fl_input ( " Name (1..63 UTF-8 bytes) " , " " ) ; if ( name ) rename_selection ( name ) ; }
else if ( picked = = items + 2 ) delete_selection ( ) ;
2026-10-27 15:46:41 +01:00
else if ( picked = = items + 3 ) split_selection ( ) ;
else if ( picked = = items + 4 ) audio_properties_dialog ( ) ;
2026-10-27 06:28:25 +01:00
}
2026-10-27 01:37:21 +01:00
redraw ( ) ;
return 1 ;
2026-10-27 00:10:45 +01:00
}
2026-10-28 10:37:01 +01:00
if ( event = = FL_DRAG & & mode_ = = 4 ) { loop_preview_ = tick ( Fl : : event_x ( ) ) ; redraw ( ) ; return 1 ; }
if ( event = = FL_RELEASE & & mode_ = = 4 ) {
loop_preview_ = tick ( Fl : : event_x ( ) ) ; mode_ = 0 ;
uint64_t begin = std : : min ( loop_anchor_ , loop_preview_ ) , end = std : : max ( loop_anchor_ , loop_preview_ ) ;
if ( end > begin & & loop_select ) {
xtc_result r = loop_select ( context_ , begin , end ) ;
if ( r ! = XTC_OK ) fl_alert ( " Loop unchanged: %s " , xtc_result_string ( r ) ) ;
}
redraw ( ) ; return 1 ;
}
2026-10-27 01:37:21 +01:00
if ( event = = FL_DRAG )
return mode_ ! = 0 ;
2026-10-27 00:10:45 +01:00
if ( event = = FL_RELEASE & & mode_ ) {
2026-10-27 01:37:21 +01:00
int mode = mode_ ;
mode_ = 0 ;
2026-10-28 10:08:13 +01:00
int64_t delta = static_cast < int64_t > ( Fl : : event_x ( ) - drag_x_ ) * 960 / scale_ ;
2026-10-27 00:10:45 +01:00
int64_t start = static_cast < int64_t > ( original_ . start_tick ) ;
int64_t length = static_cast < int64_t > ( original_ . length_tick ) ;
int64_t offset = static_cast < int64_t > ( original_ . offset_tick ) ;
uint64_t target = original_ . track_id ;
if ( mode = = 1 ) {
2026-10-28 10:08:13 +01:00
start = xtc_ui : : snap_position ( start + delta , grid_ ) ;
2026-10-27 00:10:45 +01:00
int row = ( Fl : : event_y ( ) - y ( ) - 28 ) / 56 ;
xtc_track_info t { } ;
2026-10-27 01:37:21 +01:00
if ( row > = 0 & & xtc_project_get_track ( project_ , static_cast < size_t > ( row ) , & t ) = = XTC_OK )
target = t . id ;
} else if ( mode = = 2 ) {
2026-10-28 10:08:13 +01:00
delta = xtc_ui : : snap_position ( start + delta , grid_ ) - start ;
2026-10-27 01:37:21 +01:00
start + = delta ;
offset + = delta ;
length - = delta ;
} else
2026-10-28 10:08:13 +01:00
length = xtc_ui : : snap_position ( start + length + delta , grid_ ) - start ;
2026-10-27 01:37:21 +01:00
if ( start < 0 | | offset < 0 | | length < = 0 )
return 1 ;
if ( before_edit ) {
xtc_result r = before_edit ( context_ ) ;
if ( r ! = XTC_OK ) {
changed ( r ) ;
return 1 ;
}
}
2026-10-27 00:10:45 +01:00
changed ( xtc_project_edit_clip ( project_ , clip_ , target , static_cast < uint64_t > ( start ) ,
2026-10-27 01:37:21 +01:00
static_cast < uint64_t > ( length ) , static_cast < uint64_t > ( offset ) ) ) ;
2026-10-27 00:10:45 +01:00
return 1 ;
}
2026-10-27 15:46:41 +01:00
if ( event = = FL_KEYDOWN & & ! Fl : : event_state ( FL_CTRL | FL_ALT ) ) {
if ( Fl : : event_key ( ) = = ' s ' ) { split_selection ( ) ; return 1 ; }
if ( Fl : : event_key ( ) = = ' a ' ) { audio_properties_dialog ( ) ; return 1 ; }
}
2026-11-06 00:13:48 +01:00
if ( event = = FL_KEYDOWN & & ! Fl : : event_state ( FL_CTRL | FL_ALT | FL_SHIFT ) & & ( Fl : : event_key ( ) = = FL_Left | | Fl : : event_key ( ) = = FL_Right ) ) { nudge_selection ( Fl : : event_key ( ) = = FL_Left ? - 1 : 1 ) ; return 1 ; }
2026-11-06 00:15:01 +01:00
if ( event = = FL_KEYDOWN & & Fl : : event_state ( FL_CTRL ) & & Fl : : event_key ( ) = = ' c ' ) { copy_selection ( ) ; return 1 ; }
if ( event = = FL_KEYDOWN & & Fl : : event_state ( FL_CTRL ) & & Fl : : event_key ( ) = = ' v ' ) { paste_selection ( ) ; return 1 ; }
2026-11-06 00:12:37 +01:00
if ( event = = FL_KEYDOWN & & Fl : : event_state ( FL_CTRL ) & & Fl : : event_key ( ) = = ' a ' ) { select_all_clips ( ) ; return 1 ; }
2026-10-27 06:28:25 +01:00
if ( event = = FL_KEYDOWN & & Fl : : event_state ( FL_CTRL ) & & Fl : : event_key ( ) = = ' d ' ) { duplicate_selection ( ) ; return 1 ; }
if ( event = = FL_KEYDOWN & & Fl : : event_key ( ) = = FL_F + 2 ) {
const char * name = fl_input ( " Name (1..63 UTF-8 bytes) " , " " ) ; if ( name ) rename_selection ( name ) ; return 1 ;
}
if ( event = = FL_KEYDOWN & & Fl : : event_state ( FL_ALT ) & &
( Fl : : event_key ( ) = = FL_Up | | Fl : : event_key ( ) = = FL_Down ) ) { move_track ( Fl : : event_key ( ) = = FL_Up ? - 1 : 1 ) ; return 1 ; }
2026-10-27 01:37:21 +01:00
if ( event = = FL_KEYDOWN & & Fl : : event_key ( ) = = FL_Escape ) {
mode_ = 0 ;
return 1 ;
}
if ( event = = FL_KEYDOWN & & Fl : : event_key ( ) = = FL_Delete ) {
delete_selection ( ) ;
return 1 ;
}
if ( event = = FL_FOCUS | | event = = FL_UNFOCUS )
return 1 ;
2026-10-28 10:08:13 +01:00
return Fl_Group : : handle ( event ) ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 06:28:25 +01:00
2026-11-06 00:12:37 +01:00
std : : vector < uint64_t > SequencerWidget : : selected_clips ( ) const
{
std : : vector < uint64_t > ids ; xtc_clip_info c { } ;
for ( auto id : selection_ ) if ( xtc_project_find_clip ( project_ , id , & c ) = = XTC_OK ) ids . push_back ( id ) ;
if ( ids . empty ( ) & & clip_ ! = UINT64_MAX & & xtc_project_find_clip ( project_ , clip_ , & c ) = = XTC_OK ) ids . push_back ( clip_ ) ;
return ids ;
}
bool SequencerWidget : : clip_selected ( uint64_t id ) const
{
return selection_ . empty ( ) ? id = = clip_ : std : : find ( selection_ . begin ( ) , selection_ . end ( ) , id ) ! = selection_ . end ( ) ;
}
bool SequencerWidget : : toggle_clip ( uint64_t id )
{
xtc_clip_info c { } ; if ( xtc_project_find_clip ( project_ , id , & c ) ! = XTC_OK ) return false ;
if ( selection_ . empty ( ) & & clip_ ! = UINT64_MAX ) selection_ . push_back ( clip_ ) ;
auto found = std : : find ( selection_ . begin ( ) , selection_ . end ( ) , id ) ;
if ( found = = selection_ . end ( ) ) selection_ . push_back ( id ) ; else selection_ . erase ( found ) ;
clip_ = selection_ . empty ( ) ? UINT64_MAX : selection_ . back ( ) ; track_ = c . track_id ; redraw ( ) ; return true ;
}
void SequencerWidget : : select_all_clips ( )
{
selection_ . clear ( ) ; size_t tracks = 0 , clips = 0 ; xtc_project_get_counts ( project_ , & tracks , & clips ) ;
for ( size_t i = 0 ; i < clips ; + + i ) { xtc_clip_info c { } ; xtc_project_get_clip ( project_ , i , & c ) ; selection_ . push_back ( c . id ) ; }
if ( ! selection_ . empty ( ) ) clip_ = selection_ . back ( ) ;
redraw ( ) ;
}
2026-10-27 06:28:25 +01:00
xtc_result SequencerWidget : : grid ( uint64_t ticks )
{
2026-10-28 10:08:13 +01:00
if ( xtc_ui : : snap_index ( ticks ) < 0 ) return XTC_ERR_INVALID_ARGUMENT ;
2026-10-27 06:28:25 +01:00
grid_ = ticks ; redraw ( ) ; return XTC_OK ;
}
2026-11-06 00:13:48 +01:00
xtc_result SequencerWidget : : transform_selection ( xtc_clip_operation op , int64_t amount )
{
auto ids = selected_clips ( ) ; std : : vector < uint64_t > output ( ids . size ( ) ) ;
xtc_result r = ids . empty ( ) ? XTC_ERR_NOT_FOUND : XTC_OK ;
if ( r = = XTC_OK & & before_edit ) r = before_edit ( context_ ) ;
if ( r = = XTC_OK ) r = xtc_project_transform_clips ( project_ , ids . data ( ) , ids . size ( ) , op , amount , output . data ( ) ) ;
if ( r = = XTC_OK ) {
selection_ = op = = XTC_CLIPS_DELETE ? std : : vector < uint64_t > { } : output ;
clip_ = selection_ . empty ( ) ? UINT64_MAX : selection_ . back ( ) ;
}
changed ( r ) ; return r ;
}
2026-11-06 00:17:23 +01:00
xtc_result SequencerWidget : : transform_midi ( const xtc_midi_transform & edit )
{
auto ids = selected_clips ( ) ; xtc_result r = before_edit ? before_edit ( context_ ) : XTC_OK ;
if ( r = = XTC_OK ) r = xtc_project_transform_midi ( project_ , ids . data ( ) , ids . size ( ) , & edit ) ;
changed ( r ) ; return r ;
}
2026-11-06 00:18:07 +01:00
void SequencerWidget : : gate_dialog ( )
{
const char * input = fl_input ( " Note length percentage (1..400) " , " 50 " ) ; if ( ! input ) return ;
unsigned int percent = 0 ; char extra ; std : : istringstream values ( input ) ;
if ( ! ( values > > percent ) | | ( values > > extra ) | | percent < 1 | | percent > 400 ) { fl_alert ( " Enter a whole percentage from 1 to 400. " ) ; return ; }
xtc_midi_transform edit { } ; edit . type = XTC_MIDI_TRANSFORM_GATE ; edit . gate_percent = percent ; transform_midi ( edit ) ;
}
2026-11-06 00:17:23 +01:00
void SequencerWidget : : swing_dialog ( )
{
const char * input = fl_input ( " Grid ticks (1..3840), swing (0..100), strength (0..100) " , " 240 50 100 " ) ; if ( ! input ) return ;
int grid = 0 , swing = - 1 , strength = - 1 ; char extra ;
std : : istringstream values ( input ) ;
if ( ! ( values > > grid > > swing > > strength ) | | ( values > > extra ) | | grid < 1 | | grid > 3840 | | swing < 0 | | swing > 100 | | strength < 0 | | strength > 100 ) { fl_alert ( " Enter a valid grid, swing and strength. " ) ; return ; }
xtc_midi_transform edit { } ; edit . type = XTC_MIDI_TRANSFORM_SWING ; edit . grid = static_cast < uint64_t > ( grid ) ; edit . swing = static_cast < unsigned int > ( swing ) ; edit . strength = static_cast < unsigned int > ( strength ) ; transform_midi ( edit ) ;
}
2026-11-06 00:15:42 +01:00
xtc_result SequencerWidget : : repeat_selection ( unsigned int copies )
{
auto ids = selected_clips ( ) ; xtc_result r = before_edit ? before_edit ( context_ ) : XTC_OK ;
if ( r = = XTC_OK ) r = xtc_project_repeat_clips ( project_ , ids . data ( ) , ids . size ( ) , copies ) ;
changed ( r ) ; return r ;
}
void SequencerWidget : : repeat_dialog ( )
{
const char * input = fl_input ( " Additional phrase copies (1..1024) " , " 3 " ) ; if ( ! input ) return ;
char * end = nullptr ; unsigned long copies = std : : strtoul ( input , & end , 10 ) ;
if ( end = = input | | * end | | copies < 1 | | copies > 1024 ) { fl_alert ( " Enter a whole number from 1 to 1024. " ) ; return ; }
repeat_selection ( static_cast < unsigned int > ( copies ) ) ;
}
2026-11-06 00:15:01 +01:00
xtc_result SequencerWidget : : copy_selection ( )
{
auto ids = selected_clips ( ) ; xtc_clipboard * copy = nullptr ;
xtc_result r = xtc_clipboard_create ( project_ , ids . data ( ) , ids . size ( ) , & copy ) ;
if ( r = = XTC_OK ) clipboard_ . reset ( copy ) ;
else fl_alert ( " Copy unchanged: %s " , xtc_result_string ( r ) ) ;
return r ;
}
xtc_result SequencerWidget : : paste_selection ( )
{
const uint64_t start = cursor_ , track = track_ ;
std : : vector < uint64_t > output ( xtc_clipboard_count ( clipboard_ . get ( ) ) ) ;
xtc_result r = output . empty ( ) ? XTC_ERR_INVALID_STATE : XTC_OK ;
if ( r = = XTC_OK & & before_edit ) r = before_edit ( context_ ) ;
if ( r = = XTC_OK ) r = xtc_clipboard_paste ( clipboard_ . get ( ) , project_ , track , start , output . data ( ) ) ;
if ( r = = XTC_OK ) { selection_ = output ; clip_ = selection_ . back ( ) ; }
changed ( r ) ; return r ;
}
2026-11-06 00:13:48 +01:00
xtc_result SequencerWidget : : nudge_selection ( int direction )
{
return transform_selection ( XTC_CLIPS_MOVE , direction < 0 ? - static_cast < int64_t > ( grid_ ) : static_cast < int64_t > ( grid_ ) ) ;
}
2026-10-27 06:28:25 +01:00
xtc_result SequencerWidget : : duplicate_selection ( )
{
2026-11-06 00:13:48 +01:00
auto ids = selected_clips ( ) ; uint64_t begin = XTC_PROJECT_MAX_TICK , end = 0 ;
for ( auto id : ids ) { xtc_clip_info c { } ; xtc_project_find_clip ( project_ , id , & c ) ; begin = std : : min ( begin , c . start_tick ) ; end = std : : max ( end , c . start_tick + c . length_tick ) ; }
return transform_selection ( XTC_CLIPS_DUPLICATE , static_cast < int64_t > ( end ) - static_cast < int64_t > ( begin ) ) ;
2026-10-27 06:28:25 +01:00
}
xtc_result SequencerWidget : : rename_selection ( const char * name )
{
xtc_result r = before_edit ? before_edit ( context_ ) : XTC_OK ;
if ( r = = XTC_OK ) r = clip_ ! = UINT64_MAX ? xtc_project_rename_clip ( project_ , clip_ , name ) : xtc_project_rename_track ( project_ , track_ , name ) ;
changed ( r ) ; return r ;
}
xtc_result SequencerWidget : : move_track ( int direction )
{
size_t tracks , clips , i ; xtc_project_get_counts ( project_ , & tracks , & clips ) ;
xtc_track_info track { } ;
for ( i = 0 ; i < tracks ; + + i ) { xtc_project_get_track ( project_ , i , & track ) ; if ( track . id = = track_ ) break ; }
if ( i = = tracks | | ( direction < 0 ? i = = 0 : i + 1 = = tracks ) ) return XTC_ERR_NOT_FOUND ;
xtc_result r = before_edit ? before_edit ( context_ ) : XTC_OK ;
if ( r = = XTC_OK ) r = xtc_project_move_track ( project_ , track_ , direction < 0 ? i - 1 : i + 1 ) ;
changed ( r ) ; return r ;
}
2026-10-27 15:46:41 +01:00
xtc_result SequencerWidget : : split_selection ( )
{
uint64_t right ;
const uint64_t split_tick = cursor_ ; // Stop/status refresh may move the playhead.
xtc_result result = before_edit ? before_edit ( context_ ) : XTC_OK ;
if ( result = = XTC_OK ) result = xtc_project_split_audio ( project_ , clip_ , split_tick , & right ) ;
if ( result = = XTC_OK ) clip_ = right ;
changed ( result ) ;
return result ;
}
xtc_result SequencerWidget : : audio_properties ( float gain , uint64_t in , uint64_t out )
{
xtc_result result = before_edit ? before_edit ( context_ ) : XTC_OK ;
if ( result = = XTC_OK ) result = xtc_project_set_clip_audio ( project_ , clip_ , gain , in , out ) ;
changed ( result ) ;
return result ;
}
void SequencerWidget : : audio_properties_dialog ( )
{
xtc_clip_info clip { } ;
if ( xtc_project_find_clip ( project_ , clip_ , & clip ) ! = XTC_OK | | clip . midi ) {
fl_alert ( " Select an audio clip or silent region first. " ) ;
return ;
}
char defaults [ 128 ] ;
std : : snprintf ( defaults , sizeof ( defaults ) , " %.9g %llu %llu " , static_cast < double > ( clip . gain ) ,
static_cast < unsigned long long > ( clip . fade_in_tick ) , static_cast < unsigned long long > ( clip . fade_out_tick ) ) ;
const char * text = fl_input ( " Linear gain (0..4), fade-in ticks, fade-out ticks (0 disables) " , defaults ) ;
if ( ! text ) return ;
std : : istringstream input ( text ) ;
float gain ; uint64_t in , out ;
if ( ! ( input > > gain > > in > > out ) | | ! ( input > > std : : ws ) . eof ( ) ) {
fl_alert ( " Enter gain and two fade durations in ticks. " ) ; return ;
}
audio_properties ( gain , in , out ) ;
}
void SequencerWidget : : refresh_waveforms ( ) try
{
// Control-thread loading only. Keep cached failures as well as successes;
// removed/relinked paths are evicted. Reopening rebuilds externally changed files.
size_t tracks , clips ;
xtc_project_get_counts ( project_ , & tracks , & clips ) ;
std : : vector < Wave > next ;
for ( size_t i = 0 ; i < clips & & next . size ( ) < 64 ; + + i ) {
xtc_clip_info c { } ;
xtc_project_get_clip ( project_ , i , & c ) ;
if ( ! c . asset_path [ 0 ] ) continue ;
bool present = false ;
for ( const auto & wave : next ) if ( wave . path = = c . asset_path ) { present = true ; break ; }
if ( present ) continue ;
Wave entry ;
entry . path = c . asset_path ;
for ( auto & wave : waves_ ) if ( wave . path = = entry . path ) {
entry . data = std : : move ( wave . data ) ; present = true ; break ;
}
if ( ! present ) {
xtc_waveform * summary = nullptr ;
( void ) xtc_waveform_create ( c . asset_path , & summary ) ;
entry . data . reset ( summary ) ;
}
next . push_back ( std : : move ( entry ) ) ;
}
waves_ = std : : move ( next ) ;
} catch ( const std : : bad_alloc & ) {
waves_ . clear ( ) ; // Preview failure must not interrupt the model/history edit.
}
void SequencerWidget : : draw_waveform ( const xtc_clip_info & c , int left , int top )
{
const xtc_waveform * summary = nullptr ;
for ( const auto & wave : waves_ ) if ( wave . path = = c . asset_path ) { summary = wave . data . get ( ) ; break ; }
if ( ! summary ) return ;
unsigned int rate ; size_t frames , bins ;
if ( xtc_waveform_info ( summary , & rate , & frames , & bins ) ! = XTC_OK ) return ;
double ticks_per_frame = static_cast < double > ( xtc_project_tempo ( project_ ) ) * 960 / ( rate * 60000.0 ) ;
fl_color ( fl_rgb_color ( 30 , 65 , 80 ) ) ;
for ( size_t bin = 0 ; bin < bins ; + + bin ) {
double start = static_cast < double > ( bin * frames / bins ) * ticks_per_frame ;
double end = static_cast < double > ( ( bin + 1 ) * frames / bins ) * ticks_per_frame ;
double offset = static_cast < double > ( c . offset_tick ) ;
if ( end < = offset | | start > = offset + static_cast < double > ( c . length_tick ) ) continue ;
int px = left + static_cast < int > ( ( std : : max ( start , offset ) - offset ) * scale_ / 960 ) ;
int width = std : : max ( 1 , static_cast < int > ( ( end - std : : max ( start , offset ) ) * scale_ / 960 ) ) ;
float low , high ;
if ( xtc_waveform_peak ( summary , bin , & low , & high ) ! = XTC_OK ) continue ;
int y1 = top + 28 - static_cast < int > ( std : : clamp ( high , - 1.0f , 1.0f ) * 19 ) ;
int y2 = top + 28 - static_cast < int > ( std : : clamp ( low , - 1.0f , 1.0f ) * 19 ) ;
fl_rectf ( px , y1 , width , std : : max ( 1 , y2 - y1 + 1 ) ) ;
}
}
2026-10-28 10:08:13 +01:00
Fl_Choice * SequencerWidget : : instrument_choice ( uint64_t track ) const
{
for ( const auto & entry : track_choices_ ) if ( entry . widget & & entry . track = = track ) return entry . widget ;
return nullptr ;
}
void SequencerWidget : : refresh_instruments ( ) try
{
size_t tracks = 0 , clips = 0 ;
xtc_project_get_counts ( project_ , & tracks , & clips ) ;
catalog_ . clear ( ) ;
2026-10-28 17:16:01 +01:00
for ( unsigned int i = 0 ; i < 2 ; + + i ) { xtc_plugin_spec s { } ; if ( xtc_plugin_probe ( XTC_BUILTIN_PATH , i , 48000 , & s ) = = XTC_OK ) catalog_ . push_back ( s ) ; }
2026-10-28 10:08:13 +01:00
for ( size_t i = 0 ; i < tracks ; + + i ) {
xtc_track_info t { } ; xtc_plugin_spec spec { } ;
xtc_project_get_track ( project_ , i , & t ) ;
if ( xtc_project_get_instrument ( project_ , t . id , & spec ) ! = XTC_OK ) continue ;
bool present = false ;
for ( const auto & item : catalog_ ) if ( ! std : : strcmp ( item . path , spec . path ) & & item . index = = spec . index ) present = true ;
if ( ! present ) catalog_ . push_back ( spec ) ;
}
for ( size_t i = 0 ; i < track_choices_ . size ( ) ; + + i ) {
auto & entry = track_choices_ [ i ] ;
xtc_track_info t { } ;
if ( i > = tracks | | xtc_project_get_track ( project_ , i , & t ) ! = XTC_OK | | t . is_bus ) {
if ( entry . widget ) entry . widget - > hide ( ) ;
entry . track = UINT64_MAX ; continue ;
}
if ( ! entry . widget ) {
struct GroupScope {
Fl_Group * previous ;
explicit GroupScope ( Fl_Group * group ) : previous ( Fl_Group : : current ( ) ) { Fl_Group : : current ( group ) ; }
~ GroupScope ( ) { Fl_Group : : current ( previous ) ; }
} scope ( this ) ;
2026-11-06 00:06:56 +01:00
entry . widget = new Fl_Choice ( header_left ( ) + 10 , y ( ) + 58 + static_cast < int > ( i ) * 56 , header_width_ - 22 , 20 ) ;
2026-10-28 10:08:13 +01:00
xtc_ui : : style ( entry . widget ) ; entry . widget - > textsize ( 10 ) ; entry . widget - > textcolor ( xtc_ui : : text ( ) ) ;
entry . widget - > tooltip ( " Track instrument: built-in sine, an instrument used in this project, or Load plugin... " ) ;
entry . widget - > callback ( instrument_callback , this ) ;
}
entry . track = t . id ;
2026-11-06 00:06:56 +01:00
entry . widget - > resize ( header_left ( ) + 10 , y ( ) + 58 + static_cast < int > ( i ) * 56 , header_width_ - 22 , 20 ) ;
2026-10-28 10:08:13 +01:00
entry . widget - > clear ( ) ; entry . widget - > add ( " Built-in sine " ) ;
xtc_plugin_spec current { } ; int selected = 0 ;
bool assigned = xtc_project_get_instrument ( project_ , t . id , & current ) = = XTC_OK ;
for ( size_t j = 0 ; j < catalog_ . size ( ) ; + + j ) {
std : : string name = std : : string ( xtc_plugin_format_name ( catalog_ [ j ] . format ) ) + " : " + catalog_ [ j ] . label ;
int item = entry . widget - > add ( " Instrument " ) ; entry . widget - > replace ( item , name . c_str ( ) ) ;
if ( assigned & & ! std : : strcmp ( current . path , catalog_ [ j ] . path ) & & current . index = = catalog_ [ j ] . index ) selected = static_cast < int > ( j ) + 1 ;
}
entry . widget - > add ( " Load plugin... " ) ; entry . widget - > value ( selected ) ; entry . widget - > show ( ) ;
}
} catch ( const std : : bad_alloc & ) {
catalog_ . clear ( ) ;
for ( auto & entry : track_choices_ ) if ( entry . widget ) entry . widget - > hide ( ) ;
}
void SequencerWidget : : instrument_callback ( Fl_Widget * widget , void * context )
{
auto * owner = static_cast < SequencerWidget * > ( context ) ;
auto * choice = static_cast < Fl_Choice * > ( widget ) ;
for ( const auto & entry : owner - > track_choices_ ) if ( entry . widget = = choice ) {
uint64_t track = entry . track ;
int index = choice - > value ( ) ;
bool browse = index = = static_cast < int > ( owner - > catalog_ . size ( ) ) + 1 ;
xtc_plugin_spec spec { } ;
if ( index > 0 & & ! browse ) {
if ( static_cast < size_t > ( index ) > owner - > catalog_ . size ( ) ) return ;
spec = owner - > catalog_ [ static_cast < size_t > ( index ) - 1 ] ;
}
if ( owner - > instrument_select ) owner - > instrument_select ( owner - > context_ , track , index > 0 & & ! browse ? & spec : nullptr , browse ) ;
owner - > refresh_instruments ( ) ; owner - > redraw ( ) ; return ;
}
}
2026-10-28 10:37:01 +01:00
xtc_result SequencerWidget : : set_zoom ( int scale )
{
if ( scale ! = 24 & & scale ! = 48 & & scale ! = 96 & & scale ! = 192 & & scale ! = 384 ) return XTC_ERR_INVALID_ARGUMENT ;
scale_ = scale ; extent ( ) ; redraw ( ) ; return XTC_OK ;
}