feat: ripple timeline ranges with markers and automation atomically
This commit is contained in:
parent
bd39eb1345
commit
7084e660fe
9 changed files with 95 additions and 5 deletions
|
|
@ -10,3 +10,7 @@ Edit/Markers/Set at cursor adds or renames a marker; Remove at cursor removes th
|
|||
## Phase 59 — Section Navigation and Looping
|
||||
|
||||
View/Previous marker and Next marker navigate without wrapping and stop playback before seeking. Loop current section uses the last marker at or before the cursor through its successor; the final section ends at the final clip endpoint. Empty final sections reject. Loop ranges remain existing UI preferences rather than marker metadata.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -1527,12 +1527,12 @@ Implemented: strict previous/next navigation and section queries, exposed throug
|
|||
Insert or remove bounded timeline ranges atomically, shifting clips, markers and automation; reject clips crossing edit boundaries explicitly.
|
||||
|
||||
### 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: atomic gap insertion/range removal shifts clips, markers and mixer/instrument/graph plugin automation. Crossing clips reject explicitly. Core position/automation/removal rollback tests and GUI edit/history checks pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ extern "C" {
|
|||
typedef enum { XTC_CLIPS_MOVE, XTC_CLIPS_DUPLICATE, XTC_CLIPS_DELETE, XTC_CLIPS_ALIGN, XTC_CLIPS_SLIP, XTC_CLIPS_FADE } 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);
|
||||
/* Insert a gap or delete [begin,begin+length), removing contained clips/notes
|
||||
* and shifting later clips, markers, mixer/instrument/graph plugin automation.
|
||||
* Crossing clips reject (split them first); source notes and static tempo stay
|
||||
* 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);
|
||||
/* 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,8 @@ 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/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_);
|
||||
menu->add("Edit/Markers/Remove at cursor",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->marker(nullptr);},sequencer_);
|
||||
menu->add("Edit/Select all clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->select_all_clips();},sequencer_);
|
||||
|
|
|
|||
|
|
@ -519,6 +519,19 @@ int SequencerWidget::handle(int event)
|
|||
return Fl_Group::handle(event);
|
||||
}
|
||||
|
||||
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;
|
||||
if(r==XTC_OK)r=xtc_project_ripple_time(project_,at,length,remove);
|
||||
changed(r);return r;
|
||||
}
|
||||
void SequencerWidget::ripple_dialog(bool remove)
|
||||
{
|
||||
const char *input=fl_input(remove ? "Remove time from cursor: ticks (split crossing clips first)" : "Insert time at cursor: ticks (split crossing clips first)","3840");if(!input)return;
|
||||
uint64_t length=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>length) || (values>>extra) || !length || length>XTC_PROJECT_MAX_TICK){fl_alert("Enter a positive bounded tick duration.");return;}
|
||||
ripple_time(length,remove);
|
||||
}
|
||||
xtc_result SequencerWidget::marker(const char *name)
|
||||
{
|
||||
const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
|
|
|
|||
|
|
@ -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 ripple_time(uint64_t length,bool remove);
|
||||
void ripple_dialog(bool remove);
|
||||
xtc_result marker(const char *name); /* NULL removes at cursor. */
|
||||
void marker_dialog();
|
||||
bool toggle_clip(uint64_t clip);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
#include "project_internal.h"
|
||||
#include "../graph/graph_internal.h"
|
||||
#include "xtc/xtc_editing.h"
|
||||
#include <string.h>
|
||||
|
||||
xtc_result xtc_project_marker_count(const xtc_project *p,size_t *out)
|
||||
|
|
@ -50,3 +52,43 @@ xtc_result xtc_project_section(const xtc_project *p,uint64_t tick,uint64_t *begi
|
|||
if(finish<=p->markers[i-1].tick)return XTC_ERR_INVALID_STATE;
|
||||
*begin=p->markers[i-1].tick;*end=finish;return XTC_OK;
|
||||
}
|
||||
|
||||
static xtc_result ripple_tick(uint64_t *tick,uint64_t begin,uint64_t length,int remove,int *keep)
|
||||
{
|
||||
*keep=1;
|
||||
if(*tick<begin)return XTC_OK;
|
||||
if(remove){if(*tick<begin+length)*keep=0;else *tick-=length;}
|
||||
else {if(*tick>XTC_PROJECT_MAX_TICK-length)return XTC_ERR_INVALID_ARGUMENT;*tick+=length;}
|
||||
return XTC_OK;
|
||||
}
|
||||
static xtc_result ripple_plugin(xtc_plugin_spec *spec,uint64_t begin,uint64_t length,int remove)
|
||||
{
|
||||
unsigned int i,n=0;int keep;xtc_result r;
|
||||
for(i=0;i<spec->point_count;++i) {
|
||||
xtc_plugin_point point=spec->points[i];r=ripple_tick(&point.tick,begin,length,remove,&keep);
|
||||
if(r!=XTC_OK)return r;
|
||||
if(keep)spec->points[n++]=point;
|
||||
}
|
||||
spec->point_count=n;return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_ripple_time(xtc_project *p,uint64_t begin,uint64_t length,int remove)
|
||||
{
|
||||
xtc_project *copy=NULL;size_t i,n;int keep;xtc_result r;
|
||||
if(!p || !length || begin>XTC_PROJECT_MAX_TICK || length>XTC_PROJECT_MAX_TICK-begin || (remove!=0 && remove!=1))return XTC_ERR_INVALID_ARGUMENT;
|
||||
r=xtc_project_clone(p,©);if(r!=XTC_OK)return r;
|
||||
for(i=copy->clip_count;r==XTC_OK && i>0;--i) {
|
||||
xtc_clip_info c=copy->clips[i-1];uint64_t end=c.start_tick+c.length_tick;
|
||||
if((c.start_tick<begin && end>begin) || (remove && c.start_tick<begin+length && end>begin+length)){r=XTC_ERR_INVALID_STATE;break;}
|
||||
r=ripple_tick(&c.start_tick,begin,length,remove,&keep);
|
||||
if(r==XTC_OK)r=keep ? xtc_project_edit_clip(copy,c.id,c.track_id,c.start_tick,c.length_tick,c.offset_tick) : xtc_project_remove_clip(copy,c.id);
|
||||
}
|
||||
n=0;
|
||||
for(i=0;r==XTC_OK && i<copy->marker_count;++i){xtc_marker m=copy->markers[i];r=ripple_tick(&m.tick,begin,length,remove,&keep);if(r==XTC_OK && keep)copy->markers[n++]=m;}
|
||||
copy->marker_count=n;n=0;
|
||||
for(i=0;r==XTC_OK && i<copy->automation_count;++i){xtc_automation_point a=copy->automation[i];r=ripple_tick(&a.tick,begin,length,remove,&keep);if(r==XTC_OK && keep)copy->automation[n++]=a;}
|
||||
copy->automation_count=n;
|
||||
for(i=0;r==XTC_OK && i<copy->track_count;++i)r=ripple_plugin(copy->instruments+i,begin,length,remove);
|
||||
for(i=0;r==XTC_OK && i<copy->graph->node_count;++i)if(copy->graph->nodes[i].type==XTC_NODE_PLUGIN)r=ripple_plugin(©->graph->nodes[i].plugin,begin,length,remove);
|
||||
if(r==XTC_OK){xtc_graph *old=p->graph;*p=*copy;copy->graph=old;}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1373,6 +1373,9 @@ int main(int argc, char **argv)
|
|||
CHECK(window.navigate_marker(1)==XTC_OK && s->position()==960);
|
||||
CHECK(window.loop_section()==XTC_OK);CHECK(window.navigate_marker(1)==XTC_OK && s->position()==1920);
|
||||
CHECK(window.navigate_marker(-1)==XTC_OK && s->position()==960);
|
||||
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);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -46,4 +46,22 @@ static void sections(void)
|
|||
assert(xtc_project_set_marker(p,9000,"Empty")==XTC_OK && xtc_project_section(p,9000,&begin,&end)==XTC_ERR_INVALID_STATE);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
int main(void){markers();sections();puts("Next DAW workflow tests passed.");return 0;}
|
||||
static void ripple(void)
|
||||
{
|
||||
xtc_project *p=NULL;uint64_t t,c,a;xtc_clip_info clip;xtc_marker m;xtc_automation_point point;
|
||||
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,960,&a)==XTC_OK && xtc_project_add_clip(p,t,"B",1920,960,&c)==XTC_OK);
|
||||
point=(xtc_automation_point){t,1920,XTC_AUTOMATION_GAIN,.5f,0};assert(xtc_project_set_automation(p,&point)==XTC_OK);
|
||||
assert(xtc_project_set_marker(p,1920,"B")==XTC_OK);
|
||||
assert(xtc_project_ripple_time(p,480,960,0)==XTC_ERR_INVALID_STATE);
|
||||
assert(xtc_project_ripple_time(p,960,960,0)==XTC_OK);
|
||||
assert(xtc_project_find_clip(p,c,&clip)==XTC_OK && clip.start_tick==2880);
|
||||
assert(xtc_project_get_marker(p,0,&m)==XTC_OK && m.tick==2880);
|
||||
assert(xtc_project_get_automation(p,0,&point)==XTC_OK && point.tick==2880);
|
||||
assert(xtc_project_ripple_time(p,960,1920,1)==XTC_OK);
|
||||
assert(xtc_project_find_clip(p,c,&clip)==XTC_OK && clip.start_tick==960);
|
||||
assert(xtc_project_ripple_time(p,0,960,1)==XTC_OK && xtc_project_find_clip(p,a,&clip)==XTC_ERR_NOT_FOUND);
|
||||
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;}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue