feat: navigate arrangement markers and loop named sections
This commit is contained in:
parent
64e5ab170f
commit
bd39eb1345
8 changed files with 71 additions and 5 deletions
|
|
@ -6,3 +6,7 @@ prepared-plan workflow. No new dependencies are required.
|
|||
## Phase 58 — Persistent Arrangement Markers
|
||||
|
||||
Edit/Markers/Set at cursor adds or renames a marker; Remove at cursor removes the exact tick key. Markers draw names and vertical guides in the arrangement and survive project save/load and undo. Up to 128 markers are supported. Names may overlap visually when markers are close; markers do not change playback by themselves.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -1512,12 +1512,12 @@ Implemented: sorted tick-keyed markers, bounded UTF-8 names, undoable GUI editin
|
|||
Navigate previous/next markers and loop the section between a marker and its successor or the song end.
|
||||
|
||||
### 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: strict previous/next navigation and section queries, exposed through View commands. Navigation stops playback safely; section looping shares existing loop controls. Core boundaries/final-section and GUI navigation/loop tests pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ typedef struct {uint64_t tick;char name[XTC_PROJECT_NAME_BYTES];} xtc_marker;
|
|||
xtc_result xtc_project_marker_count(const xtc_project *project,size_t *out);
|
||||
xtc_result xtc_project_get_marker(const xtc_project *project,size_t index,xtc_marker *out);
|
||||
xtc_result xtc_project_set_marker(xtc_project *project,uint64_t tick,const char *name);
|
||||
/* Strict previous/next, direction -1/+1, no wrap. Section is the last marker
|
||||
* at/before tick to the next marker, or the final clip end for the last marker.
|
||||
* Query outputs remain unchanged on error. Empty final sections reject. */
|
||||
xtc_result xtc_project_adjacent_marker(const xtc_project *project,uint64_t tick,int direction,xtc_marker *out);
|
||||
xtc_result xtc_project_section(const xtc_project *project,uint64_t tick,uint64_t *begin,uint64_t *end);
|
||||
xtc_result xtc_project_remove_marker(xtc_project *project,uint64_t tick);
|
||||
|
||||
/* Control-thread edits, transactional validation; duplicate retains complete
|
||||
|
|
|
|||
|
|
@ -137,6 +137,9 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
menu->add("Edit/Mixer comparison/Recall A",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->mix_snapshot(0,false);if(r!=XTC_OK)w->report_error("Mixer recall failed",r);},this);
|
||||
menu->add("Edit/Mixer comparison/Capture B",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->mix_snapshot(1,true);if(r!=XTC_OK)w->report_error("Mixer capture failed",r);},this);
|
||||
menu->add("Edit/Mixer comparison/Recall B",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->mix_snapshot(1,false);if(r!=XTC_OK)w->report_error("Mixer recall failed",r);},this);
|
||||
menu->add("View/Previous marker",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->navigate_marker(-1);if(r!=XTC_OK)w->report_error("Marker navigation failed",r);},this);
|
||||
menu->add("View/Next marker",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->navigate_marker(1);if(r!=XTC_OK)w->report_error("Marker navigation failed",r);},this);
|
||||
menu->add("View/Loop current section",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->loop_section();if(r!=XTC_OK)w->report_error("Section loop unchanged",r);},this);
|
||||
menu->add("View/Commands",FL_CTRL+'p',[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->show_commands();},this);
|
||||
menu->add("View/Fit song",FL_CTRL+'0',[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->fit_song();},this);
|
||||
menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this);
|
||||
|
|
@ -2561,6 +2564,20 @@ xtc_result MainWindow::mix_snapshot(unsigned int slot,bool capture)
|
|||
if(r==XTC_OK)mark_dirty();
|
||||
return r;
|
||||
}
|
||||
xtc_result MainWindow::navigate_marker(int direction)
|
||||
{
|
||||
xtc_marker marker{};if(recorder_)return XTC_ERR_INVALID_STATE;
|
||||
xtc_result r=xtc_project_adjacent_marker(project_.get(),sequencer_->position(),direction,&marker);
|
||||
if(r==XTC_OK && ready_)r=xtc_backend_stop(backend_);
|
||||
if(r==XTC_OK)r=seek(this,marker.tick);
|
||||
if(r==XTC_OK)sequencer_->position(marker.tick);
|
||||
return r;
|
||||
}
|
||||
xtc_result MainWindow::loop_section()
|
||||
{
|
||||
uint64_t begin=0,end=0;xtc_result r=xtc_project_section(project_.get(),sequencer_->position(),&begin,&end);
|
||||
return r==XTC_OK ? set_loop(begin,end,true) : r;
|
||||
}
|
||||
void MainWindow::show_commands()
|
||||
{
|
||||
std::vector<std::string> commands;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ class MainWindow : public Fl_Double_Window {
|
|||
* the current document. No engine/model changes happen in audio callbacks. */
|
||||
xtc_result create_track(bool midi);
|
||||
xtc_result set_tempo(unsigned int milli_bpm);
|
||||
xtc_result navigate_marker(int direction);
|
||||
xtc_result loop_section();
|
||||
void show_commands();
|
||||
xtc_result mix_snapshot(unsigned int slot,bool capture);
|
||||
/* Reparent the existing controls on the UI thread; closing a panel docks it. */
|
||||
|
|
|
|||
|
|
@ -31,3 +31,22 @@ xtc_result xtc_project_remove_marker(xtc_project *p,uint64_t tick)
|
|||
memmove(p->markers+i,p->markers+i+1,(p->marker_count-i-1)*sizeof(*p->markers));
|
||||
--p->marker_count;return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_adjacent_marker(const xtc_project *p,uint64_t tick,int direction,xtc_marker *out)
|
||||
{
|
||||
size_t i;if(!p || !out || tick>XTC_PROJECT_MAX_TICK || (direction!=-1 && direction!=1))return XTC_ERR_INVALID_ARGUMENT;
|
||||
if(direction>0){for(i=0;i<p->marker_count;++i)if(p->markers[i].tick>tick){*out=p->markers[i];return XTC_OK;}}
|
||||
else for(i=p->marker_count;i>0;--i)if(p->markers[i-1].tick<tick){*out=p->markers[i-1];return XTC_OK;}
|
||||
return XTC_ERR_NOT_FOUND;
|
||||
}
|
||||
xtc_result xtc_project_section(const xtc_project *p,uint64_t tick,uint64_t *begin,uint64_t *end)
|
||||
{
|
||||
size_t i,j;uint64_t finish=0;
|
||||
if(!p || !begin || !end || begin==end || tick>XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<p->marker_count && p->markers[i].tick<=tick;++i){}
|
||||
if(!i)return XTC_ERR_NOT_FOUND;
|
||||
if(i<p->marker_count)finish=p->markers[i].tick;
|
||||
else for(j=0;j<p->clip_count;++j)if(p->clips[j].start_tick+p->clips[j].length_tick>finish)finish=p->clips[j].start_tick+p->clips[j].length_tick;
|
||||
if(finish<=p->markers[i-1].tick)return XTC_ERR_INVALID_STATE;
|
||||
*begin=p->markers[i-1].tick;*end=finish;return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1369,6 +1369,10 @@ int main(int argc, char **argv)
|
|||
CHECK(window.undo()==XTC_OK);xtc_project_marker_count(s->project(),&markers);CHECK(markers==0);
|
||||
CHECK(window.undo(true)==XTC_OK);xtc_project_marker_count(s->project(),&markers);CHECK(markers==1);
|
||||
window.detach_editor(0,true);pump();window.detach_editor(0,false);
|
||||
s->position(1920);CHECK(s->marker("Chorus")==XTC_OK);s->position(0);
|
||||
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);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -31,4 +31,19 @@ static void markers(void)
|
|||
assert(xtc_project_load(q,path)==XTC_ERR_FORMAT);assert(xtc_project_marker_count(q,&count)==XTC_OK && count==1);}
|
||||
xtc_project_destroy(p);xtc_project_destroy(q);assert(unlink(path)==0);
|
||||
}
|
||||
int main(void){markers();puts("Next DAW workflow tests passed.");return 0;}
|
||||
static void sections(void)
|
||||
{
|
||||
xtc_project *p=NULL;uint64_t t,c,begin=999,end=999;xtc_marker m={0};
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
|
||||
assert(xtc_project_add_clip(p,t,"Song",0,7680,&c)==XTC_OK);
|
||||
assert(xtc_project_set_marker(p,960,"A")==XTC_OK && xtc_project_set_marker(p,3840,"B")==XTC_OK);
|
||||
assert(xtc_project_section(p,0,&begin,&end)==XTC_ERR_NOT_FOUND && begin==999);
|
||||
assert(xtc_project_section(p,960,&begin,&end)==XTC_OK && begin==960 && end==3840);
|
||||
assert(xtc_project_section(p,5000,&begin,&end)==XTC_OK && begin==3840 && end==7680);
|
||||
assert(xtc_project_adjacent_marker(p,960,1,&m)==XTC_OK && m.tick==3840);
|
||||
assert(xtc_project_adjacent_marker(p,3840,-1,&m)==XTC_OK && m.tick==960);
|
||||
assert(xtc_project_adjacent_marker(p,960,-1,&m)==XTC_ERR_NOT_FOUND && m.tick==960);
|
||||
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;}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue