feat: consolidate trimmed MIDI clips with explicit note boundary cropping
This commit is contained in:
parent
3c202cbfdc
commit
fc76bf6803
9 changed files with 68 additions and 5 deletions
|
|
@ -22,3 +22,7 @@ Edit/Duplicate complete track copies the selected non-bus track adjacent to its
|
|||
## 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.
|
||||
|
||||
## Phase 63 — MIDI Clip Consolidation
|
||||
|
||||
Edit/Consolidate MIDI clips merges two or more selected MIDI clips on one track. The new clip spans their full bounding range; gaps and overlapping notes remain. Notes outside visible trims are discarded and crossing notes are cropped to audible boundaries. IDs are fresh. Cropped notes start anew, so envelope/phase behavior can change. Audio or cross-track selections reject atomically.
|
||||
|
|
|
|||
|
|
@ -1572,12 +1572,12 @@ Implemented: trimmed MIDI splitting with fresh right-side clip/note IDs and comp
|
|||
Merge selected MIDI clips on one track into a fresh source, retaining audible notes and gaps with explicit boundary clipping and rollback.
|
||||
|
||||
### 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: transactional consolidation of selected same-track MIDI clips into a fresh source. Visible note intersections are cropped, hidden notes dropped and gaps/overlaps retained. Core trim/timing/ID checks and GUI merge/undo tests pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ xtc_result xtc_project_duplicate_track(xtc_project *project,uint64_t track,const
|
|||
* 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);
|
||||
/* Consolidate >=2 MIDI clips on one track into a fresh clip spanning their
|
||||
* bounding range. Copies only audible note intersections, crops at visible
|
||||
* boundaries, drops hidden notes, retains overlaps and gaps. Fresh IDs; atomic.
|
||||
* Cropped notes start anew; instrument envelopes/phase can change. */
|
||||
xtc_result xtc_project_merge_midi(xtc_project *project,const uint64_t *ids,size_t count,uint64_t *out);
|
||||
/* 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
|
||||
|
|
|
|||
|
|
@ -342,6 +342,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
menu->add("Edit/Repeat selected phrase...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->repeat_dialog();},sequencer_);
|
||||
menu->add("Edit/Copy clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->copy_selection();},sequencer_);
|
||||
menu->add("Edit/Paste clips at cursor",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->paste_selection();},sequencer_);
|
||||
menu->add("Edit/Consolidate MIDI clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->merge_midi();},sequencer_);
|
||||
menu->add("Edit/Duplicate complete track...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->duplicate_track_dialog();},sequencer_);
|
||||
menu->add("Edit/Time/Insert gap...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->ripple_dialog(false);},sequencer_);
|
||||
menu->add("Edit/Time/Remove range...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->ripple_dialog(true);},sequencer_);
|
||||
|
|
|
|||
|
|
@ -519,6 +519,13 @@ int SequencerWidget::handle(int event)
|
|||
return Fl_Group::handle(event);
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::merge_midi()
|
||||
{
|
||||
auto ids=selected_clips();uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_merge_midi(project_,ids.data(),ids.size(),&id);
|
||||
if(r==XTC_OK){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);select(c.track_id,id);}
|
||||
changed(r);return r;
|
||||
}
|
||||
xtc_result SequencerWidget::duplicate_track(const char *name)
|
||||
{
|
||||
const uint64_t source=track_;uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class SequencerWidget : public Fl_Group {
|
|||
Fl_Choice *instrument_choice(uint64_t track) const;
|
||||
void refresh_instruments();
|
||||
bool select(uint64_t track, uint64_t clip = UINT64_MAX);
|
||||
xtc_result merge_midi();
|
||||
xtc_result duplicate_track(const char *name);
|
||||
void duplicate_track_dialog();
|
||||
xtc_result ripple_time(uint64_t length,bool remove);
|
||||
|
|
|
|||
|
|
@ -233,3 +233,29 @@ xtc_result xtc_project_split_midi(xtc_project *p,uint64_t clip,uint64_t tick,uin
|
|||
if(r==XTC_OK){publish(p,copy);*out=id;}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_merge_midi(xtc_project *p,const uint64_t *ids,size_t count,uint64_t *out)
|
||||
{
|
||||
xtc_project *copy=NULL;size_t i,j;uint64_t begin=XTC_PROJECT_MAX_TICK,end=0,track=UINT64_MAX,id=0;
|
||||
xtc_result r=selection_valid(p,ids,count);if(r!=XTC_OK)return r;
|
||||
if(count<2 || !out)return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<count;++i){xtc_clip_info c;xtc_project_find_clip(p,ids[i],&c);
|
||||
if(!c.midi || (i && c.track_id!=track))return XTC_ERR_INVALID_STATE;
|
||||
track=c.track_id;if(c.start_tick<begin)begin=c.start_tick;if(c.start_tick+c.length_tick>end)end=c.start_tick+c.length_tick;
|
||||
}
|
||||
r=xtc_project_clone(p,©);if(r!=XTC_OK)return r;
|
||||
for(i=0;r==XTC_OK && i<count;++i)r=xtc_project_remove_clip(copy,ids[i]);
|
||||
if(r==XTC_OK)r=xtc_project_add_midi_clip(copy,track,begin,end-begin,&id);
|
||||
if(r==XTC_OK)r=xtc_project_rename_clip(copy,id,"Consolidated MIDI");
|
||||
for(i=0;r==XTC_OK && i<p->note_count;++i) {
|
||||
const xtc_note_info *n=p->notes+i;xtc_clip_info c;uint64_t a,b,note;
|
||||
for(j=0;j<count;++j)if(n->clip_id==ids[j])break;
|
||||
if(j==count)continue;
|
||||
xtc_project_find_clip(p,n->clip_id,&c);
|
||||
a=n->start_tick>c.offset_tick ? n->start_tick : c.offset_tick;
|
||||
b=n->start_tick+n->length_tick<c.offset_tick+c.length_tick ? n->start_tick+n->length_tick : c.offset_tick+c.length_tick;
|
||||
if(b>a)r=xtc_project_add_note(copy,id,c.start_tick-begin+a-c.offset_tick,b-a,n->pitch,n->velocity,n->channel,¬e);
|
||||
}
|
||||
if(r==XTC_OK){publish(p,copy);*out=id;}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1382,6 +1382,9 @@ int main(int argc, char **argv)
|
|||
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);
|
||||
CHECK(s->select(0,0));CHECK(s->duplicate_selection()==XTC_OK);s->select_all_clips();CHECK(s->merge_midi()==XTC_OK);
|
||||
xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(clips==1);
|
||||
CHECK(window.undo()==XTC_OK);xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(clips==2);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -95,4 +95,20 @@ static void split_midi(void)
|
|||
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;}
|
||||
static void merge_midi(void)
|
||||
{
|
||||
xtc_project *p=NULL;uint64_t t,ids[2],n,out=999;xtc_note_info note;size_t count;
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p,t,960,960,&ids[0])==XTC_OK && xtc_project_add_midi_clip(p,t,1920,960,&ids[1])==XTC_OK);
|
||||
assert(xtc_project_add_note(p,ids[0],0,240,60,90,0,&n)==XTC_OK);
|
||||
assert(xtc_project_add_note(p,ids[0],600,240,64,100,1,&n)==XTC_OK);
|
||||
assert(xtc_project_add_note(p,ids[1],0,480,67,110,2,&n)==XTC_OK);
|
||||
assert(xtc_project_edit_clip(p,ids[0],t,960,480,240)==XTC_OK && xtc_project_edit_clip(p,ids[1],t,1920,480,240)==XTC_OK);
|
||||
assert(xtc_project_merge_midi(p,ids,1,&out)==XTC_ERR_INVALID_ARGUMENT && out==999);
|
||||
assert(xtc_project_merge_midi(p,ids,2,&out)==XTC_OK);
|
||||
assert(xtc_project_note_count(p,&count)==XTC_OK && count==2);
|
||||
assert(xtc_project_get_note(p,0,¬e)==XTC_OK && note.start_tick==360 && note.length_tick==120 && note.pitch==64 && note.channel==1);
|
||||
assert(xtc_project_get_note(p,1,¬e)==XTC_OK && note.start_tick==960 && note.length_tick==240 && note.pitch==67);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
int main(void){markers();sections();ripple();duplicate_track();split_midi();merge_midi();puts("Next DAW workflow tests passed.");return 0;}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue