feat: collect portable project media with transactional cleanup and qualify composition workflows

This commit is contained in:
Johannes Findeisen 2026-11-06 00:49:37 +01:00
commit b0cfa79131
15 changed files with 225 additions and 10 deletions

View file

@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/audio/normalize.c 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
CORE_SOURCES := src/persistence/collect.c src/audio/normalize.c 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)

View file

@ -42,3 +42,7 @@ Edit/MIDI tools/Generate arpeggio replaces source notes with a written pattern.
## Phase 67 — Audio Peak Normalization
Edit/Audio tools/Normalize source peak accepts a target from -60 to 0 dBFS. The C API accepts a linear target above 0 and at most 1. Analysis uses the audible native-rate source slice and the same mono downmix as playback, before fades or mixer controls. It replaces clip gain without writing WAV data. Silence, missing/invalid media, MIDI selections or required gain above 4 reject the entire batch. This is sample-peak normalization, not true-peak limiting or LUFS loudness normalization. Analysis runs synchronously on the control thread.
## Phase 68 — Portable Project Collection
File/Collect project media... requires a new absolute folder. The original document and files remain unchanged; open project.xtc inside the new folder to use the copy. Referenced audio and track/graph sampler WAVs are deduplicated and copied byte-for-byte, with relative references. Limits are 128 distinct assets, 256 MiB per file, 1 GiB total and a canonical folder path shorter than 480 bytes. Existing destinations, missing media, nonregular files and native graph nodes reject. External plugin binaries/state are not collected; plugins must remain installed. Invalid WAV data is copied rather than repaired. The folder is visible during synchronous collection; failures remove owned outputs, and the project file is saved last.

View file

@ -1,6 +1,7 @@
# Modern editing workflows
These control-thread edits preserve project format 15. Session selections,
Phases 47–57 introduced these control-thread edits without changing format 15.
The current writer uses format 16 for arrangement markers and still reads older projects. Session selections,
clipboard and comparison snapshots are not saved in projects. Audio callbacks
continue to consume prepared plans; edits use the existing stop/history workflow.

View file

@ -1647,12 +1647,31 @@ Implemented: native-rate audible-slice sample-peak analysis adjusts nondestructi
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.
- [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: new-folder media collection deduplicates canonical audio and sampler references, copies bounded regular files, writes relative project references last and cleans owned outputs on failure. Relocation after deleting originals, rendered sample content, oversized/FIFO cleanup, existing destinations, allocation rollback and GUI collection pass. Integrated validation below covers all eleven phases.
### Integrated validation for Phases 58–68
All eleven numbered phases have separate implementation commits and passing
warning-enabled model and focused GUI checks. Final integration passed the full
core/offline suite, allocation-failure rollback and ID preservation, complete GUI
and LADSPA/CLAP/VST2/VST3 effect/instrument fixture matrix, and software JACK
start/stop/reconnect/record/active-close checks. ASan/UBSan core and full GUI runs
passed with only the existing system Fontconfig leak suppression. Clang analysis
passed for every core source; both staged installation layouts and C/C++
pkg-config consumers passed. Collection additionally covers relocation after
source deletion, copied render content, deduplication and partial-output cleanup.
The current writer uses project format 16 and reads versions 1–15. These phases
add no dependencies or callback I/O/allocation. See
[COMPOSITION_WORKFLOWS.md](COMPOSITION_WORKFLOWS.md) for commands and bounded
behavior. Human listening, physical hardware, arbitrary third-party plugins and
feature parity with commercial DAWs remain unqualified.
---

View file

@ -109,7 +109,7 @@ See [MIDI.md](MIDI.md#phase-21-composition-commands) for shortcuts, timing and l
## Audio clip tools
Select an audio clip, click the ruler at the desired cut while stopped, then
choose Edit / Split audio at cursor (S with timeline focus). Ctrl+D duplicates;
choose Edit / Split clip at cursor (S with timeline focus). Ctrl+D duplicates;
Delete removes the clip reference. Drag the body to move and edges to trim.
Edit / Audio clip gain and fades (A) sets linear gain and fade durations in ticks.
All are undoable and preserve the WAV file. Waveforms show cached raw source
@ -216,3 +216,5 @@ Phases 47–57 add additive clip selection, atomic group commands, phrase clipbo
and repeat, alignment/slip tools, MIDI swing/gate/legato/scale/velocity transforms,
batch audio fades and static mixer A/B recall. See
[MODERN_EDITING.md](MODERN_EDITING.md) for commands, scope and limitations.
See [arrangement and composition workflows](COMPOSITION_WORKFLOWS.md) for markers, MIDI composition, normalization and portable media collection.

View file

@ -235,3 +235,7 @@ Phases 47–57 add additive clip selection, atomic group commands, phrase clipbo
and repeat, alignment/slip tools, MIDI swing/gate/legato/scale/velocity transforms,
batch audio fades and static mixer A/B recall. See
[MODERN_EDITING.md](MODERN_EDITING.md) for commands, scope and limitations.
Phases 58–68 add arrangement markers and sections, ripple edits, complete track
duplication, MIDI splitting/consolidation and composition tools, peak normalization
and portable media collection. See [COMPOSITION_WORKFLOWS.md](COMPOSITION_WORKFLOWS.md).

View file

@ -63,9 +63,9 @@ typedef struct {
unsigned int velocity_percent; /* VELOCITY: 0..400, rounded scaling then offset, clamp to 1..127. */
int velocity_offset; /* -127..127 */
unsigned int root,scale; /* SCALE: root 0..11 (C..B), scale 0 chromatic, 1 major, 2 natural minor; nearest ties lower. */
unsigned int gate_percent; /* GATE: 1..400, rounded length, minimum 1 tick. */
unsigned int seed; /* HUMANIZE: stable note-ID keyed 32-bit PRNG seed. */
unsigned int timing_variation,velocity_variation; /* 0..960 ticks, 0..127 velocity; source overflow rejects, velocity clamps. */
unsigned int gate_percent; /* GATE: 1..400, rounded length, minimum 1 tick. */
} xtc_midi_transform;
xtc_result xtc_project_transform_midi(xtc_project *project,const uint64_t *ids,size_t count,const xtc_midi_transform *edit);
/* Session static mixer comparison. Capture owns an independent snapshot of

View file

@ -225,6 +225,14 @@ xtc_result xtc_project_tick_to_sample(const xtc_project *project, uint64_t tick,
*/
xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sample, unsigned int sample_rate,
uint64_t *out_tick);
/* Control-thread portable media copy into a NEW absolute directory. Writes
* project.xtc and deduplicated media-NNN.wav files with relative saved refs.
* Original project/files unchanged. Existing destination rejects; folder is
* visible while building, project is saved last. Failure removes owned outputs.
* Max 128 distinct regular assets, 256 MiB/file and 1 GiB total; no DSP/plugin
* execution. Native graphs reject; external plugin binaries/state are not copied.
* Byte-copy preserves referenced files; this does not repair invalid WAV data. */
xtc_result xtc_project_collect(const xtc_project *project,const char *directory);
/* Control-thread I/O. Versioned, bounded format; see docs/PROJECT_FORMAT.md.
* Load is transactional. Save fsyncs a same-directory temporary and renames it;
* failures before rename preserve the previous destination. No asset I/O. v7+ saves local media paths relative to the project folder; load

View file

@ -140,6 +140,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
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("File/Collect project media...",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);const char *path=fl_input("New absolute project folder (must not exist)","");if(path){auto r=w->collect_project(path);if(r!=XTC_OK)w->report_error("Project collection failed",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);
@ -2587,6 +2588,12 @@ 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;
}
xtc_result MainWindow::collect_project(const char *directory)
{
if(recorder_)return XTC_ERR_INVALID_STATE;
BusyOperation busy(*this,status_,"Collecting project media…");
return xtc_project_collect(project_.get(),directory);
}
void MainWindow::show_commands()
{
std::vector<std::string> commands;

View file

@ -35,6 +35,7 @@ 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 collect_project(const char *directory);
xtc_result navigate_marker(int direction);
xtc_result loop_section();
void show_commands();

View file

@ -1,5 +1,4 @@
#include <cmath>
#include <sstream>
#include "sequencer_widget.hpp"
#include "theme.hpp"
#include "snap.hpp"
@ -107,6 +106,12 @@ void SequencerWidget::extent()
xtc_project_get_clip(project_, i, &c);
end = std::max(end, c.start_tick + c.length_tick + 3840U);
}
size_t markers=0;
if(xtc_project_marker_count(project_,&markers)==XTC_OK && markers) {
xtc_marker marker{};
if(xtc_project_get_marker(project_,markers-1,&marker)==XTC_OK)
end=std::max(end,std::min(XTC_PROJECT_MAX_TICK,marker.tick)+UINT64_C(3840));
}
size(static_cast<int>(end * static_cast<unsigned int>(scale_) / 960U + static_cast<unsigned int>(header_width_+20)),
std::max(400, static_cast<int>(tracks) * 56 + 100));
refresh_instruments();

82
src/persistence/collect.c Normal file
View file

@ -0,0 +1,82 @@
#define _XOPEN_SOURCE 700
#define _POSIX_C_SOURCE 200809L
#include "../project/project_internal.h"
#include "../graph/graph_internal.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define COLLECT_ASSETS 128U
#define FILE_LIMIT UINT64_C(268435456)
#define TOTAL_LIMIT UINT64_C(1073741824)
typedef struct {char source[4096],target[512];int owned;} media_entry;
typedef struct {size_t count;uint64_t total;media_entry media[COLLECT_ASSETS];} collection;
static xtc_result reference(collection *c,char *path,int rewrite)
{
char canonical[4096];size_t i;
if(!path[0])return XTC_OK;
if(!realpath(path,canonical))return XTC_ERR_IO;
for(i=0;i<c->count;++i)if(!strcmp(c->media[i].source,canonical))break;
if(rewrite){if(i==c->count)return XTC_ERR_IO;strcpy(path,c->media[i].target);return XTC_OK;}
if(i<c->count)return XTC_OK;
if(c->count==COLLECT_ASSETS)return XTC_ERR_CAPACITY;
strcpy(c->media[c->count++].source,canonical);return XTC_OK;
}
static xtc_result references(collection *c,xtc_project *p,int rewrite)
{
size_t i;xtc_result r=XTC_OK;
for(i=0;r==XTC_OK && i<p->clip_count;++i)r=reference(c,p->clips[i].asset_path,rewrite);
for(i=0;r==XTC_OK && i<p->track_count;++i)r=reference(c,p->instruments[i].sample_path,rewrite);
for(i=0;r==XTC_OK && i<p->graph->node_count;++i){
if(p->graph->nodes[i].type==XTC_NODE_NATIVE)return XTC_ERR_UNSUPPORTED;
if(p->graph->nodes[i].type==XTC_NODE_PLUGIN)r=reference(c,p->graph->nodes[i].plugin.sample_path,rewrite);
}
return r;
}
static xtc_result copy_media(collection *c,media_entry *entry)
{
int input=-1,output=-1;struct stat st;unsigned char buffer[65536];uint64_t left;xtc_result r=XTC_ERR_IO;
input=open(entry->source,O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK);if(input<0)return XTC_ERR_IO;
if(fstat(input,&st) || !S_ISREG(st.st_mode))goto done;
if(st.st_size<0 || (uint64_t)st.st_size>FILE_LIMIT || c->total>TOTAL_LIMIT-(uint64_t)st.st_size){r=XTC_ERR_CAPACITY;goto done;}
left=(uint64_t)st.st_size;
output=open(entry->target,O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC,0600);if(output<0)goto done;
entry->owned=1;
while(left) {
size_t want=left>sizeof(buffer) ? sizeof(buffer) : (size_t)left,used=0;ssize_t got;
do {got=read(input,buffer,want);}while(got<0 && errno==EINTR);
if(got<=0)goto done;
while(used<(size_t)got){ssize_t n;do {n=write(output,buffer+used,(size_t)got-used);}while(n<0 && errno==EINTR);if(n<=0)goto done;used+=(size_t)n;}
left-=(uint64_t)got;
}
{ssize_t n;do {n=read(input,buffer,1);}while(n<0 && errno==EINTR);if(n!=0)goto done;}
if(fsync(output))goto done;
c->total+=(uint64_t)st.st_size;r=XTC_OK;
done:
if(input>=0 && close(input) && r==XTC_OK)r=XTC_ERR_IO;
if(output>=0 && close(output) && r==XTC_OK)r=XTC_ERR_IO;
return r;
}
xtc_result xtc_project_collect(const xtc_project *p,const char *directory)
{
xtc_project *copy=NULL;collection *c=NULL;char canonical[4096],root[480],project[512]={0};size_t i;int created=0;xtc_result r;
if(!p || !directory || directory[0]!='/' || !directory[1] || strnlen(directory,480)>=480)return XTC_ERR_INVALID_ARGUMENT;
r=xtc_project_validate(p);if(r!=XTC_OK)return r;
r=xtc_project_clone(p,&copy);if(r!=XTC_OK)return r;
c=calloc(1,sizeof(*c));if(!c){xtc_project_destroy(copy);return XTC_ERR_OUT_OF_MEMORY;}
r=references(c,copy,0);if(r!=XTC_OK)goto done;
if(mkdir(directory,0700)){r=XTC_ERR_IO;goto done;}created=1;
if(!realpath(directory,canonical)){r=XTC_ERR_IO;goto done;}
if(strlen(canonical)>=sizeof(root)){r=XTC_ERR_CAPACITY;goto done;}strcpy(root,canonical);
for(i=0;r==XTC_OK && i<c->count;++i){snprintf(c->media[i].target,sizeof(c->media[i].target),"%s/media-%03zu.wav",root,i);r=copy_media(c,c->media+i);}
if(r==XTC_OK)r=references(c,copy,1);
if(r==XTC_OK){snprintf(project,sizeof(project),"%s/project.xtc",root);r=xtc_project_save(copy,project);}
done:
if(r!=XTC_OK && created){if(project[0])unlink(project);for(i=0;i<c->count;++i)if(c->media[i].owned)unlink(c->media[i].target);rmdir(directory);}
free(c);xtc_project_destroy(copy);return r;
}

View file

@ -1396,6 +1396,8 @@ int main(int argc, char **argv)
CHECK(window.import_audio(sample)==XTC_OK);CHECK(s->normalize_audio(.25f)==XTC_OK);
xtc_clip_info normalized{};xtc_project_find_clip(s->project(),s->selected_clip(),&normalized);CHECK(normalized.gain>0 && normalized.gain<=4);
CHECK(window.undo()==XTC_OK);
std::string folder="/tmp/xtc-gui-collect-"+std::to_string(getpid());CHECK(window.collect_project(folder.c_str())==XTC_OK);
CHECK(access((folder+"/project.xtc").c_str(),F_OK)==0);unlink((folder+"/project.xtc").c_str());unlink((folder+"/media-000.wav").c_str());rmdir(folder.c_str());
window.do_callback();
}
if (failures != 0) return 1;

View file

@ -1,3 +1,4 @@
#define _XOPEN_SOURCE 700
/* GNU ld allocation interposition: fail preparation at every allocation and
* reject host allocation/reclamation inside processing. No production hooks. */
#include "xtc/xtc_realtime.h"
@ -5,6 +6,7 @@
#include "xtc/xtc_control.h"
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_editing.h"
#include "xtc/xtc_audio.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
@ -221,8 +223,54 @@ static void modern_edit_failures(void)
}
xtc_clipboard_destroy(board);xtc_mix_snapshot_destroy(mix);xtc_project_destroy(p);
}
static void composition_failures(void)
{
xtc_project *p=NULL;uint64_t t,ids[2],note,audio;char sample[4096],root[]="/tmp/xtc-compose-fail-XXXXXX";
int operation,failure;xtc_midi_transform human={0};int intervals[2]={4,7};
assert(mkdtemp(root));assert(getcwd(sample,sizeof(sample)));assert(strlen(sample)+40<sizeof(sample));strcat(sample,"/examples/projects/original-pluck.wav");
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
assert(xtc_project_add_midi_clip(p,t,0,1920,&ids[0])==XTC_OK && xtc_project_add_midi_clip(p,t,3840,1920,&ids[1])==XTC_OK);
assert(xtc_project_add_note(p,ids[0],480,240,60,100,0,&note)==XTC_OK && xtc_project_add_note(p,ids[1],480,240,64,100,0,&note)==XTC_OK);
assert(xtc_project_import_audio(p,t,sample,7680,&audio)==XTC_OK);
fail_after=0;assert(xtc_project_set_marker(p,3840,"Section")==XTC_OK);fail_after=-1;
human.type=XTC_MIDI_TRANSFORM_HUMANIZE;human.seed=42;human.timing_variation=12;human.velocity_variation=8;
for(operation=0;operation<9;++operation) {
int succeeded=0;
for(failure=0;failure<128;++failure) {
xtc_project *copy=NULL;xtc_clip_info c;uint64_t output=999;xtc_result r;size_t tracks,clips,notes;
char dest[512],file[512];snprintf(dest,sizeof(dest),"%s/copy-%d-%d",root,operation,failure);
assert(xtc_project_clone(p,&copy)==XTC_OK);fail_after=failure;
switch(operation) {
case 0:r=xtc_project_ripple_time(copy,1920,960,0);break;
case 1:r=xtc_project_duplicate_track(copy,t,"Copy",&output);break;
case 2:r=xtc_project_split_midi(copy,ids[0],960,&output);break;
case 3:r=xtc_project_merge_midi(copy,ids,2,&output);break;
case 4:r=xtc_project_transform_midi(copy,ids,2,&human);break;
case 5:r=xtc_project_chordify(copy,ids,2,intervals,2);break;
case 6:r=xtc_project_arpeggiate(copy,ids,2,120,50,0);break;
case 7:r=xtc_project_normalize_audio(copy,&audio,1,.25f);break;
default:r=xtc_project_collect(copy,dest);break;
}
fail_after=-1;
if(r==XTC_OK)succeeded=1;
else {
assert((r==XTC_ERR_OUT_OF_MEMORY || (operation==8 && r==XTC_ERR_IO)) && output==999);
assert(xtc_project_get_counts(copy,&tracks,&clips)==XTC_OK && tracks==1 && clips==3);
assert(xtc_project_note_count(copy,&notes)==XTC_OK && notes==2);
assert(xtc_project_find_clip(copy,ids[1],&c)==XTC_OK && c.start_tick==3840);
assert(access(dest,F_OK)!=0);
{uint64_t probe;assert(xtc_project_duplicate_clip(copy,ids[0],t,9600,&probe)==XTC_OK && probe==3);}
}
if(operation==8 && succeeded){snprintf(file,sizeof(file),"%s/copy-%d-%d/project.xtc",root,operation,failure);assert(unlink(file)==0);snprintf(file,sizeof(file),"%s/copy-%d-%d/media-000.wav",root,operation,failure);assert(unlink(file)==0 && rmdir(dest)==0);}
xtc_project_destroy(copy);if(succeeded)break;
}
assert(succeeded);
}
xtc_project_destroy(p);assert(rmdir(root)==0);
}
int main(void)
{
composition_failures();
modern_edit_failures();
builtin_failures();
graph_limits();

View file

@ -8,6 +8,7 @@
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <math.h>
static void put32(unsigned char *p,uint32_t value){p[0]=(unsigned char)value;p[1]=(unsigned char)(value>>8);p[2]=(unsigned char)(value>>16);p[3]=(unsigned char)(value>>24);}
@ -191,4 +192,35 @@ static void normalize(void)
assert(xtc_project_normalize_audio(p,ids,1,NAN)==XTC_ERR_INVALID_ARGUMENT);
xtc_project_destroy(p);assert(unlink(path)==0 && rmdir(dir)==0);
}
int main(void){markers();sections();ripple();duplicate_track();split_midi();merge_midi();humanize();chords();arpeggio();normalize();puts("Next DAW workflow tests passed.");return 0;}
static void collect(void)
{
xtc_project *p=NULL,*q=NULL;xtc_audio_plan *audio=NULL;xtc_midi_plan *midi=NULL;xtc_plugin_spec spec;
uint64_t t,c,n;xtc_clip_info original,loaded;char root[]="/tmp/xtc-collect-XXXXXX",source[512],dest[512],moved[512],project[512],media[512],failed[512];
assert(mkdtemp(root));snprintf(source,sizeof(source),"%s/source.wav",root);test_wave(source,.5f);
snprintf(dest,sizeof(dest),"%s/collected",root);snprintf(moved,sizeof(moved),"%s/moved",root);snprintf(failed,sizeof(failed),"%s/failed",root);
assert(xtc_project_create(&p)==XTC_OK && xtc_project_create(&q)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
assert(xtc_project_import_audio(p,t,source,0,&c)==XTC_OK && xtc_project_find_clip(p,c,&original)==XTC_OK);
assert(xtc_project_add_midi_clip(p,t,960,960,&n)==XTC_OK);
assert(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,4,48000,&spec)==XTC_OK);strcpy(spec.sample_path,source);
assert(xtc_project_set_instrument(p,t,&spec)==XTC_OK);
{char large[512];uint64_t extra;FILE *f;
snprintf(large,sizeof(large),"%s/large.wav",root);f=fopen(large,"wb");assert(f && ftruncate(fileno(f),268435457)==0 && fclose(f)==0);
assert(xtc_project_add_clip(p,t,"Oversized",1920,960,&extra)==XTC_OK && xtc_project_set_clip_asset(p,extra,large)==XTC_OK);
assert(xtc_project_collect(p,failed)==XTC_ERR_CAPACITY && access(failed,F_OK)!=0);
assert(unlink(large)==0 && mkfifo(large,0600)==0);
assert(xtc_project_collect(p,failed)==XTC_ERR_IO && access(failed,F_OK)!=0);
assert(xtc_project_remove_clip(p,extra)==XTC_OK && unlink(large)==0);}
assert(xtc_project_collect(p,dest)==XTC_OK && xtc_project_collect(p,dest)==XTC_ERR_IO);
assert(xtc_project_find_clip(p,c,&loaded)==XTC_OK && !strcmp(loaded.asset_path,original.asset_path));
assert(rename(dest,moved)==0 && unlink(source)==0);
snprintf(project,sizeof(project),"%s/moved/project.xtc",root);snprintf(media,sizeof(media),"%s/moved/media-000.wav",root);
assert(xtc_project_load(q,project)==XTC_OK && xtc_project_find_clip(q,c,&loaded)==XTC_OK && !strcmp(loaded.asset_path,media));
assert(xtc_project_get_instrument(q,t,&spec)==XTC_OK && !strcmp(spec.sample_path,media));
assert(xtc_audio_plan_create(q,48000,&audio)==XTC_OK && xtc_audio_plan_issues(audio,NULL)==0);
{float sample=0;assert(xtc_audio_plan_render(audio,0,&sample,1)==XTC_OK && fabsf(sample-.5f)<.00001f);}
assert(xtc_midi_plan_create(q,48000,&midi)==XTC_OK && xtc_midi_plan_asset_issues(midi)==0);
assert(xtc_project_collect(p,failed)==XTC_ERR_IO && access(failed,F_OK)!=0);
xtc_audio_plan_destroy(audio);xtc_midi_plan_destroy(midi);xtc_project_destroy(p);xtc_project_destroy(q);
assert(unlink(project)==0 && unlink(media)==0 && rmdir(moved)==0 && rmdir(root)==0);
}
int main(void){markers();sections();ripple();duplicate_track();split_midi();merge_midi();humanize();chords();arpeggio();normalize();collect();puts("Next DAW workflow tests passed.");return 0;}