From 7a01e2d2f3246b2e785a6bc555af825d0116eb58 Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 5 Nov 2026 23:53:03 +0100 Subject: [PATCH] feat: add tempo-synced stereo delay and coupled room reverb --- docs/BUILTIN_DEVICES.md | 31 ++++++ docs/ROADMAP.md | 18 ++-- examples/builtin_preview.c | 2 +- include/xtc/xtc_plugin.h | 3 + src/engine/offline_engine.c | 19 ++-- src/gui/main_window.cpp | 11 +- src/plugin/builtin.c | 153 +++++++++++++++++++-------- src/plugin/builtin.h | 1 + src/plugin/host.c | 7 ++ tests/integration/test_gui.cpp | 7 ++ tests/integration/test_reliability.c | 5 +- tests/unit/test_builtin.c | 47 +++++++- 12 files changed, 241 insertions(+), 63 deletions(-) diff --git a/docs/BUILTIN_DEVICES.md b/docs/BUILTIN_DEVICES.md index 80df35d..8589df1 100644 --- a/docs/BUILTIN_DEVICES.md +++ b/docs/BUILTIN_DEVICES.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f39a007..58abdff 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. --- diff --git a/examples/builtin_preview.c b/examples/builtin_preview.c index 4796cfc..e558652 100644 --- a/examples/builtin_preview.c +++ b/examples/builtin_preview.c @@ -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; } diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index ad55868..69980c9 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -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); diff --git a/src/engine/offline_engine.c b/src/engine/offline_engine.c index 35ccf86..bf7a6c1 100644 --- a/src/engine/offline_engine.c +++ b/src/engine/offline_engine.c @@ -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) { diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index 7f70fc5..48af1a8 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -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(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) { diff --git a/src/plugin/builtin.c b/src/plugin/builtin.c index 13b6306..a086a02 100644 --- a/src/plugin/builtin.c +++ b/src/plugin/builtin.c @@ -7,18 +7,23 @@ #include #include #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=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;icount;++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;icontrol_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;ipoint_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;ipoint_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;icontrol_count;++i)if(s->ports[i]!=i || !isfinite(s->values[i]) || + s->values[i]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;ipoint_count;++i) { + const xtc_plugin_point *q=s->points+i; + if(q->port>=s->control_count || !isfinite(q->value) || q->valuelow[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;icontrol_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;ispec.index<5 || !out || frames>p->max_frames || position>UINT64_MAX-frames || out==out_right)return XTC_ERR_INVALID_ARGUMENT; + for(i=0;ispec.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; diff --git a/src/plugin/builtin.h b/src/plugin/builtin.h index 2315822..7a2d86c 100644 --- a/src/plugin/builtin.h +++ b/src/plugin/builtin.h @@ -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 diff --git a/src/plugin/host.c b/src/plugin/host.c index 4400ec5..e246ab8 100644 --- a/src/plugin/host.c +++ b/src/plugin/host.c @@ -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; diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 05aa1f3..ff9aeef 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -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(); diff --git a/tests/integration/test_reliability.c b/tests/integration/test_reliability.c index b85c41a..71c3907 100644 --- a/tests/integration/test_reliability.c +++ b/tests/integration/test_reliability.c @@ -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); diff --git a/tests/unit/test_builtin.c b/tests/unit/test_builtin.c index f137d5d..28a261b 100644 --- a/tests/unit/test_builtin.c +++ b/tests/unit/test_builtin.c @@ -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 #include @@ -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; }