feat: select audible sample boundaries with threshold and padding controls
This commit is contained in:
parent
ddef6cd73d
commit
392e437d32
8 changed files with 32 additions and 5 deletions
|
|
@ -41,3 +41,7 @@ Sample fades offers four offline curves: linear in/out and equal-power in/out (s
|
|||
## Phase 76
|
||||
|
||||
Gain / normalize processes only selected samples. Mode 0 accepts linear gain 0..16; mode 1 accepts a linear peak target above 0 and at most 1, rejecting silent selections or required gain above 16. Finite values above unity are not clipped; float overflow rejects atomically. This changes sample data in the local snapshot, unlike the existing clip-gain normalization command; it provides neither true-peak nor LUFS processing.
|
||||
|
||||
## Phase 77
|
||||
|
||||
Select audible bounds finds the first and last sample strictly above the chosen dBFS threshold and pads within the existing selection. It only changes selection; use Trim clip to selection to apply a nondestructive timeline trim, or export the selection. Entirely silent/below-threshold ranges leave selection unchanged. This is boundary detection, not automatic removal of internal gaps or a noise gate.
|
||||
|
|
|
|||
|
|
@ -1807,12 +1807,12 @@ Implemented: selected sample gain (0..16) and peak normalization (target >0..1,
|
|||
Find audible selected-range bounds using a user threshold and padding; select detected bounds for export or nondestructive clip trimming. Silence is an explicit no-result error.
|
||||
|
||||
### 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: threshold-based audible-range detection with bounded padding and atomic selection updates. Exact threshold/padding, silent no-result, NaN and GUI unchanged/successful selection checks pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ typedef enum { XTC_SAMPLE_REVERSE, XTC_SAMPLE_INVERT, XTC_SAMPLE_REMOVE_DC, XTC_
|
|||
* One-frame fades reject; fade endpoints are exactly 0 and 1. 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);
|
||||
/* First/last sample strictly above linear threshold 0..1, padded and clamped
|
||||
* to the input range. All-silent returns NOT_FOUND; outputs unchanged on error. */
|
||||
xtc_result xtc_sample_audible_bounds(const xtc_sample *sample,size_t begin,size_t end,
|
||||
double threshold,size_t padding,size_t *first,size_t *last);
|
||||
/* 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);
|
||||
|
|
|
|||
|
|
@ -76,3 +76,14 @@ xtc_result xtc_sample_process(const xtc_sample *s,size_t begin,size_t end,
|
|||
}
|
||||
if(r!=XTC_OK){xtc_sample_destroy(copy);return r;}*out=copy;return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_sample_audible_bounds(const xtc_sample *s,size_t begin,size_t end,
|
||||
double threshold,size_t padding,size_t *first,size_t *last)
|
||||
{
|
||||
size_t b=begin,e=end;
|
||||
if(!range(s,begin,end)||!first||!last||first==last||!isfinite(threshold)||threshold<0||threshold>1||padding>XTC_SAMPLE_MAX_FRAMES)return XTC_ERR_INVALID_ARGUMENT;
|
||||
while(b<end && fabs(s->wave.samples[b])<=threshold)++b;
|
||||
if(b==end)return XTC_ERR_NOT_FOUND;
|
||||
while(e>b && fabs(s->wave.samples[e-1])<=threshold)--e;
|
||||
*first=b-begin>padding ? b-padding : begin;*last=end-e>padding ? e+padding : end;return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ SampleEditor::SampleEditor(xtc_sample *sample):Fl_Double_Window(1100,650,"xtc
|
|||
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 *gain=new Fl_Button(704,562,160,30,"Gain / normalize...");gain->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);const char *text=fl_input("Mode (0 gain, 1 normalize), linear gain 0..16 or target peak >0..1","1 0.9");if(!text)return;unsigned int mode;double value;char extra;if(std::sscanf(text,"%u %lf %c",&mode,&value,&extra)!=2||mode>1){fl_alert("Enter mode and linear value.");return;}auto r=e->process(mode ? XTC_SAMPLE_NORMALIZE : XTC_SAMPLE_GAIN,value);if(r!=XTC_OK)fl_alert("Gain edit failed: %s",xtc_result_string(r));},this);
|
||||
auto *silence=new Fl_Button(876,562,200,30,"Select audible bounds...");silence->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);const char *text=fl_input("Silence threshold dBFS (-120..0), padding ms (0..10000)","-60 2");if(!text)return;double db,ms;char extra;if(std::sscanf(text,"%lf %lf %c",&db,&ms,&extra)!=2||!std::isfinite(db)||!std::isfinite(ms)||db< -120||db>0||ms<0||ms>10000){fl_alert("Enter a threshold and padding within the shown bounds.");return;}auto r=e->select_audible(std::pow(10.,db/20),static_cast<size_t>(std::min(static_cast<double>(e->frames_),ms*e->rate_/1000)));if(r!=XTC_OK)fl_alert("Audible bounds not found: %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();
|
||||
|
|
@ -66,3 +67,5 @@ xtc_result SampleEditor::process(xtc_sample_operation operation,double amount)
|
|||
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;}
|
||||
|
||||
xtc_result SampleEditor::select_audible(double threshold,size_t padding){size_t b=0,e=0;auto r=xtc_sample_audible_bounds(sample_.get(),begin_,end_,threshold,padding,&b,&e);return r==XTC_OK ? select(b,e) : r;}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public:
|
|||
xtc_result export_selection(const char *path);
|
||||
xtc_result process(xtc_sample_operation operation,double amount=0);
|
||||
xtc_result undo_sample();
|
||||
xtc_result select_audible(double threshold,size_t padding);
|
||||
void run();
|
||||
xtc_result select(size_t begin,size_t end);
|
||||
xtc_result view(size_t begin,unsigned int zoom);
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ static int sample_editor_tests()
|
|||
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);
|
||||
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);size_t b=e->begin(),z=e->end();CHECK(e->select_audible(1,0)==XTC_ERR_NOT_FOUND && e->begin()==b && e->end()==z);CHECK(e->select(0,e->frames())==XTC_OK);CHECK(e->select_audible(.01,2)==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);
|
||||
|
|
|
|||
|
|
@ -55,5 +55,9 @@ int main(void)
|
|||
assert(xtc_sample_process(s,4,6,XTC_SAMPLE_NORMALIZE,1,&q)==XTC_ERR_INVALID_STATE && q==NULL);
|
||||
assert(xtc_sample_process(s,1,7,XTC_SAMPLE_GAIN,NAN,&q)==XTC_ERR_INVALID_ARGUMENT && q==NULL);
|
||||
assert(xtc_sample_process(s,1,7,XTC_SAMPLE_GAIN,17,&q)==XTC_ERR_INVALID_ARGUMENT && q==NULL);}
|
||||
{size_t b=999,e=999;assert(xtc_sample_audible_bounds(s,0,8,.25,0,&b,&e)==XTC_OK && b==2 && e==7);
|
||||
assert(xtc_sample_audible_bounds(s,0,8,.25,2,&b,&e)==XTC_OK && b==0 && e==8);
|
||||
b=e=999;assert(xtc_sample_audible_bounds(s,4,6,0,0,&b,&e)==XTC_ERR_NOT_FOUND && b==999 && e==999);
|
||||
assert(xtc_sample_audible_bounds(s,0,8,NAN,0,&b,&e)==XTC_ERR_INVALID_ARGUMENT && b==999);}
|
||||
xtc_sample_destroy(s);assert(unlink(path)==0 && rmdir(root)==0);puts("Sample editor core tests passed.");return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue