feat: repeat arrangement phrases with bounded atomic pattern copies

This commit is contained in:
Johannes Findeisen 2026-11-06 00:15:42 +01:00
commit ea9789b188
10 changed files with 70 additions and 4 deletions

View file

@ -541,3 +541,7 @@ $(BUILD_DIR)/test_modern_editing: tests/unit/test_modern_editing.c $(BUILD_DIR)/
.PHONY: test-modern-editing
test-modern-editing: $(BUILD_DIR)/test_modern_editing
./$(BUILD_DIR)/test_modern_editing
.PHONY: test-gui-modern-editing
test-gui-modern-editing: $(BUILD_DIR)/test_gui
XTC_TEST_MODERN_EDITING_ONLY=1 xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui

View file

@ -15,3 +15,7 @@ Use Left/Right in the sequencer (or Edit/Nudge selection) to move the selected p
## Phase 49
Ctrl+C/Ctrl+V in the sequencer, or Edit/Copy clips and Paste clips at cursor, copy/paste the selection. Earliest clip lands on the cursor. The earliest source track maps to the currently selected destination track; intervening track spacing is retained. All required tracks must already exist and must not be buses. Instruments, automation and media bytes are not copied. The clipboard survives document changes within the window but is not the OS clipboard.
## Phase 50
Edit/Repeat selected phrase asks for 1..1024 additional copies. The bounding span includes gaps and overlaps; copies remain on their original tracks. The entire repeat is one undo step. Clip/note capacity and timeline endpoints can limit the requested count.

View file

@ -1363,12 +1363,12 @@ Implemented: independent control-thread clipboard snapshots, fresh-ID paste at t
Repeat a selected phrase by its complete bounding span with bounded copy counts and all-or-nothing capacity validation.
### Tasks and acceptance criteria
- [ ] Provide the control/model behavior and accessible GUI commands.
- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety.
- [ ] Test the new workflow and document scope and limitations.
- [x] Provide the control/model behavior and accessible GUI commands.
- [x] Validate bounds and failure rollback; preserve existing project compatibility and callback safety.
- [x] Test the new workflow and document scope and limitations.
### Implementation status
Planned.
Implemented: bounded phrase repeat uses the complete selected bounding span, including gaps, with fresh clip/note IDs and atomic capacity/range validation. Core repeat/capacity tests and focused GUI phrase workflows pass.
---

View file

@ -13,6 +13,9 @@ extern "C" {
typedef enum { XTC_CLIPS_MOVE, XTC_CLIPS_DUPLICATE, XTC_CLIPS_DELETE } xtc_clip_operation;
xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,size_t count,
xtc_clip_operation operation,int64_t amount,uint64_t *out_ids);
/* Repeat the bounding phrase span, including gaps, 1..1024 additional copies.
* Originals remain selected by the caller; all copies publish together. */
xtc_result xtc_project_repeat_clips(xtc_project *project,const uint64_t *ids,size_t count,unsigned int copies);
/* Independent clipboard snapshot; captures notes/trim/media references, no media
* bytes or instrument assignments. Paste maps source track spacing to existing
* destination tracks beginning at destination_track; rejects missing/bus tracks.

View file

@ -322,6 +322,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
Fl_Group::current(parent);
menu->add("Edit/Nudge selection left",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(-1);},sequencer_);
menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(1);},sequencer_);
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/Select all clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->select_all_clips();},sequencer_);

View file

@ -555,6 +555,19 @@ xtc_result SequencerWidget::transform_selection(xtc_clip_operation op,int64_t am
}
changed(r);return r;
}
xtc_result SequencerWidget::repeat_selection(unsigned int copies)
{
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
if(r==XTC_OK)r=xtc_project_repeat_clips(project_,ids.data(),ids.size(),copies);
changed(r);return r;
}
void SequencerWidget::repeat_dialog()
{
const char *input=fl_input("Additional phrase copies (1..1024)","3");if(!input)return;
char *end=nullptr;unsigned long copies=std::strtoul(input,&end,10);
if(end==input || *end || copies<1 || copies>1024){fl_alert("Enter a whole number from 1 to 1024.");return;}
repeat_selection(static_cast<unsigned int>(copies));
}
xtc_result SequencerWidget::copy_selection()
{
auto ids=selected_clips();xtc_clipboard *copy=nullptr;

View file

@ -38,6 +38,8 @@ class SequencerWidget : public Fl_Group {
bool clip_selected(uint64_t clip) const;
std::vector<uint64_t> selected_clips() const;
xtc_result transform_selection(xtc_clip_operation operation,int64_t amount);
xtc_result repeat_selection(unsigned int copies);
void repeat_dialog();
xtc_result copy_selection();
xtc_result paste_selection();
xtc_result nudge_selection(int direction);

View file

@ -88,3 +88,24 @@ xtc_result xtc_clipboard_paste(const xtc_clipboard *b,xtc_project *p,uint64_t tr
if(r==XTC_OK){publish(p,copy);if(out_ids)memcpy(out_ids,output,b->count*sizeof(*output));}
xtc_project_destroy(copy);return r;
}
xtc_result xtc_project_repeat_clips(xtc_project *p,const uint64_t *ids,size_t count,unsigned int copies)
{
xtc_project *copy=NULL;size_t i;unsigned int k;uint64_t begin=XTC_PROJECT_MAX_TICK,end=0,span;
xtc_result r=selection_valid(p,ids,count);if(r!=XTC_OK)return r;
if(!copies || copies>XTC_PROJECT_MAX_CLIPS)return XTC_ERR_INVALID_ARGUMENT;
if(count>(XTC_PROJECT_MAX_CLIPS-p->clip_count)/copies)return XTC_ERR_CAPACITY;
for(i=0;i<count;++i){xtc_clip_info c;xtc_project_find_clip(p,ids[i],&c);
if(c.start_tick<begin)begin=c.start_tick;
if(c.start_tick+c.length_tick>end)end=c.start_tick+c.length_tick;
}
span=end-begin;
if(span>(XTC_PROJECT_MAX_TICK-end)/copies)return XTC_ERR_INVALID_ARGUMENT;
r=xtc_project_clone(p,&copy);
for(k=1;r==XTC_OK && k<=copies;++k)for(i=0;r==XTC_OK && i<count;++i) {
xtc_clip_info c;uint64_t id;xtc_project_find_clip(p,ids[i],&c);
r=xtc_project_duplicate_clip(copy,ids[i],c.track_id,c.start_tick+span*k,&id);
}
if(r==XTC_OK)publish(p,copy);
xtc_project_destroy(copy);return r;
}

View file

@ -190,6 +190,7 @@ void xtc_backend_collect(xtc_backend *backend)
}
int main(int argc, char **argv)
{
if(!std::getenv("XTC_TEST_MODERN_EDITING_ONLY")) {
reset();
{
MainWindow window(false,"");
@ -1307,6 +1308,7 @@ int main(int argc, char **argv)
}
unlink(path.c_str());
}
}
reset();
{
MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK);
@ -1323,6 +1325,7 @@ int main(int argc, char **argv)
s->select_all_clips();CHECK(s->copy_selection()==XTC_OK);s->position(7680);
CHECK(s->paste_selection()==XTC_OK && s->selected_clips().size()==2);
xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==4);
CHECK(s->repeat_selection(2)==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==8);
window.do_callback();
}
if (failures != 0) return 1;

View file

@ -2,8 +2,23 @@
#include <assert.h>
#include <stdio.h>
static void test_repeat(void)
{
xtc_project *p=NULL;uint64_t t,ids[2];xtc_clip_info c;size_t tracks,clips;
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
assert(xtc_project_add_clip(p,t,"A",0,240,&ids[0])==XTC_OK);
assert(xtc_project_add_clip(p,t,"B",960,240,&ids[1])==XTC_OK);
assert(xtc_project_repeat_clips(p,ids,2,3)==XTC_OK);
assert(xtc_project_get_counts(p,&tracks,&clips)==XTC_OK && clips==8);
assert(xtc_project_get_clip(p,7,&c)==XTC_OK && c.start_tick==4560);
assert(xtc_project_repeat_clips(p,ids,2,1024)==XTC_ERR_CAPACITY);
assert(xtc_project_get_counts(p,&tracks,&clips)==XTC_OK && clips==8);
assert(xtc_project_repeat_clips(p,ids,2,0)==XTC_ERR_INVALID_ARGUMENT);
xtc_project_destroy(p);
}
int main(void)
{
test_repeat();
xtc_project *p=NULL;uint64_t track,a,b,n,ids[2],out[2]={999,999};xtc_clip_info c;
size_t tracks,clips,notes;
assert(xtc_project_create(&p)==XTC_OK);