From 4c855ee5d70b554deedfa0c7cff76d1963afc989 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 00:33:58 +0100 Subject: [PATCH] feat: duplicate complete tracks with instruments routing and automation --- docs/COMPOSITION_WORKFLOWS.md | 4 ++++ docs/ROADMAP.md | 8 ++++---- include/xtc/xtc_editing.h | 4 ++++ src/gui/main_window.cpp | 1 + src/gui/sequencer_widget.cpp | 11 +++++++++++ src/gui/sequencer_widget.hpp | 2 ++ src/project/editing.c | 17 +++++++++++++++++ tests/integration/test_gui.cpp | 3 +++ tests/unit/test_next_daw.c | 20 +++++++++++++++++++- 9 files changed, 65 insertions(+), 5 deletions(-) diff --git a/docs/COMPOSITION_WORKFLOWS.md b/docs/COMPOSITION_WORKFLOWS.md index 51c4bc4..405c1fe 100644 --- a/docs/COMPOSITION_WORKFLOWS.md +++ b/docs/COMPOSITION_WORKFLOWS.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9a9b955..0f52bd1 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. --- diff --git a/include/xtc/xtc_editing.h b/include/xtc/xtc_editing.h index 4a4018e..3b2c562 100644 --- a/include/xtc/xtc_editing.h +++ b/include/xtc/xtc_editing.h @@ -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 diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index a729d7c..2c51625 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -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(c)->repeat_dialog();},sequencer_); menu->add("Edit/Copy clips",0,[](Fl_Widget *,void *c){static_cast(c)->copy_selection();},sequencer_); menu->add("Edit/Paste clips at cursor",0,[](Fl_Widget *,void *c){static_cast(c)->paste_selection();},sequencer_); + menu->add("Edit/Duplicate complete track...",0,[](Fl_Widget *,void *c){static_cast(c)->duplicate_track_dialog();},sequencer_); menu->add("Edit/Time/Insert gap...",0,[](Fl_Widget *,void *c){static_cast(c)->ripple_dialog(false);},sequencer_); menu->add("Edit/Time/Remove range...",0,[](Fl_Widget *,void *c){static_cast(c)->ripple_dialog(true);},sequencer_); menu->add("Edit/Markers/Set at cursor...",0,[](Fl_Widget *,void *c){static_cast(c)->marker_dialog();},sequencer_); diff --git a/src/gui/sequencer_widget.cpp b/src/gui/sequencer_widget.cpp index b149ffe..4882c89 100644 --- a/src/gui/sequencer_widget.cpp +++ b/src/gui/sequencer_widget.cpp @@ -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; diff --git a/src/gui/sequencer_widget.hpp b/src/gui/sequencer_widget.hpp index fc2bbfe..3d11426 100644 --- a/src/gui/sequencer_widget.hpp +++ b/src/gui/sequencer_widget.hpp @@ -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. */ diff --git a/src/project/editing.c b/src/project/editing.c index 569b571..3641990 100644 --- a/src/project/editing.c +++ b/src/project/editing.c @@ -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;indextrack_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,©);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 && iclip_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 && iautomation_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; +} diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 2aad622..5d3902e 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -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; diff --git a/tests/unit/test_next_daw.c b/tests/unit/test_next_daw.c index 1f35a2f..3f606fe 100644 --- a/tests/unit/test_next_daw.c +++ b/tests/unit/test_next_daw.c @@ -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 #include #include @@ -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;}