From 7489f86351445ebf17355e95565c2460e8edcdbf Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 01:34:08 +0100 Subject: [PATCH] feat(project): persist bounded per-track effect chains (phase 81) --- Makefile | 8 ++++ docs/ROADMAP.md | 8 ++-- docs/TRACK_INSERTS.md | 16 +++++++ include/xtc/xtc_plugin.h | 8 ++++ include/xtc/xtc_project.h | 2 +- src/persistence/project_file.c | 50 ++++++++++++++++++++ src/project/editing.c | 2 +- src/project/project.c | 36 ++++++++++++++ src/project/project_internal.h | 1 + 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_project_diagnostics.c | 2 +- tests/unit/test_track_fx.c | 28 +++++++++++ 15 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 docs/TRACK_INSERTS.md create mode 100644 tests/unit/test_track_fx.c diff --git a/Makefile b/Makefile index cc73a68..23ce43c 100644 --- a/Makefile +++ b/Makefile @@ -574,3 +574,11 @@ test: test-sample test-gui-sample: $(BUILD_DIR)/test_gui XTC_TEST_SAMPLE_EDITOR_ONLY=1 xvfb-run -a -s "-screen 0 1280x900x24" ./$(BUILD_DIR)/test_gui -include $(BUILD_DIR)/src/gui/sample_editor.d $(BUILD_DIR)/tests/unit/test_sample.d + +$(BUILD_DIR)/test_track_fx: $(BUILD_DIR)/tests/unit/test_track_fx.o $(BUILD_DIR)/libxtc.a + $(CC) $(LDFLAGS) -o $@ $^ -lm -ldl +.PHONY: test-track-fx +test-track-fx: $(BUILD_DIR)/test_track_fx + ./$(BUILD_DIR)/test_track_fx +test: test-track-fx +-include $(BUILD_DIR)/tests/unit/test_track_fx.d diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ff91f96..ec17728 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1895,12 +1895,12 @@ Implemented: sequencer Create menu retains empty/audio/MIDI track creation, WAV Add a bounded C99 per-track insert model with validation, clone/history/duplicate support and backward-compatible project persistence. Loading metadata must not load plugin binaries. ### Tasks and acceptance criteria -- [ ] Implement the described workflow with validated errors and safe ownership. -- [ ] Pass relevant core and/or GUI tests. -- [ ] Document behavior and limitations; commit this phase separately. +- [x] Implement the described workflow with validated errors and safe ownership. +- [x] Pass relevant core and/or GUI tests. +- [x] Document behavior and limitations; commit this phase separately. ### Implementation status -Planned. +Implemented: eight validated metadata-only insert slots per non-bus track, including enabled/wet/output values, format 17 persistence and reads of formats 1–16 with empty chains. Clone/history use copied state; duplicate/move/remove retain track association. Full warning-enabled core suite and dedicated metadata, persistence and invalid-value tests passed. See TRACK_INSERTS.md. Playback integration follows in phase 82. --- diff --git a/docs/TRACK_INSERTS.md b/docs/TRACK_INSERTS.md new file mode 100644 index 0000000..d57b593 --- /dev/null +++ b/docs/TRACK_INSERTS.md @@ -0,0 +1,16 @@ +# Track inserts + +The sequencer's **Create** dropdown groups track creation, WAV import and +instrument loading. **View → Track inspector** opens the optional inspector; +it is hidden when the application starts to leave more room for editing. + +Project format 17 stores up to eight effect slots per track, their order, +bypass, wet amount and chain output gain. Formats 1–16 read with empty chains. +Metadata load, clone and history never open executable plugin modules. Track +move, duplicate and removal retain the association with stable track IDs. +Nonempty bus chains are rejected. The model accepts mono effect specifications +for LADSPA, VST2, VST3, CLAP and internal effects; this alone does not establish +that a third-party binary is compatible with the host. + +The chain API is declared in `xtc_plugin.h`. Setter errors preserve the old +chain; plugin identity and layout are checked when preparing playback. diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index 2367e50..b052d74 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -31,6 +31,14 @@ typedef struct { unsigned int tempo, point_count; /* milli-BPM, 20000..300000 */ xtc_plugin_point points[XTC_PLUGIN_POINTS]; } xtc_plugin_spec; +#define XTC_TRACK_INSERTS 8U +typedef struct { xtc_plugin_spec plugin; unsigned int enabled; float wet; } xtc_track_insert; +typedef struct { size_t count; float output_gain; xtc_track_insert slots[XTC_TRACK_INSERTS]; } xtc_track_chain; +/* Metadata only; control thread. Empty chain has unity output. No module is + * opened here. Nonempty chains are mono effects on non-bus tracks. */ +xtc_result xtc_track_chain_validate(const xtc_track_chain *chain); +xtc_result xtc_project_get_chain(const xtc_project *project,uint64_t track,xtc_track_chain *out); +xtc_result xtc_project_set_chain(xtc_project *project,uint64_t track,const xtc_track_chain *chain); /* Copied discovery metadata, never descriptor/module pointers. Modern adapters * currently expose the name only (zero controls). LADSPA provides * no standardized units; unit is empty. Flags describe input control hints. */ diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index a898cef..7f4408b 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 16U +#define XTC_PROJECT_FORMAT_VERSION 17U #define XTC_PROJECT_MAX_TRACKS 64U #define XTC_PROJECT_MAX_CLIPS 1024U #define XTC_PROJECT_NAME_BYTES 64U diff --git a/src/persistence/project_file.c b/src/persistence/project_file.c index a707857..c6dbc7a 100644 --- a/src/persistence/project_file.c +++ b/src/persistence/project_file.c @@ -502,6 +502,40 @@ static xtc_result read_project(reader *input, xtc_project *project, const char * if(!decode_name(input->tokens[2],project->markers[i].name))return XTC_ERR_FORMAT; } } + for(i=0;itrack_count;++i)project->chains[i].output_gain=1; + if(format_version>=17U) { + READ("CHAINS",2U);NUM(1U,value);if(value!=project->track_count)return XTC_ERR_FORMAT; + for(i=0;itrack_count;++i) { + xtc_track_chain *c=project->chains+i;size_t k; + READ("CHAIN",4U);NUM(1U,value);if(value!=project->tracks[i].id)return XTC_ERR_FORMAT; + NUM(2U,value);if(value>XTC_TRACK_INSERTS)return XTC_ERR_FORMAT;c->count=(size_t)value; + if(!decode_float(input->tokens[3],&c->output_gain))return XTC_ERR_FORMAT; + for(j=0;jcount;++j) { + xtc_track_insert *slot=c->slots+j;xtc_plugin_spec *p=&slot->plugin; + READ("INSERT",9U);NUM(1U,value);if(value>1)return XTC_ERR_FORMAT;slot->enabled=(unsigned int)value; + if(!decode_float(input->tokens[2],&slot->wet))return XTC_ERR_FORMAT; + NUM(3U,value);if(value>XTC_PLUGIN_BUILTIN)return XTC_ERR_FORMAT;p->format=(xtc_plugin_format)value; + NUM(4U,value);if(value>=1024)return XTC_ERR_FORMAT;p->index=(unsigned int)value; + NUM(5U,value);if(value>UINT32_MAX)return XTC_ERR_FORMAT;p->unique_id=(unsigned int)value; + NUM(6U,value);if(value!=1)return XTC_ERR_FORMAT;p->inputs=1; + NUM(7U,value);if(value<20000 || value>300000)return XTC_ERR_FORMAT;p->tempo=(unsigned int)value; + if(!decode_name(input->tokens[8],p->label))return XTC_ERR_FORMAT; + READ("MODULE",2U);if(!decode_bytes(input->tokens[1],p->path,sizeof(p->path)))return XTC_ERR_FORMAT; + READ("IDENTITY",2U);if(strcmp(input->tokens[1],"-") && !decode_bytes(input->tokens[1],p->identifier,sizeof(p->identifier)))return XTC_ERR_FORMAT; + READ("CONTROLS",2U);NUM(1U,value);if(value>XTC_PLUGIN_CONTROLS)return XTC_ERR_FORMAT;p->control_count=(unsigned int)value; + for(k=0;kcontrol_count;++k) { + READ("CONTROL",3U);NUM(1U,value);if(value>=64)return XTC_ERR_FORMAT;p->ports[k]=(unsigned int)value; + if(!decode_float(input->tokens[2],p->values+k))return XTC_ERR_FORMAT; + } + READ("PARAMETERS",2U);NUM(1U,value);if(value>XTC_PLUGIN_POINTS)return XTC_ERR_FORMAT;p->point_count=(unsigned int)value; + for(k=0;kpoint_count;++k) { + xtc_plugin_point *a=p->points+k;READ("PARAM",5U);NUM(1U,a->tick);NUM(2U,value);if(value>=64)return XTC_ERR_FORMAT;a->port=(unsigned int)value; + NUM(3U,value);if(value>1)return XTC_ERR_FORMAT;a->step=(unsigned int)value; + if(!decode_float(input->tokens[4],&a->value))return XTC_ERR_FORMAT; + } + } + } + } READ("END", 1U); if (fgetc(input->file) != EOF) return XTC_ERR_FORMAT; @@ -681,6 +715,22 @@ static xtc_result write_project(FILE *file, const xtc_project *project, const ch } 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; + if(fprintf(file,"CHAINS %zu\n",project->track_count)<0)return XTC_ERR_IO; + for(i=0;itrack_count;++i) { + const xtc_track_chain *c=project->chains+i;size_t j,k;uint32_t bits; + memcpy(&bits,&c->output_gain,4); + if(fprintf(file,"CHAIN %" PRIu64 " %zu %08" PRIx32 "\n",project->tracks[i].id,c->count,bits)<0)return XTC_ERR_IO; + for(j=0;jcount;++j) { + const xtc_track_insert *slot=c->slots+j;const xtc_plugin_spec *p=&slot->plugin;memcpy(&bits,&slot->wet,4); + if(fprintf(file,"INSERT %u %08" PRIx32 " %u %u %u %u %u ",slot->enabled,bits,(unsigned int)p->format,p->index,p->unique_id,p->inputs,p->tempo)<0 || !write_name(file,p->label))return XTC_ERR_IO; + if(fputs("MODULE ",file)==EOF || !write_name(file,p->path) || fputs("IDENTITY ",file)==EOF)return XTC_ERR_IO; + if(p->identifier[0]) {if(!write_name(file,p->identifier))return XTC_ERR_IO;}else if(fputs("-\n",file)==EOF)return XTC_ERR_IO; + if(fprintf(file,"CONTROLS %u\n",p->control_count)<0)return XTC_ERR_IO; + for(k=0;kcontrol_count;++k) {memcpy(&bits,p->values+k,4);if(fprintf(file,"CONTROL %u %08" PRIx32 "\n",p->ports[k],bits)<0)return XTC_ERR_IO;} + if(fprintf(file,"PARAMETERS %u\n",p->point_count)<0)return XTC_ERR_IO; + for(k=0;kpoint_count;++k) {const xtc_plugin_point *a=p->points+k;memcpy(&bits,&a->value,4);if(fprintf(file,"PARAM %" PRIu64 " %u %u %08" PRIx32 "\n",a->tick,a->port,a->step,bits)<0)return XTC_ERR_IO;} + } + } return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK; } diff --git a/src/project/editing.c b/src/project/editing.c index 3500b89..3b9430f 100644 --- a/src/project/editing.c +++ b/src/project/editing.c @@ -223,7 +223,7 @@ xtc_result xtc_project_duplicate_track(xtc_project *p,uint64_t track,const char if(p->tracks[index].is_bus)return XTC_ERR_INVALID_STATE; r=xtc_project_clone(p,©);if(r!=XTC_OK)return r; r=xtc_project_add_track(copy,name,&id); - if(r==XTC_OK){xtc_track_info *target=copy->tracks+copy->track_count-1;*target=p->tracks[index];target->id=id;memcpy(target->name,name,strlen(name)+1);copy->instruments[copy->track_count-1]=p->instruments[index];} + if(r==XTC_OK){xtc_track_info *target=copy->tracks+copy->track_count-1;*target=p->tracks[index];target->id=id;memcpy(target->name,name,strlen(name)+1);copy->instruments[copy->track_count-1]=p->instruments[index];copy->chains[copy->track_count-1]=p->chains[index];} for(i=0;r==XTC_OK && iclip_count;++i)if(p->clips[i].track_id==track){uint64_t clip;r=xtc_project_duplicate_clip(copy,p->clips[i].id,id,p->clips[i].start_tick,&clip);} for(i=0;r==XTC_OK && iautomation_count;++i)if(p->automation[i].track_id==track){xtc_automation_point point=p->automation[i];point.track_id=id;r=xtc_project_set_automation(copy,&point);} if(r==XTC_OK)r=xtc_project_move_track(copy,id,index+1); diff --git a/src/project/project.c b/src/project/project.c index 7014ed5..b0198cb 100644 --- a/src/project/project.c +++ b/src/project/project.c @@ -306,6 +306,8 @@ xtc_result xtc_project_add_track(xtc_project *project, const char *name, uint64_ track.gain = 1.0f; track.bus_id = UINT64_MAX; memcpy(track.name, name, strlen(name) + 1U); + memset(project->chains+project->track_count,0,sizeof(*project->chains)); + project->chains[project->track_count].output_gain=1; project->tracks[project->track_count++] = track; *id = track.id; return XTC_OK; @@ -333,6 +335,9 @@ xtc_result xtc_project_remove_track(xtc_project *project, uint64_t id) for (i = 0; i < project->track_count; ++i) if (project->tracks[i].bus_id == id) project->tracks[i].bus_id = UINT64_MAX; if (project->live_instrument == id) project->live_instrument = UINT64_MAX; + memmove(project->chains+index,project->chains+index+1, + (project->track_count-index-1)*sizeof(*project->chains)); + memset(project->chains+project->track_count-1,0,sizeof(*project->chains)); memmove(project->instruments+index,project->instruments+index+1, (project->track_count-index-1)*sizeof(*project->instruments)); memset(project->instruments+project->track_count-1,0,sizeof(*project->instruments)); @@ -544,6 +549,8 @@ xtc_result xtc_project_validate(const xtc_project *project) if (target == SIZE_MAX || !project->instruments[target].instrument) return XTC_ERR_FORMAT; } for (i = 0U; i < project->track_count; ++i) { + if(xtc_track_chain_validate(project->chains+i)!=XTC_OK || + (project->tracks[i].is_bus && project->chains[i].count))return XTC_ERR_FORMAT; if (project->instruments[i].instrument && (project->tracks[i].is_bus || xtc_plugin_spec_validate(project->instruments+i) != XTC_OK)) return XTC_ERR_FORMAT; if (project->tracks[i].id >= project->next_track_id || !xtc_project_valid_name(project->tracks[i].name)) @@ -689,6 +696,10 @@ xtc_result xtc_project_move_track(xtc_project *p, uint64_t id, size_t destinatio size_t i; xtc_track_info track; if (!p || destination >= p->track_count) return XTC_ERR_INVALID_ARGUMENT; i = track_index(p,id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND; + { xtc_track_chain chain=p->chains[i]; + if(ichains+i,p->chains+i+1,(destination-i)*sizeof(chain)); + else if(i>destination)memmove(p->chains+destination+1,p->chains+destination,(i-destination)*sizeof(chain)); + p->chains[destination]=chain; } { xtc_plugin_spec instrument = p->instruments[i]; if (i < destination) memmove(p->instruments+i,p->instruments+i+1,(destination-i)*sizeof(instrument)); else if (i > destination) memmove(p->instruments+destination+1,p->instruments+destination,(i-destination)*sizeof(instrument)); @@ -772,3 +783,28 @@ xtc_result xtc_project_set_live_instrument(xtc_project *p, uint64_t track) } xtc_result xtc_project_get_live_instrument(const xtc_project *p, uint64_t *track) { if (!p || !track) return XTC_ERR_INVALID_ARGUMENT; *track = p->live_instrument; return XTC_OK; } + +xtc_result xtc_track_chain_validate(const xtc_track_chain *c) +{ + size_t i; + if(!c || c->count>XTC_TRACK_INSERTS || !isfinite(c->output_gain) || c->output_gain<0 || c->output_gain>4)return XTC_ERR_INVALID_ARGUMENT; + for(i=0;icount;++i) { + const xtc_track_insert *s=c->slots+i; + if(s->enabled>1 || !isfinite(s->wet) || s->wet<0 || s->wet>1 || s->plugin.instrument || + s->plugin.inputs!=1 || s->plugin.sample_path[0] || xtc_plugin_spec_validate(&s->plugin)!=XTC_OK)return XTC_ERR_INVALID_ARGUMENT; + } + return XTC_OK; +} +xtc_result xtc_project_get_chain(const xtc_project *p,uint64_t track,xtc_track_chain *out) +{ + size_t i;if(!p || !out)return XTC_ERR_INVALID_ARGUMENT; + i=track_index(p,track);if(i==SIZE_MAX)return XTC_ERR_NOT_FOUND; + *out=p->chains[i];return XTC_OK; +} +xtc_result xtc_project_set_chain(xtc_project *p,uint64_t track,const xtc_track_chain *c) +{ + size_t i;if(!p || xtc_track_chain_validate(c)!=XTC_OK)return XTC_ERR_INVALID_ARGUMENT; + i=track_index(p,track);if(i==SIZE_MAX)return XTC_ERR_NOT_FOUND; + if(p->tracks[i].is_bus && c->count)return XTC_ERR_UNSUPPORTED; + p->chains[i]=*c;return XTC_OK; +} diff --git a/src/project/project_internal.h b/src/project/project_internal.h index 6b2ae98..a52f6e9 100644 --- a/src/project/project_internal.h +++ b/src/project/project_internal.h @@ -12,6 +12,7 @@ struct xtc_project { xtc_marker markers[XTC_PROJECT_MAX_MARKERS]; uint64_t live_instrument; xtc_plugin_spec instruments[XTC_PROJECT_MAX_TRACKS]; + xtc_track_chain chains[XTC_PROJECT_MAX_TRACKS]; uint64_t next_track_id, next_clip_id; unsigned int tempo; unsigned char control_map[16][3]; diff --git a/tests/integration/test_install.py b/tests/integration/test_install.py index 38f9a86..7717311 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..16' in diagnose and 'JACK compiled:' in diagnose + assert 'Project formats: 1..17' 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 146654b..b17a35f 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..16")), + (base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..17")), ("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 215470b..bd51c3d 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,"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"); + if(!strncmp(line,"CHAINS ",7) || !strncmp(line,"CHAIN ",6) || !strncmp(line,"MARKERS ",8) || !strncmp(line,"SAMPLE ",7) || !strncmp(line,"INSTRUMENT_CONTROLS",19) || !strncmp(line,"INSTRUMENT_AUTOMATION",21))continue; + if(!strncmp(line,"XTC_PROJECT 17",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_project_diagnostics.c b/tests/unit/test_project_diagnostics.c index ecd12d1..f3cf3ba 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..16")); + diagnostic.format_version == 999 && !strcmp(diagnostic.record,"XTC_PROJECT") && strstr(diagnostic.message,"1..17")); 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")); diff --git a/tests/unit/test_track_fx.c b/tests/unit/test_track_fx.c new file mode 100644 index 0000000..0317831 --- /dev/null +++ b/tests/unit/test_track_fx.c @@ -0,0 +1,28 @@ +#define _POSIX_C_SOURCE 200809L +#include "xtc/xtc_plugin.h" +#include "xtc/xtc_editing.h" +#include +#include +#include +#include +#include +#include +int main(void) +{ + xtc_project *p=NULL,*q=NULL;uint64_t a,b,duplicate;xtc_track_chain c={0},got={0}; + char file[]="/tmp/xtc-chain-XXXXXX";int fd=mkstemp(file);assert(fd>=0);close(fd); + assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Audio",&a)==XTC_OK && xtc_project_add_track(p,"Other",&b)==XTC_OK); + assert(xtc_project_get_chain(p,a,&got)==XTC_OK && got.count==0 && got.output_gain==1); + assert(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,2,48000,&c.slots[0].plugin)==XTC_OK); + c.count=1;c.output_gain=.75f;c.slots[0].enabled=1;c.slots[0].wet=.5f; + assert(xtc_project_set_chain(p,a,&c)==XTC_OK); + assert(xtc_project_save(p,file)==XTC_OK && xtc_project_create(&q)==XTC_OK && xtc_project_load(q,file)==XTC_OK); + assert(xtc_project_get_chain(q,a,&got)==XTC_OK && got.count==1 && got.output_gain==.75f && got.slots[0].wet==.5f && !strcmp(got.slots[0].plugin.label,c.slots[0].plugin.label)); + assert(xtc_project_duplicate_track(q,a,"Copy",&duplicate)==XTC_OK && xtc_project_get_chain(q,duplicate,&got)==XTC_OK && got.count==1); + assert(xtc_project_move_track(q,a,2)==XTC_OK && xtc_project_remove_track(q,b)==XTC_OK && xtc_project_get_chain(q,a,&got)==XTC_OK && got.count==1); + c.output_gain=NAN;assert(xtc_project_set_chain(q,a,&c)==XTC_ERR_INVALID_ARGUMENT); + assert(xtc_project_get_chain(q,a,&got)==XTC_OK && got.output_gain==.75f); + c.output_gain=1;c.slots[0].wet=2;assert(xtc_project_set_chain(q,a,&c)==XTC_ERR_INVALID_ARGUMENT); + c.slots[0].wet=1;c.count=XTC_TRACK_INSERTS+1;assert(xtc_track_chain_validate(&c)==XTC_ERR_INVALID_ARGUMENT); + xtc_project_destroy(p);xtc_project_destroy(q);unlink(file);puts("Track insert metadata/persistence passed.");return 0; +}