feat: duplicate complete tracks with instruments routing and automation

This commit is contained in:
Johannes Findeisen 2026-11-06 00:33:58 +01:00
commit 4c855ee5d7
9 changed files with 65 additions and 5 deletions

View file

@ -14,3 +14,7 @@ View/Previous marker and Next marker navigate without wrapping and stop playback
## Phase 60 — Ripple Time Editing
Edit/Time inserts a gap or removes a tick range beginning at the cursor. Clips crossing either edit boundary reject the entire operation; split them first. Contained clips and their notes are removed; later clips, markers and absolute automation points shift together. Source notes within retained clips do not move. No interpolation guard points are generated, so automation ramps across the boundary may change slope. Loop preferences are unchanged.
## 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.

View file

@ -1542,12 +1542,12 @@ Implemented: atomic gap insertion/range removal shifts clips, markers and mixer/
Duplicate a non-bus track with fresh IDs, clips, notes, mixer controls, routing, instrument settings and track automation in one transaction.
### 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: complete non-bus track duplication with fresh IDs, adjacent ordering, source media/notes, instrument settings, mix, bus routing and automation. Core ownership/property checks and GUI undo tests pass.
---

View file

@ -23,6 +23,10 @@ xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,
* unchanged. No interpolated automation guard points are synthesized. Atomic.
* remove must be 0 or 1. GUI loop preferences are outside the project. */
xtc_result xtc_project_ripple_time(xtc_project *project,uint64_t begin,uint64_t length,int remove);
/* Non-bus track copy with fresh track/clip/note IDs. Copies source references,
* 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 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

View file

@ -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/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_);
menu->add("Edit/Markers/Set at cursor...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->marker_dialog();},sequencer_);

View file

@ -519,6 +519,17 @@ int SequencerWidget::handle(int event)
return Fl_Group::handle(event);
}
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;
if(r==XTC_OK)r=xtc_project_duplicate_track(project_,source,name,&id);
if(r==XTC_OK)select(id);
changed(r);return r;
}
void SequencerWidget::duplicate_track_dialog()
{
const char *name=fl_input("Name for complete track duplicate","Track copy");if(name)duplicate_track(name);
}
xtc_result SequencerWidget::ripple_time(uint64_t length,bool remove)
{
const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK;

View file

@ -33,6 +33,8 @@ 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 duplicate_track(const char *name);
void duplicate_track_dialog();
xtc_result ripple_time(uint64_t length,bool remove);
void ripple_dialog(bool remove);
xtc_result marker(const char *name); /* NULL removes at cursor. */

View file

@ -202,3 +202,20 @@ xtc_result xtc_mix_snapshot_recall(const xtc_mix_snapshot *snapshot,xtc_project
if(r==XTC_OK)publish(p,copy);
xtc_project_destroy(copy);return r;
}
xtc_result xtc_project_duplicate_track(xtc_project *p,uint64_t track,const char *name,uint64_t *out)
{
xtc_project *copy=NULL;size_t i,index;uint64_t id=0;xtc_result r;
if(!p || !out || !xtc_project_valid_name(name))return XTC_ERR_INVALID_ARGUMENT;
for(index=0;index<p->track_count;++index)if(p->tracks[index].id==track)break;
if(index==p->track_count)return XTC_ERR_NOT_FOUND;
if(p->tracks[index].is_bus)return XTC_ERR_INVALID_STATE;
r=xtc_project_clone(p,&copy);if(r!=XTC_OK)return r;
r=xtc_project_add_track(copy,name,&id);
if(r==XTC_OK){xtc_track_info *target=copy->tracks+copy->track_count-1;*target=p->tracks[index];target->id=id;memcpy(target->name,name,strlen(name)+1);copy->instruments[copy->track_count-1]=p->instruments[index];}
for(i=0;r==XTC_OK && i<p->clip_count;++i)if(p->clips[i].track_id==track){uint64_t clip;r=xtc_project_duplicate_clip(copy,p->clips[i].id,id,p->clips[i].start_tick,&clip);}
for(i=0;r==XTC_OK && i<p->automation_count;++i)if(p->automation[i].track_id==track){xtc_automation_point point=p->automation[i];point.track_id=id;r=xtc_project_set_automation(copy,&point);}
if(r==XTC_OK)r=xtc_project_move_track(copy,id,index+1);
if(r==XTC_OK){publish(p,copy);*out=id;}
xtc_project_destroy(copy);return r;
}

View file

@ -1376,6 +1376,9 @@ int main(int argc, char **argv)
s->position(3840);CHECK(s->ripple_time(960,false)==XTC_OK);
xtc_marker moved{};xtc_project_get_marker(s->project(),1,&moved);CHECK(moved.tick==1920);
CHECK(window.undo()==XTC_OK);
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);
window.do_callback();
}
if (failures != 0) return 1;

View file

@ -2,6 +2,7 @@
#include "xtc/xtc_editing.h"
#include "xtc/xtc_mixer.h"
#include "xtc/xtc_audio.h"
#include "xtc/xtc_plugin.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
@ -64,4 +65,21 @@ static void ripple(void)
assert(xtc_project_find_clip(p,c,&clip)==XTC_OK && clip.start_tick==0);
xtc_project_destroy(p);
}
int main(void){markers();sections();ripple();puts("Next DAW workflow tests passed.");return 0;}
static void duplicate_track(void)
{
xtc_project *p=NULL;uint64_t t,b,c,n,id=999;xtc_track_info track;xtc_plugin_spec spec,actual;size_t count;
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Keys",&t)==XTC_OK);
assert(xtc_project_add_bus(p,"Bus",&b)==XTC_OK && xtc_project_route_track(p,t,b)==XTC_OK);
assert(xtc_project_add_midi_clip(p,t,960,1920,&c)==XTC_OK && xtc_project_add_note(p,c,240,120,60,90,1,&n)==XTC_OK);
assert(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,0,48000,&spec)==XTC_OK && xtc_project_set_instrument(p,t,&spec)==XTC_OK);
assert(xtc_project_set_mix(p,t,.5f,-.5f,1,0)==XTC_OK);
{xtc_automation_point a={t,960,XTC_AUTOMATION_GAIN,.75f,0};assert(xtc_project_set_automation(p,&a)==XTC_OK);}
assert(xtc_project_duplicate_track(p,b,"Bus copy",&id)==XTC_ERR_INVALID_STATE && id==999);
assert(xtc_project_duplicate_track(p,t,"Keys copy",&id)==XTC_OK && id!=t);
assert(xtc_project_get_track(p,1,&track)==XTC_OK && track.id==id && track.gain==.5f && track.mute && track.bus_id==b);
assert(xtc_project_get_instrument(p,id,&actual)==XTC_OK && actual.index==spec.index);
assert(xtc_project_note_count(p,&count)==XTC_OK && count==2);
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;}