From 9f1ff5f1e4741efc3a1c68204dbc242b509ec15f Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 01:05:19 +0100 Subject: [PATCH] feat: remove selected audio DC offset with double precision analysis --- docs/AUDIO_EDITOR.md | 4 ++++ docs/ROADMAP.md | 8 ++++---- include/xtc/xtc_sample.h | 2 +- src/audio/sample.c | 7 ++++--- src/gui/sample_editor.cpp | 1 + tests/integration/test_gui.cpp | 1 + tests/unit/test_sample.c | 5 +++++ 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/AUDIO_EDITOR.md b/docs/AUDIO_EDITOR.md index ed8d5ae..5828bf4 100644 --- a/docs/AUDIO_EDITOR.md +++ b/docs/AUDIO_EDITOR.md @@ -29,3 +29,7 @@ Reverse selection changes only the chosen source frames in a new local snapshot. ## 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. + +## 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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index df6f018..6ffb308 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. ### 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: 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. --- diff --git a/include/xtc/xtc_sample.h b/include/xtc/xtc_sample.h index 3f638db..0f1f38a 100644 --- a/include/xtc/xtc_sample.h +++ b/include/xtc/xtc_sample.h @@ -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_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_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. * amount is reserved/ignored for reversal. Failure preserves *out. */ xtc_result xtc_sample_process(const xtc_sample *sample,size_t begin,size_t end, diff --git a/src/audio/sample.c b/src/audio/sample.c index 091afa9..f9674e6 100644 --- a/src/audio/sample.c +++ b/src/audio/sample.c @@ -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_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||(operationXTC_SAMPLE_INVERT)||!isfinite(amount))return XTC_ERR_INVALID_ARGUMENT; + xtc_sample *copy;size_t i;xtc_result r=XTC_OK;xtc_sample_stats stats; + if(!range(s,begin,end)||!out||(operationXTC_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->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[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;} copy->wave.samples[i]=(float)value; } diff --git a/src/gui/sample_editor.cpp b/src/gui/sample_editor.cpp index 8dfab81..b221189 100644 --- a/src/gui/sample_editor.cpp +++ b/src/gui/sample_editor.cpp @@ -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(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 *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(); diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index a374eb8..7815a3b 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -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)); 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);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_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); diff --git a/tests/unit/test_sample.c b/tests/unit/test_sample.c index ddc7a8a..649c880 100644 --- a/tests/unit/test_sample.c +++ b/tests/unit/test_sample.c @@ -40,5 +40,10 @@ int main(void) 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 *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; }