xtc/src/gui/sample_editor.cpp

68 lines
8.7 KiB
C++

#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>
#include <FL/fl_ask.H>
#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);
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);
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);
auto *reverse=new Fl_Button(16,562,160,30,"Reverse selection");reverse->callback([](Fl_Widget *,void *c){auto r=static_cast<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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<SampleEditor *>(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_operation>(XTC_SAMPLE_FADE_IN+mode));if(r!=XTC_OK)fl_alert("Fade failed: %s",xtc_result_string(r));},this);
auto *gain=new Fl_Button(704,562,160,30,"Gain / normalize...");gain->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);const char *text=fl_input("Mode (0 gain, 1 normalize), linear gain 0..16 or target peak >0..1","1 0.9");if(!text)return;unsigned int mode;double value;char extra;if(std::sscanf(text,"%u %lf %c",&mode,&value,&extra)!=2||mode>1){fl_alert("Enter mode and linear value.");return;}auto r=e->process(mode ? XTC_SAMPLE_NORMALIZE : XTC_SAMPLE_GAIN,value);if(r!=XTC_OK)fl_alert("Gain edit 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<SampleEditor *>(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<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\nMin %.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();}
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;}