feat: navigate arrangement markers and loop named sections

This commit is contained in:
Johannes Findeisen 2026-11-06 00:31:08 +01:00
commit bd39eb1345
8 changed files with 71 additions and 5 deletions

View file

@ -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.

View file

@ -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.
---

View file

@ -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

View file

@ -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;

View file

@ -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. */

View file

@ -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;
}

View file

@ -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;

View file

@ -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;}