From 62cee7a7b04fb802dbd25d13838ff6608fb3a407 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 01:35:53 +0100 Subject: [PATCH] feat(engine): render prepared per-track insert graphs (phase 82) --- Makefile | 2 +- docs/ROADMAP.md | 8 +-- docs/TRACK_INSERTS.md | 13 ++++ include/xtc/xtc_plugin.h | 2 + src/audio/arrangement.c | 9 +++ src/midi/instrument.c | 9 +++ src/mixer/track_fx.c | 101 +++++++++++++++++++++++++++ src/mixer/track_fx.h | 12 ++++ tests/integration/test_reliability.c | 29 ++++++++ tests/unit/test_track_fx.c | 35 ++++++++++ 10 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 src/mixer/track_fx.c create mode 100644 src/mixer/track_fx.h diff --git a/Makefile b/Makefile index 23ce43c..cb7ef6d 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude LDFLAGS ?= -CORE_SOURCES := src/audio/sample.c 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 +CORE_SOURCES := src/mixer/track_fx.c src/audio/sample.c 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) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ec17728..2dd9892 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1910,12 +1910,12 @@ Implemented: eight validated metadata-only insert slots per non-bus track, inclu Compile track inserts into serial modular graphs before track gain/pan. Cover audio and MIDI rendering, export, bypass, cleanup and callback safety; explicitly reject unsupported routing. ### 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: generated serial graphs process summed pre-fader audio or MIDI per track; prepared renderer plans feed JACK/export and post-effect meters. Wet/dry branches, bypass and output gain are compiled on the control thread. Exact impulse/delay, overlapping clips, block continuity, seek reset, MIDI samples and hybrid rejection pass. Full core suite and allocation-failure/callback guards pass. Bus/hybrid inserts reject explicitly; external adapters retain mono/zero-latency limits. See TRACK_INSERTS.md. --- diff --git a/docs/TRACK_INSERTS.md b/docs/TRACK_INSERTS.md index d57b593..8bdc4b6 100644 --- a/docs/TRACK_INSERTS.md +++ b/docs/TRACK_INSERTS.md @@ -14,3 +14,16 @@ 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. + +Playback prepares an independent serial graph for each affected track. Audio +clips on the same track sum before its effects; instrument and scheduled MIDI +contributions also pass through their track graph. Effects run before track +and bus gain/pan, and meters observe their output. Wet/dry uses parallel gain +branches. Internal effect history resets at timeline discontinuities. + +Each renderer owns its plans and preallocated raw buffers (up to 256 KiB per +affected track); processing uses blocks of at most 256 frames, with no host +allocation or I/O. Mono effect adapters require zero reported latency. External +plugin state on seeks follows the existing host contract. Bus inserts and +tracks containing both audio assets and MIDI reject preparation explicitly. +Live unassigned sine monitoring remains a separate master input. diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index b052d74..d3ec23a 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -36,6 +36,8 @@ typedef struct { xtc_plugin_spec plugin; unsigned int enabled; float wet; } xtc_ 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. */ +/* Allocates the exact serial/parallel graph used by playback. Caller owns it. */ +xtc_result xtc_track_chain_graph(const xtc_track_chain *chain,xtc_graph **out); 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); diff --git a/src/audio/arrangement.c b/src/audio/arrangement.c index 0c56fb5..e20be93 100644 --- a/src/audio/arrangement.c +++ b/src/audio/arrangement.c @@ -1,6 +1,7 @@ #define _XOPEN_SOURCE 700 #include "wave_internal.h" #include "../mixer/mix_internal.h" +#include "../mixer/track_fx.h" #include "../graph/graph_internal.h" #include "xtc/xtc_dsp.h" #include @@ -17,6 +18,7 @@ struct xtc_audio_plan { size_t fade_frames, monitor_stride; float *monitor; xtc_mix_snapshot mix; + xtc_track_fx *fx; xtc_wave waves[64]; char paths[64][XTC_ASSET_PATH_BYTES]; region regions[XTC_PROJECT_MAX_CLIPS]; @@ -112,6 +114,7 @@ void xtc_audio_plan_destroy(xtc_audio_plan *plan) return; for (i = 0; i < plan->assets; ++i) free(plan->waves[i].samples); + xtc_track_fx_destroy(plan->fx); free(plan); } @@ -149,6 +152,8 @@ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate, free(plan); return result; } + result=xtc_track_fx_create(project,rate,0,&plan->fx); + if(result!=XTC_OK){xtc_audio_plan_destroy(plan);return result;} for (i = 0; i < project->clip_count; ++i) { const xtc_clip_info *clip = project->clips + i; size_t a; @@ -246,6 +251,7 @@ xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t pos memset(output, 0, frames * sizeof(*output)); if (right) memset(right, 0, frames * sizeof(*right)); + xtc_track_fx_begin(plan->fx,position,frames); for (i = 0; i < plan->count; ++i) { const region *r = plan->regions + i; const xtc_wave *wave = plan->waves + r->asset; @@ -289,12 +295,15 @@ xtc_result xtc_audio_plan_render_stereo(const xtc_audio_plan *plan, uint64_t pos } sample *= r->gain * envelope; } + if(xtc_track_fx_add(plan->fx,r->track,j,sample))continue; if (plan->monitor) xtc_mix_monitor(&plan->mix, plan->monitor, plan->monitor_stride, r->track, position+j, j, sample, right != NULL); output[j] += sample * xtc_mix_level(&plan->mix, r->track, position + j, right ? 0U : 2U); if (right) right[j] += sample * xtc_mix_level(&plan->mix, r->track, position + j, 1); } } + {xtc_result r=xtc_track_fx_finish(plan->fx,&plan->mix,position,output,right,frames,plan->monitor,plan->monitor_stride); + if(r!=XTC_OK){memset(output,0,frames*sizeof(*output));if(right)memset(right,0,frames*sizeof(*right));return r;}} for (j = 0; j < frames; ++j) { output[j] = xtc_dsp_finite(output[j]); if (right) right[j] = xtc_dsp_finite(right[j]); diff --git a/src/midi/instrument.c b/src/midi/instrument.c index 20b1e92..457e1df 100644 --- a/src/midi/instrument.c +++ b/src/midi/instrument.c @@ -2,6 +2,7 @@ #include "xtc/xtc_dsp.h" #include "xtc/xtc_plugin.h" #include "../mixer/mix_internal.h" +#include "../mixer/track_fx.h" #include #include #include @@ -33,6 +34,7 @@ struct xtc_midi_plan { unsigned int sustain[16]; unsigned char control_map[16][3]; xtc_mix_snapshot mix; + xtc_track_fx *fx; span notes[XTC_PROJECT_MAX_NOTES]; xtc_plugin *plugins[XTC_PROJECT_MAX_TRACKS]; size_t live_track; @@ -209,6 +211,7 @@ xtc_result xtc_midi_plan_create(const xtc_project *project, unsigned int rate, x } if (project->live_instrument == p->mix.tracks[i].id) p->live_track = i; } + {xtc_result r=xtc_track_fx_create(project,rate,1,&p->fx);if(r!=XTC_OK){xtc_midi_plan_destroy(p);return r;}} memcpy(p->control_map,project->control_map,sizeof(p->control_map)); p->controller_frames = rate / 200U; if (!p->controller_frames) p->controller_frames = 1; @@ -228,6 +231,7 @@ void xtc_midi_plan_destroy(xtc_midi_plan *p) size_t i; if (!p) return; for (i = 0; i < XTC_PROJECT_MAX_TRACKS; ++i) xtc_plugin_destroy(p->plugins[i]); + xtc_track_fx_destroy(p->fx); free(p); } @@ -362,6 +366,7 @@ static void segment(xtc_midi_plan *p, uint64_t position, float *out, float *righ if (distance < p->fade_frames) sample *= (float)distance / (float)p->fade_frames; } + if(xtc_track_fx_add(p->fx,s->track,(size_t)(position-p->monitor_position)+j,sample))continue; if (p->monitor) xtc_mix_monitor(&p->mix, p->monitor, p->monitor_stride, s->track, position+j, (size_t)(position-p->monitor_position)+j, sample, right != NULL); out[j] += sample * xtc_mix_level(&p->mix, s->track, position + j, right ? 0U : 2U); @@ -445,6 +450,7 @@ static xtc_result instruments(xtc_midi_plan *p, uint64_t position, const xtc_mid if (r != XTC_OK) return r; for (j = 0; j < n; ++j) { float sample = p->scratch[j]; + if(xtc_track_fx_add(p->fx,track,offset+j,sample))continue; if (p->monitor) xtc_mix_monitor(&p->mix,p->monitor,p->monitor_stride,track,begin+j,offset+j,sample,right != NULL); out[offset+j] += sample*xtc_mix_level(&p->mix,track,begin+j,right ? 0U : 2U); if (right) right[offset+j] += sample*xtc_mix_level(&p->mix,track,begin+j,1); @@ -486,6 +492,7 @@ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, cons return XTC_OK; if (p->next_position != position) xtc_midi_plan_reset(p); + xtc_track_fx_begin(p->fx,position,frames); memset(out, 0, frames * sizeof(*out)); if (right) memset(right, 0, frames * sizeof(*right)); @@ -504,6 +511,8 @@ xtc_result xtc_midi_plan_render_stereo(xtc_midi_plan *p, uint64_t position, cons xtc_midi_plan_reset(p); return r; } } + {xtc_result r=xtc_track_fx_finish(p->fx,&p->mix,position,out,right,frames,p->monitor,p->monitor_stride); + if(r!=XTC_OK){memset(out,0,frames*sizeof(*out));if(right)memset(right,0,frames*sizeof(*right));xtc_midi_plan_reset(p);return r;}} for (i = 0; i < frames; ++i) { out[i] = xtc_dsp_finite(out[i]); if (right) right[i] = xtc_dsp_finite(right[i]); diff --git a/src/mixer/track_fx.c b/src/mixer/track_fx.c new file mode 100644 index 0000000..d0e0414 --- /dev/null +++ b/src/mixer/track_fx.c @@ -0,0 +1,101 @@ +#include "track_fx.h" +#include "../project/project_internal.h" +#include "xtc/xtc_dsp.h" +#include +#include +struct xtc_track_fx { + xtc_plan *plans[XTC_PROJECT_MAX_TRACKS]; + float *raw[XTC_PROJECT_MAX_TRACKS]; + uint64_t next; + float scratch[256]; +}; +/* Public metadata graph: the graph displayed by the editor is also compiled. */ +xtc_result xtc_track_chain_graph(const xtc_track_chain *c,xtc_graph **out) +{ + xtc_graph *g=NULL;size_t previous=0,output=0,i;xtc_result r; + if(!out || xtc_track_chain_validate(c)!=XTC_OK)return XTC_ERR_INVALID_ARGUMENT; + r=xtc_graph_create(64,&g); + if(r==XTC_OK)r=xtc_graph_add_gain(g,1,&previous); + for(i=0;r==XTC_OK && icount;++i) { + const xtc_track_insert *s=c->slots+i;size_t plugin=0,wet=0,dry=0,sum=0; + if(!s->enabled || s->wet==0)continue; + r=xtc_graph_add_plugin(g,&s->plugin,&plugin); + if(r==XTC_OK)r=xtc_graph_connect(g,previous,plugin); + if(s->wet==1){previous=plugin;continue;} + if(r==XTC_OK)r=xtc_graph_add_gain(g,s->wet,&wet); + if(r==XTC_OK)r=xtc_graph_add_gain(g,1-s->wet,&dry); + if(r==XTC_OK)r=xtc_graph_add_gain(g,1,&sum); + if(r==XTC_OK)r=xtc_graph_connect(g,plugin,wet); + if(r==XTC_OK)r=xtc_graph_connect(g,previous,dry); + if(r==XTC_OK)r=xtc_graph_connect(g,wet,sum); + if(r==XTC_OK)r=xtc_graph_connect(g,dry,sum); + previous=sum; + } + if(r==XTC_OK)r=xtc_graph_add_gain(g,c->output_gain,&output); + if(r==XTC_OK)r=xtc_graph_connect(g,previous,output); + previous=output; + if(r==XTC_OK)r=xtc_graph_add_output(g,&output); + if(r==XTC_OK)r=xtc_graph_connect(g,previous,output); + if(r!=XTC_OK){xtc_graph_destroy(g);return r;} + *out=g;return XTC_OK; +} +void xtc_track_fx_destroy(xtc_track_fx *f) +{ + size_t i;if(!f)return; + for(i=0;iplans[i]);free(f->raw[i]);} + free(f); +} +xtc_result xtc_track_fx_create(const xtc_project *p,unsigned int rate,int midi,xtc_track_fx **out) +{ + size_t i,j;xtc_track_fx *f=NULL;xtc_result r=XTC_OK;int needed=0; + if(!p || !out || !rate)return XTC_ERR_INVALID_ARGUMENT; + *out=NULL; + for(i=0;itrack_count;++i)if(p->chains[i].count || p->chains[i].output_gain!=1)needed=1; + if(!needed)return XTC_OK; + f=calloc(1,sizeof(*f));if(!f)return XTC_ERR_OUT_OF_MEMORY;f->next=UINT64_MAX; + for(i=0;r==XTC_OK && itrack_count;++i) { + const xtc_track_chain *c=p->chains+i;xtc_graph *g=NULL; + int has_midi=p->instruments[i].instrument || p->live_instrument==p->tracks[i].id,has_audio=0; + if(!c->count && c->output_gain==1)continue; + for(j=0;jclip_count;++j)if(p->clips[j].track_id==p->tracks[i].id) { + if(p->clips[j].midi)has_midi=1;else if(p->clips[j].asset_path[0])has_audio=1; + } + if(p->tracks[i].is_bus || (has_midi && has_audio)){r=XTC_ERR_UNSUPPORTED;break;} + if(has_midi!=midi)continue; + r=xtc_track_chain_graph(c,&g); + if(r==XTC_OK)r=xtc_graph_compile(g,rate,256,&f->plans[i]); + xtc_graph_destroy(g); + if(r==XTC_OK){f->raw[i]=calloc(65536,sizeof(float));if(!f->raw[i])r=XTC_ERR_OUT_OF_MEMORY;} + } + if(r!=XTC_OK){xtc_track_fx_destroy(f);return r;} + *out=f;return XTC_OK; +} +void xtc_track_fx_begin(xtc_track_fx *f,uint64_t position,size_t frames) +{ + size_t i;if(!f)return; + for(i=0;iraw[i]) { + memset(f->raw[i],0,frames*sizeof(float)); + if(f->next!=position)xtc_plan_reset_history(f->plans[i]); + } +} +int xtc_track_fx_add(xtc_track_fx *f,size_t track,size_t frame,float sample) +{ + if(!f || track>=XTC_PROJECT_MAX_TRACKS || !f->raw[track] || frame>=65536)return 0; + f->raw[track][frame]+=sample;return 1; +} +xtc_result xtc_track_fx_finish(xtc_track_fx *f,const xtc_mix_snapshot *mix,uint64_t position,float *left,float *right,size_t frames,float *monitor,size_t stride) +{ + size_t i,offset,j;if(!f)return XTC_OK; + for(i=0;icount;++i)if(f->raw[i])for(offset=0;offsetplans[i],position+offset); + if(r==XTC_OK)r=xtc_plan_process(f->plans[i],f->raw[i]+offset,f->scratch,n); + if(r!=XTC_OK)return r; + for(j=0;jscratch[j]); + if(monitor)xtc_mix_monitor(mix,monitor,stride,i,position+frame,frame,sample,right!=NULL); + left[frame]+=sample*xtc_mix_level(mix,i,position+frame,right ? 0U : 2U); + if(right)right[frame]+=sample*xtc_mix_level(mix,i,position+frame,1); + } + } + f->next=position+frames;return XTC_OK; +} diff --git a/src/mixer/track_fx.h b/src/mixer/track_fx.h new file mode 100644 index 0000000..331f6ff --- /dev/null +++ b/src/mixer/track_fx.h @@ -0,0 +1,12 @@ +#ifndef XTC_TRACK_FX_H +#define XTC_TRACK_FX_H +#include "mix_internal.h" +typedef struct xtc_track_fx xtc_track_fx; +/* Separate renderer owners; mixed audio+MIDI tracks with inserts reject. */ +xtc_result xtc_track_fx_create(const xtc_project *project,unsigned int rate,int midi,xtc_track_fx **out); +void xtc_track_fx_destroy(xtc_track_fx *fx); +void xtc_track_fx_begin(xtc_track_fx *fx,uint64_t position,size_t frames); +/* Returns nonzero when accumulated, otherwise caller uses legacy mix path. */ +int xtc_track_fx_add(xtc_track_fx *fx,size_t track,size_t frame,float sample); +xtc_result xtc_track_fx_finish(xtc_track_fx *fx,const xtc_mix_snapshot *mix,uint64_t position,float *left,float *right,size_t frames,float *monitor,size_t stride); +#endif diff --git a/tests/integration/test_reliability.c b/tests/integration/test_reliability.c index 3ff55b4..81d963a 100644 --- a/tests/integration/test_reliability.c +++ b/tests/integration/test_reliability.c @@ -294,8 +294,37 @@ static void sample_failures(void) } xtc_sample_destroy(source);xtc_project_destroy(p);assert(rmdir(root)==0); } +static void track_insert_failures(void) +{ + xtc_project *p=NULL;xtc_track_chain c={0};uint64_t t,clip;int mode,failure; + assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK); + 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=1;c.slots[0].wet=.5f;c.slots[0].enabled=1; + assert(xtc_project_set_chain(p,t,&c)==XTC_OK); + for(mode=0;mode<2;++mode) { + int succeeded=0; + if(mode)assert(xtc_project_add_midi_clip(p,t,0,960,&clip)==XTC_OK); + for(failure=0;failure<128;++failure) { + xtc_audio_plan *a=NULL;xtc_midi_plan *m=NULL;float out[512];xtc_result r; + fail_after=failure; + r=mode ? xtc_midi_plan_create(p,48000,&m) : xtc_audio_plan_create(p,48000,&a); + fail_after=-1; + if(r==XTC_OK) { + processing=1; + if(mode)assert(xtc_midi_plan_render(m,0,NULL,0,out,512)==XTC_OK); + else assert(xtc_audio_plan_render(a,0,out,512)==XTC_OK); + processing=0;succeeded=1; + }else assert(r==XTC_ERR_OUT_OF_MEMORY && !a && !m); + xtc_audio_plan_destroy(a);xtc_midi_plan_destroy(m); + if(succeeded)break; + } + assert(succeeded); + } + xtc_project_destroy(p); +} int main(void) { + track_insert_failures(); sample_failures(); composition_failures(); modern_edit_failures(); diff --git a/tests/unit/test_track_fx.c b/tests/unit/test_track_fx.c index 0317831..8b38820 100644 --- a/tests/unit/test_track_fx.c +++ b/tests/unit/test_track_fx.c @@ -1,14 +1,49 @@ #define _POSIX_C_SOURCE 200809L #include "xtc/xtc_plugin.h" #include "xtc/xtc_editing.h" +#include "xtc/xtc_audio.h" #include #include #include #include #include #include +static void rendering(void) +{ + xtc_project *p=NULL;xtc_audio_plan *audio=NULL;xtc_midi_plan *midi=NULL; + xtc_track_chain c={0};xtc_recorder *rec=NULL;uint64_t track,clip,copy,note; + char wave[]="/tmp/xtc-fx-audio-XXXXXX";float impulse[32]={1},out[32],right[32];int fd=mkstemp(wave);assert(fd>=0);close(fd);unlink(wave); + assert(xtc_recorder_create(wave,8000,32,&rec)==XTC_OK);xtc_recorder_push(rec,impulse,32);assert(xtc_recorder_finish(rec)==XTC_OK);xtc_recorder_destroy(rec); + assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Audio",&track)==XTC_OK); + assert(xtc_project_import_audio(p,track,wave,0,&clip)==XTC_OK); + assert(xtc_project_duplicate_clip(p,clip,track,0,©)==XTC_OK); + assert(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,2,8000,&c.slots[0].plugin)==XTC_OK); + c.count=1;c.output_gain=.5f;c.slots[0].enabled=1;c.slots[0].wet=1; + c.slots[0].plugin.values[0]=1;c.slots[0].plugin.values[1]=0;c.slots[0].plugin.values[2]=1; + assert(xtc_project_set_chain(p,track,&c)==XTC_OK && xtc_audio_plan_create(p,8000,&audio)==XTC_OK); + assert(xtc_audio_plan_set_fade_frames(audio,0)==XTC_OK && xtc_audio_plan_render_stereo(audio,0,out,right,32)==XTC_OK); + for(size_t j=0;j<32;++j)assert(fabsf(out[j]-(j==8 ? 1.f : 0.f))<.00001f && right[j]==out[j]); + /* Discontinuity resets internal delay, and block boundaries preserve history. */ + assert(xtc_audio_plan_render(audio,0,out,4)==XTC_OK && out[0]==0); + assert(xtc_audio_plan_render(audio,4,out,12)==XTC_OK && fabsf(out[4]-1)<.00001f); + xtc_audio_plan_destroy(audio);audio=NULL; + c.slots[0].enabled=0;assert(xtc_project_set_chain(p,track,&c)==XTC_OK && xtc_audio_plan_create(p,8000,&audio)==XTC_OK); + assert(xtc_audio_plan_set_fade_frames(audio,0)==XTC_OK && xtc_audio_plan_render(audio,0,out,32)==XTC_OK && out[0]==1 && out[8]==0); + xtc_audio_plan_destroy(audio);audio=NULL; + /* Reject split ownership of a nonlinear chain on a hybrid track. */ + assert(xtc_project_add_midi_clip(p,track,0,960,&clip)==XTC_OK); + assert(xtc_audio_plan_create(p,8000,&audio)==XTC_ERR_UNSUPPORTED && audio==NULL); + xtc_project_destroy(p);p=NULL; + assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"MIDI",&track)==XTC_OK && xtc_project_add_midi_clip(p,track,0,960,&clip)==XTC_OK); + assert(xtc_project_add_note(p,clip,0,960,69,127,0,¬e)==XTC_OK); + c.slots[0].enabled=1;assert(xtc_project_set_chain(p,track,&c)==XTC_OK && xtc_midi_plan_create(p,8000,&midi)==XTC_OK); + assert(xtc_midi_plan_set_fade_frames(midi,0)==XTC_OK && xtc_midi_plan_render(midi,0,NULL,0,out,32)==XTC_OK); + for(size_t j=0;j<32;++j)assert(fabsf(out[j]-(j<8 ? 0.f : .05f*sinf(6.28318530718f*440.f*(float)(j-8)/8000.f)))<.00001f); + xtc_midi_plan_destroy(midi);xtc_project_destroy(p);unlink(wave); +} int main(void) { + rendering(); 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);