feat: remove selected audio DC offset with double precision analysis

This commit is contained in:
Johannes Findeisen 2026-11-06 01:05:19 +01:00
commit 9f1ff5f1e4
7 changed files with 20 additions and 8 deletions

View file

@ -29,3 +29,7 @@ Reverse selection changes only the chosen source frames in a new local snapshot.
## Phase 73 ## 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. 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.
## Phase 74
Remove DC offset subtracts the mean of the selected range from only that range. This is a constant offset correction, not a high-pass filter. Selection boundaries may acquire discontinuities; use fades or zero-crossing tools where appropriate. Finite over-range results are retained; float overflow rejects the edit.

View file

@ -1762,12 +1762,12 @@ Implemented: selected-range polarity inversion preserves source length and every
Measure and subtract the selected range mean using double precision accumulation, preserve other frames and reject invalid ranges atomically. Measure and subtract the selected range mean using double precision accumulation, preserve other frames and reject invalid ranges atomically.
### 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: double-precision selected-range mean subtraction in a new sample snapshot; nonfinite results reject without mutation. Core residual mean/exact surrounding samples and 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_INVERT } xtc_sample_operation; typedef enum { XTC_SAMPLE_REVERSE, XTC_SAMPLE_INVERT, XTC_SAMPLE_REMOVE_DC } 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

@ -53,14 +53,15 @@ xtc_result xtc_sample_write(const xtc_sample *s,size_t begin,size_t end,const ch
xtc_result xtc_sample_process(const xtc_sample *s,size_t begin,size_t end, 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;xtc_sample_stats stats;
if(!range(s,begin,end)||!out||(operation<XTC_SAMPLE_REVERSE||operation>XTC_SAMPLE_INVERT)||!isfinite(amount))return XTC_ERR_INVALID_ARGUMENT; if(!range(s,begin,end)||!out||(operation<XTC_SAMPLE_REVERSE||operation>XTC_SAMPLE_REMOVE_DC)||!isfinite(amount))return XTC_ERR_INVALID_ARGUMENT;
r=xtc_sample_analyze(s,begin,end,&stats);if(r!=XTC_OK)return r;
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_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;} switch(operation){case XTC_SAMPLE_REMOVE_DC:value=(double)s->wave.samples[i]-stats.mean;break;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

@ -43,6 +43,7 @@ SampleEditor::SampleEditor(xtc_sample *sample):Fl_Double_Window(1100,650,"xtc
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 *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 *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_REMOVE_DC)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);
CHECK(e->process(XTC_SAMPLE_INVERT)==XTC_OK);CHECK(e->undo_sample()==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);

View file

@ -40,5 +40,10 @@ int main(void)
assert(xtc_sample_process(q,1,7,XTC_SAMPLE_INVERT,0,&twice)==XTC_OK); 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]);} 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;xtc_sample_stats after;assert(xtc_sample_analyze(s,1,7,&stats)==XTC_OK);
assert(xtc_sample_process(s,1,7,XTC_SAMPLE_REMOVE_DC,0,&q)==XTC_OK);
assert(xtc_sample_analyze(q,1,7,&after)==XTC_OK && fabs(after.mean)<.000001);
for(size_t j=0;j<8;++j){assert(xtc_sample_read(q,j,&v)==XTC_OK && fabs(v-(j>=1&&j<7 ? values[j]-stats.mean : values[j]))<.000001);}
xtc_sample_destroy(q);}
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;
} }