feat: add tempo-synced stereo delay and coupled room reverb
This commit is contained in:
parent
35bf6a00a1
commit
7a01e2d2f3
12 changed files with 241 additions and 63 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue