feat: trim audio clips from sample editor ranges with conservative tick alignment
This commit is contained in:
parent
7a04c6d922
commit
3d031a0010
10 changed files with 59 additions and 6 deletions
|
|
@ -13,3 +13,7 @@ editor loads an independent snapshot of the complete source, up to 4,194,304 fra
|
|||
RMS and mean/DC before clip gain, fades or mixer processing. Display clamps to
|
||||
±1; sample statistics retain finite over-range values. Editing project MIDI still
|
||||
opens the piano roll. This editor does not audition or stream long files.
|
||||
|
||||
## Phase 70
|
||||
|
||||
Trim clip to selection applies source-frame endpoints within the current audible clip. Start rounds up and end rounds down to musical ticks; empty sub-tick ranges reject. Leading removal moves the timeline start by the same amount, preserving alignment. Source content, reference and clip gain/fade settings remain intact; project Undo restores the prior trim.
|
||||
|
|
|
|||
|
|
@ -1702,12 +1702,12 @@ Implemented: bounded C99 source snapshots, per-range min/max/RMS/DC analysis and
|
|||
Apply sample-editor selection as audible clip trims with conservative frame-to-musical-tick rounding, preserved source content and one project undo step.
|
||||
|
||||
### 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: native frame bounds trim audio inside its current audible region with conservative tick rounding and preserved timeline/source alignment. Core rounding/rejection/unchanged-state checks and GUI trim/project undo pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ 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);
|
||||
/* Select native source frame bounds inside the clip's current audible trim.
|
||||
* Start rounds up to a tick; end rounds down. Sub-tick empty ranges reject.
|
||||
* Timeline placement follows the removed leading source, preserving alignment.
|
||||
* No media I/O, source content or fade changes; caller supplies source rate. */
|
||||
xtc_result xtc_project_trim_source_frames(xtc_project *project,uint64_t clip,unsigned int rate,size_t begin,size_t end);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -25,3 +25,15 @@ xtc_result xtc_sample_analyze(const xtc_sample *s,size_t begin,size_t end,xtc_sa
|
|||
for(i=begin;i<end;++i){float v=s->wave.samples[i];if(v<result.minimum)result.minimum=v;if(v>result.maximum)result.maximum=v;sum+=v;square+=(double)v*v;}
|
||||
result.mean=sum/(double)(end-begin);result.rms=sqrt(square/(double)(end-begin));*out=result;return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_trim_source_frames(xtc_project *p,uint64_t id,unsigned int rate,size_t begin,size_t end)
|
||||
{
|
||||
xtc_clip_info c;uint64_t first,last,denom,factor;xtc_result r;
|
||||
if(!p||!rate||rate>768000||begin>=end||end>XTC_SAMPLE_MAX_FRAMES)return XTC_ERR_INVALID_ARGUMENT;
|
||||
r=xtc_project_find_clip(p,id,&c);if(r!=XTC_OK)return r;
|
||||
if(c.midi||!c.asset_path[0])return XTC_ERR_INVALID_STATE;
|
||||
factor=(uint64_t)xtc_project_tempo(p)*XTC_TICKS_PER_QUARTER;denom=(uint64_t)rate*60000U;
|
||||
first=((uint64_t)begin*factor+denom-1)/denom;last=(uint64_t)end*factor/denom;
|
||||
if(first>=last||first<c.offset_tick||last>c.offset_tick+c.length_tick)return XTC_ERR_INVALID_ARGUMENT;
|
||||
return xtc_project_edit_clip(p,id,c.track_id,c.start_tick+first-c.offset_tick,last-first,first);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2597,6 +2597,15 @@ xtc_result MainWindow::collect_project(const char *directory)
|
|||
BusyOperation busy(*this,status_,"Collecting project media…");
|
||||
return xtc_project_collect(project_.get(),directory);
|
||||
}
|
||||
xtc_result MainWindow::trim_sample_clip(uint64_t id,unsigned int rate,size_t begin,size_t end)
|
||||
{
|
||||
auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r;
|
||||
xtc_project *candidate=nullptr;r=xtc_project_clone(project_.get(),&candidate);
|
||||
if(r==XTC_OK)r=xtc_project_trim_source_frames(candidate,id,rate,begin,end);
|
||||
if(r==XTC_OK)r=install_project(candidate,false);
|
||||
if(r!=XTC_OK){xtc_project_destroy(candidate);return r;}
|
||||
xtc_clip_info clip{};xtc_project_find_clip(project_.get(),id,&clip);sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK;
|
||||
}
|
||||
xtc_result MainWindow::open_sample_editor()
|
||||
{
|
||||
if(recorder_)return XTC_ERR_INVALID_STATE;
|
||||
|
|
@ -2604,7 +2613,12 @@ xtc_result MainWindow::open_sample_editor()
|
|||
if(r!=XTC_OK)return r;
|
||||
if(clip.midi||!clip.asset_path[0])return XTC_ERR_INVALID_STATE;
|
||||
xtc_sample *sample=nullptr;r=xtc_sample_load(clip.asset_path,&sample);if(r!=XTC_OK)return r;
|
||||
SampleEditor editor(sample);editor.run();return XTC_OK;
|
||||
SampleEditor editor(sample);
|
||||
uint64_t first=0,last=0;unsigned int rate;size_t frames;xtc_sample_info(sample,&rate,&frames);
|
||||
if(xtc_project_tick_to_sample(project_.get(),clip.offset_tick,rate,&first)==XTC_OK && xtc_project_tick_to_sample(project_.get(),clip.offset_tick+clip.length_tick,rate,&last)==XTC_OK)
|
||||
editor.select(static_cast<size_t>(std::min(first,static_cast<uint64_t>(frames))),static_cast<size_t>(std::min(last,static_cast<uint64_t>(frames))));
|
||||
editor.trim=[this,id=clip.id](unsigned int sr,size_t b,size_t e){return trim_sample_clip(id,sr,b,e);};
|
||||
editor.run();return XTC_OK;
|
||||
}
|
||||
void MainWindow::show_commands()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
Fl_Widget *velocity_lane() const { return velocity_lane_; }
|
||||
/* Control-thread plugin commands, transactional model adoption. */
|
||||
xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index);
|
||||
xtc_result trim_sample_clip(uint64_t clip,unsigned int rate,size_t begin,size_t end);
|
||||
xtc_result open_sample_editor();
|
||||
void export_audio();
|
||||
xtc_result load_sampler(uint64_t track, const char *path);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <FL/Fl_Button.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
|
@ -37,6 +38,7 @@ SampleEditor::SampleEditor(xtc_sample *sample):Fl_Double_Window(1100,650,"xtc
|
|||
auto *plus=new Fl_Button(750,58,100,28,"Zoom in");plus->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);e->view(e->begin_,std::min(64U,e->zoom_*2));},this);
|
||||
auto *minus=new Fl_Button(860,58,100,28,"Zoom out");minus->callback([](Fl_Widget *,void *c){auto *e=static_cast<SampleEditor *>(c);e->view(e->origin_,std::max(1U,e->zoom_/2));},this);
|
||||
canvas_=new SampleCanvas(16,104,1068,340,*this);stats_=new Fl_Box(16,452,1068,50);stats_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
|
||||
auto *trim_button=new Fl_Button(16,520,180,30,"Trim clip to selection");trim_button->callback([](Fl_Widget *,void *c){auto r=static_cast<SampleEditor *>(c)->trim_clip();if(r!=XTC_OK)fl_alert("Clip trim unchanged: %s",xtc_result_string(r));},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();
|
||||
}
|
||||
|
|
@ -44,3 +46,5 @@ xtc_result SampleEditor::select(size_t b,size_t e){if(b>=e||e>frames_)return XTC
|
|||
xtc_result SampleEditor::view(size_t b,unsigned int zoom){if(b>=frames_||!zoom||zoom>64)return XTC_ERR_INVALID_ARGUMENT;origin_=b;zoom_=zoom;refresh();return XTC_OK;}
|
||||
void SampleEditor::refresh(){begin_input_->value(static_cast<double>(begin_));end_input_->value(static_cast<double>(end_));origin_input_->value(static_cast<double>(origin_));xtc_sample_stats s{};xtc_sample_analyze(sample_.get(),begin_,end_,&s);char text[256];std::snprintf(text,sizeof(text),"%u Hz / %zu source frames / selection %zu..%zu (%0.6f s) / zoom %ux\nMin %.6f Max %.6f RMS %.6f DC %.6f",rate_,frames_,begin_,end_,static_cast<double>(end_-begin_)/rate_,zoom_,s.minimum,s.maximum,s.rms,s.mean);stats_->copy_label(text);canvas_->redraw();}
|
||||
void SampleEditor::run(){show();while(shown())Fl::wait();}
|
||||
|
||||
xtc_result SampleEditor::trim_clip(){return trim ? trim(rate_,begin_,end_) : XTC_ERR_INVALID_STATE;}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ class SampleEditor : public Fl_Double_Window {
|
|||
public:
|
||||
/* Takes ownership of loaded snapshot. Modal edits never touch audio callbacks. */
|
||||
explicit SampleEditor(xtc_sample *sample);
|
||||
std::function<xtc_result(unsigned int,size_t,size_t)> trim;
|
||||
xtc_result trim_clip();
|
||||
void run();
|
||||
xtc_result select(size_t begin,size_t end);
|
||||
xtc_result view(size_t begin,unsigned int zoom);
|
||||
|
|
|
|||
|
|
@ -195,7 +195,11 @@ 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);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);window.do_callback();return failures ? 1 : 0;
|
||||
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);
|
||||
CHECK(window.undo()==XTC_OK);CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && clip.offset_tick==0);
|
||||
window.do_callback();return failures ? 1 : 0;
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,5 +18,12 @@ int main(void)
|
|||
assert(xtc_sample_read(s,2,&v)==XTC_OK && v==-.5f);
|
||||
assert(xtc_sample_analyze(s,2,2,&stats)==XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_sample_load("/missing/xtc.wav",&sentinel)==XTC_ERR_IO && sentinel==NULL);
|
||||
{xtc_project *p=NULL;uint64_t t,c;xtc_clip_info clip;assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Audio",&t)==XTC_OK);
|
||||
assert(xtc_project_add_clip(p,t,"Audio",10,960,&c)==XTC_OK && xtc_project_set_clip_asset(p,c,path)==XTC_OK);
|
||||
assert(xtc_project_trim_source_frames(p,c,48000,1000,2000)==XTC_OK);
|
||||
assert(xtc_project_find_clip(p,c,&clip)==XTC_OK && clip.start_tick==50 && clip.offset_tick==40 && clip.length_tick==40);
|
||||
assert(xtc_project_trim_source_frames(p,c,48000,1001,1002)==XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_find_clip(p,c,&clip)==XTC_OK && clip.offset_tick==40 && clip.length_tick==40);
|
||||
xtc_project_destroy(p);}
|
||||
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