diff --git a/docs/AUDIO_EDITOR.md b/docs/AUDIO_EDITOR.md index 5d86fcc..a26b9fe 100644 --- a/docs/AUDIO_EDITOR.md +++ b/docs/AUDIO_EDITOR.md @@ -21,3 +21,7 @@ Trim clip to selection applies source-frame endpoints within the current audible ## Phase 71 Export selected frames writes a new mono float32 WAV of the half-open selection at native rate. Save full copy and relink writes the complete source and relinks only the selected clip after successful preparation. The original source is never overwritten. Full-copy adoption requires matching source frame count/rate. Project undo restores the reference but retains the new file, which may be referenced by redo/history or other projects. Failed adoption removes its owned new file. Export/copy runs synchronously outside audio callbacks. + +## Phase 72 + +Reverse selection changes only the chosen source frames in a new local snapshot. Undo / redo toggles the most recent processing step; selection and view are unchanged. Processing does not affect playback until Save full copy and relink. Close discards unsaved local processing. Sources are bounded to 16 MiB of float samples; preparing a new edit can temporarily retain three snapshots (48 MiB), then two (32 MiB). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6d27c9d..3cc17d6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1732,12 +1732,12 @@ Implemented: no-clobber mono float32 selection export and full-source copy adopt Reverse a selected source frame range in an independent sample snapshot, with bounded local undo/redo and save-copy adoption. ### Tasks and acceptance criteria -- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. -- [ ] Preserve source files, atomic error handling, history and callback safety. -- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. +- [x] Implement bounded C99 analysis/processing and accessible FLTK controls. +- [x] Preserve source files, atomic error handling, history and callback safety. +- [x] Pass meaningful core and GUI checks; document exact behavior and limits. ### Implementation status -Planned. +Implemented: immutable whole-source snapshots reverse only selected frames; the editor provides one-level local undo/redo and save-copy adoption. Exact partial-range and double-reversal tests, unmodified surrounding samples and GUI processing/undo pass. --- diff --git a/include/xtc/xtc_sample.h b/include/xtc/xtc_sample.h index c97509a..fc315f6 100644 --- a/include/xtc/xtc_sample.h +++ b/include/xtc/xtc_sample.h @@ -15,6 +15,11 @@ void xtc_sample_destroy(xtc_sample *sample); xtc_result xtc_sample_info(const xtc_sample *sample,unsigned int *rate,size_t *frames); xtc_result xtc_sample_read(const xtc_sample *sample,size_t frame,float *out); xtc_result xtc_sample_analyze(const xtc_sample *sample,size_t begin,size_t end,xtc_sample_stats *out); +typedef enum { XTC_SAMPLE_REVERSE } xtc_sample_operation; +/* Independent full-source result; only [begin,end) changes. Never mutates input. + * amount is reserved/ignored for reversal. Failure preserves *out. */ +xtc_result xtc_sample_process(const xtc_sample *sample,size_t begin,size_t end, + xtc_sample_operation operation,double amount,xtc_sample **out); /* Control-thread bounded mono float32 WAV save. Absolute path <512 bytes, * NEW destination only. Failure/cancel publishes nothing and removes temporary. */ xtc_result xtc_sample_write(const xtc_sample *sample,size_t begin,size_t end,const char *path); diff --git a/src/audio/sample.c b/src/audio/sample.c index 2405c76..10f7e2d 100644 --- a/src/audio/sample.c +++ b/src/audio/sample.c @@ -49,3 +49,20 @@ xtc_result xtc_sample_write(const xtc_sample *s,size_t begin,size_t end,const ch if(r!=XTC_OK)xtc_recorder_abort(rec); xtc_recorder_destroy(rec);return r; } + +xtc_result xtc_sample_process(const xtc_sample *s,size_t begin,size_t end, + xtc_sample_operation operation,double amount,xtc_sample **out) +{ + xtc_sample *copy;size_t i;xtc_result r=XTC_OK; + if(!range(s,begin,end)||!out||operation!=XTC_SAMPLE_REVERSE||!isfinite(amount))return XTC_ERR_INVALID_ARGUMENT; + copy=calloc(1,sizeof(*copy));if(!copy)return XTC_ERR_OUT_OF_MEMORY; + copy->wave=s->wave;copy->wave.samples=malloc(s->wave.frames*sizeof(float)); + if(!copy->wave.samples){free(copy);return XTC_ERR_OUT_OF_MEMORY;} + for(i=0;iwave.frames;++i)copy->wave.samples[i]=s->wave.samples[i]; + for(i=begin;iwave.samples[end-1-(i-begin)];break;default:value=0;r=XTC_ERR_INVALID_ARGUMENT;break;} + if(!isfinite(value)||fabs(value)>3.4028234663852886e38){r=XTC_ERR_CAPACITY;break;} + copy->wave.samples[i]=(float)value; + } + if(r!=XTC_OK){xtc_sample_destroy(copy);return r;}*out=copy;return XTC_OK; +} diff --git a/src/gui/sample_editor.cpp b/src/gui/sample_editor.cpp index 24b23de..d0956bb 100644 --- a/src/gui/sample_editor.cpp +++ b/src/gui/sample_editor.cpp @@ -41,6 +41,8 @@ SampleEditor::SampleEditor(xtc_sample *sample):Fl_Double_Window(1100,650,"xtc 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 *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(); } @@ -53,3 +55,10 @@ xtc_result SampleEditor::trim_clip(){return trim ? trim(rate_,begin_,end_) : XTC 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;} diff --git a/src/gui/sample_editor.hpp b/src/gui/sample_editor.hpp index 65259c5..d5f2a38 100644 --- a/src/gui/sample_editor.hpp +++ b/src/gui/sample_editor.hpp @@ -16,6 +16,8 @@ public: std::function adopt; xtc_result save_copy(const char *path,bool use_in_project); xtc_result export_selection(const char *path); + xtc_result process(xtc_sample_operation operation,double amount=0); + xtc_result undo_sample(); void run(); xtc_result select(size_t begin,size_t end); xtc_result view(size_t begin,unsigned int zoom); @@ -26,6 +28,7 @@ public: void refresh(); private: std::unique_ptr sample_{nullptr,xtc_sample_destroy}; + std::unique_ptr previous_{nullptr,xtc_sample_destroy}; size_t frames_=0,begin_=0,end_=0,origin_=0;unsigned int rate_=0,zoom_=1; Fl_Value_Input *begin_input_,*end_input_,*origin_input_;Fl_Box *stats_;SampleCanvas *canvas_; }; diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 3d1c742..dd16cc6 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -194,7 +194,8 @@ static int sample_editor_tests() reset(); MainWindow window(false,"");window.show();auto *s=timeline(&window);char path[4096];CHECK(realpath("examples/projects/original-pluck.wav",path)); CHECK(window.import_audio(path)==XTC_OK); - Fl::add_timeout(.1,[](void *){auto *e=dynamic_cast(Fl::modal());CHECK(e!=nullptr);if(e){CHECK(e->select(10,100)==XTC_OK);CHECK(e->select(100,10)==XTC_ERR_INVALID_ARGUMENT);CHECK(e->view(10,8)==XTC_OK);CHECK(e->view(e->frames(),1)==XTC_ERR_INVALID_ARGUMENT);e->hide();}},nullptr); + Fl::add_timeout(.1,[](void *){auto *e=dynamic_cast(Fl::modal());CHECK(e!=nullptr);if(e){CHECK(e->select(10,100)==XTC_OK);float before=0,after=0;CHECK(xtc_sample_read(e->sample(),10,&before)==XTC_OK); + CHECK(e->process(XTC_SAMPLE_REVERSE)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);CHECK(xtc_sample_read(e->sample(),10,&after)==XTC_OK && before==after);CHECK(e->undo_sample()==XTC_OK);CHECK(e->select(100,10)==XTC_ERR_INVALID_ARGUMENT);CHECK(e->view(10,8)==XTC_OK);CHECK(e->view(e->frames(),1)==XTC_ERR_INVALID_ARGUMENT);e->hide();}},nullptr); CHECK(window.open_sample_editor()==XTC_OK);CHECK(s->selected_clip()!=UINT64_MAX); uint64_t id=s->selected_clip();CHECK(window.trim_sample_clip(id,48000,2400,12000)==XTC_OK); xtc_clip_info clip{};CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && clip.offset_tick==96 && clip.length_tick==384); diff --git a/tests/unit/test_sample.c b/tests/unit/test_sample.c index 6332a1c..11645fb 100644 --- a/tests/unit/test_sample.c +++ b/tests/unit/test_sample.c @@ -30,5 +30,10 @@ int main(void) assert(xtc_sample_load(copy,&q)==XTC_OK && xtc_sample_info(q,&rate,&frames)==XTC_OK && frames==6 && rate==8000); for(size_t j=0;j<6;++j){assert(xtc_sample_read(q,j,&v)==XTC_OK && v==values[j+1]);} xtc_sample_destroy(q);assert(unlink(copy)==0);assert(xtc_sample_write(s,0,8,"relative.wav")==XTC_ERR_INVALID_ARGUMENT);} + {xtc_sample *q=NULL,*twice=NULL;assert(xtc_sample_process(s,1,7,XTC_SAMPLE_REVERSE,0,&q)==XTC_OK); + for(size_t j=0;j<8;++j){assert(xtc_sample_read(q,j,&v)==XTC_OK && v==(j>=1&&j<7 ? values[7-j] : values[j]));} + assert(xtc_sample_process(q,1,7,XTC_SAMPLE_REVERSE,0,&twice)==XTC_OK); + for(size_t j=0;j<8;++j){assert(xtc_sample_read(twice,j,&v)==XTC_OK && v==values[j]);} + xtc_sample_destroy(q);xtc_sample_destroy(twice);} xtc_sample_destroy(s);assert(unlink(path)==0 && rmdir(root)==0);puts("Sample editor core tests passed.");return 0; }