feat: add tempo-synced stereo delay and coupled room reverb

This commit is contained in:
Johannes Findeisen 2026-11-05 23:53:03 +01:00
commit 7a01e2d2f3
12 changed files with 241 additions and 63 deletions

View file

@ -121,3 +121,34 @@ license/provenance in SAMPLE_LICENSE.md. Both are installed with the examples.
Tests cover source-rate conversion, one-shot endpoint, looping, release,
partitioning, preset references, missing assets, GUI relinking/undo/reload and
preparation-allocation/callback guards.
## Phase 43 stereo effects
**File → Add stereo delay/reverb** and **Edit → Insert stereo delay/reverb on
wire** add new devices (indices 5/6). Original Delay/Reverb (indices 2/3) retain
their original algorithms, values and saved identities. Existing control and
automation editors, undo and persistence support both generations.
Stereo Delay adds integer **Sync**: 0 milliseconds, 1 quarter, 2 eighth,
3 dotted eighth, 4 eighth triplet. Nonzero sync follows project tempo; changing
tempo uses ordinary stopped project preparation. Time changes crossfade two
fractional read heads for 5 ms. A crossfade finishes before adopting the latest
automated target, bounding transition work and avoiding read-head jumps.
Feedback cross-couples channels (80% same, 20% opposite), with a 0.95 bound.
Delay storage covers four seconds, sufficient for all divisions at 20..300 BPM.
Stereo Reverb adds **Room** (0..1, scales comb spacing) and **Diffusion** (0..1,
cross-couples damped feedback). The right comb timings differ from the left.
Decay/damping retain 0..0.95 bounds. This is a simple algorithmic reverb, without
physical room simulation or a measured RT60. Room automation can alter coloration.
The graph evaluates both channels' inputs before calling a coupled internal
processor. Each stereo device has one prepared state, with no callback allocation.
Mono processing excites both sides and folds the wet output to mono. External
plugins and the original effects retain independent mono instances.
Tests compare whole/split blocks, tempo-synced impulse placement, left-to-right
energy, bounded extreme feedback/diffusion and graph/direct stereo output
(including the engine's subnormal-to-zero policy). GUI tests save/reopen both
identities; callback guards and ASan/UBSan cover processing. `make builtin-preview`
also writes files 5/6 for repeatable listening; human listening remains pending.

View file

@ -1217,15 +1217,19 @@ See BUILTIN_DEVICES.md for memory/format and transposition limits.
Address the documented sound-design and transition limits of the basic effects.
### Tasks and acceptance criteria
- [ ] Smooth or crossfade delay-time changes and define automation behavior without abrupt read-head jumps.
- [ ] Add tempo-synced delay divisions with deterministic tempo/seek behavior.
- [ ] Add true stereo processing and reverb diffusion/room controls rather than independent mono instances alone.
- [ ] Preserve old projects' effect identities, parameters and sound, or provide an explicit compatibility mode.
- [ ] Test stability at parameter extremes, stereo response, automation partitions and transition artifacts; document listening results.
- [x] Smooth or crossfade delay-time changes and define automation behavior without abrupt read-head jumps.
- [x] Add tempo-synced delay divisions with deterministic tempo/seek behavior.
- [x] Add true stereo processing and reverb diffusion/room controls rather than independent mono instances alone.
- [x] Preserve old projects' effect identities, parameters and sound, or provide an explicit compatibility mode.
- [x] Test stability at parameter extremes, stereo response, automation partitions and transition artifacts; document listening results.
### Implementation status
Planned. Delay time can click; reverb uses four damped mono combs without a room
model or stereo diffusion. Current decay is a feedback coefficient, not seconds.
Implemented as new Stereo Delay/Reverb identities, preserving original devices.
Delay uses fractional read-head crossfades and tempo divisions; reverb adds room
spacing and coupled stereo diffusion. Graph evaluation now processes coupled
channels together. Core, graph/direct DSP, partition, tempo, GUI persistence,
callback-guard and sanitizer tests pass. Listening previews/procedure are
documented; human listening is unverified. See BUILTIN_DEVICES.md.
---

View file

@ -35,6 +35,6 @@ int main(int argc,char **argv)
{
unsigned int id;
if(argc!=2) {fprintf(stderr,"Usage: %s OUTPUT_PREFIX\n",argv[0]);return EXIT_FAILURE;}
for(id=0;id<4;++id) if(preview(argv[1],id)!=XTC_OK)return EXIT_FAILURE;
for(id=0;id<7;++id) if(id!=4 && preview(argv[1],id)!=XTC_OK)return EXIT_FAILURE;
return EXIT_SUCCESS;
}

View file

@ -89,6 +89,9 @@ xtc_result xtc_project_set_instrument(xtc_project *project, uint64_t track, cons
xtc_result xtc_project_get_instrument(const xtc_project *project, uint64_t track, xtc_plugin_spec *out);
xtc_result xtc_project_set_live_instrument(xtc_project *project, uint64_t track);
xtc_result xtc_project_get_live_instrument(const xtc_project *project, uint64_t *track);
/* Coupled stereo internal effects (indices 5/6); channels may be in-place,
* output channels must be distinct. NULL right output renders a mono fold-down. */
xtc_result xtc_plugin_process_stereo_at(xtc_plugin *plugin,const float *left,const float *right,float *out,float *out_right,size_t frames,uint64_t position);
xtc_result xtc_plugin_process_at(xtc_plugin *plugin, const float *input, float *output, size_t frames, uint64_t position);
xtc_result xtc_plugin_process(xtc_plugin *plugin, const float *input, float *output, size_t frames);
xtc_result xtc_graph_add_plugin(xtc_graph *graph, const xtc_plugin_spec *spec, size_t *id);

View file

@ -126,6 +126,7 @@ xtc_result xtc_graph_compile(const xtc_graph *graph, double sample_rate, size_t
if (plan->nodes[node].type == XTC_NODE_PLUGIN) {
size_t side;
for (side = 0; side < 2; ++side) {
if(side && plan->nodes[node].plugin.format==XTC_PLUGIN_BUILTIN && plan->nodes[node].plugin.index>=5)continue;
result = xtc_plugin_create(&plan->nodes[node].plugin,sample_rate,max_frames,
&plan->plugins[side * plan->node_count + node]);
if (result != XTC_OK) { xtc_plan_destroy(plan); return result; }
@ -221,11 +222,11 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
return XTC_OK;
/* All external input reads finish before output is written, allowing aliasing.
* Nonfinite input/arithmetic becomes silence per sample, never persistent DSP state. */
for (channel = 0; channel < (right ? 2U : 1U); ++channel) {
const float *audio_source = channel ? timeline_right : timeline;
const float *midi_source = channel ? instrument_right : instrument;
for (index = 0U; index < plan->node_count; ++index) {
size_t node = plan->order[index];
for (index = 0U; index < plan->node_count; ++index) {
size_t node = plan->order[index];
for (channel = 0; channel < (right ? 2U : 1U); ++channel) {
const float *audio_source = channel ? timeline_right : timeline;
const float *midi_source = channel ? instrument_right : instrument;
const xtc_node *spec = &plan->nodes[node];
float *buffer = plan->buffers + (channel * plan->node_count + node) * plan->max_frames;
float peak = 0.0f;
@ -270,7 +271,7 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
return result;
}
}
if (spec->type == XTC_NODE_PLUGIN) {
if (spec->type == XTC_NODE_PLUGIN && !(spec->plugin.format==XTC_PLUGIN_BUILTIN && spec->plugin.index>=5)) {
xtc_result result = xtc_plugin_process_at(plan->plugins[channel * plan->node_count + node],
spec->plugin.inputs ? buffer : NULL,buffer,frame_count,plan->position);
if (result != XTC_OK) {
@ -284,6 +285,12 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
if (spec->type == XTC_NODE_METER)
plan->peaks[node] = channel && plan->peaks[node] > peak ? plan->peaks[node] : peak;
}
if(plan->nodes[node].type==XTC_NODE_PLUGIN && plan->nodes[node].plugin.format==XTC_PLUGIN_BUILTIN && plan->nodes[node].plugin.index>=5) {
float *l=plan->buffers+node*plan->max_frames;
float *r=right ? plan->buffers+(plan->node_count+node)*plan->max_frames : NULL;
xtc_result result=xtc_plugin_process_stereo_at(plan->plugins[node],l,r ? r : l,l,r,frame_count,plan->position);
if(result!=XTC_OK) {memset(output,0,frame_count*sizeof(*output));if(right)memset(right,0,frame_count*sizeof(*right));return result;}
}
}
/* Finish both channels before writing either caller buffer. */
for (channel = 0; channel < (right ? 2U : 1U); ++channel) {

View file

@ -135,6 +135,10 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
menu->add("File/Load instrument preset",0,instrument_callback,this);
menu->add("Edit/Insert internal delay on wire", 0, plugin_callback, this);
menu->add("Edit/Insert internal reverb on wire", 0, plugin_callback, this);
menu->add("Edit/Insert stereo delay on wire",0,plugin_callback,this);
menu->add("Edit/Insert stereo reverb on wire",0,plugin_callback,this);
menu->add("File/Add stereo delay",0,plugin_callback,this);
menu->add("File/Add stereo reverb",0,plugin_callback,this);
menu->add("File/Add internal delay", 0, plugin_callback, this);
menu->add("File/Add internal reverb", 0, plugin_callback, this);
menu->add("File/Load plugin", 0, plugin_callback, this);
@ -2106,7 +2110,12 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
const char *label = menu->mvalue()->label();
xtc_result r;
if (std::strcmp(label,"Insert internal delay on wire")==0 || std::strcmp(label,"Insert internal reverb on wire")==0) {
if(std::strcmp(label,"Insert stereo delay on wire")==0 || std::strcmp(label,"Insert stereo reverb on wire")==0) {
xtc_connection_info wire{};
r=w->graph_widget_->selected_connection(wire) ? w->insert_plugin(wire,XTC_BUILTIN_PATH,std::strcmp(label,"Insert stereo delay on wire")==0 ? 5U : 6U) : XTC_ERR_INVALID_STATE;
} else if(std::strcmp(label,"Add stereo delay")==0 || std::strcmp(label,"Add stereo reverb")==0) {
r=w->add_plugin(XTC_BUILTIN_PATH,std::strcmp(label,"Add stereo delay")==0 ? 5U : 6U);
} else if (std::strcmp(label,"Insert internal delay on wire")==0 || std::strcmp(label,"Insert internal reverb on wire")==0) {
xtc_connection_info wire{};
r=w->graph_widget_->selected_connection(wire) ? w->insert_plugin(wire,XTC_BUILTIN_PATH,std::strcmp(label,"Insert internal delay on wire")==0 ? 2U : 3U) : XTC_ERR_INVALID_STATE;
} else if (std::strcmp(label,"Add internal delay") == 0 || std::strcmp(label,"Add internal reverb") == 0) {

View file

@ -7,18 +7,23 @@
#include <string.h>
#include <stdio.h>
#define TAU 6.2831853071795864769
static const char *names[] = {"Soft Keys", "Sub Bass", "Delay", "Reverb", "Sample Player"};
static const char *controls[2][3] = {{"Time", "Feedback", "Mix"}, {"Decay", "Damping", "Mix"}};
static const float defaults[2][3] = {{350, .35f, .25f}, {.7f, .45f, .25f}};
static const float lows[2][3] = {{1,0,0},{0,0,0}};
static const float highs[2][3] = {{2000,.95f,1},{.95f,.95f,1}};
static const char *sample_names[]={"Root key", "Loop", "Attack", "Release", "Gain"};
static const float sample_low[]={0,0,.1f,1,0},sample_high[]={127,1,2000,5000,4},sample_defaults[]={60,0,8,80,1};
static const char *synth_names[]={"Harmonic", "Attack", "Release", "Cutoff", "Bend range"};
static const float synth_low[]={0, .1f, 1, 20, 0};
static const float synth_high[]={1, 2000, 5000, 24000, 24};
typedef struct {
const char *name;unsigned int instrument,count;
const char *controls[5];float low[5],high[5],defaults[5];
} device;
static const device devices[]={
{"Soft Keys",1,5,{"Harmonic","Attack","Release","Cutoff","Bend range"},{0,.1f,1,20,0},{1,2000,5000,24000,24},{.3f,8,80,24000,2}},
{"Sub Bass",1,5,{"Harmonic","Attack","Release","Cutoff","Bend range"},{0,.1f,1,20,0},{1,2000,5000,24000,24},{0,8,80,24000,2}},
{"Delay",0,3,{"Time","Feedback","Mix"},{1,0,0},{2000,.95f,1},{350,.35f,.25f}},
{"Reverb",0,3,{"Decay","Damping","Mix"},{0,0,0},{.95f,.95f,1},{.7f,.45f,.25f}},
{"Sample Player",1,5,{"Root key","Loop","Attack","Release","Gain"},{0,0,.1f,1,0},{127,1,2000,5000,4},{60,0,8,80,1}},
{"Stereo Delay",0,4,{"Time","Feedback","Mix","Sync"},{1,0,0,0},{2000,.95f,1,4},{350,.35f,.25f,0}},
{"Stereo Reverb",0,5,{"Decay","Damping","Mix","Room","Diffusion"},{0,0,0,0,0},{.95f,.95f,1,1,1},{.7f,.45f,.25f,.5f,.7f}}
};
static int integer_port(unsigned int id,unsigned int port)
{return (id==4 && port<2) || (id==5 && port==3);}
static float synth_default(unsigned int id,unsigned int port)
{ static const float d[]={.3f,8,80,24000,2};return port==0 && id==1 ? 0 : d[port]; }
{return devices[id].defaults[port];}
typedef struct { double phase, step; float level, velocity, filter; unsigned int note, channel; int active, released, held; } voice;
struct xtc_builtin {
xtc_plugin_spec spec;
@ -33,48 +38,54 @@ struct xtc_builtin {
float bend[16];
xtc_wave sample;
size_t sample_issue;
double delay_frames,delay_target;
size_t delay_remaining,delay_transition;
int delay_started;
float previous[2][4];
};
xtc_result xtc_builtin_probe(unsigned int index, xtc_plugin_spec *out)
xtc_result xtc_builtin_probe(unsigned int index,xtc_plugin_spec *out)
{
unsigned int i; xtc_plugin_spec s = {0};
if (!out) return XTC_ERR_INVALID_ARGUMENT;
if (index >= 5) return XTC_ERR_NOT_FOUND;
strcpy(s.path,XTC_BUILTIN_PATH); strcpy(s.label,names[index]);
s.format = XTC_PLUGIN_BUILTIN; s.index = index; s.unique_id = 0x58544300U+index;
s.instrument = index < 2 || index==4; s.inputs = s.instrument ? 0U : 1U; s.tempo = 120000;
s.control_count = s.instrument ? 5U : 3U;
for (i=0;i<s.control_count;++i) { s.ports[i]=i; s.values[i]=index==4 ? sample_defaults[i] : index<2 ? synth_default(index,i) : defaults[index-2][i]; }
*out=s; return XTC_OK;
xtc_plugin_spec s={0};unsigned int i;const device *d;
if(!out)return XTC_ERR_INVALID_ARGUMENT;
if(index>=sizeof(devices)/sizeof(devices[0]))return XTC_ERR_NOT_FOUND;
d=devices+index;strcpy(s.path,XTC_BUILTIN_PATH);strcpy(s.label,d->name);
s.format=XTC_PLUGIN_BUILTIN;s.index=index;s.unique_id=0x58544300U+index;
s.instrument=d->instrument;s.inputs=d->instrument ? 0U : 1U;s.tempo=120000;s.control_count=d->count;
for(i=0;i<d->count;++i){s.ports[i]=i;s.values[i]=d->defaults[i];}
*out=s;return XTC_OK;
}
xtc_result xtc_builtin_validate(const xtc_plugin_spec *s)
{
xtc_plugin_spec a; unsigned int i;
if (!s || xtc_builtin_probe(s->index,&a)!=XTC_OK || strcmp(s->path,a.path) || strcmp(s->label,a.label) ||
s->identifier[0] || s->unique_id!=a.unique_id || s->instrument!=a.instrument || s->inputs!=a.inputs ||
(s->control_count!=a.control_count && !(s->index<2 && !s->control_count))) return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<s->control_count;++i) if(s->ports[i]!=i || !isfinite(s->values[i]) ||
s->values[i]<(s->index==4 ? sample_low[i] : s->index<2 ? synth_low[i] : lows[s->index-2][i]) || s->values[i]>(s->index==4 ? sample_high[i] : s->index<2 ? synth_high[i] : highs[s->index-2][i])) return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<s->point_count;++i) if(s->points[i].port>=s->control_count || !isfinite(s->points[i].value) ||
s->points[i].value<(s->index==4 ? sample_low[s->points[i].port] : s->index<2 ? synth_low[s->points[i].port] : lows[s->index-2][s->points[i].port]) ||
s->points[i].value>(s->index==4 ? sample_high[s->points[i].port] : s->index<2 ? synth_high[s->points[i].port] : highs[s->index-2][s->points[i].port])) return XTC_ERR_INVALID_ARGUMENT;
if(s->index==4) {if(s->values[0]!=floorf(s->values[0]) || s->values[1]!=floorf(s->values[1]))return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<s->point_count;++i)if(s->points[i].port<2 && (!s->points[i].step || s->points[i].value!=floorf(s->points[i].value)))return XTC_ERR_INVALID_ARGUMENT;}
xtc_plugin_spec actual;const device *d;unsigned int i;
if(!s || xtc_builtin_probe(s->index,&actual)!=XTC_OK)return XTC_ERR_INVALID_ARGUMENT;
d=devices+s->index;
if(strcmp(s->path,actual.path) || strcmp(s->label,actual.label) || s->identifier[0] ||
s->unique_id!=actual.unique_id || s->instrument!=actual.instrument || s->inputs!=actual.inputs ||
(s->control_count!=d->count && !(s->index<2 && !s->control_count)))return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<s->control_count;++i)if(s->ports[i]!=i || !isfinite(s->values[i]) ||
s->values[i]<d->low[i] || s->values[i]>d->high[i] ||
(integer_port(s->index,i) && s->values[i]!=floorf(s->values[i])))return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<s->point_count;++i) {
const xtc_plugin_point *q=s->points+i;
if(q->port>=s->control_count || !isfinite(q->value) || q->value<d->low[q->port] || q->value>d->high[q->port] ||
(integer_port(s->index,q->port) && (!q->step || q->value!=floorf(q->value))))return XTC_ERR_INVALID_ARGUMENT;
}
return XTC_OK;
}
xtc_result xtc_builtin_describe(const xtc_plugin_spec *s, xtc_plugin_details *out)
xtc_result xtc_builtin_describe(const xtc_plugin_spec *s,xtc_plugin_details *out)
{
unsigned int i;
if (!out || xtc_plugin_spec_validate(s)!=XTC_OK) return XTC_ERR_INVALID_ARGUMENT;
memset(out,0,sizeof(*out)); snprintf(out->name,sizeof(out->name),"%s",s->label);
strcpy(out->maker,"xtc"); strcpy(out->copyright,"See xtc LICENSE"); out->hard_realtime=1;
unsigned int i;const device *d;
if(!out || xtc_plugin_spec_validate(s)!=XTC_OK)return XTC_ERR_INVALID_ARGUMENT;
d=devices+s->index;memset(out,0,sizeof(*out));strcpy(out->name,d->name);strcpy(out->maker,"xtc");
strcpy(out->copyright,"See xtc LICENSE");out->hard_realtime=1;
for(i=0;i<s->control_count;++i) {
xtc_parameter_info *p=out->parameters+i; p->id=i;
strcpy(p->name,s->index==4 ? sample_names[i] : s->index<2 ? synth_names[i] : controls[s->index-2][i]);
if(s->index<2) {strcpy(p->unit,i==1 || i==2 ? "ms" : i==3 ? "Hz" : i==4 ? "st" : "");} if(s->index==2 && i==0) strcpy(p->unit,"ms");
p->minimum=s->index==4 ? sample_low[i] : s->index<2 ? synth_low[i] : lows[s->index-2][i]; p->maximum=s->index==4 ? sample_high[i] : s->index<2 ? synth_high[i] : highs[s->index-2][i]; p->default_value=s->index==4 ? sample_defaults[i] : s->index<2 ? synth_default(s->index,i) : defaults[s->index-2][i];
xtc_parameter_info *p=out->parameters+i;p->id=i;strcpy(p->name,d->controls[i]);
p->minimum=d->low[i];p->maximum=d->high[i];p->default_value=d->defaults[i];
if((s->index<2 && (i==1 || i==2)) || (s->index==4 && (i==2 || i==3)) || ((s->index==2 || s->index==5) && i==0))strcpy(p->unit,"ms");
if(s->index<2 && i==3)strcpy(p->unit,"Hz");
if(s->index<2 && i==4)strcpy(p->unit,"st");
out->flags[i]=XTC_PLUGIN_BOUNDED_BELOW|XTC_PLUGIN_BOUNDED_ABOVE;
if(s->index==4 && i<2)out->flags[i]|=XTC_PLUGIN_INTEGER;
if(s->index==4 && (i==2 || i==3))strcpy(p->unit,"ms");
if(integer_port(s->index,i))out->flags[i]|=XTC_PLUGIN_INTEGER;
}
return XTC_OK;
}
@ -83,8 +94,8 @@ xtc_result xtc_builtin_create(const xtc_plugin_spec *s, double rate, size_t fram
xtc_builtin *p; unsigned int i;
if (!out || xtc_plugin_spec_validate(s)!=XTC_OK || !isfinite(rate) || rate<1 || rate>768000 || !frames || frames>65536) return XTC_ERR_INVALID_ARGUMENT;
*out=NULL; p=calloc(1,sizeof(*p)); if(!p) return XTC_ERR_OUT_OF_MEMORY;
p->spec=*s; p->rate=rate; p->attack=(float)(1-exp(-1/(.008*rate))); p->release=(float)exp(-1/(.08*rate)); p->max_frames=frames; p->length=(size_t)ceil(rate*2)+2;
if(!s->instrument) { p->line=calloc(p->length*4,sizeof(float)); if(!p->line) {free(p);return XTC_ERR_OUT_OF_MEMORY;} }
p->spec=*s; p->rate=rate; p->attack=(float)(1-exp(-1/(.008*rate))); p->release=(float)exp(-1/(.08*rate)); p->max_frames=frames;p->delay_transition=(size_t)ceil(rate*.005); p->length=(size_t)ceil(rate*(s->index>=5 ? 4 : 2))+2;
if(!s->instrument) { p->line=calloc(p->length*(s->index>=5 ? 8U : 4U),sizeof(float)); if(!p->line) {free(p);return XTC_ERR_OUT_OF_MEMORY;} }
for(i=0;i<128;++i) p->frequencies[i]=440*pow(2,((double)i-69)/12)/rate;
for(i=0;i<4;++i) { static const double times[]={.0297,.0371,.0411,.0437}; p->comb[i]=(size_t)(rate*times[i])+1; }
if(s->index==4) {
@ -114,6 +125,7 @@ xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size
{
size_t i; unsigned int j;
if(!p || !out || frames>p->max_frames || p->spec.instrument || position>UINT64_MAX-frames) return XTC_ERR_INVALID_ARGUMENT;
if(p->spec.index>=5)return xtc_builtin_stereo(p,in,in,out,NULL,frames,position);
for(i=0;i<frames;++i) {
float x=in && isfinite(in[i]) ? in[i] : 0, wet=0;
float a=parameter(p,0,position+i), b=parameter(p,1,position+i), mix=parameter(p,2,position+i);
@ -133,6 +145,57 @@ xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size
}
return XTC_OK;
}
/* Both channels are consumed before writing either output, including in-place use. */
xtc_result xtc_builtin_stereo(xtc_builtin *p,const float *left,const float *right,float *out,float *out_right,size_t frames,uint64_t position)
{
size_t i;unsigned int side,j;
if(!p || p->spec.index<5 || !out || frames>p->max_frames || position>UINT64_MAX-frames || out==out_right)return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<frames;++i) {
float input[2]={left && isfinite(left[i]) ? left[i] : 0,right && isfinite(right[i]) ? right[i] : 0};
float wet[2]={0,0};float a=parameter(p,0,position+i),b=parameter(p,1,position+i),mix=parameter(p,2,position+i);
if(p->spec.index==5) {
unsigned int sync=(unsigned int)parameter(p,3,position+i);
static const double beats[]={0,1,.5,.75,1.0/3};
double target=sync ? beats[sync]*60000.0*p->rate/p->spec.tempo : a*p->rate/1000;
if(target<1)target=1;
if(target>(double)p->length-2)target=(double)p->length-2;
if(!p->delay_started){p->delay_frames=target;p->delay_started=1;}
/* Finish each 5 ms crossfade before accepting another target. */
if(!p->delay_remaining && fabs(target-p->delay_frames)>1e-9) {
p->delay_target=target;p->delay_remaining=p->delay_transition;
}
for(side=0;side<2;++side) {
float *line=p->line+side*p->length;unsigned int head;
float values[2];double delays[2]={p->delay_frames,p->delay_remaining ? p->delay_target : p->delay_frames};
for(head=0;head<2;++head) {
size_t delay=(size_t)delays[head];
size_t n=(p->cursor+p->length-delay)%p->length,m=(n+p->length-1)%p->length;
values[head]=line[n]+(line[m]-line[n])*(float)(delays[head]-(double)delay);
}
{float blend=p->delay_remaining ? (float)(p->delay_transition-p->delay_remaining+1)/(float)p->delay_transition : 0;
wet[side]=values[0]+(values[1]-values[0])*blend;}
}
if(p->delay_remaining && !--p->delay_remaining)p->delay_frames=p->delay_target;
for(side=0;side<2;++side)p->line[side*p->length+p->cursor]=input[side]+b*(.8f*wet[side]+.2f*wet[1-side]);
} else {
float room=parameter(p,3,position+i),diffusion=parameter(p,4,position+i);
float next[2][4];
for(side=0;side<2;++side)for(j=0;j<4;++j) {
float *line=p->line+(side*4+j)*p->length;
size_t delay=(size_t)((double)p->comb[j]*(.5+room*1.5)*(side ? 1.071 : 1));
float y=line[(p->cursor+p->length-delay)%p->length];
next[side][j]=(1-b)*y+b*p->previous[side][j];
line[p->cursor]=input[side]+a*((1-.5f*diffusion)*next[side][j]+.5f*diffusion*p->previous[1-side][j]);
wet[side]+=y*.25f;
}
memcpy(p->previous,next,sizeof(next));
}
if(out_right) {out[i]=(1-mix)*input[0]+mix*wet[0];out_right[i]=(1-mix)*input[1]+mix*wet[1];}
else out[i]=(1-mix)*input[0]+mix*(wet[0]+wet[1])*.5f;
p->cursor=(p->cursor+1)%p->length;
}
return XTC_OK;
}
static float sample_voice(xtc_builtin *p,voice *v,uint64_t position)
{
size_t a,b;float value;double advance;

View file

@ -10,5 +10,6 @@ size_t xtc_builtin_sample_frames(const xtc_builtin *p);
size_t xtc_builtin_asset_issues(const xtc_builtin *p);
void xtc_builtin_destroy(xtc_builtin *p);
xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size_t frames, uint64_t position);
xtc_result xtc_builtin_stereo(xtc_builtin *p,const float *left,const float *right,float *out,float *out_right,size_t frames,uint64_t position);
xtc_result xtc_builtin_midi(xtc_builtin *p, const xtc_midi_event *events, size_t count, float *out, size_t frames, uint64_t position);
#endif

View file

@ -75,6 +75,13 @@ xtc_result xtc_plugin_create(const xtc_plugin_spec *s, double rate, size_t frame
if (r != XTC_OK) { xtc_plugin_destroy(p); return r; }
*out = p; return XTC_OK;
}
xtc_result xtc_plugin_process_stereo_at(xtc_plugin *p,const float *left,const float *right,float *out,float *out_right,size_t frames,uint64_t position)
{
xtc_result r;if(!p || !p->builtin)return XTC_ERR_UNSUPPORTED;
r=xtc_builtin_stereo(p->builtin,left,right,out,out_right,frames,position);
if(r==XTC_OK)p->next=position+frames;
return r;
}
xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *in, float *out, size_t frames, uint64_t position)
{
xtc_result r;

View file

@ -758,6 +758,13 @@ int main(int argc, char **argv)
CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK);unlink("/tmp/xtc-sampler-gui.xtc");
size_t added=SIZE_MAX;CHECK(window.add_plugin(XTC_BUILTIN_PATH,effect,&added)==XTC_OK);
CHECK(added!=id);CHECK(window.undo()==XTC_OK);
unsigned int stereo=instrument+5;
CHECK(window.add_plugin(XTC_BUILTIN_PATH,stereo,&added)==XTC_OK);
CHECK(window.set_plugin_control(added,stereo==5 ? 3U : 4U,1)==XTC_OK);
CHECK(window.save_project("/tmp/xtc-stereo-gui.xtc")==XTC_OK);
CHECK(window.new_project()==XTC_OK && window.open_project("/tmp/xtc-stereo-gui.xtc")==XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),added,&spec)==XTC_OK && spec.index==stereo);
unlink("/tmp/xtc-stereo-gui.xtc");
const xtc_project *before=sequence->project();reserve_result=XTC_ERR_OUT_OF_MEMORY;
CHECK(window.add_plugin(XTC_BUILTIN_PATH,effect)==XTC_ERR_OUT_OF_MEMORY && sequence->project()==before);
reserve_result=XTC_OK;unlink("/tmp/xtc-internal-gui.xtc");window.do_callback();

View file

@ -157,7 +157,7 @@ static void history_reservations(void)
static void builtin_failures(void)
{
unsigned int id;
for(id=0;id<5;++id) {
for(id=0;id<7;++id) {
int failure, succeeded=0; xtc_plugin_spec spec;
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,id,48000,&spec)==XTC_OK);
if(id==4) {assert(getcwd(spec.sample_path,sizeof(spec.sample_path)));strcat(spec.sample_path,"/examples/projects/original-pluck.wav");}
@ -168,7 +168,8 @@ static void builtin_failures(void)
processing=1;
if(spec.instrument) {xtc_midi_event event={0,XTC_MIDI_NOTE_ON,0,60,100};
assert(xtc_plugin_process_midi(plugin,&event,1,out,256)==XTC_OK);
} else assert(xtc_plugin_process(plugin,in,out,256)==XTC_OK);
} else if(id>=5) {float right[256];assert(xtc_plugin_process_stereo_at(plugin,in,in,out,right,256,0)==XTC_OK);}
else assert(xtc_plugin_process(plugin,in,out,256)==XTC_OK);
processing=0;xtc_plugin_destroy(plugin);succeeded=1;break;
}
assert(succeeded);

View file

@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_dsp.h"
#include "../../src/audio/wave_internal.h"
#include <unistd.h>
#include <stdlib.h>
@ -221,7 +222,51 @@ static void sampler(void)
for(i=0;i<256;++i)assert(x[i]==0);
xtc_plugin_destroy(a);unlink(preset);
}
static void stereo_effect(unsigned int id)
{
xtc_plugin_spec s;xtc_plugin *a=NULL,*b=NULL;float in[256]={0},zero[256]={0},l[256],r[256],x[256],y[256];
size_t i,k;double left_energy=0,right_energy=0;
assert(xtc_plugin_probe(XTC_BUILTIN_PATH,id,48000,&s)==XTC_OK);
s.values[2]=1;
s.point_count=2;s.points[0]=(xtc_plugin_point){0,0,1,id==5 ? 10 : .5f};
s.points[1]=(xtc_plugin_point){960,0,1,id==5 ? 40 : .95f};
if(id==5)s.values[1]=.9f;else {s.values[3]=1;s.values[4]=1;}
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK && xtc_plugin_create(&s,48000,256,&b)==XTC_OK);
for(k=0;k<1200;++k) {
in[0]=k==0 ? 1 : 0;
assert(xtc_plugin_process_stereo_at(a,in,zero,l,r,256,k*256)==XTC_OK);
assert(xtc_plugin_process_stereo_at(b,in,zero,x,y,128,k*256)==XTC_OK);
assert(xtc_plugin_process_stereo_at(b,in+128,zero+128,x+128,y+128,128,k*256+128)==XTC_OK);
assert(!memcmp(l,x,sizeof(l)) && !memcmp(r,y,sizeof(r)));
for(i=0;i<256;++i){assert(isfinite(l[i]) && isfinite(r[i]) && fabsf(l[i])<=1 && fabsf(r[i])<=1);left_energy+=fabs(l[i]);right_energy+=fabs(r[i]);}
}
assert(left_energy>0 && right_energy>0);xtc_plugin_destroy(a);xtc_plugin_destroy(b);
{
xtc_graph *graph=NULL;xtc_plan *plan=NULL;size_t source,node,output;
assert(xtc_graph_create(3,&graph)==XTC_OK);
assert(xtc_graph_add_audio(graph,&source)==XTC_OK && xtc_graph_add_plugin(graph,&s,&node)==XTC_OK && xtc_graph_add_output(graph,&output)==XTC_OK);
assert(xtc_graph_connect(graph,source,node)==XTC_OK && xtc_graph_connect(graph,node,output)==XTC_OK);
assert(xtc_graph_compile(graph,48000,256,&plan)==XTC_OK);
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK);
for(k=0;k<200;++k) {
in[0]=k==0 ? 1 : 0;
assert(xtc_plan_process_stereo(plan,NULL,in,zero,NULL,NULL,l,r,256)==XTC_OK);
assert(xtc_plugin_process_stereo_at(a,in,zero,x,y,256,k*256)==XTC_OK);
for(i=0;i<256;++i)assert(l[i]==xtc_dsp_finite(x[i]) && r[i]==xtc_dsp_finite(y[i]));
}
xtc_plugin_destroy(a);xtc_plan_destroy(plan);xtc_graph_destroy(graph);
}
if(id==5) {
s.point_count=0;s.values[3]=1;s.values[1]=0;s.tempo=120000;
assert(xtc_plugin_create(&s,48000,256,&a)==XTC_OK);
for(k=0;k<100;++k) {
in[0]=k==0 ? 1 : 0;
assert(xtc_plugin_process_stereo_at(a,in,zero,l,r,256,k*256)==XTC_OK);
for(i=0;i<256;++i)assert(l[i]==(k*256+i==24000 ? 1 : 0));
}xtc_plugin_destroy(a);
}
}
int main(void) {
sampler();editable();transport(0);transport(1);automation();roundtrip(0,2);roundtrip(1,3);effects(2);effects(3);synth(0);synth(1);
stereo_effect(5);stereo_effect(6);sampler();editable();transport(0);transport(1);automation();roundtrip(0,2);roundtrip(1,3);effects(2);effects(3);synth(0);synth(1);
synth_partition(0);synth_partition(1);puts("Built-in devices passed.");return 0;
}