feat(engine): render prepared per-track insert graphs (phase 82)

This commit is contained in:
Johannes Findeisen 2026-11-06 01:35:53 +01:00
commit 62cee7a7b0
10 changed files with 215 additions and 5 deletions

View file

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

View file

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

View file

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

View file

@ -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);

View file

@ -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 <math.h>
@ -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]);

View file

@ -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 <math.h>
#include <stdlib.h>
#include <string.h>
@ -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]);

101
src/mixer/track_fx.c Normal file
View file

@ -0,0 +1,101 @@
#include "track_fx.h"
#include "../project/project_internal.h"
#include "xtc/xtc_dsp.h"
#include <stdlib.h>
#include <string.h>
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 && i<c->count;++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;i<XTC_PROJECT_MAX_TRACKS;++i){xtc_plan_destroy(f->plans[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;i<p->track_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 && i<p->track_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;j<p->clip_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;i<XTC_PROJECT_MAX_TRACKS;++i)if(f->raw[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;i<mix->count;++i)if(f->raw[i])for(offset=0;offset<frames;offset+=256) {
size_t n=frames-offset<256 ? frames-offset : 256;xtc_result r=xtc_plan_position(f->plans[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;j<n;++j) {
size_t frame=offset+j;float sample=xtc_dsp_finite(f->scratch[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;
}

12
src/mixer/track_fx.h Normal file
View file

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

View file

@ -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();

View file

@ -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 <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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,&copy)==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,&note)==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);