From 64e5ab170f602a5f272c379cd24c64b602512e71 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 00:29:48 +0100 Subject: [PATCH] feat: plan composition phases and persist named arrangement markers --- Makefile | 11 +- docs/COMPOSITION_WORKFLOWS.md | 8 + docs/PROJECT_FORMAT.md | 10 +- docs/ROADMAP.md | 165 +++++++++++++++++++ include/xtc/xtc_project.h | 11 +- src/gui/main_window.cpp | 2 + src/gui/sequencer_widget.cpp | 18 ++ src/gui/sequencer_widget.hpp | 2 + src/persistence/project_file.c | 9 + src/project/project.c | 3 + src/project/project_internal.h | 2 + src/project/sections.c | 33 ++++ tests/integration/test_gui.cpp | 12 ++ tests/integration/test_install.py | 2 +- tests/integration/test_project_inspection.py | 2 +- tests/unit/test_builtin.c | 4 +- tests/unit/test_mixer.c | 2 +- tests/unit/test_next_daw.c | 34 ++++ tests/unit/test_project_diagnostics.c | 2 +- 19 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 docs/COMPOSITION_WORKFLOWS.md create mode 100644 src/project/sections.c create mode 100644 tests/unit/test_next_daw.c diff --git a/Makefile b/Makefile index 26e98b0..af1f4f2 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude LDFLAGS ?= -CORE_SOURCES := src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c +CORE_SOURCES := src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes) ifeq ($(JACK_AVAILABLE),yes) JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack) @@ -545,3 +545,12 @@ 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 + +$(BUILD_DIR)/test_next_daw: tests/unit/test_next_daw.c $(BUILD_DIR)/libxtc.a + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $< $(BUILD_DIR)/libxtc.a -lm -ldl +.PHONY: test-next-daw test-gui-next-daw +test-next-daw: $(BUILD_DIR)/test_next_daw + ./$(BUILD_DIR)/test_next_daw +test: test-next-daw +test-gui-next-daw: $(BUILD_DIR)/test_gui + XTC_TEST_MODERN_EDITING_ONLY=1 XTC_TEST_NEXT_DAW_ONLY=1 xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui diff --git a/docs/COMPOSITION_WORKFLOWS.md b/docs/COMPOSITION_WORKFLOWS.md new file mode 100644 index 0000000..82e28ff --- /dev/null +++ b/docs/COMPOSITION_WORKFLOWS.md @@ -0,0 +1,8 @@ +# Arrangement and composition workflows + +All mutations run on the control thread through the existing stop, history and +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. diff --git a/docs/PROJECT_FORMAT.md b/docs/PROJECT_FORMAT.md index 52c5cdd..a16123a 100644 --- a/docs/PROJECT_FORMAT.md +++ b/docs/PROJECT_FORMAT.md @@ -1,4 +1,4 @@ -# Project format v15 (reads v1–v14) +# Project format v16 (reads v1–v15) Phases 5–31 use a bounded text format implemented in C99 without a parser library. The extension is `.xtc`. File operations run exclusively on the control thread. @@ -418,3 +418,11 @@ Missing/invalid samples remain references and become reported silence during preparation. Controls describe root key, whole-file loop, attack, release and gain. Earlier projects have empty sample paths. Rebuild public API consumers for the additional copied `sample_path` field in `xtc_plugin_spec`. + +## Version 16: arrangement markers + +After `LIVE_INSTRUMENT` and before `END`, version 16 writes `MARKERS count` +followed by `MARKER tick hex_name` records. Count is 0..128; ticks must be strictly +increasing and at most MAX_TICK. Names use the existing bounded UTF-8 hex encoding. +Versions 1–15 migrate to an empty marker list. Older builds reject version 16. +Markers are copied by snapshots and history; the tick is their stable key. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d527cb2..459da23 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1491,6 +1491,171 @@ remain unqualified. --- +## Phase 58 — Persistent Arrangement Markers + +### Goal +Store up to 128 named, tick-keyed markers with versioned migration, undoable GUI editing and ruler display. + +### Tasks and acceptance criteria +- [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 +Implemented: sorted tick-keyed markers, bounded UTF-8 names, undoable GUI editing and ruler display. Format 16 appends marker records and reads versions 1–15. Core, legacy migration, malformed-load rollback and focused GUI history/docking tests pass. + +--- + +## Phase 59 — Section Navigation and Looping + +### Goal +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. + +### Implementation status +Planned. + +--- + +## Phase 60 — Ripple Time Editing + +### Goal +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. + +### Implementation status +Planned. + +--- + +## Phase 61 — Complete Track Duplication + +### Goal +Duplicate a non-bus track with fresh IDs, clips, notes, mixer controls, routing, instrument settings and track automation in one transaction. + +### 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. + +### Implementation status +Planned. + +--- + +## Phase 62 — Nondestructive MIDI Clip Splitting + +### Goal +Split trimmed MIDI clips at the cursor while sharing source-relative note content and preserving exact rendered boundaries. + +### 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. + +### Implementation status +Planned. + +--- + +## Phase 63 — MIDI Clip Consolidation + +### Goal +Merge selected MIDI clips on one track into a fresh source, retaining audible notes and gaps with explicit boundary clipping and rollback. + +### 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. + +### Implementation status +Planned. + +--- + +## Phase 64 — Deterministic MIDI Humanization + +### Goal +Apply seeded timing and velocity variation to MIDI clip sources with reproducible results, bounds checks and undo. + +### 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. + +### Implementation status +Planned. + +--- + +## Phase 65 — Chord Construction + +### Goal +Generate bounded custom chord intervals from source notes with fresh IDs, preserved articulation and transactional pitch/capacity checks. + +### 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. + +### Implementation status +Planned. + +--- + +## Phase 66 — Offline Arpeggio Generation + +### Goal +Convert source note chords into bounded ascending/descending stepped patterns with gate controls, deterministic ordering and undo. + +### 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. + +### Implementation status +Planned. + +--- + +## Phase 67 — Audio Peak Normalization + +### Goal +Analyze the audible source slice outside callbacks and adjust nondestructive clip gain to a requested sample-peak target; validate silence, missing media and gain limits. + +### 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. + +### Implementation status +Planned. + +--- + +## Phase 68 — Portable Project Collection + +### Goal +Collect referenced audio and sampler media into a new self-contained project folder, deduplicate references, preserve original files and reject existing destinations or unsupported references safely. + +### 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. + +### Implementation status +Planned. + +--- + ## Recommended Execution Order Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification. diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index 83989cf..02389b5 100644 --- a/include/xtc/xtc_project.h +++ b/include/xtc/xtc_project.h @@ -7,7 +7,7 @@ extern "C" { #endif -#define XTC_PROJECT_FORMAT_VERSION 15U +#define XTC_PROJECT_FORMAT_VERSION 16U #define XTC_PROJECT_MAX_TRACKS 64U #define XTC_PROJECT_MAX_CLIPS 1024U #define XTC_PROJECT_NAME_BYTES 64U @@ -15,6 +15,15 @@ extern "C" { #define XTC_TICKS_PER_QUARTER 960U #define XTC_PROJECT_MAX_TICK UINT64_C(38400000) typedef struct xtc_project xtc_project; +#define XTC_PROJECT_MAX_MARKERS 128U +/* Control-thread, sorted unique tick keys. Setting an existing tick renames it; + * no allocations, playback implications or external resources. */ +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); +xtc_result xtc_project_remove_marker(xtc_project *project,uint64_t tick); + /* Control-thread edits, transactional validation; duplicate retains complete * source notes/trim/media references, allocates fresh IDs, copies no audio. */ xtc_result xtc_project_duplicate_clip(xtc_project *project, uint64_t clip, uint64_t track, uint64_t start, uint64_t *out); diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index 8fede06..1c3e253 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -339,6 +339,8 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) menu->add("Edit/Repeat selected phrase...",0,[](Fl_Widget *,void *c){static_cast(c)->repeat_dialog();},sequencer_); menu->add("Edit/Copy clips",0,[](Fl_Widget *,void *c){static_cast(c)->copy_selection();},sequencer_); menu->add("Edit/Paste clips at cursor",0,[](Fl_Widget *,void *c){static_cast(c)->paste_selection();},sequencer_); + menu->add("Edit/Markers/Set at cursor...",0,[](Fl_Widget *,void *c){static_cast(c)->marker_dialog();},sequencer_); + menu->add("Edit/Markers/Remove at cursor",0,[](Fl_Widget *,void *c){static_cast(c)->marker(nullptr);},sequencer_); menu->add("Edit/Select all clips",0,[](Fl_Widget *,void *c){static_cast(c)->select_all_clips();},sequencer_); menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast(c)->duplicate_selection(); },sequencer_); menu->add("Edit/Split audio at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast(c)->split_selection(); }, sequencer_); diff --git a/src/gui/sequencer_widget.cpp b/src/gui/sequencer_widget.cpp index 0a4a657..f4f2462 100644 --- a/src/gui/sequencer_widget.cpp +++ b/src/gui/sequencer_widget.cpp @@ -319,6 +319,14 @@ void SequencerWidget::draw() fl_color(loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted()); fl_rectf(left,y()+23,std::max(0,right-left),4); } + size_t markers=0;xtc_project_marker_count(project_,&markers); + for(size_t i=0;i(marker.tick*static_cast(scale_)/960U); + if(pxcx+cw)continue; + fl_color(xtc_ui::accent());fl_line(px,y()+24,px,y()+h()); + fl_push_clip(px,y(),160,12);fl_font(FL_HELVETICA,10);fl_draw(marker.name,px+3,y()+10);fl_pop_clip(); + } int playhead = x()+header_width_+static_cast(cursor_*static_cast(scale_)/960U); if (playhead >= cx && playhead <= cx+cw) { fl_color(xtc_ui::accent()); fl_line(playhead,y(),playhead,y()+h()); @@ -511,6 +519,16 @@ int SequencerWidget::handle(int event) return Fl_Group::handle(event); } +xtc_result SequencerWidget::marker(const char *name) +{ + const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; + if(r==XTC_OK)r=name ? xtc_project_set_marker(project_,at,name) : xtc_project_remove_marker(project_,at); + changed(r);return r; +} +void SequencerWidget::marker_dialog() +{ + const char *name=fl_input("Marker at cursor (1..63 UTF-8 bytes)","Section");if(name)marker(name); +} std::vector SequencerWidget::selected_clips() const { std::vector ids;xtc_clip_info c{}; diff --git a/src/gui/sequencer_widget.hpp b/src/gui/sequencer_widget.hpp index cfa9090..3ef4e3e 100644 --- a/src/gui/sequencer_widget.hpp +++ b/src/gui/sequencer_widget.hpp @@ -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 marker(const char *name); /* NULL removes at cursor. */ + void marker_dialog(); bool toggle_clip(uint64_t clip); void select_all_clips(); bool clip_selected(uint64_t clip) const; diff --git a/src/persistence/project_file.c b/src/persistence/project_file.c index adbbbdc..a707857 100644 --- a/src/persistence/project_file.c +++ b/src/persistence/project_file.c @@ -495,6 +495,13 @@ static xtc_result read_project(reader *input, xtc_project *project, const char * } READ("LIVE_INSTRUMENT",2U); NUM(1U,project->live_instrument); } + if(format_version>=16U) { + READ("MARKERS",2U);NUM(1U,value);if(value>XTC_PROJECT_MAX_MARKERS)return XTC_ERR_FORMAT; + project->marker_count=(size_t)value; + for(i=0;imarker_count;++i){READ("MARKER",3U);NUM(1U,project->markers[i].tick); + if(!decode_name(input->tokens[2],project->markers[i].name))return XTC_ERR_FORMAT; + } + } READ("END", 1U); if (fgetc(input->file) != EOF) return XTC_ERR_FORMAT; @@ -672,6 +679,8 @@ static xtc_result write_project(FILE *file, const xtc_project *project, const ch } if (fprintf(file,"LIVE_INSTRUMENT %" PRIu64 "\n",project->live_instrument) < 0) return XTC_ERR_IO; } + if(fprintf(file,"MARKERS %zu\n",project->marker_count)<0)return XTC_ERR_IO; + for(i=0;imarker_count;++i)if(fprintf(file,"MARKER %" PRIu64 " ",project->markers[i].tick)<0 || !write_name(file,project->markers[i].name))return XTC_ERR_IO; return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK; } diff --git a/src/project/project.c b/src/project/project.c index cd03b56..7014ed5 100644 --- a/src/project/project.c +++ b/src/project/project.c @@ -529,6 +529,9 @@ xtc_result xtc_project_validate(const xtc_project *project) if (project->tempo < 20000U || project->tempo > 300000U || project->track_count > XTC_PROJECT_MAX_TRACKS || project->clip_count > XTC_PROJECT_MAX_CLIPS || project->note_count > XTC_PROJECT_MAX_NOTES) return XTC_ERR_FORMAT; + if(project->marker_count>XTC_PROJECT_MAX_MARKERS)return XTC_ERR_FORMAT; + for(i=0;imarker_count;++i)if(project->markers[i].tick>XTC_PROJECT_MAX_TICK || + !xtc_project_valid_name(project->markers[i].name) || (i && project->markers[i-1].tick>=project->markers[i].tick))return XTC_ERR_FORMAT; for (i = 0; i < 16; ++i) for (j = 0; j < 3; ++j) { unsigned int cc = project->control_map[i][j]; size_t k; diff --git a/src/project/project_internal.h b/src/project/project_internal.h index ff9fe03..6b2ae98 100644 --- a/src/project/project_internal.h +++ b/src/project/project_internal.h @@ -8,6 +8,8 @@ struct xtc_project { xtc_graph *graph; + size_t marker_count; + xtc_marker markers[XTC_PROJECT_MAX_MARKERS]; uint64_t live_instrument; xtc_plugin_spec instruments[XTC_PROJECT_MAX_TRACKS]; uint64_t next_track_id, next_clip_id; diff --git a/src/project/sections.c b/src/project/sections.c new file mode 100644 index 0000000..9b0c577 --- /dev/null +++ b/src/project/sections.c @@ -0,0 +1,33 @@ +#include "project_internal.h" +#include + +xtc_result xtc_project_marker_count(const xtc_project *p,size_t *out) +{ + if(!p || !out)return XTC_ERR_INVALID_ARGUMENT; + *out=p->marker_count;return XTC_OK; +} +xtc_result xtc_project_get_marker(const xtc_project *p,size_t index,xtc_marker *out) +{ + if(!p || !out)return XTC_ERR_INVALID_ARGUMENT; + if(index>=p->marker_count)return XTC_ERR_NOT_FOUND; + *out=p->markers[index];return XTC_OK; +} +xtc_result xtc_project_set_marker(xtc_project *p,uint64_t tick,const char *name) +{ + size_t i;xtc_marker marker; + if(!p || tick>XTC_PROJECT_MAX_TICK || !xtc_project_valid_name(name))return XTC_ERR_INVALID_ARGUMENT; + memset(&marker,0,sizeof(marker));marker.tick=tick;memcpy(marker.name,name,strlen(name)+1); + for(i=0;imarker_count && p->markers[i].tickmarker_count && p->markers[i].tick==tick){p->markers[i]=marker;return XTC_OK;} + if(p->marker_count==XTC_PROJECT_MAX_MARKERS)return XTC_ERR_CAPACITY; + memmove(p->markers+i+1,p->markers+i,(p->marker_count-i)*sizeof(marker)); + p->markers[i]=marker;++p->marker_count;return XTC_OK; +} +xtc_result xtc_project_remove_marker(xtc_project *p,uint64_t tick) +{ + size_t i;if(!p)return XTC_ERR_INVALID_ARGUMENT; + for(i=0;imarker_count;++i)if(p->markers[i].tick==tick)break; + if(i==p->marker_count)return XTC_ERR_NOT_FOUND; + memmove(p->markers+i,p->markers+i+1,(p->marker_count-i-1)*sizeof(*p->markers)); + --p->marker_count;return XTC_OK; +} diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 78f756d..ea25ef6 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -1309,6 +1309,7 @@ int main(int argc, char **argv) unlink(path.c_str()); } } + if(!std::getenv("XTC_TEST_NEXT_DAW_ONLY")) { reset(); { MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK); @@ -1359,6 +1360,17 @@ int main(int argc, char **argv) CHECK(window.new_project()==XTC_OK && window.mix_snapshot(0,false)==XTC_ERR_NOT_FOUND); window.do_callback(); } + } + reset(); + { + MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK); + auto *s=timeline(&window);s->position(960);CHECK(s->marker("Verse")==XTC_OK); + size_t markers=0;xtc_project_marker_count(s->project(),&markers);CHECK(markers==1); + 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); + window.do_callback(); + } if (failures != 0) return 1; std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes); return 0; diff --git a/tests/integration/test_install.py b/tests/integration/test_install.py index 2637611..38f9a86 100644 --- a/tests/integration/test_install.py +++ b/tests/integration/test_install.py @@ -56,7 +56,7 @@ def main(): run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')]) assert (docs / 'WORKSPACE.md').is_file() diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True) - assert 'Project formats: 1..15' in diagnose and 'JACK compiled:' in diagnose + assert 'Project formats: 1..16' in diagnose and 'JACK compiled:' in diagnose # Replacing a file symlink must not overwrite an unrelated target. protected = stage / 'unrelated-target' protected.write_text('keep unrelated target\n') diff --git a/tests/integration/test_project_inspection.py b/tests/integration/test_project_inspection.py index a508608..146654b 100644 --- a/tests/integration/test_project_inspection.py +++ b/tests/integration/test_project_inspection.py @@ -84,7 +84,7 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory: project.write_text(malformed) check("--validate", project, 1) for text, messages in [ - (base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..15")), + (base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..16")), ("XTC_PROJECT 10\n", ("line 2", "APP")), (base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")), (base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)), diff --git a/tests/unit/test_builtin.c b/tests/unit/test_builtin.c index bb721ec..215470b 100644 --- a/tests/unit/test_builtin.c +++ b/tests/unit/test_builtin.c @@ -167,8 +167,8 @@ static void editable(void) assert(xtc_plugin_preset_save(&legacy,path)==XTC_OK); {FILE *f=fopen(path,"r");char lines[32768],line[2048];size_t used=0;assert(f); while(fgets(line,sizeof(line),f)) { - if(!strncmp(line,"SAMPLE ",7) || !strncmp(line,"INSTRUMENT_CONTROLS",19) || !strncmp(line,"INSTRUMENT_AUTOMATION",21))continue; - if(!strncmp(line,"XTC_PROJECT 15",14))strcpy(line,"XTC_PROJECT 13\n"); + if(!strncmp(line,"MARKERS ",8) || !strncmp(line,"SAMPLE ",7) || !strncmp(line,"INSTRUMENT_CONTROLS",19) || !strncmp(line,"INSTRUMENT_AUTOMATION",21))continue; + if(!strncmp(line,"XTC_PROJECT 16",14))strcpy(line,"XTC_PROJECT 13\n"); assert(used+strlen(line)= 0); } assert(fclose(f) == 0 && fclose(legacy) == 0); diff --git a/tests/unit/test_next_daw.c b/tests/unit/test_next_daw.c new file mode 100644 index 0000000..023bd21 --- /dev/null +++ b/tests/unit/test_next_daw.c @@ -0,0 +1,34 @@ +#define _POSIX_C_SOURCE 200809L +#include "xtc/xtc_editing.h" +#include "xtc/xtc_mixer.h" +#include "xtc/xtc_audio.h" +#include +#include +#include +#include +#include + +static void markers(void) +{ + xtc_project *p=NULL,*q=NULL;xtc_marker m;size_t count;char path[]="/tmp/xtc-sections-XXXXXX"; + int fd=mkstemp(path);assert(fd>=0 && close(fd)==0); + assert(xtc_project_create(&p)==XTC_OK && xtc_project_create(&q)==XTC_OK); + assert(xtc_project_set_marker(p,3840,"Chorus")==XTC_OK); + assert(xtc_project_set_marker(p,0,"Intro")==XTC_OK); + assert(xtc_project_set_marker(p,0,"Intro ä")==XTC_OK); + assert(xtc_project_get_marker(p,0,&m)==XTC_OK && !strcmp(m.name,"Intro ä")); + assert(xtc_project_set_marker(p,1,"Bad\n")==XTC_ERR_INVALID_ARGUMENT); + assert(xtc_project_save(p,path)==XTC_OK && xtc_project_load(q,path)==XTC_OK); + assert(xtc_project_marker_count(q,&count)==XTC_OK && count==2); + assert(xtc_project_get_marker(q,1,&m)==XTC_OK && m.tick==3840 && !strcmp(m.name,"Chorus")); + assert(xtc_project_remove_marker(q,0)==XTC_OK && xtc_project_remove_marker(q,0)==XTC_ERR_NOT_FOUND); + for(size_t i=1;i<127;++i)assert(xtc_project_set_marker(p,i,"M")==XTC_OK); + assert(xtc_project_set_marker(p,999,"Full")==XTC_ERR_CAPACITY); + /* Malformed marker ordering rejects transactionally. */ + {FILE *f=fopen(path,"r");char data[32768];size_t n;assert(f);n=fread(data,1,sizeof(data)-1,f);data[n]=0;fclose(f); + char *at=strstr(data,"MARKER 3840");assert(at);memcpy(at+7,"0000",4); + f=fopen(path,"w");assert(f && fwrite(data,1,n,f)==n && fclose(f)==0); + 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;} diff --git a/tests/unit/test_project_diagnostics.c b/tests/unit/test_project_diagnostics.c index b12416b..ecd12d1 100644 --- a/tests/unit/test_project_diagnostics.c +++ b/tests/unit/test_project_diagnostics.c @@ -81,7 +81,7 @@ int main(void) } write_text(bad,"XTC_PROJECT 999\n"); assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 && - diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..15")); + diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..16")); write_text(bad,"XTC_PROJECT 10\nAPP 0 1 0\nTEMPO nope\n"); assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 && !strcmp(diagnostic.record,"TEMPO"));