#include "sample_editor.hpp" #include "theme.hpp" #include #include #include #include #include #include #include #include #include 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(std::max(0,std::min(w()-1,px-x())))*width/static_cast(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(px)*width/static_cast(w());size_t e=std::min(editor_.frames(),editor_.origin()+static_cast(px+1)*width/static_cast(w()));if(b>=editor_.frames())break;if(e<=b)e=b+1; if(beditor_.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(std::max(-1.f,std::min(1.f,stats.maximum))*(h()/2-8));int z=static_cast(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(frames_));input->when(FL_WHEN_RELEASE);input->callback([](Fl_Widget *,void *c){auto *e=static_cast(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_&&oframes_){e->select(static_cast(b),static_cast(z));e->view(static_cast(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(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(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); 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(c)->trim_clip();if(r!=XTC_OK)fl_alert("Clip trim unchanged: %s",xtc_result_string(r));},this); auto *save=new Fl_Button(210,520,210,30,"Save full copy and relink...");save->callback([](Fl_Widget *,void *c){auto *e=static_cast(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(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); auto *reverse=new Fl_Button(16,562,160,30,"Reverse selection");reverse->callback([](Fl_Widget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_REVERSE);if(r!=XTC_OK)fl_alert("Reversal failed: %s",xtc_result_string(r));},this); auto *invert=new Fl_Button(188,562,160,30,"Invert polarity");invert->callback([](Fl_Widget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_INVERT);if(r!=XTC_OK)fl_alert("Polarity edit failed: %s",xtc_result_string(r));},this); auto *dc=new Fl_Button(360,562,160,30,"Remove DC offset");dc->callback([](Fl_Widget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_REMOVE_DC);if(r!=XTC_OK)fl_alert("DC removal failed: %s",xtc_result_string(r));},this); auto *fade=new Fl_Button(532,562,160,30,"Sample fades...");fade->callback([](Fl_Widget *,void *c){auto *e=static_cast(c);const char *text=fl_input("Fade: 0 linear in, 1 linear out, 2 equal-power in, 3 equal-power out","0");if(!text)return;unsigned int mode;char extra;if(std::sscanf(text,"%u %c",&mode,&extra)!=1||mode>3){fl_alert("Enter a fade mode 0..3.");return;}auto r=e->process(static_cast(XTC_SAMPLE_FADE_IN+mode));if(r!=XTC_OK)fl_alert("Fade failed: %s",xtc_result_string(r));},this); auto *undo=new Fl_Button(850,604,110,30,"Undo / redo");undo->callback([](Fl_Widget *,void *c){auto r=static_cast(c)->undo_sample();if(r!=XTC_OK)fl_alert("No local sample edit to undo.");},this); auto *close=new Fl_Button(970,604,110,30,"Close");close->callback([](Fl_Widget *,void *c){static_cast(c)->hide();},this);callback([](Fl_Widget *,void *c){static_cast(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(begin_));end_input_->value(static_cast(end_));origin_input_->value(static_cast(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\nMin %.6f Max %.6f RMS %.6f DC %.6f",rate_,frames_,begin_,end_,static_cast(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();} xtc_result SampleEditor::trim_clip(){return trim ? trim(rate_,begin_,end_) : XTC_ERR_INVALID_STATE;} 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);} xtc_result SampleEditor::process(xtc_sample_operation operation,double amount) { xtc_sample *next=nullptr;auto r=xtc_sample_process(sample_.get(),begin_,end_,operation,amount,&next);if(r!=XTC_OK)return r; previous_=std::move(sample_);sample_.reset(next);copy_label("xtc — Sample editor / local sample changes; save a copy to keep");refresh();return XTC_OK; } xtc_result SampleEditor::undo_sample(){if(!previous_)return XTC_ERR_NOT_FOUND;sample_.swap(previous_);refresh();return XTC_OK;}