feat: repeat arrangement phrases with bounded atomic pattern copies
This commit is contained in:
parent
b60501f317
commit
ea9789b188
10 changed files with 70 additions and 4 deletions
4
Makefile
4
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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_);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,©);
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue