2026-11-06 01:01:37 +01:00
# include "sample_editor.hpp"
# include "theme.hpp"
# include <FL/Fl.H>
# include <FL/Fl_Value_Input.H>
# include <FL/Fl_Button.H>
# include <FL/Fl_Box.H>
# include <FL/fl_draw.H>
2026-11-06 01:02:08 +01:00
# include <FL/fl_ask.H>
2026-11-06 01:01:37 +01:00
# include <algorithm>
# include <cmath>
# include <cstdio>
class SampleCanvas : public Fl_Widget {
SampleEditor & editor_ ; size_t anchor_ = 0 ;
size_t frame ( int px ) const { size_t width = std : : max ( size_t ( 1 ) , editor_ . frames ( ) / editor_ . zoom ( ) ) ; return std : : min ( editor_ . frames ( ) - 1 , editor_ . origin ( ) + static_cast < size_t > ( std : : max ( 0 , std : : min ( w ( ) - 1 , px - x ( ) ) ) ) * width / static_cast < size_t > ( w ( ) ) ) ; }
public :
SampleCanvas ( int x , int y , int w , int h , SampleEditor & e ) : Fl_Widget ( x , y , w , h ) , editor_ ( e ) { }
int handle ( int event ) override {
if ( event = = FL_PUSH & & Fl : : event_button ( ) = = FL_LEFT_MOUSE ) { anchor_ = frame ( Fl : : event_x ( ) ) ; editor_ . select ( anchor_ , anchor_ + 1 ) ; return 1 ; }
if ( event = = FL_DRAG ) { size_t f = frame ( Fl : : event_x ( ) ) ; editor_ . select ( std : : min ( f , anchor_ ) , std : : max ( f , anchor_ ) + 1 ) ; return 1 ; }
return Fl_Widget : : handle ( event ) ;
}
void draw ( ) override {
fl_push_clip ( x ( ) , y ( ) , w ( ) , h ( ) ) ; fl_color ( xtc_ui : : background ( ) ) ; fl_rectf ( x ( ) , y ( ) , w ( ) , h ( ) ) ;
size_t width = std : : max ( size_t ( 1 ) , editor_ . frames ( ) / editor_ . zoom ( ) ) ;
for ( int px = 0 ; px < w ( ) ; + + px ) { size_t b = editor_ . origin ( ) + static_cast < size_t > ( px ) * width / static_cast < size_t > ( w ( ) ) ; size_t e = std : : min ( editor_ . frames ( ) , editor_ . origin ( ) + static_cast < size_t > ( px + 1 ) * width / static_cast < size_t > ( w ( ) ) ) ; if ( b > = editor_ . frames ( ) ) break ; if ( e < = b ) e = b + 1 ;
if ( b < editor_ . end ( ) & & e > editor_ . begin ( ) ) { fl_color ( fl_rgb_color ( 45 , 76 , 80 ) ) ; fl_rectf ( x ( ) + px , y ( ) , 1 , h ( ) ) ; }
xtc_sample_stats stats { } ; if ( xtc_sample_analyze ( editor_ . sample ( ) , b , e , & stats ) = = XTC_OK ) { fl_color ( fl_rgb_color ( 95 , 204 , 182 ) ) ; int a = static_cast < int > ( std : : max ( - 1.f , std : : min ( 1.f , stats . maximum ) ) * ( h ( ) / 2 - 8 ) ) ; int z = static_cast < int > ( std : : max ( - 1.f , std : : min ( 1.f , stats . minimum ) ) * ( h ( ) / 2 - 8 ) ) ; fl_line ( x ( ) + px , y ( ) + h ( ) / 2 - a , x ( ) + px , y ( ) + h ( ) / 2 - z ) ; }
}
fl_color ( xtc_ui : : border ( ) ) ; fl_line ( x ( ) , y ( ) + h ( ) / 2 , x ( ) + w ( ) , y ( ) + h ( ) / 2 ) ; fl_pop_clip ( ) ;
}
} ;
SampleEditor : : SampleEditor ( xtc_sample * sample ) : Fl_Double_Window ( 1100 , 650 , " xtc — Sample editor " ) , sample_ ( sample , xtc_sample_destroy )
{
xtc_sample_info ( sample_ . get ( ) , & rate_ , & frames_ ) ; end_ = frames_ ; set_modal ( ) ; Fl_Group : : begin ( ) ;
auto * hint = new Fl_Box ( 16 , 12 , 1068 , 34 , " Source frames / mono downmix — drag a range; numeric endpoints are exclusive. Original file unchanged. " ) ; hint - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
begin_input_ = new Fl_Value_Input ( 90 , 58 , 140 , 28 , " Start frame " ) ; end_input_ = new Fl_Value_Input ( 330 , 58 , 140 , 28 , " End frame " ) ; origin_input_ = new Fl_Value_Input ( 580 , 58 , 140 , 28 , " View start " ) ;
for ( auto * input : { begin_input_ , end_input_ , origin_input_ } ) { input - > step ( 1 ) ; input - > bounds ( 0 , static_cast < double > ( frames_ ) ) ; input - > when ( FL_WHEN_RELEASE ) ; input - > callback ( [ ] ( Fl_Widget * , void * c ) { auto * e = static_cast < SampleEditor * > ( c ) ; double b = e - > begin_input_ - > value ( ) , z = e - > end_input_ - > value ( ) , o = e - > origin_input_ - > value ( ) ; if ( std : : isfinite ( b ) & & std : : isfinite ( z ) & & std : : isfinite ( o ) & & b > = 0 & & z > = 0 & & o > = 0 & & b < = e - > frames_ & & z < = e - > frames_ & & o < e - > frames_ ) { e - > select ( static_cast < size_t > ( b ) , static_cast < size_t > ( z ) ) ; e - > view ( static_cast < size_t > ( o ) , e - > zoom_ ) ; } e - > refresh ( ) ; } , this ) ; }
auto * plus = new Fl_Button ( 750 , 58 , 100 , 28 , " Zoom in " ) ; plus - > callback ( [ ] ( Fl_Widget * , void * c ) { auto * e = static_cast < SampleEditor * > ( c ) ; e - > view ( e - > begin_ , std : : min ( 64U , e - > zoom_ * 2 ) ) ; } , this ) ;
auto * minus = new Fl_Button ( 860 , 58 , 100 , 28 , " Zoom out " ) ; minus - > callback ( [ ] ( Fl_Widget * , void * c ) { auto * e = static_cast < SampleEditor * > ( c ) ; e - > view ( e - > origin_ , std : : max ( 1U , e - > zoom_ / 2 ) ) ; } , this ) ;
canvas_ = new SampleCanvas ( 16 , 104 , 1068 , 340 , * this ) ; stats_ = new Fl_Box ( 16 , 452 , 1068 , 50 ) ; stats_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
2026-11-06 01:02:08 +01:00
auto * trim_button = new Fl_Button ( 16 , 520 , 180 , 30 , " Trim clip to selection " ) ; trim_button - > callback ( [ ] ( Fl_Widget * , void * c ) { auto r = static_cast < SampleEditor * > ( c ) - > trim_clip ( ) ; if ( r ! = XTC_OK ) fl_alert ( " Clip trim unchanged: %s " , xtc_result_string ( r ) ) ; } , this ) ;
2026-11-06 01:03:31 +01:00
auto * save = new Fl_Button ( 210 , 520 , 210 , 30 , " Save full copy and relink... " ) ; save - > callback ( [ ] ( Fl_Widget * , void * c ) { auto * e = static_cast < SampleEditor * > ( c ) ; const char * path = fl_input ( " NEW absolute WAV path (full source; project undo keeps the new file) " , " " ) ; if ( path ) { auto r = e - > save_copy ( path , true ) ; if ( r ! = XTC_OK ) fl_alert ( " Copy not adopted: %s " , xtc_result_string ( r ) ) ; } } , this ) ;
auto * export_button = new Fl_Button ( 434 , 520 , 200 , 30 , " Export selected frames... " ) ; export_button - > callback ( [ ] ( Fl_Widget * , void * c ) { auto * e = static_cast < SampleEditor * > ( c ) ; const char * path = fl_input ( " NEW absolute WAV path (selection only) " , " " ) ; if ( path ) { auto r = e - > export_selection ( path ) ; if ( r ! = XTC_OK ) fl_alert ( " Export failed: %s " , xtc_result_string ( r ) ) ; } } , this ) ;
2026-11-06 01:01:37 +01:00
auto * close = new Fl_Button ( 970 , 604 , 110 , 30 , " Close " ) ; close - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < SampleEditor * > ( c ) - > hide ( ) ; } , this ) ; callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < SampleEditor * > ( c ) - > hide ( ) ; } , this ) ;
Fl_Group : : end ( ) ; xtc_ui : : style ( this ) ; refresh ( ) ;
}
xtc_result SampleEditor : : select ( size_t b , size_t e ) { if ( b > = e | | e > frames_ ) return XTC_ERR_INVALID_ARGUMENT ; begin_ = b ; end_ = e ; refresh ( ) ; return XTC_OK ; }
xtc_result SampleEditor : : view ( size_t b , unsigned int zoom ) { if ( b > = frames_ | | ! zoom | | zoom > 64 ) return XTC_ERR_INVALID_ARGUMENT ; origin_ = b ; zoom_ = zoom ; refresh ( ) ; return XTC_OK ; }
void SampleEditor : : refresh ( ) { begin_input_ - > value ( static_cast < double > ( begin_ ) ) ; end_input_ - > value ( static_cast < double > ( end_ ) ) ; origin_input_ - > value ( static_cast < double > ( origin_ ) ) ; xtc_sample_stats s { } ; xtc_sample_analyze ( sample_ . get ( ) , begin_ , end_ , & s ) ; char text [ 256 ] ; std : : snprintf ( text , sizeof ( text ) , " %u Hz / %zu source frames / selection %zu..%zu (%0.6f s) / zoom %ux \n Min %.6f Max %.6f RMS %.6f DC %.6f " , rate_ , frames_ , begin_ , end_ , static_cast < double > ( end_ - begin_ ) / rate_ , zoom_ , s . minimum , s . maximum , s . rms , s . mean ) ; stats_ - > copy_label ( text ) ; canvas_ - > redraw ( ) ; }
void SampleEditor : : run ( ) { show ( ) ; while ( shown ( ) ) Fl : : wait ( ) ; }
2026-11-06 01:02:08 +01:00
xtc_result SampleEditor : : trim_clip ( ) { return trim ? trim ( rate_ , begin_ , end_ ) : XTC_ERR_INVALID_STATE ; }
2026-11-06 01:03:31 +01:00
xtc_result SampleEditor : : save_copy ( const char * path , bool use_in_project ) { if ( use_in_project ) return adopt ? adopt ( sample_ . get ( ) , path ) : XTC_ERR_INVALID_STATE ; return xtc_sample_write ( sample_ . get ( ) , 0 , frames_ , path ) ; }
xtc_result SampleEditor : : export_selection ( const char * path ) { return xtc_sample_write ( sample_ . get ( ) , begin_ , end_ , path ) ; }