feat: plan composition phases and persist named arrangement markers

This commit is contained in:
Johannes Findeisen 2026-11-06 00:29:48 +01:00
commit 64e5ab170f
19 changed files with 323 additions and 9 deletions

View file

@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?= 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) JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes) ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack) JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
@ -545,3 +545,12 @@ test-modern-editing: $(BUILD_DIR)/test_modern_editing
.PHONY: test-gui-modern-editing .PHONY: test-gui-modern-editing
test-gui-modern-editing: $(BUILD_DIR)/test_gui 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 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

View file

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

View file

@ -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. 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. 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 preparation. Controls describe root key, whole-file loop, attack, release and
gain. Earlier projects have empty sample paths. Rebuild public API consumers gain. Earlier projects have empty sample paths. Rebuild public API consumers
for the additional copied `sample_path` field in `xtc_plugin_spec`. 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.

View file

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

View file

@ -7,7 +7,7 @@
extern "C" { extern "C" {
#endif #endif
#define XTC_PROJECT_FORMAT_VERSION 15U #define XTC_PROJECT_FORMAT_VERSION 16U
#define XTC_PROJECT_MAX_TRACKS 64U #define XTC_PROJECT_MAX_TRACKS 64U
#define XTC_PROJECT_MAX_CLIPS 1024U #define XTC_PROJECT_MAX_CLIPS 1024U
#define XTC_PROJECT_NAME_BYTES 64U #define XTC_PROJECT_NAME_BYTES 64U
@ -15,6 +15,15 @@ extern "C" {
#define XTC_TICKS_PER_QUARTER 960U #define XTC_TICKS_PER_QUARTER 960U
#define XTC_PROJECT_MAX_TICK UINT64_C(38400000) #define XTC_PROJECT_MAX_TICK UINT64_C(38400000)
typedef struct xtc_project xtc_project; 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 /* Control-thread edits, transactional validation; duplicate retains complete
* source notes/trim/media references, allocates fresh IDs, copies no audio. */ * 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); xtc_result xtc_project_duplicate_clip(xtc_project *project, uint64_t clip, uint64_t track, uint64_t start, uint64_t *out);

View file

@ -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<SequencerWidget *>(c)->repeat_dialog();},sequencer_); 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/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/Paste clips at cursor",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->paste_selection();},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_); menu->add("Edit/Select all clips",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->select_all_clips();},sequencer_);
menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->duplicate_selection(); },sequencer_); menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->duplicate_selection(); },sequencer_);
menu->add("Edit/Split audio at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->split_selection(); }, sequencer_); menu->add("Edit/Split audio at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->split_selection(); }, sequencer_);

View file

@ -319,6 +319,14 @@ void SequencerWidget::draw()
fl_color(loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted()); fl_color(loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted());
fl_rectf(left,y()+23,std::max(0,right-left),4); 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<markers;++i) {
xtc_marker marker{};xtc_project_get_marker(project_,i,&marker);
int px=x()+header_width_+static_cast<int>(marker.tick*static_cast<unsigned int>(scale_)/960U);
if(px<cx || px>cx+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<int>(cursor_*static_cast<unsigned int>(scale_)/960U); int playhead = x()+header_width_+static_cast<int>(cursor_*static_cast<unsigned int>(scale_)/960U);
if (playhead >= cx && playhead <= cx+cw) { if (playhead >= cx && playhead <= cx+cw) {
fl_color(xtc_ui::accent()); fl_line(playhead,y(),playhead,y()+h()); 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); 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<uint64_t> SequencerWidget::selected_clips() const std::vector<uint64_t> SequencerWidget::selected_clips() const
{ {
std::vector<uint64_t> ids;xtc_clip_info c{}; std::vector<uint64_t> ids;xtc_clip_info c{};

View file

@ -33,6 +33,8 @@ class SequencerWidget : public Fl_Group {
Fl_Choice *instrument_choice(uint64_t track) const; Fl_Choice *instrument_choice(uint64_t track) const;
void refresh_instruments(); void refresh_instruments();
bool select(uint64_t track, uint64_t clip = UINT64_MAX); 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); bool toggle_clip(uint64_t clip);
void select_all_clips(); void select_all_clips();
bool clip_selected(uint64_t clip) const; bool clip_selected(uint64_t clip) const;

View file

@ -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); 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;i<project->marker_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); READ("END", 1U);
if (fgetc(input->file) != EOF) if (fgetc(input->file) != EOF)
return XTC_ERR_FORMAT; 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,"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;i<project->marker_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; return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK;
} }

View file

@ -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 || 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) project->clip_count > XTC_PROJECT_MAX_CLIPS || project->note_count > XTC_PROJECT_MAX_NOTES)
return XTC_ERR_FORMAT; return XTC_ERR_FORMAT;
if(project->marker_count>XTC_PROJECT_MAX_MARKERS)return XTC_ERR_FORMAT;
for(i=0;i<project->marker_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) { for (i = 0; i < 16; ++i) for (j = 0; j < 3; ++j) {
unsigned int cc = project->control_map[i][j]; unsigned int cc = project->control_map[i][j];
size_t k; size_t k;

View file

@ -8,6 +8,8 @@
struct xtc_project { struct xtc_project {
xtc_graph *graph; xtc_graph *graph;
size_t marker_count;
xtc_marker markers[XTC_PROJECT_MAX_MARKERS];
uint64_t live_instrument; uint64_t live_instrument;
xtc_plugin_spec instruments[XTC_PROJECT_MAX_TRACKS]; xtc_plugin_spec instruments[XTC_PROJECT_MAX_TRACKS];
uint64_t next_track_id, next_clip_id; uint64_t next_track_id, next_clip_id;

33
src/project/sections.c Normal file
View file

@ -0,0 +1,33 @@
#include "project_internal.h"
#include <string.h>
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;i<p->marker_count && p->markers[i].tick<tick;++i){}
if(i<p->marker_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;i<p->marker_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;
}

View file

@ -1309,6 +1309,7 @@ int main(int argc, char **argv)
unlink(path.c_str()); unlink(path.c_str());
} }
} }
if(!std::getenv("XTC_TEST_NEXT_DAW_ONLY")) {
reset(); reset();
{ {
MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK); 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); CHECK(window.new_project()==XTC_OK && window.mix_snapshot(0,false)==XTC_ERR_NOT_FOUND);
window.do_callback(); 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; if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes); std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0; return 0;

View file

@ -56,7 +56,7 @@ def main():
run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')]) run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')])
assert (docs / 'WORKSPACE.md').is_file() assert (docs / 'WORKSPACE.md').is_file()
diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True) 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. # Replacing a file symlink must not overwrite an unrelated target.
protected = stage / 'unrelated-target' protected = stage / 'unrelated-target'
protected.write_text('keep unrelated target\n') protected.write_text('keep unrelated target\n')

View file

@ -84,7 +84,7 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
project.write_text(malformed) project.write_text(malformed)
check("--validate", project, 1) check("--validate", project, 1)
for text, messages in [ 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")), ("XTC_PROJECT 10\n", ("line 2", "APP")),
(base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")), (base.replace("TEMPO 120000", "TEMPO 0"), ("line 3", "TEMPO")),
(base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)), (base + "OPTIONAL_FUTURE 1\n", ("Unknown fields",)),

View file

@ -167,8 +167,8 @@ static void editable(void)
assert(xtc_plugin_preset_save(&legacy,path)==XTC_OK); 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); {FILE *f=fopen(path,"r");char lines[32768],line[2048];size_t used=0;assert(f);
while(fgets(line,sizeof(line),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,"MARKERS ",8) || !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,"XTC_PROJECT 16",14))strcpy(line,"XTC_PROJECT 13\n");
assert(used+strlen(line)<sizeof(lines));strcpy(lines+used,line);used+=strlen(line); assert(used+strlen(line)<sizeof(lines));strcpy(lines+used,line);used+=strlen(line);
}fclose(f);f=fopen(path,"w");assert(f && fwrite(lines,1,used,f)==used);fclose(f);} }fclose(f);f=fopen(path,"w");assert(f && fwrite(lines,1,used,f)==used);fclose(f);}
assert(xtc_plugin_preset_load(path,&t)==XTC_OK && !t.control_count && t.index==0); assert(xtc_plugin_preset_load(path,&t)==XTC_OK && !t.control_count && t.index==0);

View file

@ -197,7 +197,7 @@ int main(void)
fputc('\n',legacy); continue; fputc('\n',legacy); continue;
} }
if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) && if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) &&
strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6) && strncmp(line, "ROUTE ", 6) && strncmp(line,"CONTROL_MAPS ",13) && strncmp(line,"MAP ",4) && strncmp(line,"TRACK_INSTRUMENTS ",18) && strncmp(line,"LIVE_INSTRUMENT ",16)) strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6) && strncmp(line, "ROUTE ", 6) && strncmp(line,"CONTROL_MAPS ",13) && strncmp(line,"MAP ",4) && strncmp(line,"TRACK_INSTRUMENTS ",18) && strncmp(line,"LIVE_INSTRUMENT ",16) && strncmp(line,"MARKERS ",8) && strncmp(line,"MARKER ",7))
assert(fputs(line, legacy) >= 0); assert(fputs(line, legacy) >= 0);
} }
assert(fclose(f) == 0 && fclose(legacy) == 0); assert(fclose(f) == 0 && fclose(legacy) == 0);

View file

@ -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 <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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;}

View file

@ -81,7 +81,7 @@ int main(void)
} }
write_text(bad,"XTC_PROJECT 999\n"); write_text(bad,"XTC_PROJECT 999\n");
assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_UNSUPPORTED && diagnostic.line == 1 && 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"); 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 && assert(xtc_project_load_diagnostic(loaded,bad,&diagnostic) == XTC_ERR_FORMAT && diagnostic.line == 3 &&
!strcmp(diagnostic.record,"TEMPO")); !strcmp(diagnostic.record,"TEMPO"));