feat: apply reversible linear and equal-power fades to sample selections

This commit is contained in:
Johannes Findeisen 2026-11-06 01:06:24 +01:00
commit 15714fa764
7 changed files with 23 additions and 7 deletions

View file

@ -33,3 +33,7 @@ Invert polarity negates selected float samples; it is not time reversal. Over-ra
## 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.
## Phase 75
Sample fades offers four offline curves: linear in/out and equal-power in/out (sinusoidal amplitude). The entire selection is the fade duration; both endpoints are included in the curve. Fewer than two frames reject. These curves change copied sample data and stack with existing playback clip fades after adoption; they do not form a linked clip crossfade.

View file

@ -1777,12 +1777,12 @@ Implemented: double-precision selected-range mean subtraction in a new sample sn
Apply linear or equal-power fade-in/out over selected source frames with exact endpoint behavior, local undo and explicit distinction from nondestructive clip fades.
### 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: selected-range linear/equal-power fade-in/out processing with explicit endpoints, one-frame rejection and unchanged surrounding frames. Core curve/interior/endpoint checks and GUI local 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_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_REMOVE_DC } xtc_sample_operation;
typedef enum { XTC_SAMPLE_REVERSE, XTC_SAMPLE_INVERT, XTC_SAMPLE_REMOVE_DC, XTC_SAMPLE_FADE_IN, XTC_SAMPLE_FADE_OUT, XTC_SAMPLE_FADE_IN_POWER, XTC_SAMPLE_FADE_OUT_POWER } 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,

View file

@ -54,14 +54,20 @@ 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;xtc_sample_stats stats;
if(!range(s,begin,end)||!out||(operation<XTC_SAMPLE_REVERSE||operation>XTC_SAMPLE_REMOVE_DC)||!isfinite(amount))return XTC_ERR_INVALID_ARGUMENT;
if(!range(s,begin,end)||!out||(operation<XTC_SAMPLE_REVERSE||operation>XTC_SAMPLE_FADE_OUT_POWER)||!isfinite(amount))return XTC_ERR_INVALID_ARGUMENT;
if(operation>=XTC_SAMPLE_FADE_IN && end-begin<2)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;i<s->wave.frames;++i)copy->wave.samples[i]=s->wave.samples[i];
for(i=begin;i<end;++i){double value;
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;}
switch(operation){
case XTC_SAMPLE_FADE_IN:case XTC_SAMPLE_FADE_OUT:case XTC_SAMPLE_FADE_IN_POWER:case XTC_SAMPLE_FADE_OUT_POWER:{
double fraction=(double)(i-begin)/(double)(end-begin-1);if(operation==XTC_SAMPLE_FADE_OUT||operation==XTC_SAMPLE_FADE_OUT_POWER)fraction=1-fraction;
if(operation==XTC_SAMPLE_FADE_IN_POWER||operation==XTC_SAMPLE_FADE_OUT_POWER)fraction=sin(fraction*1.57079632679489661923);
value=s->wave.samples[i]*fraction;break;}
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;
}

View file

@ -44,6 +44,7 @@ SampleEditor::SampleEditor(xtc_sample *sample):Fl_Double_Window(1100,650,"xtc
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 *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();

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));
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);
CHECK(e->process(XTC_SAMPLE_FADE_IN_POWER)==XTC_OK);CHECK(e->undo_sample()==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);

View file

@ -45,5 +45,9 @@ int main(void)
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);}
for(int mode=0;mode<4;++mode){xtc_sample *q=NULL;assert(xtc_sample_process(s,1,7,(xtc_sample_operation)(XTC_SAMPLE_FADE_IN+mode),0,&q)==XTC_OK);
for(size_t j=0;j<8;++j){double scale=1;if(j>=1&&j<7){scale=(double)(j-1)/5;if(mode==1||mode==3)scale=1-scale;if(mode>=2)scale=sin(scale*1.57079632679489661923);}assert(xtc_sample_read(q,j,&v)==XTC_OK && fabs(v-values[j]*scale)<.000001);}
xtc_sample_destroy(q);}
{xtc_sample *q=NULL;assert(xtc_sample_process(s,1,2,XTC_SAMPLE_FADE_IN,0,&q)==XTC_ERR_INVALID_ARGUMENT && q==NULL);}
xtc_sample_destroy(s);assert(unlink(path)==0 && rmdir(root)==0);puts("Sample editor core tests passed.");return 0;
}