diff --git a/docs/COMPOSITION_WORKFLOWS.md b/docs/COMPOSITION_WORKFLOWS.md index 82e28ff..d0946e7 100644 --- a/docs/COMPOSITION_WORKFLOWS.md +++ b/docs/COMPOSITION_WORKFLOWS.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 459da23..efcb5b6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. --- diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index 02389b5..73a6ce8 100644 --- a/include/xtc/xtc_project.h +++ b/include/xtc/xtc_project.h @@ -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 diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index 1c3e253..c154ffd 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -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(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(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(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(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(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(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(c)->show_commands();},this); menu->add("View/Fit song",FL_CTRL+'0',[](Fl_Widget *,void *c){static_cast(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 commands; diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index ebbb27e..892b19e 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -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. */ diff --git a/src/project/sections.c b/src/project/sections.c index 9b0c577..df4909b 100644 --- a/src/project/sections.c +++ b/src/project/sections.c @@ -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;imarker_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].tickmarkers[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;imarker_count && p->markers[i].tick<=tick;++i){} + if(!i)return XTC_ERR_NOT_FOUND; + if(imarker_count)finish=p->markers[i].tick; + else for(j=0;jclip_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; +} diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index ea25ef6..7dfdaa0 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -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; diff --git a/tests/unit/test_next_daw.c b/tests/unit/test_next_daw.c index 023bd21..89a36dd 100644 --- a/tests/unit/test_next_daw.c +++ b/tests/unit/test_next_daw.c @@ -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;}