feat: invert selected audio sample polarity without changing surrounding frames

This commit is contained in:
Johannes Findeisen 2026-11-06 01:04:45 +01:00
commit f528a169f8
7 changed files with 18 additions and 7 deletions

View file

@ -25,3 +25,7 @@ Export selected frames writes a new mono float32 WAV of the half-open selection
## Phase 72 ## 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). 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).
## Phase 73
Invert polarity negates selected float samples; it is not time reversal. Over-range finite samples remain over-range; no implicit limiting is performed. Outside-selection frames are bit-preserved in the copied snapshot.

View file

@ -1747,12 +1747,12 @@ Implemented: immutable whole-source snapshots reverse only selected frames; the
Invert selected samples without altering frames outside the selection, with reversible local processing and no implicit clipping. Invert selected samples without altering frames outside the selection, with reversible local processing and no implicit clipping.
### Tasks and acceptance criteria ### Tasks and acceptance criteria
- [ ] Implement bounded C99 analysis/processing and accessible FLTK controls. - [x] Implement bounded C99 analysis/processing and accessible FLTK controls.
- [ ] Preserve source files, atomic error handling, history and callback safety. - [x] Preserve source files, atomic error handling, history and callback safety.
- [ ] Pass meaningful core and GUI checks; document exact behavior and limits. - [x] Pass meaningful core and GUI checks; document exact behavior and limits.
### Implementation status ### Implementation status
Planned. Implemented: selected-range polarity inversion preserves source length and every surrounding sample, supports local undo/redo and full-copy adoption. Exact signs, two inversions restoring input and focused GUI processing/undo pass.
--- ---

View file

@ -15,7 +15,7 @@ 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_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_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); 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; typedef enum { XTC_SAMPLE_REVERSE, XTC_SAMPLE_INVERT } xtc_sample_operation;
/* Independent full-source result; only [begin,end) changes. Never mutates input. /* Independent full-source result; only [begin,end) changes. Never mutates input.
* amount is reserved/ignored for reversal. Failure preserves *out. */ * 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_result xtc_sample_process(const xtc_sample *sample,size_t begin,size_t end,

View file

@ -54,13 +54,13 @@ 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_operation operation,double amount,xtc_sample **out)
{ {
xtc_sample *copy;size_t i;xtc_result r=XTC_OK; 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; if(!range(s,begin,end)||!out||(operation<XTC_SAMPLE_REVERSE||operation>XTC_SAMPLE_INVERT)||!isfinite(amount))return XTC_ERR_INVALID_ARGUMENT;
copy=calloc(1,sizeof(*copy));if(!copy)return XTC_ERR_OUT_OF_MEMORY; 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)); 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;} if(!copy->wave.samples){free(copy);return XTC_ERR_OUT_OF_MEMORY;}
for(i=0;i<s->wave.frames;++i)copy->wave.samples[i]=s->wave.samples[i]; for(i=0;i<s->wave.frames;++i)copy->wave.samples[i]=s->wave.samples[i];
for(i=begin;i<end;++i){double value; for(i=begin;i<end;++i){double value;
switch(operation){case XTC_SAMPLE_REVERSE:value=s->wave.samples[end-1-(i-begin)];break;default:value=0;r=XTC_ERR_INVALID_ARGUMENT;break;} switch(operation){case XTC_SAMPLE_INVERT:value=-(double)s->wave.samples[i];break;case XTC_SAMPLE_REVERSE:value=s->wave.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;} if(!isfinite(value)||fabs(value)>3.4028234663852886e38){r=XTC_ERR_CAPACITY;break;}
copy->wave.samples[i]=(float)value; copy->wave.samples[i]=(float)value;
} }

View file

@ -42,6 +42,7 @@ SampleEditor::SampleEditor(xtc_sample *sample):Fl_Double_Window(1100,650,"xtc
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 *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 *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 *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 *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 *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); 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(); Fl_Group::end();xtc_ui::style(this);refresh();

View file

@ -195,6 +195,7 @@ static int sample_editor_tests()
MainWindow window(false,"");window.show();auto *s=timeline(&window);char path[4096];CHECK(realpath("examples/projects/original-pluck.wav",path)); 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); CHECK(window.import_audio(path)==XTC_OK);
Fl::add_timeout(.1,[](void *){auto *e=dynamic_cast<SampleEditor *>(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); Fl::add_timeout(.1,[](void *){auto *e=dynamic_cast<SampleEditor *>(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_INVERT)==XTC_OK);CHECK(e->undo_sample()==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(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); 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); uint64_t id=s->selected_clip();CHECK(window.trim_sample_clip(id,48000,2400,12000)==XTC_OK);

View file

@ -35,5 +35,10 @@ int main(void)
assert(xtc_sample_process(q,1,7,XTC_SAMPLE_REVERSE,0,&twice)==XTC_OK); 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]);} 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(q);xtc_sample_destroy(twice);}
{xtc_sample *q=NULL,*twice=NULL;assert(xtc_sample_process(s,1,7,XTC_SAMPLE_INVERT,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[j] : values[j]));}
assert(xtc_sample_process(q,1,7,XTC_SAMPLE_INVERT,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; xtc_sample_destroy(s);assert(unlink(path)==0 && rmdir(root)==0);puts("Sample editor core tests passed.");return 0;
} }