feat: split MIDI clips nondestructively with source-relative note slices
This commit is contained in:
parent
4c855ee5d7
commit
3c202cbfdc
8 changed files with 51 additions and 9 deletions
|
|
@ -18,3 +18,7 @@ Edit/Time inserts a gap or removes a tick range beginning at the cursor. Clips c
|
|||
## Phase 61 — Complete Track Duplication
|
||||
|
||||
Edit/Duplicate complete track copies the selected non-bus track adjacent to its source. Clips, notes, instrument settings, static mix, bus routing and mixer automation are independent model copies with fresh IDs; WAV references remain shared. The modular graph and live-input target are unchanged. Bus duplication is rejected. Capacity errors leave the original document unchanged.
|
||||
|
||||
## Phase 62 — Nondestructive MIDI Clip Splitting
|
||||
|
||||
The existing Split clip at cursor command (S) now handles MIDI as well as audio. Left retains its ID; right gets fresh IDs, the same source content and an adjusted source offset. Hidden notes remain editable. Split is nondestructive, but instrument note envelopes may retrigger at the new boundary; original sine rendering retains source phase. Capacity failures preserve the project.
|
||||
|
|
|
|||
|
|
@ -1554,15 +1554,15 @@ Implemented: complete non-bus track duplication with fresh IDs, adjacent orderin
|
|||
## Phase 62 — Nondestructive MIDI Clip Splitting
|
||||
|
||||
### Goal
|
||||
Split trimmed MIDI clips at the cursor while sharing source-relative note content and preserving exact rendered boundaries.
|
||||
Split trimmed MIDI clips at the cursor while preserving source-relative note content and verifying boundary scheduling.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [ ] Implement bounded C99 model/control APIs and accessible GUI commands.
|
||||
- [ ] Preserve transactional errors, history, compatibility and callback safety.
|
||||
- [ ] Verify the feature with automated model/GUI tests and document limitations.
|
||||
- [x] Implement bounded C99 model/control APIs and accessible GUI commands.
|
||||
- [x] Preserve transactional errors, history, compatibility and callback safety.
|
||||
- [x] Verify the feature with automated model/GUI tests and document limitations.
|
||||
|
||||
### Implementation status
|
||||
Planned.
|
||||
Implemented: trimmed MIDI splitting with fresh right-side clip/note IDs and complete retained source notes. Original sine rendering is identical across the split in regression tests; MIDI instruments may retrigger at boundaries. Core range/render and GUI split/undo tests pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ xtc_result xtc_project_ripple_time(xtc_project *project,uint64_t begin,uint64_t
|
|||
* instrument settings, mix, bus route and mixer automation; inserts adjacent to
|
||||
* source. Shared modular graph and live-input target are unchanged. Atomic. */
|
||||
xtc_result xtc_project_duplicate_track(xtc_project *project,uint64_t track,const char *name,uint64_t *out);
|
||||
/* MIDI split strictly inside the visible clip. Keeps left ID; right gets fresh
|
||||
* clip/note IDs and copied complete source notes with an adjusted source offset.
|
||||
* Hidden source notes remain editable. MIDI instruments may retrigger at the new
|
||||
* clip boundary; the original sine preserves source phase. Atomic. */
|
||||
xtc_result xtc_project_split_midi(xtc_project *project,uint64_t clip,uint64_t tick,uint64_t *out_right);
|
||||
/* MIDI transforms affect ALL source notes in selected MIDI clips, including
|
||||
* notes hidden by trims. Mixed audio/MIDI selections reject; IDs and unaffected
|
||||
* fields persist. Initialize the edit struct to zero and set the fields for its
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
menu->add("Edit/Markers/Remove at cursor",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->marker(nullptr);},sequencer_);
|
||||
menu->add("Edit/Select all clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->select_all_clips();},sequencer_);
|
||||
menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->duplicate_selection(); },sequencer_);
|
||||
menu->add("Edit/Split audio at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->split_selection(); }, sequencer_);
|
||||
menu->add("Edit/Split clip at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->split_selection(); }, sequencer_);
|
||||
menu->add("Edit/Audio clip gain and fades", 0, [](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->audio_properties_dialog(); }, sequencer_);
|
||||
menu->add("Edit/Rename clip or track",FL_F+2,[](Fl_Widget *, void *c) {
|
||||
const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) (void)static_cast<SequencerWidget *>(c)->rename_selection(name);
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@ int SequencerWidget::handle(int event)
|
|||
mode_ = 0;
|
||||
Fl_Menu_Item items[6]{};
|
||||
items[0].text = "Duplicate clip (Ctrl+D)"; items[1].text = "Rename (F2)"; items[2].text = "Delete (undoable)";
|
||||
items[3].text = "Split audio at cursor (S)"; items[4].text = "Audio gain and fades (A)";
|
||||
items[3].text = "Split clip at cursor (S)"; items[4].text = "Audio gain and fades (A)";
|
||||
const Fl_Menu_Item *picked = items->popup(Fl::event_x(),Fl::event_y());
|
||||
if (picked == items) duplicate_selection();
|
||||
else if (picked == items+1) { const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); }
|
||||
|
|
@ -703,7 +703,10 @@ xtc_result SequencerWidget::split_selection()
|
|||
uint64_t right;
|
||||
const uint64_t split_tick = cursor_; // Stop/status refresh may move the playhead.
|
||||
xtc_result result = before_edit ? before_edit(context_) : XTC_OK;
|
||||
if (result == XTC_OK) result = xtc_project_split_audio(project_, clip_, split_tick, &right);
|
||||
if (result == XTC_OK) {
|
||||
xtc_clip_info c{};result=xtc_project_find_clip(project_,clip_,&c);
|
||||
if(result==XTC_OK)result=c.midi ? xtc_project_split_midi(project_,clip_,split_tick,&right) : xtc_project_split_audio(project_,clip_,split_tick,&right);
|
||||
}
|
||||
if (result == XTC_OK) {clip_ = right;selection_.clear();}
|
||||
changed(result);
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -219,3 +219,17 @@ xtc_result xtc_project_duplicate_track(xtc_project *p,uint64_t track,const char
|
|||
if(r==XTC_OK){publish(p,copy);*out=id;}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_split_midi(xtc_project *p,uint64_t clip,uint64_t tick,uint64_t *out)
|
||||
{
|
||||
xtc_clip_info c;xtc_project *copy=NULL;uint64_t id=0,left;xtc_result r;
|
||||
if(!p || !out)return XTC_ERR_INVALID_ARGUMENT;
|
||||
r=xtc_project_find_clip(p,clip,&c);if(r!=XTC_OK)return r;
|
||||
if(!c.midi || tick<=c.start_tick || tick>=c.start_tick+c.length_tick)return XTC_ERR_INVALID_ARGUMENT;
|
||||
left=tick-c.start_tick;r=xtc_project_clone(p,©);if(r!=XTC_OK)return r;
|
||||
r=xtc_project_duplicate_clip(copy,clip,c.track_id,c.start_tick,&id);
|
||||
if(r==XTC_OK)r=xtc_project_edit_clip(copy,id,c.track_id,tick,c.length_tick-left,c.offset_tick+left);
|
||||
if(r==XTC_OK)r=xtc_project_edit_clip(copy,clip,c.track_id,c.start_tick,left,c.offset_tick);
|
||||
if(r==XTC_OK){publish(p,copy);*out=id;}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1379,6 +1379,9 @@ int main(int argc, char **argv)
|
|||
CHECK(s->select(0));CHECK(s->duplicate_track("Keys copy")==XTC_OK);
|
||||
size_t tracks=0,clips=0;xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(tracks==2 && clips==2);
|
||||
CHECK(window.undo()==XTC_OK);xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(tracks==1 && clips==1);
|
||||
CHECK(s->select(0,0));s->position(1920);CHECK(s->split_selection()==XTC_OK);
|
||||
xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(clips==2);
|
||||
CHECK(window.undo()==XTC_OK);xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(clips==1);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -82,4 +82,17 @@ static void duplicate_track(void)
|
|||
assert(xtc_project_automation_count(p,&count)==XTC_OK && count==2);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
int main(void){markers();sections();ripple();duplicate_track();puts("Next DAW workflow tests passed.");return 0;}
|
||||
static void split_midi(void)
|
||||
{
|
||||
xtc_project *p=NULL;xtc_midi_plan *before=NULL,*after=NULL;uint64_t t,c,n,right=999;xtc_clip_info slice;float a[512],b[512];
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p,t,0,960,&c)==XTC_OK && xtc_project_add_note(p,c,0,960,60,90,0,&n)==XTC_OK);
|
||||
assert(xtc_midi_plan_create(p,48000,&before)==XTC_OK);
|
||||
assert(xtc_project_split_midi(p,c,0,&right)==XTC_ERR_INVALID_ARGUMENT && right==999);
|
||||
assert(xtc_project_split_midi(p,c,480,&right)==XTC_OK && right!=c);
|
||||
assert(xtc_project_find_clip(p,right,&slice)==XTC_OK && slice.offset_tick==480 && slice.length_tick==480);
|
||||
assert(xtc_midi_plan_create(p,48000,&after)==XTC_OK);
|
||||
assert(xtc_midi_plan_render(before,11800,NULL,0,a,512)==XTC_OK && xtc_midi_plan_render(after,11800,NULL,0,b,512)==XTC_OK);
|
||||
assert(!memcmp(a,b,sizeof(a)));xtc_midi_plan_destroy(before);xtc_midi_plan_destroy(after);xtc_project_destroy(p);
|
||||
}
|
||||
int main(void){markers();sections();ripple();duplicate_track();split_midi();puts("Next DAW workflow tests passed.");return 0;}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue