Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
fr_public/v2/synth_core.cpp
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
3352 lines (2817 sloc)
81.6 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "types.h" | |
| #include "synth.h" | |
| #include <math.h> | |
| #include <assert.h> | |
| #include <string.h> | |
| #include <stdlib.h> | |
| // TODO: | |
| // - VU meters? | |
| // Ye olde original V2 bugs you can turn on and off :) | |
| #define BUG_V2_FM_RANGE 1 // Broken sine range reduction for FM oscis | |
| // Debugging tools | |
| #define DEBUGSCOPES 0 | |
| #define COVERAGE 0 | |
| // -------------------------------------------------------------------------- | |
| // Debug scopes | |
| // -------------------------------------------------------------------------- | |
| #if DEBUGSCOPES | |
| #include "scope.h" | |
| #define DEBUG_PLOT_OPEN(which, title, rate, w, h) scopeOpen((which), (title), (rate), (w), (h)) | |
| #define DEBUG_PLOT_VAL(which, value) do { float t=value; scopeSubmit((which), &t, 1); } while(0) | |
| #define DEBUG_PLOT(which, data, nsamples) scopeSubmit((which), (data), (nsamples)) | |
| #define DEBUG_PLOT_STRIDED(which, data, stride, nsamples) scopeSubmitStrided((which), (data), (stride), (nsamples)) | |
| #define DEBUG_PLOT_UPDATE() scopeUpdateAll() | |
| #else | |
| #define DEBUG_PLOT_OPEN(which, title, rate, w, h) | |
| #define DEBUG_PLOT_VAL(which, value) | |
| #define DEBUG_PLOT(which, data, nsamples) | |
| #define DEBUG_PLOT_STRIDED(which, data, stride, nsamples) | |
| #define DEBUG_PLOT_UPDATE() | |
| #endif | |
| #define DEBUG_PLOT_CHAN(which, ch) ((unsigned char *)(which)+(ch)) | |
| #define DEBUG_PLOT_STEREO(which, data, nsamples) \ | |
| DEBUG_PLOT_STRIDED(DEBUG_PLOT_CHAN(which, 0), &(data)->l, 2, (nsamples)); \ | |
| DEBUG_PLOT_STRIDED(DEBUG_PLOT_CHAN(which, 1), &(data)->r, 2, (nsamples)) | |
| // -------------------------------------------------------------------------- | |
| // Code coverage | |
| // -------------------------------------------------------------------------- | |
| #if COVERAGE | |
| #include <stdio.h> | |
| static const char *code_coverage[500]; | |
| #define COVER(desc) code_coverage[__COUNTER__] = (desc) | |
| static sInt synthGetNumCoverage(); | |
| #else | |
| #define COVER(desc) | |
| #endif | |
| // -------------------------------------------------------------------------- | |
| // Constants. | |
| // -------------------------------------------------------------------------- | |
| // Natural constants | |
| static const sF32 fclowest = 1.220703125e-4f; // 2^(-13) - clamp EGs to 0 below this (their nominal range is 0..128) | |
| static const sF32 fcpi_2 = 1.5707963267948966192313216916398f; | |
| static const sF32 fcpi = 3.1415926535897932384626433832795f; | |
| static const sF32 fc1p5pi = 4.7123889803846898576939650749193f; | |
| static const sF32 fc2pi = 6.28318530717958647692528676655901f; | |
| static const sF32 fc32bit = 2147483648.0f; // 2^31 (original code has (2^31)-1, but this ends up rounding up to 2^31 anyway) | |
| // Synth constants | |
| static const sF32 fcoscbase = 261.6255653f; // Oscillator base freq | |
| static const sF32 fcsrbase = 44100.0f; // Base sampling rate | |
| static const sF32 fcboostfreq = 150.0f; // Bass boost cut-off freq | |
| static const sF32 fcframebase = 128.0f; // size of a frame in samples | |
| static const sF32 fcdcflt = 126.0f; | |
| static const sF32 fccfframe = 11.0f; | |
| static const sF32 fcfmmax = 2.0f; | |
| static const sF32 fcattackmul = -0.09375f; // -0.0859375 | |
| static const sF32 fcattackadd = 7.0f; | |
| static const sF32 fcsusmul = 0.0019375f; | |
| static const sF32 fcgain = 0.6f; | |
| static const sF32 fcgainh = 0.6f; | |
| static const sF32 fcmdlfomul = 1973915.49f; | |
| static const sF32 fccpdfalloff = 0.9998f; // @@@BUG this should probably depend on sampling rate. | |
| static const sF32 fcdcoffset = 3.814697265625e-6f; // 2^-18 | |
| // -------------------------------------------------------------------------- | |
| // General helper functions. | |
| // -------------------------------------------------------------------------- | |
| #define COUNTOF(x) (sizeof(x)/sizeof(*(x))) | |
| // Float bitcasts. Union-based type punning to maximize compiler | |
| // compatibility. | |
| union FloatBits | |
| { | |
| sF32 f; | |
| sU32 u; | |
| }; | |
| static sF32 bits2float(sU32 u) | |
| { | |
| FloatBits x; | |
| x.u = u; | |
| return x.f; | |
| } | |
| // Fast arctangent | |
| static sF32 fastatan(sF32 x) | |
| { | |
| // extract sign | |
| sF32 sign = 1.0f; | |
| if (x < 0.0f) | |
| { | |
| sign = -1.0f; | |
| x = -x; | |
| } | |
| // we have two rational approximations: one for |x| < 1.0 and one for | |
| // |x| >= 1.0, both of the general form | |
| // r(x) = (cx1*x + cx3*x^3) / (cxm0 + cxm2*x^2 + cxm4*x^4) + bias | |
| // original V2 code uses doubles here but frankly the coefficients | |
| // just aren't accurate enough to warrant it :) | |
| static const sF32 coeffs[2][6] = { | |
| // cx1 cx3 cxm0 cxm2 cxm4 bias | |
| { 1.0f, 0.43157974f, 1.0f, 0.05831938f, 0.76443945f, 0.0f }, | |
| { -0.431597974f, -1.0f, 0.05831938f, 1.0f, 0.76443945f, 1.57079633f }, | |
| }; | |
| const sF32 *c = coeffs[x >= 1.0f]; // interestingly enough, V2 code does this test wrong (cmovge instead of cmovae) | |
| sF32 x2 = x*x; | |
| sF32 r = (c[1]*x2 + c[0])*x / ((c[4]*x2 + c[3])*x2 + c[2]) + c[5]; | |
| return r * sign; | |
| } | |
| // Fast sine for x in [-pi/2, pi/2] | |
| // This is a single-precision odd Minimax polynomial, not a Taylor series! | |
| // Coefficients courtesy of Robin Green. | |
| static sF32 fastsin(sF32 x) | |
| { | |
| sF32 x2 = x*x; | |
| return (((-0.00018542f*x2 + 0.0083143f)*x2 - 0.16666f)*x2 + 1.0f) * x; | |
| } | |
| // Fast sine with range check (for x >= 0) | |
| // Applies symmetries, then funnels into fastsin. | |
| static sF32 fastsinrc(sF32 x) | |
| { | |
| // @@@BUG | |
| // NB this range reduction really only works for values >=0, | |
| // yet FM-sine oscillators will also pass in negative values. | |
| // This is not a good idea. At all. But it's what the original | |
| // V2 code does. :) | |
| // first range reduction: mod with 2pi | |
| x = fmodf(x, fc2pi); | |
| // now x in [-2pi,2pi] | |
| #if !BUG_V2_FM_RANGE | |
| if (x < 0.0f) | |
| x += fc2pi; | |
| #endif | |
| // need to reduce to [-pi/2, pi/2] to call fastsin | |
| if (x > fc1p5pi) // x in (3pi/2,2pi] | |
| x -= fc2pi; // sin(x) = sin(x-2pi) | |
| else if (x > fcpi_2) // x in (pi/2,3pi/2] | |
| x = fcpi - x; // sin(x) = -sin(x-pi) = sin(-(x-pi)) = sin(pi-x) | |
| return fastsin(x); | |
| } | |
| static sF32 calcfreq(sF32 x) | |
| { | |
| return powf(2.0f, (x - 1.0f)*10.0f); | |
| } | |
| static sF32 calcfreq2(sF32 x) | |
| { | |
| return powf(2.0f, (x - 1.0f)*fccfframe); | |
| } | |
| // square | |
| static inline sF32 sqr(sF32 x) | |
| { | |
| return x*x; | |
| } | |
| template<typename T> | |
| static inline T min(T a, T b) | |
| { | |
| return a < b ? a : b; | |
| } | |
| template<typename T> | |
| static inline T max(T a, T b) | |
| { | |
| return a > b ? a : b; | |
| } | |
| template<typename T> | |
| static inline T clamp(T x, T min, T max) | |
| { | |
| return (x < min) ? min : (x > max) ? max : x; | |
| } | |
| // uniform randon number generator | |
| // just a linear congruential generator, nothing fancy. | |
| static inline sU32 urandom(sU32 *seed) | |
| { | |
| *seed = *seed * 196314165 + 907633515; | |
| return *seed; | |
| } | |
| // uniform random float in [-1,1) | |
| static inline sF32 frandom(sU32 *seed) | |
| { | |
| sU32 bits = urandom(seed); // random 32-bit value | |
| sF32 f = bits2float((bits >> 9) | 0x40000000); // random float in [2,4) | |
| return f - 3.0f; // uniform random float in [-1,1) | |
| } | |
| // 32-bit value into float with 23 bits percision | |
| static inline sF32 utof23(sU32 x) | |
| { | |
| sF32 f = bits2float((x >> 9) | 0x3f800000); // 1 + x/(2^32) | |
| return f - 1.0f; | |
| } | |
| // float from [0,1) into 0.32 unsigned fixed-point | |
| // this loses a bit, but that's what V2 does. | |
| static inline sU32 ftou32(sF32 v) | |
| { | |
| return 2u * (sInt)(v * fc32bit); | |
| } | |
| // linear interpolation between a and b using t. | |
| static inline sF32 lerp(sF32 a, sF32 b, sF32 t) | |
| { | |
| return a + t * (b-a); | |
| } | |
| // DEBUG | |
| #include <stdarg.h> | |
| #include <stdio.h> | |
| extern "C" void __stdcall OutputDebugStringA(const char *what); | |
| static void dprintf(const char *fmt, ...) | |
| { | |
| char buf[256]; | |
| va_list arg; | |
| va_start(arg, fmt); | |
| vsprintf_s(buf, fmt, arg); | |
| va_end(arg); | |
| OutputDebugStringA(buf); | |
| } | |
| // -------------------------------------------------------------------------- | |
| // Building blocks | |
| // -------------------------------------------------------------------------- | |
| union StereoSample | |
| { | |
| struct | |
| { | |
| sF32 l, r; | |
| }; | |
| sF32 ch[2]; | |
| }; | |
| // LRC filter. | |
| // The state variables are 'l' and 'b'. The time series for l and b | |
| // correspond to a resonant low-pass and band-pass respectively, hence | |
| // the name. 'step' returns 'h', which is just the "missing" resonant | |
| // high-pass. | |
| // | |
| // Note that 'freq' here isn't actually a frequency at all, it's actually | |
| // 2*(1 - cos(2pi*freq/SR)), but V2 calls this "frequency" anyway :) | |
| struct V2LRC | |
| { | |
| sF32 l, b; | |
| void init() | |
| { | |
| l = b = 0.0f; | |
| } | |
| // Single step | |
| sF32 step(sF32 in, sF32 freq, sF32 reso) | |
| { | |
| l += freq * b; | |
| sF32 h = in - b*reso - l; | |
| b += freq * h; | |
| return h; | |
| } | |
| // 2x oversampled step (the good stuff) | |
| sF32 step_2x(sF32 in, sF32 freq, sF32 reso) | |
| { | |
| // the filters get slightly biased inputs to avoid the state variables | |
| // getting too close to 0 for prolonged periods of time (which would | |
| // cause denormals to appear) | |
| in += fcdcoffset; | |
| // step 1 | |
| l += freq * b - fcdcoffset; // undo bias here (1 sample delay) | |
| b += freq * (in - b*reso - l); | |
| // step 2 | |
| l += freq * b; | |
| sF32 h = in - b*reso - l; | |
| b += freq * h; | |
| return h; | |
| } | |
| }; | |
| // Moog filter state | |
| struct V2Moog | |
| { | |
| sF32 b[5]; // filter state | |
| void init() | |
| { | |
| b[0] = b[1] = b[2] = b[3] = b[4] = 0.0f; | |
| } | |
| sF32 step(sF32 realin, sF32 f, sF32 p, sF32 q) | |
| { | |
| sF32 in = realin + fcdcoffset; // again, biased in | |
| sF32 t1, t2, t3, b4; | |
| in -= q * b[4]; // feedback | |
| t1 = b[1]; b[1] = (in + b[0]) * p - b[1] * f; | |
| t2 = b[2]; b[2] = (t1 + b[1]) * p - b[2] * f; | |
| t3 = b[3]; b[3] = (t2 + b[2]) * p - b[3] * f; | |
| b4 = (t3 + b[3]) * p - b[4] * f; | |
| b4 -= b4*b4*b4 * (1.0f/6.0f); // clipping | |
| b4 -= fcdcoffset; // un-bias | |
| b[4] = b4 - fcdcoffset; | |
| b[0] = realin; | |
| return b4; | |
| } | |
| }; | |
| // DC filter state. Just a highpass used with a very low cut-off | |
| // to remove DC offsets from a signal. | |
| struct V2DCF | |
| { | |
| sF32 xm1; // x(n-1) | |
| sF32 ym1; // y(n-1) | |
| void init() | |
| { | |
| xm1 = ym1 = 0.0f; | |
| } | |
| sF32 step(sF32 in, sF32 R) | |
| { | |
| // y(n) = x(n) - x(n-1) + R*y(n-1) | |
| sF32 y = (fcdcoffset + R*ym1 - xm1 + in) - fcdcoffset; | |
| xm1 = in; | |
| ym1 = y; | |
| return y; | |
| } | |
| }; | |
| // Constant-length delay line | |
| class V2Delay | |
| { | |
| sU32 pos, len; | |
| sF32 *buf; | |
| public: | |
| V2Delay() | |
| : pos(0), len(0), buf(0) | |
| { | |
| } | |
| void init(sF32 *buf, sU32 len) | |
| { | |
| this->buf = buf; | |
| this->len = len; | |
| reset(); | |
| } | |
| template<sU32 N> | |
| void init(sF32 (&buf)[N]) | |
| { | |
| init(buf, N); | |
| } | |
| void reset() | |
| { | |
| memset(buf, 0, sizeof(*buf)*len); | |
| pos = 0; | |
| } | |
| inline sF32 fetch() const | |
| { | |
| return buf[pos]; | |
| } | |
| inline void feed(sF32 v) | |
| { | |
| buf[pos] = v; | |
| if (++pos == len) | |
| pos = 0; | |
| } | |
| }; | |
| // debug stuff | |
| static void checkRange(const sF32 *src, sInt nsamples) | |
| { | |
| for (sInt i=0; i < nsamples; i++) | |
| assert(src[i] >= -1.5f && src[i] < 1.5f); | |
| } | |
| static void checkRange(const StereoSample *src, sInt nsamples) | |
| { | |
| checkRange(&src[0].l, nsamples * 2); | |
| } | |
| // -------------------------------------------------------------------------- | |
| // V2 Instance | |
| // -------------------------------------------------------------------------- | |
| struct V2Instance | |
| { | |
| static const int MAX_FRAME_SIZE = 280; // in samples | |
| // Stuff that depends on the sample rate | |
| sF32 SRfcsamplesperms; | |
| sF32 SRfcobasefrq; | |
| sF32 SRfclinfreq; | |
| sF32 SRfcBoostCos, SRfcBoostSin; | |
| sF32 SRfcdcfilter; | |
| sInt SRcFrameSize; | |
| sF32 SRfciframe; | |
| // buffers | |
| sF32 vcebuf[MAX_FRAME_SIZE]; | |
| sF32 vcebuf2[MAX_FRAME_SIZE]; | |
| StereoSample levelbuf[MAX_FRAME_SIZE]; // original V2 overlaps level buffer with voice buffers | |
| StereoSample chanbuf[MAX_FRAME_SIZE]; | |
| sF32 aux1buf[MAX_FRAME_SIZE]; | |
| sF32 aux2buf[MAX_FRAME_SIZE]; | |
| StereoSample mixbuf[MAX_FRAME_SIZE]; | |
| StereoSample auxabuf[MAX_FRAME_SIZE]; | |
| StereoSample auxbbuf[MAX_FRAME_SIZE]; | |
| void calcNewSampleRate(sInt samplerate) | |
| { | |
| sF32 sr = (sF32)samplerate; | |
| SRfcsamplesperms = sr / 1000.0f; | |
| SRfcobasefrq = (fcoscbase * fc32bit) / sr; | |
| SRfclinfreq = fcsrbase / sr; | |
| SRfcdcfilter = 1.0f - fcdcflt / sr; | |
| // frame size | |
| SRcFrameSize = (sInt)(fcframebase * sr / fcsrbase + 0.5f); | |
| SRfciframe = 1.0f / (sF32)SRcFrameSize; | |
| assert(SRcFrameSize <= MAX_FRAME_SIZE); | |
| // low shelving EQ | |
| sF32 boost = (fcboostfreq * fc2pi) / sr; | |
| SRfcBoostCos = cos(boost); | |
| SRfcBoostSin = sin(boost); | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Oscillator | |
| // -------------------------------------------------------------------------- | |
| struct syVOsc | |
| { | |
| sF32 mode; // OSC_* (as float. it's all floats in here) | |
| sF32 ring; | |
| sF32 pitch; | |
| sF32 detune; | |
| sF32 color; | |
| sF32 gain; | |
| }; | |
| struct V2Osc | |
| { | |
| enum Mode | |
| { | |
| OSC_OFF = 0, | |
| OSC_TRI_SAW = 1, | |
| OSC_PULSE = 2, | |
| OSC_SIN = 3, | |
| OSC_NOISE = 4, | |
| OSC_FM_SIN = 5, | |
| OSC_AUXA = 6, | |
| OSC_AUXB = 7, | |
| }; | |
| sInt mode; // OSC_* | |
| bool ring; // ring modulation on/off | |
| sU32 cnt; // wave counter | |
| sInt freq; // wave counter inc (8x/sample) | |
| sU32 brpt; // break point for tri/pulse wave | |
| sF32 nffrq, nfres; // noise filter freq/resonance | |
| sU32 nseed; // noise random seed | |
| sF32 gain; // output gain | |
| V2LRC nf; // noise filter | |
| sF32 note; | |
| sF32 pitch; | |
| V2Instance *inst; // V2 instance we belong to. | |
| void init(V2Instance *instance, sInt idx) | |
| { | |
| static const sU32 seeds[] = { 0xdeadbeefu, 0xbaadf00du, 0xd3adc0deu }; | |
| assert(idx < COUNTOF(seeds)); | |
| cnt = 0; | |
| nf.init(); | |
| nseed = seeds[idx]; | |
| inst = instance; | |
| } | |
| void noteOn() | |
| { | |
| chgPitch(); | |
| } | |
| void chgPitch() | |
| { | |
| nffrq = inst->SRfclinfreq * calcfreq((pitch + 64.0f) / 128.0f); | |
| freq = (sInt)(inst->SRfcobasefrq * pow(2.0f, (pitch + note - 60.0f) / 12.0f)); | |
| } | |
| void set(const syVOsc *para) | |
| { | |
| mode = (sInt)para->mode; | |
| ring = (((sInt)para->ring) & 1) != 0; | |
| pitch = (para->pitch - 64.0f) + (para->detune - 64.0f) / 128.0f; | |
| chgPitch(); | |
| gain = para->gain / 128.0f; | |
| sF32 col = para->color / 128.0f; | |
| brpt = ftou32(col); | |
| nfres = 1.0f - sqrtf(col); | |
| } | |
| void render(sF32 *dest, sInt nsamples) | |
| { | |
| switch (mode & 7) | |
| { | |
| case OSC_OFF: break; | |
| case OSC_TRI_SAW: renderTriSaw(dest, nsamples); break; | |
| case OSC_PULSE: renderPulse(dest, nsamples); break; | |
| case OSC_SIN: renderSin(dest, nsamples); break; | |
| case OSC_NOISE: renderNoise(dest, nsamples); break; | |
| case OSC_FM_SIN: renderFMSin(dest, nsamples); break; | |
| case OSC_AUXA: renderAux(dest, inst->auxabuf, nsamples); break; | |
| case OSC_AUXB: renderAux(dest, inst->auxbbuf, nsamples); break; | |
| } | |
| DEBUG_PLOT(this, dest, nsamples); | |
| } | |
| private: | |
| inline void output(sF32 *dest, sF32 x) | |
| { | |
| if (ring) | |
| *dest *= x; | |
| else | |
| *dest += x; | |
| } | |
| // Oscillator state machine (read description of renderTriSaw for context) | |
| // | |
| // We keep track of whether the current sample is in the up or down phase, | |
| // whether the previous sample was, and if the waveform counter wrapped | |
| // around on the transition. This allows us to figure out which of | |
| // the cases above we fall into. Note this code uses a different bit ordering | |
| // from the ASM version that is hopefully a bit easier to understand. | |
| // | |
| // For reference: our bits map to the ASM version as follows (MSB->LSB order) | |
| // (o)ld_up | |
| // (c)arry | |
| // (n)ew_up | |
| enum OSMTransitionCode // carry:old_up:new_up | |
| { | |
| OSMTC_DOWN = 0, // old=down, new=down, no carry | |
| // 1 is an invalid configuration | |
| OSMTC_UP_DOWN = 2, // old=up, new=down, no carry | |
| OSMTC_UP = 3, // old=up, new=up, no carry | |
| OSMTC_DOWN_UP_DOWN = 4, // old=down, new=down, carry | |
| OSMTC_DOWN_UP = 5, // old=down, new=up, carry | |
| // 6 is an invalid configuration | |
| OSMTC_UP_DOWN_UP = 7 // old=up, new=up, carry | |
| }; | |
| inline sU32 osm_init() // our state field: old_up:new_up | |
| { | |
| return (cnt - freq) < brpt ? 3 : 0; | |
| } | |
| inline sU32 osm_tick(sU32 &state) // returns transition code | |
| { | |
| // old_up = new_up, new_up = (cnt < brpt) | |
| state = ((state << 1) | (cnt < brpt)) & 3; | |
| // we added freq to cnt going from the previous sample to the current one. | |
| // so if cnt is less than freq, we carried. | |
| sU32 transition_code = state | (cnt < (sU32)freq ? 4 : 0); | |
| // finally, tick the oscillator | |
| cnt += freq; | |
| return transition_code; | |
| } | |
| void renderTriSaw(sF32 *dest, sInt nsamples) | |
| { | |
| // Okay, so here's the general idea: instead of the classical sawtooth | |
| // or triangle waves, V2 uses a generalized triangle wave that looks like | |
| // this: | |
| // | |
| // /\ /\ | |
| // / \ / \ | |
| // / \ / \ | |
| // ---/---------\---------/---------\> t | |
| // / \ / | |
| // / \ / | |
| // / \/ | |
| // [-----1 period-----] | |
| // [-----] "break point" (brpt) | |
| // | |
| // If brpt=1/2 (ignoring fixed-point scaling), you get a regular triangle | |
| // wave. The example shows brpt=1/3, which gives an asymmetrical triangle | |
| // wave. At the extremes, brpt=0 gives a pure saw-down wave, and brpt=1 | |
| // (if that was a possible value, which it isn't) gives a pure saw-up wave. | |
| // | |
| // Purely point-sampling this (or any other) waveform would cause notable | |
| // aliasing. The standard ways to avoid this issue are to either: | |
| // 1) Over-sample by a certain amount and then use a low-pass filter to | |
| // (hopefully) get rid of the frequencies that would alias, or | |
| // 2) Generate waveforms from a Fourier series that's cut off below the | |
| // Nyquist frequency, ensuring there's no aliasing to begin with. | |
| // V2 does neither. Instead it computes the convolution of the continuous | |
| // waveform with an analytical low-pass filter. The ideal low-pass in | |
| // terms of frequency response would be a sinc filter, which unfortunately | |
| // has infinite support. Instead, V2 just uses a simple box filter. This | |
| // doesn't exactly have favorable frequency-domain characteristics, but | |
| // it's still much better than point sampling and has the advantage that | |
| // it's fairly simple analytically. It boils down to computing the average | |
| // value of the waveform over the interval [t,t+h], where t is the current | |
| // time and h = 1/SR (SR=sampling rate), which is in turn: | |
| // | |
| // f_box(t) = 1/h * (integrate(x=t..t+h) f(x) dx) | |
| // | |
| // Now there's a bunch of cases for these intervals [t,t+h] that we need to | |
| // consider. Bringing up the diagram again, and adding some intervals at the | |
| // bottom: | |
| // | |
| // /\ /\ | |
| // / \ / \ | |
| // / \ / \ | |
| // ---/---------\---------/---------\> t | |
| // / \ / | |
| // / \ / | |
| // / \/ | |
| // [-a-] [-c] | |
| // [--b--] [-d--] | |
| // [-----------e-----------] | |
| // [-----------f-----------] | |
| // | |
| // a) is purely in the saw-up region, | |
| // b) starts in the saw-up region and ends in saw-down, | |
| // c) is purely in the saw-down region, | |
| // d) starts during saw-down and ends in saw-up. | |
| // e) starts during saw-up and ends in saw-up, but passes through saw-down | |
| // f) starts saw-down, ends saw-down, passes through saw-up. | |
| // | |
| // For simplicity here, I draw different-sized intervals sampling a fixed- | |
| // frequency wave, even though in practice it's the other way round, but | |
| // this way it's easier to put it all into a single picture. | |
| // | |
| // The original assembly code goes through a few gyrations to encode all | |
| // these possible cases into a bitmask and then does a single switch. | |
| // In practice, for all but very high-frequency waves, we're hitting the | |
| // "easy" cases a) and c) almost all the time. | |
| COVER("Osc tri/saw"); | |
| // calc helper values | |
| sF32 f = utof23(freq); | |
| sF32 omf = 1.0f - f; | |
| sF32 rcpf = 1.0f / f; | |
| sF32 col = utof23(brpt); | |
| // m1 = 2/col = slope of saw-up wave | |
| // m2 = -2/(1-col) = slope of saw-down wave | |
| // c1 = gain/2*m1 = gain/col = scaled integration constant | |
| // c2 = gain/2*m2 = -gain/(1-col) = scaled integration constant | |
| sF32 c1 = gain / col; | |
| sF32 c2 = -gain / (1.0f - col); | |
| sU32 state = osm_init(); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 p = utof23(cnt) - col; | |
| sF32 y = 0.0f; | |
| // state machine action | |
| switch (osm_tick(state)) | |
| { | |
| case OSMTC_UP: // case a) | |
| // average of linear function = just sample in the middle | |
| y = c1 * (p + p - f); | |
| break; | |
| case OSMTC_DOWN: // case c) | |
| // again, average of a linear function | |
| y = c2 * (p + p - f); | |
| break; | |
| case OSMTC_UP_DOWN: // case b) | |
| y = rcpf * (c2 * sqr(p) - c1 * sqr(p-f)); | |
| break; | |
| case OSMTC_DOWN_UP: // case d) | |
| y = -rcpf * (gain + c2*sqr(p + omf) - c1*sqr(p)); | |
| break; | |
| case OSMTC_UP_DOWN_UP: // case e) | |
| y = -rcpf * (gain + c1*omf*(p + p + omf)); | |
| break; | |
| case OSMTC_DOWN_UP_DOWN: // case f) | |
| y = -rcpf * (gain + c2*omf*(p + p + omf)); | |
| break; | |
| // INVALID CASES | |
| default: | |
| assert(false); | |
| break; | |
| } | |
| output(dest + i, y + gain); | |
| } | |
| } | |
| void renderPulse(sF32 *dest, sInt nsamples) | |
| { | |
| // This follows the same general pattern as renderTriSaw above, except | |
| // this time the waveform is a pulse wave with variable pulse width, | |
| // which means we get very simple integrals. The state machine works | |
| // the exact same way, see above for description. | |
| COVER("Osc pulse"); | |
| // calc helper values | |
| sF32 f = utof23(freq); | |
| sF32 gdf = gain / f; | |
| sF32 col = utof23(brpt); | |
| sF32 cc121 = gdf * 2.0f * (col - 1.0f) + gain; | |
| sF32 cc212 = gdf * 2.0f * col - gain; | |
| sU32 state = osm_init(); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 p = utof23(cnt); | |
| sF32 out = 0.0f; | |
| switch (osm_tick(state)) | |
| { | |
| case OSMTC_UP: | |
| out = gain; | |
| break; | |
| case OSMTC_DOWN: | |
| out = -gain; | |
| break; | |
| case OSMTC_UP_DOWN: | |
| out = gdf * 2.0f * (col - p) + gain; | |
| break; | |
| case OSMTC_DOWN_UP: | |
| out = gdf * 2.0f * p - gain; | |
| break; | |
| case OSMTC_UP_DOWN_UP: | |
| out = cc121; | |
| break; | |
| case OSMTC_DOWN_UP_DOWN: | |
| out = cc212; | |
| break; | |
| // INVALID CASES | |
| default: | |
| assert(false); | |
| break; | |
| } | |
| output(dest + i, out); | |
| } | |
| } | |
| void renderSin(sF32 *dest, sInt nsamples) | |
| { | |
| COVER("Osc sin"); | |
| // Sine is already a perfectly bandlimited waveform, so we needn't | |
| // worry about aliasing here. | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| // Brace yourselves: The name is a lie! It's actually a cosine wave! | |
| sU32 phase = cnt + 0x40000000; // quarter-turn (pi/2) phase offset | |
| cnt += freq; // step the oscillator | |
| // range reduce to [0,pi] | |
| if (phase & 0x80000000) // Symmetry: cos(x) = cos(-x) | |
| phase = ~phase; // V2 uses ~ not - which is slightly off but who cares | |
| // convert to t in [1,2) | |
| sF32 t = bits2float((phase >> 8) | 0x3f800000); // 1.0f + (phase / (2^31)) | |
| // and then to t in [-pi/2,pi/2) | |
| // i know the V2 ASM code says "scale/move to (-pi/4 .. pi/4)". | |
| // trust me, it's lying. | |
| t = t * fcpi - fc1p5pi; | |
| output(dest + i, gain * fastsin(t)); | |
| } | |
| } | |
| void renderNoise(sF32 *dest, sInt nsamples) | |
| { | |
| COVER("Osc noise"); | |
| V2LRC flt = nf; | |
| sU32 seed = nseed; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| // uniform random value (noise) | |
| sF32 n = frandom(&seed); | |
| // filter | |
| sF32 h = flt.step(n, nffrq, nfres); | |
| sF32 x = nfres*(flt.l + h) + flt.b; | |
| output(dest + i, gain * x); | |
| } | |
| flt = nf; | |
| nseed = seed; | |
| } | |
| void renderFMSin(sF32 *dest, sInt nsamples) | |
| { | |
| COVER("Osc FM"); | |
| // V2's take on FM is a bit unconventional but fairly slick and flexible. | |
| // The carrier wave is always a sine, but the modulator is whatever happens | |
| // to be in the voice buffer at that point - which is the output of the | |
| // previous oscillators. So you can use all the oscillator waveforms | |
| // (including noise, other FMs and the aux bus oscillators!) as modulator | |
| // if you are so inclined. | |
| // | |
| // And it's very little code too :) | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 mod = dest[i] * fcfmmax; | |
| sF32 t = (utof23(cnt) + mod) * fc2pi; | |
| cnt += freq; | |
| sF32 out = gain * fastsinrc(t); | |
| if (ring) | |
| dest[i] *= out; | |
| else | |
| dest[i] = out; | |
| } | |
| } | |
| void renderAux(sF32 *dest, const StereoSample *src, sInt nsamples) | |
| { | |
| COVER("Osc aux"); | |
| sF32 g = gain * fcgain; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 aux = g * (src[i].l + src[i].r); | |
| if (ring) | |
| aux *= dest[i]; | |
| dest[i] = aux; | |
| } | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Envelope Generator | |
| // -------------------------------------------------------------------------- | |
| struct syVEnv | |
| { | |
| sF32 ar; // attack rate | |
| sF32 dr; // decay rate | |
| sF32 sl; // sustain level | |
| sF32 sr; // sustain rate | |
| sF32 rr; // release rate | |
| sF32 vol; // volume | |
| }; | |
| struct V2Env | |
| { | |
| // Slightly different state ordering here than in V2 code, to make | |
| // things simpler. | |
| enum State | |
| { | |
| OFF, | |
| RELEASE, | |
| ATTACK, | |
| DECAY, | |
| SUSTAIN, | |
| }; | |
| sF32 out; | |
| State state; | |
| sF32 val; // output value (0.0-128.0) | |
| sF32 atd; // attack delta (added every frame in stAttack, transition ->stDecay at 128.0) | |
| sF32 dcf; // decay factor (mul'd every frame in stDecay, transition ->stSustain at sul) | |
| sF32 sul; // sustain level (defines stDecay->stSustain transition point) | |
| sF32 suf; // sustain factor (mul'd every frame in stSustain, transition ->stRelease at gate off or ->stOff at 0.0) | |
| sF32 ref; // release factor (mul'd every frame in stRelease, transition ->stOff at 0.0) | |
| sF32 gain; | |
| void init(V2Instance *) | |
| { | |
| state = OFF; | |
| } | |
| void set(const syVEnv *para) | |
| { | |
| // ar: 2^7 (128) to 2^-4 (0.03, ca. 10 secs at 344frames/sec) | |
| atd = powf(2.0f, para->ar * fcattackmul + fcattackadd); | |
| // dcf: 0 (5msecs thanks to volramping) up to almost 1 | |
| dcf = 1.0f - calcfreq2(1.0f - para->dr / 128.0f); | |
| // sul: 0..127 is fine already | |
| sul = para->sl; | |
| // suf: 1/128 (15ms till it's gone) up to 128 (15ms till it's fully there) | |
| suf = powf(2.0f, fcsusmul * (para->sr - 64.0f)); | |
| // ref: 0 (5ms thanks to volramping) up to almost 1 | |
| ref = 1.0f - calcfreq2(1.0f - para->rr / 128.0f); | |
| gain = para->vol / 128.0f; | |
| } | |
| void tick(bool gate) | |
| { | |
| // process immediate gate transition | |
| if (state <= RELEASE && gate) // gate on | |
| state = ATTACK; | |
| else if (state >= ATTACK && !gate) // gate off | |
| state = RELEASE; | |
| // process current state | |
| switch (state) | |
| { | |
| case OFF: | |
| COVER("EG off"); | |
| val = 0.0f; | |
| break; | |
| case ATTACK: | |
| COVER("EG atk"); | |
| val += atd; | |
| if (val >= 128.0f) | |
| { | |
| val = 128.0f; | |
| state = DECAY; | |
| } | |
| break; | |
| case DECAY: | |
| COVER("EG dcy"); | |
| val *= dcf; | |
| if (val <= sul) | |
| { | |
| val = sul; | |
| state = SUSTAIN; | |
| } | |
| break; | |
| case SUSTAIN: | |
| COVER("EG sus"); | |
| val *= suf; | |
| if (val > 128.0f) | |
| val = 128.0f; | |
| break; | |
| case RELEASE: | |
| COVER("EG rel"); | |
| val *= ref; | |
| break; | |
| } | |
| // avoid underflow to denormals | |
| if (val <= fclowest) | |
| { | |
| val = 0.0f; | |
| state = OFF; | |
| } | |
| out = val * gain; | |
| DEBUG_PLOT_VAL(this, out / 128.0f); | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Filter | |
| // -------------------------------------------------------------------------- | |
| struct syVFlt | |
| { | |
| sF32 mode; | |
| sF32 cutoff; | |
| sF32 reso; | |
| }; | |
| struct V2Flt | |
| { | |
| enum Mode | |
| { | |
| BYPASS, | |
| LOW, | |
| BAND, | |
| HIGH, | |
| NOTCH, | |
| ALL, | |
| MOOGL, | |
| MOOGH | |
| }; | |
| sInt mode; | |
| sF32 cfreq; | |
| sF32 res; | |
| sF32 moogf, moogp, moogq; // moog filter coeffs | |
| V2LRC lrc; | |
| V2Moog moog; | |
| V2Instance *inst; | |
| void init(V2Instance *instance) | |
| { | |
| lrc.init(); | |
| moog.init(); | |
| inst = instance; | |
| } | |
| void set(const syVFlt *para) | |
| { | |
| mode = (sInt)para->mode; | |
| sF32 f = calcfreq(para->cutoff / 128.0f) * inst->SRfclinfreq; | |
| sF32 r = para->reso / 128.0f; | |
| if (mode < MOOGL) | |
| { | |
| COVER("VCF set regular"); | |
| res = 1.0f - r; | |
| cfreq = f; | |
| } | |
| else | |
| { | |
| COVER("VCF set moog"); | |
| // @@@BUG? V2 code for this part looks suspicious. | |
| f *= 0.25f; | |
| sF32 t = 1.0f - f; | |
| moogp = f + 0.8f * f * t; | |
| moogf = 1.0f - moogp - moogp; | |
| moogq = 4.0f * r * (1.0f + 0.5f * t * (1.0f - t + 5.6f * t * t)); | |
| } | |
| } | |
| void render(sF32 *dest, const sF32 *src, sInt nsamples, sInt step=1) | |
| { | |
| V2LRC flt; | |
| V2Moog m; | |
| switch (mode & 7) | |
| { | |
| case BYPASS: | |
| COVER("VCF bypass"); | |
| // @@@BUG ignores step? this is wrong but I suppose this | |
| // never gets hit for stereo case. | |
| if (dest != src) | |
| memmove(dest, src, nsamples * sizeof(sF32)); | |
| break; | |
| case LOW: | |
| COVER("VCF low"); | |
| flt = lrc; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| flt.step_2x(src[i*step], cfreq, res); | |
| dest[i*step] = flt.l; | |
| } | |
| lrc = flt; | |
| break; | |
| case BAND: | |
| COVER("VCF band"); | |
| flt = lrc; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| flt.step_2x(src[i*step], cfreq, res); | |
| dest[i*step] = flt.b; | |
| } | |
| lrc = flt; | |
| break; | |
| case HIGH: | |
| COVER("VCF high"); | |
| flt = lrc; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 h = flt.step_2x(src[i*step], cfreq, res); | |
| dest[i*step] = h; | |
| } | |
| lrc = flt; | |
| break; | |
| case NOTCH: | |
| COVER("VCF notch"); | |
| flt = lrc; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 h = flt.step_2x(src[i*step], cfreq, res); | |
| dest[i*step] = flt.l + h; | |
| } | |
| lrc = flt; | |
| break; | |
| case ALL: | |
| COVER("VCF all"); | |
| flt = lrc; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 h = flt.step_2x(src[i*step], cfreq, res); | |
| dest[i*step] = flt.l + flt.b + h; | |
| } | |
| lrc = flt; | |
| break; | |
| case MOOGL: | |
| COVER("VCF moog low"); | |
| // Moog filters are 2x oversampled, so run filter twice. | |
| m = moog; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 in = src[i*step]; | |
| m.step(in, moogf, moogp, moogq); | |
| dest[i*step] = m.step(in, moogf, moogp, moogq); | |
| } | |
| moog = m; | |
| break; | |
| case MOOGH: | |
| COVER("VCF moog high"); | |
| m = moog; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 in = src[i*step]; | |
| m.step(in, moogf, moogp, moogq); | |
| dest[i*step] = in - m.step(in, moogf, moogp, moogq); | |
| } | |
| moog = m; | |
| break; | |
| } | |
| DEBUG_PLOT_STRIDED(this, dest, step, nsamples); | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Low Frequency Oscillator | |
| // -------------------------------------------------------------------------- | |
| struct syVLFO | |
| { | |
| sF32 mode; // 0=saw, 1=tri, 2=pulse, 3=sin, 4=s&h | |
| sF32 sync; // 0=free, 1=in sync with keyon | |
| sF32 egmode; // 0=continuous 1=one-shot (EG mode) | |
| sF32 rate; // rate (0Hz..~43Hz) | |
| sF32 phase; // start phase shift | |
| sF32 pol; // polarity: +, -, +/- | |
| sF32 amp; // amplification (0..1) | |
| }; | |
| struct V2LFO | |
| { | |
| enum Mode | |
| { | |
| SAW, | |
| TRI, | |
| PULSE, | |
| SIN, | |
| S_H | |
| }; | |
| sF32 out; | |
| sInt mode; // mode | |
| bool sync; // sync mode | |
| bool eg; // envelope generator mode | |
| sInt freq; // frequency | |
| sU32 cntr; // counter | |
| sU32 cphase; // counter sync phase | |
| sF32 gain; // output gain | |
| sF32 dc; // output dc | |
| sU32 nseed; // random seed | |
| sU32 last; // last counter value (for s&h transition) | |
| void init(V2Instance *) | |
| { | |
| cntr = last = 0; | |
| nseed = rand(); // not really, but close enough... | |
| } | |
| void set(const syVLFO *para) | |
| { | |
| mode = (sInt)para->mode; | |
| sync = (sInt)para->sync != 0; | |
| eg = (sInt)para->egmode != 0; | |
| freq = (sInt)(0.5f * fc32bit * calcfreq(para->rate / 128.0f)); | |
| cphase = ftou32(para->phase / 128.0f); | |
| switch ((sInt)para->pol) | |
| { | |
| case 0: // + | |
| COVER("LFO pol +"); | |
| gain = para->amp; | |
| dc = 0.0f; | |
| break; | |
| case 1: // - | |
| COVER("LFO pol -"); | |
| gain = -para->amp; | |
| dc = 0.0f; | |
| break; | |
| case 2: // +/- | |
| COVER("LFO pol +/-"); | |
| gain = para->amp; | |
| dc = -0.5f * para->amp; | |
| break; | |
| } | |
| } | |
| void keyOn() | |
| { | |
| if (sync) | |
| { | |
| COVER("LFO sync"); | |
| cntr = cphase; | |
| last = ~0u; | |
| } | |
| } | |
| void tick() | |
| { | |
| sF32 v; | |
| sU32 x; | |
| switch (mode & 7) | |
| { | |
| case SAW: | |
| default: | |
| COVER("LFO saw"); | |
| v = utof23(cntr); | |
| break; | |
| case TRI: | |
| COVER("LFO tri"); | |
| x = (cntr << 1) ^ (sS32(cntr) >> 31); | |
| v = utof23(x); | |
| break; | |
| case PULSE: | |
| COVER("LFO pulse"); | |
| x = sS32(cntr) >> 31; | |
| v = utof23(x); | |
| break; | |
| case SIN: | |
| COVER("LFO sin"); | |
| v = utof23(cntr); | |
| v = fastsinrc(v * fc2pi) * 0.5f + 0.5f; | |
| break; | |
| case S_H: | |
| COVER("LFO sample+hold"); | |
| if (cntr < last) | |
| nseed = urandom(&nseed); | |
| last = cntr; | |
| v = utof23(nseed); | |
| break; | |
| } | |
| out = v * gain + dc; | |
| cntr += freq; | |
| if (cntr < (sU32)freq && eg) // in one-shot mode, clamp at wrap-around | |
| cntr = ~0u; | |
| DEBUG_PLOT_VAL(this, out / 128.0f); | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Distortion | |
| // -------------------------------------------------------------------------- | |
| struct syVDist | |
| { | |
| sF32 mode; // see below | |
| sF32 ingain; // -12dB .. 36dB | |
| sF32 param1; // outgain / crush / outfreq / cutoff | |
| sF32 param2; // offset / xor / jitter / reso | |
| }; | |
| struct V2Dist | |
| { | |
| enum Mode | |
| { | |
| OFF = 0, | |
| OVERDRIVE, | |
| CLIP, | |
| BITCRUSHER, | |
| DECIMATOR, | |
| FLT_BASE = DECIMATOR, | |
| FLT_LOW = FLT_BASE + V2Flt::LOW, | |
| FLT_BAND = FLT_BASE + V2Flt::BAND, | |
| FLT_HIGH = FLT_BASE + V2Flt::HIGH, | |
| FLT_NOTCH = FLT_BASE + V2Flt::NOTCH, | |
| FLT_ALL = FLT_BASE + V2Flt::ALL, | |
| FLT_MOOGL = FLT_BASE + V2Flt::MOOGL, | |
| FLT_MOOGH = FLT_BASE + V2Flt::MOOGH, | |
| }; | |
| sInt mode; | |
| sF32 gain1; // input gain for all fx | |
| sF32 gain2; // output gain for od/clip | |
| sF32 offs; // offs for od/clip | |
| sF32 crush1; // 1/crush_factor | |
| sInt crush2; // crush_factor | |
| sInt crxor; // xor value for crush | |
| sU32 dcount; // decimator counter | |
| sU32 dfreq; // decimator frequency | |
| sF32 dvall; // last decimator value (mono/left) | |
| sF32 dvalr; // last decimator value (mono/right) | |
| V2Flt fltl; // filter mono/left | |
| V2Flt fltr; // filter right | |
| void init(V2Instance *instance) | |
| { | |
| dcount = 0; | |
| dvall = dvalr = 0.0f; | |
| fltl.init(instance); | |
| fltr.init(instance); | |
| } | |
| void set(const syVDist *para) | |
| { | |
| sF32 x; | |
| mode = (sInt)para->mode; | |
| gain1 = powf(2.0f, (para->ingain - 32.0f) / 16.0f); | |
| switch (mode) | |
| { | |
| case OFF: | |
| break; | |
| case OVERDRIVE: | |
| gain2 = (para->param1 / 128.0f) / atan(gain1); | |
| offs = gain1 * 2.0f * ((para->param2 / 128.0f) - 0.5f); | |
| break; | |
| case CLIP: | |
| gain2 = para->param1 / 128.0f; | |
| offs = gain1 * 2.0f * ((para->param2 / 128.0f) - 0.5f); | |
| break; | |
| case BITCRUSHER: | |
| x = para->param1 * 256.0f + 1.0f; | |
| crush2 = (sInt)x; | |
| crush1 = gain1 * (32768.0f / x); | |
| crxor = ((sInt)para->param2) << 9; | |
| break; | |
| case DECIMATOR: | |
| dfreq = ftou32(calcfreq(para->param1 / 128.0f)); | |
| break; | |
| default: // filters | |
| { | |
| syVFlt setup; | |
| setup.cutoff = para->param1; | |
| setup.reso = para->param2; | |
| setup.mode = (sF32)(mode - FLT_BASE); | |
| fltl.set(&setup); | |
| fltr.set(&setup); | |
| } | |
| break; | |
| } | |
| } | |
| void renderMono(sF32 *dest, const sF32 *src, sInt nsamples) | |
| { | |
| switch (mode) | |
| { | |
| case OFF: | |
| if (dest != src) | |
| memmove(dest, src, nsamples * sizeof(sF32)); | |
| break; | |
| case OVERDRIVE: | |
| COVER("DIST overdrive"); | |
| for (sInt i=0; i < nsamples; i++) | |
| dest[i] = overdrive(src[i]); | |
| break; | |
| case CLIP: | |
| COVER("DIST clip"); | |
| for (sInt i=0; i < nsamples; i++) | |
| dest[i] = clip(src[i]); | |
| break; | |
| case BITCRUSHER: | |
| COVER("DIST bitcrusher"); | |
| for (sInt i=0; i < nsamples; i++) | |
| dest[i] = bitcrusher(src[i]); | |
| break; | |
| case DECIMATOR: | |
| COVER("DIST mono decimator"); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| decimator_tick(src[i], 0.0f); | |
| dest[i] = dvall; | |
| } | |
| break; | |
| default: // filters | |
| COVER("DIST mono filter"); | |
| fltl.render(dest, src, nsamples); | |
| break; | |
| } | |
| DEBUG_PLOT(this, dest, nsamples); | |
| } | |
| void renderStereo(StereoSample *dest, const StereoSample *src, sInt nsamples) | |
| { | |
| // @@@BUG this matches the original V2 code, but frankly I have my doubts | |
| // that always running the Moog filters in Mono mode is intentional... | |
| switch (mode) | |
| { | |
| case DECIMATOR: | |
| COVER("DIST stereo decimator"); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| decimator_tick(src[i].l, src[i].r); | |
| dest[i].l = dvall; | |
| dest[i].r = dvalr; | |
| } | |
| break; | |
| case FLT_LOW: | |
| case FLT_BAND: | |
| case FLT_HIGH: | |
| case FLT_NOTCH: | |
| case FLT_ALL: | |
| COVER("DIST stereo filter"); | |
| fltl.render(&dest[0].l, &src[0].l, nsamples, 2); | |
| fltr.render(&dest[0].r, &src[0].r, nsamples, 2); | |
| break; | |
| default: | |
| // everything else we presume to be stateless and just pass through the | |
| // mono version. | |
| renderMono(&dest[0].l, &src[0].l, nsamples*2); | |
| } | |
| DEBUG_PLOT_STEREO(this, dest, nsamples); | |
| } | |
| private: | |
| inline sF32 overdrive(sF32 in) | |
| { | |
| return gain2 * fastatan(in * gain1 + offs); | |
| } | |
| inline sF32 clip(sF32 in) | |
| { | |
| return gain2 * clamp(in * gain1 + offs, -1.0f, 1.0f); | |
| } | |
| inline sF32 bitcrusher(sF32 in) | |
| { | |
| sInt t = (sInt)(in * crush1); | |
| t = clamp(t * crush2, -0x7fff, 0x7fff) ^ crxor; | |
| return (sF32)t / 32768.0f; | |
| } | |
| inline void decimator_tick(sF32 l, sF32 r) | |
| { | |
| dcount += dfreq; | |
| if (dcount < dfreq) // carry | |
| { | |
| dvall = l; | |
| dvalr = r; | |
| } | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // DC filter | |
| // -------------------------------------------------------------------------- | |
| // This is just a high-pass with very low cut-off to remove DC offsets from | |
| // the signal. | |
| struct V2DCFilter | |
| { | |
| V2DCF fl; // left/mono filter state | |
| V2DCF fr; // right filter state | |
| V2Instance *inst; | |
| void init(V2Instance *instance) | |
| { | |
| inst = instance; | |
| fl.init(); | |
| fr.init(); | |
| } | |
| void renderMono(sF32 *dest, const sF32 *src, sInt nsamples) | |
| { | |
| sF32 R = inst->SRfcdcfilter; | |
| V2DCF l = fl; | |
| for (sInt i=0; i < nsamples; i++) | |
| dest[i] = l.step(src[i], R); | |
| fl = l; | |
| } | |
| void renderStereo(StereoSample *dest, const StereoSample *src, sInt nsamples) | |
| { | |
| sF32 R = inst->SRfcdcfilter; | |
| V2DCF l = fl; | |
| V2DCF r = fr; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| dest[i].l = l.step(src[i].l, R); | |
| dest[i].r = r.step(src[i].r, R); | |
| } | |
| fl = l; | |
| fr = r; | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // V2 Voice | |
| // -------------------------------------------------------------------------- | |
| struct syVV2 | |
| { | |
| // Voice parameters. | |
| // changing any of these *will* invalidate all V2M files! | |
| static const sInt NOSC = 3; // number of oscillators | |
| static const sInt NFLT = 2; // number of filters (NB: changing this would complicate routing too) | |
| static const sInt NENV = 2; // number of envelope generators | |
| static const sInt NLFO = 2; // number of LFOs | |
| sF32 panning; | |
| sF32 transp; // transpose | |
| syVOsc osc[NOSC]; | |
| syVFlt flt[NFLT]; | |
| sF32 routing; // 0: single 1: serial 2: parallel | |
| sF32 fltbal; // parallel filter balance | |
| syVDist dist; | |
| syVEnv env[NENV]; | |
| syVLFO lfo[NLFO]; | |
| sF32 oscsync; // 0: none 1: osc 2: full | |
| }; | |
| struct V2Voice | |
| { | |
| enum FilterRouting | |
| { | |
| FLTR_SINGLE = 0, | |
| FLTR_SERIAL, | |
| FLTR_PARALLEL | |
| }; | |
| enum KeySync | |
| { | |
| SYNC_NONE = 0, | |
| SYNC_OSC, | |
| SYNC_FULL | |
| }; | |
| sInt note; | |
| sF32 velo; | |
| bool gate; | |
| sF32 curvol; | |
| sF32 volramp; | |
| sF32 xpose; // transpose | |
| sInt fmode; // FLTR_* | |
| sF32 lvol; // left volume | |
| sF32 rvol; // right volume | |
| sF32 f1gain; // filter 1 gain | |
| sF32 f2gain; // filter 2 gain | |
| sInt keysync; | |
| V2Osc osc[syVV2::NOSC]; | |
| V2Flt vcf[syVV2::NFLT]; | |
| V2Env env[syVV2::NENV]; | |
| V2LFO lfo[syVV2::NLFO]; | |
| V2Dist dist; // distorter | |
| V2DCFilter dcf; // post DC filter | |
| V2Instance *inst; | |
| void init(V2Instance *instance) | |
| { | |
| for (sInt i=0; i < syVV2::NOSC; i++) | |
| osc[i].init(instance, i); | |
| for (sInt i=0; i < syVV2::NFLT; i++) | |
| vcf[i].init(instance); | |
| for (sInt i=0; i < syVV2::NENV; i++) | |
| env[i].init(instance); | |
| for (sInt i=0; i < syVV2::NLFO; i++) | |
| lfo[i].init(instance); | |
| dist.init(instance); | |
| dcf.init(instance); | |
| inst = instance; | |
| } | |
| void tick() | |
| { | |
| for (sInt i=0; i < syVV2::NENV; i++) | |
| env[i].tick(gate); | |
| for (sInt i=0; i < syVV2::NLFO; i++) | |
| lfo[i].tick(); | |
| // volume ramping slope | |
| volramp = (env[0].out / 128.0f - curvol) * inst->SRfciframe; | |
| DEBUG_PLOT_VAL(&curvol, curvol); | |
| } | |
| void render(StereoSample *dest, sInt nsamples) | |
| { | |
| assert(nsamples <= V2Instance::MAX_FRAME_SIZE); | |
| // clear voice buffer | |
| sF32 *voice = inst->vcebuf; | |
| sF32 *voice2 = inst->vcebuf2; | |
| memset(voice, 0, nsamples * sizeof(*voice)); | |
| // oscillators -> voice buffer | |
| for (sInt i=0; i < syVV2::NOSC; i++) | |
| osc[i].render(voice, nsamples); | |
| // voice buffer -> filters -> voice buffer | |
| switch (fmode) | |
| { | |
| case FLTR_SINGLE: | |
| COVER("VOICE filter single"); | |
| vcf[0].render(voice, voice, nsamples); | |
| break; | |
| case FLTR_SERIAL: | |
| default: | |
| COVER("VOICE filter serial"); | |
| vcf[0].render(voice, voice, nsamples); | |
| vcf[1].render(voice, voice, nsamples); | |
| break; | |
| case FLTR_PARALLEL: | |
| COVER("VOICE filter parallel"); | |
| vcf[1].render(voice2, voice, nsamples); | |
| vcf[0].render(voice, voice, nsamples); | |
| for (sInt i=0; i < nsamples; i++) | |
| voice[i] = voice[i]*f1gain + voice2[i]*f2gain; | |
| break; | |
| } | |
| // voice buffer -> distortion -> voice buffer | |
| dist.renderMono(voice, voice, nsamples); | |
| // voice buffer -> dc filter -> voice buffer | |
| dcf.renderMono(voice, voice, nsamples); | |
| DEBUG_PLOT(this, voice, nsamples); | |
| // voice buffer (mono) -> +=output buffer (stereo) | |
| // original ASM code has chan buffer hardwired as output here | |
| sF32 cv = curvol; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 out = voice[i] * cv; | |
| cv += volramp; | |
| dest[i].l += lvol * out + fcdcoffset; | |
| dest[i].r += rvol * out + fcdcoffset; | |
| } | |
| curvol = cv; | |
| } | |
| void set(const syVV2 *para) | |
| { | |
| xpose = para->transp - 64.0f; | |
| updateNote(); | |
| fmode = (sInt)para->routing; | |
| keysync = (sInt)para->oscsync; | |
| // equal power panning | |
| sF32 p = para->panning / 128.0f; | |
| lvol = sqrtf(1.0f - p); | |
| rvol = sqrtf(p); | |
| // filter balance for parallel | |
| sF32 x = (para->fltbal - 64.0f) / 64.0f; | |
| if (x >= 0.0f) | |
| { | |
| f2gain = 1.0f; | |
| f1gain = 1.0f - x; | |
| } | |
| else | |
| { | |
| f1gain = 1.0f; | |
| f2gain = 1.0f + x; | |
| } | |
| // subsections | |
| for (sInt i=0; i < syVV2::NOSC; i++) | |
| osc[i].set(¶->osc[i]); | |
| for (sInt i=0; i < syVV2::NENV; i++) | |
| env[i].set(¶->env[i]); | |
| for (sInt i=0; i < syVV2::NFLT; i++) | |
| vcf[i].set(¶->flt[i]); | |
| for (sInt i=0; i < syVV2::NLFO; i++) | |
| lfo[i].set(¶->lfo[i]); | |
| dist.set(¶->dist); | |
| } | |
| void noteOn(sInt note, sInt vel) | |
| { | |
| this->note = note; | |
| updateNote(); | |
| velo = (sF32)vel; | |
| gate = true; | |
| // reset EGs | |
| for (sInt i=0; i < syVV2::NENV; i++) | |
| env[i].state = V2Env::ATTACK; | |
| // process sync | |
| switch (keysync) | |
| { | |
| case SYNC_FULL: | |
| COVER("VOICE noteOn sync full"); | |
| for (sInt i=0; i < syVV2::NENV; i++) | |
| env[i].val = 0.0f; | |
| curvol = 0.0f; | |
| for (sInt i=0; i < syVV2::NOSC; i++) | |
| osc[i].init(inst, i); | |
| for (sInt i=0; i < syVV2::NFLT; i++) | |
| vcf[i].init(inst); | |
| dist.init(inst); | |
| // fall-through | |
| case SYNC_OSC: | |
| COVER("VOICE noteOn sync osc"); | |
| for (sInt i=0; i < syVV2::NOSC; i++) | |
| osc[i].cnt = 0; | |
| // fall-through | |
| case SYNC_NONE: | |
| default: | |
| break; | |
| } | |
| for (sInt i=0; i < syVV2::NOSC; i++) | |
| osc[i].chgPitch(); | |
| for (sInt i=0; i < syVV2::NLFO; i++) | |
| lfo[i].keyOn(); | |
| dcf.init(inst); | |
| } | |
| void noteOff() | |
| { | |
| gate = false; | |
| } | |
| private: | |
| void updateNote() | |
| { | |
| sF32 n = xpose + (sF32)note; | |
| for (sInt i=0; i < syVV2::NOSC; i++) | |
| osc[i].note = n; | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Bass boost (fixed low shelving EQ) | |
| // -------------------------------------------------------------------------- | |
| struct syVBoost | |
| { | |
| sF32 amount; // boost in dB (0..18) | |
| }; | |
| struct V2Boost | |
| { | |
| bool enabled; | |
| sF32 a1, a2; // normalized filter coeffs | |
| sF32 b0, b1, b2; | |
| sF32 x1[2]; // state variables | |
| sF32 x2[2]; | |
| sF32 y1[2]; | |
| sF32 y2[2]; | |
| V2Instance *inst; | |
| void init(V2Instance *instance) | |
| { | |
| inst = instance; | |
| } | |
| void set(const syVBoost *para) | |
| { | |
| enabled = ((sInt)para->amount) != 0; | |
| if (!enabled) | |
| return; | |
| COVER("BOOST set"); | |
| // A = 10^(dBgain/40), or a rough approximation anyway | |
| sF32 A = powf(2.0f, para->amount / 128.0f); | |
| // V2 code computes beta = sqrt((A^2 + 1) - (A-1)^2) for some reason | |
| // but applying the binomial formula just gives sqrt(2A) | |
| sF32 beta = sqrtf(2.0f * A); | |
| // temp vars | |
| sF32 bs = beta * inst->SRfcBoostSin; | |
| sF32 Am1 = A - 1.0f; | |
| sF32 Ap1 = A + 1.0f; | |
| sF32 cAm1 = Am1 * inst->SRfcBoostCos; | |
| sF32 cAp1 = Ap1 * inst->SRfcBoostCos; | |
| // a0 = (A+1) + (A-1)*cos + beta*sin | |
| sF32 ia0 = 1.0f / (Ap1 + cAm1 + bs); | |
| b1 = 2.0f * A * (Am1 - cAp1) * ia0; | |
| a1 = -2.0f * (Am1 + cAp1) * ia0; | |
| a2 = (Ap1 + cAm1 - bs) * ia0; | |
| b0 = A * (Ap1 - cAm1 + bs) * ia0; | |
| b2 = A * (Ap1 - cAm1 - bs) * ia0; | |
| } | |
| void render(StereoSample *buf, sInt nsamples) | |
| { | |
| if (!enabled) | |
| return; | |
| COVER("BOOST render"); | |
| for (sInt ch=0; ch < 2; ch++) | |
| { | |
| sF32 xm1 = x1[ch], xm2 = x2[ch]; | |
| sF32 ym1 = y1[ch], ym2 = y2[ch]; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 x = buf[i].ch[ch] + fcdcoffset; | |
| // Second-order IIR filter | |
| sF32 y = b0*x + b1*xm1 + b2*xm2 - a1*ym1 - a2*ym2; | |
| ym2 = ym1; ym1 = y; | |
| xm2 = xm1; xm1 = x; | |
| buf[i].ch[ch] = y; | |
| } | |
| x1[ch] = xm1; x2[ch] = xm2; | |
| y1[ch] = ym1; y2[ch] = ym2; | |
| } | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Modulating delay | |
| // -------------------------------------------------------------------------- | |
| struct syVModDel | |
| { | |
| sF32 amount; // dry/wet value (0=-wet, 64=dry, 127=wet) | |
| sF32 fb; // feedback (0=-100%, 64=0%, 127=~100%) | |
| sF32 llength; // length of left delay | |
| sF32 rlength; // length of right delay | |
| sF32 mrate; // modulation rate | |
| sF32 mdepth; // modulation depth | |
| sF32 mphase; // modulation stereo phase (0=-180deg, 64=0deg, 127=180deg) | |
| }; | |
| struct V2ModDel | |
| { | |
| sF32 *db[2]; // left/right delay buffer | |
| sU32 dbufmask; // delay buffer mask (size-1, must be pow2) | |
| sU32 dbptr; // buffer write pos | |
| sU32 dboffs[2]; // buffer read offset | |
| sU32 mcnt; // mod counter | |
| sInt mfreq; // mod freq | |
| sU32 mphase; // mod phase | |
| sU32 mmaxoffs; // mod max offs (2048samples*depth) | |
| sF32 fbval; // feedback val | |
| sF32 dryout; | |
| sF32 wetout; | |
| V2Instance *inst; | |
| void init(V2Instance *instance, sF32 *buf1, sF32 *buf2, sInt buflen) | |
| { | |
| assert(buflen != 0 && (buflen & (buflen - 1)) == 0); | |
| db[0] = buf1; | |
| db[1] = buf2; | |
| dbufmask = buflen - 1; | |
| inst = instance; | |
| reset(); | |
| } | |
| void reset() | |
| { | |
| dbptr = 0; | |
| mcnt = 0; | |
| memset(db[0], 0, (dbufmask + 1) * sizeof(sF32)); | |
| memset(db[1], 0, (dbufmask + 1) * sizeof(sF32)); | |
| } | |
| void set(const syVModDel *para) | |
| { | |
| wetout = (para->amount - 64.0f) / 64.0f; | |
| dryout = 1.0f - fabsf(wetout); | |
| fbval = (para->fb - 64.0f) / 64.0f; | |
| sF32 lenscale = ((sF32)dbufmask - 1023.0f) / 128.0f; | |
| dboffs[0] = (sInt)(para->llength * lenscale); | |
| dboffs[1] = (sInt)(para->rlength * lenscale); | |
| mfreq = (sInt)(inst->SRfclinfreq * fcmdlfomul * calcfreq(para->mrate / 128.0f)); | |
| mmaxoffs = (sInt)(para->mdepth * 1023.0f / 128.0f); | |
| mphase = ftou32((para->mphase - 64.0f) / 128.0f); | |
| } | |
| void renderAux2Main(StereoSample *dest, sInt nsamples) | |
| { | |
| if (!wetout) | |
| return; | |
| COVER("MODDEL aux->main"); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| StereoSample x; | |
| sF32 in = inst->aux2buf[i] + fcdcoffset; | |
| processSample(&x, in, in, 0.0f); | |
| dest[i].l += x.l; | |
| dest[i].r += x.r; | |
| } | |
| } | |
| void renderChan(StereoSample *chanbuf, sInt nsamples) | |
| { | |
| if (!wetout) | |
| return; | |
| COVER("MODDEL chan"); | |
| sF32 dry = dryout; | |
| for (sInt i=0; i < nsamples; i++) | |
| processSample(&chanbuf[i], chanbuf[i].l + fcdcoffset, chanbuf[i].r + fcdcoffset, dry); | |
| } | |
| private: | |
| inline sF32 processChanSample(sF32 in, sInt ch, sF32 dry) | |
| { | |
| // modulation is a triangle wave | |
| sU32 counter = mcnt + (ch ? mphase : 0); | |
| counter = (counter < 0x80000000u) ? counter*2 : 0xffffffffu - counter*2; | |
| // determine effective offset | |
| sU64 offs32_32 = (sU64)counter * mmaxoffs; // 32.32 fixed point | |
| sU32 offs_int = sU32(offs32_32 >> 32) + dboffs[ch]; | |
| sU32 index = dbptr - offs_int; | |
| // linear interpolation using low-order bits of offs32_32. | |
| sF32 *delaybuf = db[ch]; | |
| sF32 x = utof23((sU32)(offs32_32 & 0xffffffffu)); | |
| sF32 delayed = lerp(delaybuf[(index - 0) & dbufmask], delaybuf[(index - 1) & dbufmask], x); | |
| // mix and output | |
| delaybuf[dbptr] = in + delayed*fbval; | |
| return in*dry + delayed*wetout; | |
| } | |
| inline void processSample(StereoSample *out, sF32 l, sF32 r, sF32 dry) | |
| { | |
| out->l = processChanSample(l, 0, dry); | |
| out->r = processChanSample(r, 1, dry); | |
| // tick | |
| mcnt += mfreq; | |
| dbptr = (dbptr + 1) & dbufmask; | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Stereo Compressor | |
| // -------------------------------------------------------------------------- | |
| struct syVComp | |
| { | |
| sF32 mode; // 0=off, 1=Peak, 2=RMS | |
| sF32 stereo; // 0=mono, 1=stereo | |
| sF32 autogain; // 0=off, 1=on | |
| sF32 lookahead; // lookahead in ms | |
| sF32 threshold; // threshold (-54dB .. 6dB) | |
| sF32 ratio; // (0=1:1 .. 127=1:inf) | |
| sF32 attack; // attack value | |
| sF32 release; // release value | |
| sF32 outgain; // output gain | |
| }; | |
| struct V2Comp | |
| { | |
| static const int COMPDLEN = 5700; | |
| static const int RMSLEN = 8192; // must be a power of 2 | |
| enum Mode | |
| { | |
| MODE_OFF = 0, | |
| MODE_PEAK, | |
| MODE_RMS, | |
| }; | |
| enum ModeBits | |
| { | |
| MODE_BIT_PEAK = 0, | |
| MODE_BIT_RMS = 1, | |
| MODE_BIT_MONO = 0, | |
| MODE_BIT_STEREO = 2, | |
| MODE_BIT_ON = 0, | |
| MODE_BIT_OFF = 4, | |
| }; | |
| sInt mode; // bit 0: Peak/RMS, bit 1: Stereo, bit 2: off | |
| sF32 invol; // input gain (1/threshold, internal threshold is always 0dB) | |
| sF32 ratio; | |
| sF32 outvol; // output gain (outgain * threshold) | |
| sF32 attack; // attack (lpf coeff, 0..1) | |
| sF32 release; // release (lpf coeff, 0..1) | |
| sU32 dblen; // lookahead buffer length | |
| sU32 dbcnt; // lookahead buffer offset | |
| sF32 curgain[2]; // current gain | |
| sF32 peakval[2]; // peak value | |
| sF32 rmsval[2]; // rms current value | |
| sU32 rmscnt; // rms counter | |
| StereoSample dbuf[COMPDLEN]; // lookahead delay buffer | |
| StereoSample rmsbuf[RMSLEN]; // RMS ring buffer | |
| V2Instance *inst; | |
| void init(V2Instance *instance) | |
| { | |
| mode = MODE_BIT_STEREO; | |
| memset(dbuf, 0, sizeof(dbuf)); | |
| inst = instance; | |
| reset(); | |
| } | |
| void reset() | |
| { | |
| for (sInt i=0; i < 2; i++) | |
| { | |
| peakval[i] = 0.0f; | |
| rmsval[i] = 0.0f; | |
| curgain[i] = 1.0f; | |
| } | |
| memset(rmsbuf, 0, sizeof(rmsbuf)); | |
| rmscnt = 0; | |
| } | |
| void set(const syVComp *para) | |
| { | |
| sInt oldmode = mode; | |
| switch ((sInt)para->mode) | |
| { | |
| case MODE_OFF: mode = MODE_BIT_OFF; break; | |
| case MODE_PEAK: mode = MODE_BIT_PEAK | MODE_BIT_ON; break; | |
| case MODE_RMS: mode = MODE_BIT_RMS | MODE_BIT_ON; break; | |
| default: assert(false); | |
| } | |
| if (para->stereo != 0.0f) | |
| mode |= MODE_BIT_STEREO; | |
| if (mode != oldmode) | |
| reset(); | |
| // @@@BUG: original V2 code uses "fcsamplesperms" here which is | |
| // hard-coded to 44.1kHz | |
| dblen = (sInt)(para->lookahead * inst->SRfcsamplesperms); | |
| sF32 thresh = 8.0f * calcfreq(para->threshold / 128.0f); | |
| invol = 1.0f / thresh; | |
| if (para->autogain != 0.0f) | |
| thresh = 1.0f; | |
| outvol = thresh * powf(2.0f, (para->outgain - 64.0f) / 16.0f); | |
| ratio = para->ratio / 128.0f; | |
| // attack: 0 (!) ... 200ms (5Hz) | |
| attack = powf(2.0f, -para->attack * 12.0f / 128.0f); | |
| // release: 5ms .. 5s | |
| release = powf(2.0f, -para->release * 16.0f / 128.0f); | |
| } | |
| void render(StereoSample *buf, sInt nsamples) | |
| { | |
| if (mode & MODE_BIT_OFF) | |
| return; | |
| COVER("COMP render"); | |
| // Step 1: level detect (fills LD buffers) | |
| StereoSample *levels = inst->levelbuf; | |
| switch (mode & (MODE_BIT_RMS | MODE_BIT_STEREO)) | |
| { | |
| case MODE_BIT_PEAK | MODE_BIT_MONO: | |
| COVER("COMP level peak mono"); | |
| for (sInt i=0; i < nsamples; i++) | |
| levels[i].l = levels[i].r = invol * doPeak(0.5f * (buf[i].l + buf[i].r), 0); | |
| break; | |
| case MODE_BIT_RMS | MODE_BIT_MONO: | |
| COVER("COMP level rms mono"); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| levels[i].l = levels[i].r = invol * doRMS(0.5f * (buf[i].l + buf[i].r), 0); | |
| rmscnt = (rmscnt + 1) & (RMSLEN - 1); | |
| } | |
| break; | |
| case MODE_BIT_PEAK | MODE_BIT_STEREO: | |
| COVER("COMP level peak stereo"); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| levels[i].l = invol * doPeak(buf[i].l, 0); | |
| levels[i].r = invol * doPeak(buf[i].r, 1); | |
| } | |
| break; | |
| case MODE_BIT_RMS | MODE_BIT_STEREO: | |
| COVER("COMP level rms stereo"); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| levels[i].l = invol * doRMS(buf[i].l, 0); | |
| levels[i].r = invol * doRMS(buf[i].r, 1); | |
| rmscnt = (rmscnt + 1) & (RMSLEN - 1); | |
| } | |
| break; | |
| } | |
| // Step 2: compress! | |
| for (sInt ch=0; ch < 2; ch++) | |
| { | |
| sF32 gain = curgain[ch]; | |
| sU32 dbind = dbcnt; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| // lookahead delay line | |
| sF32 v = outvol * dbuf[dbind].ch[ch]; | |
| dbuf[dbind].ch[ch] = invol * buf[i].ch[ch]; | |
| if (++dbind >= dblen) | |
| dbind = 0; | |
| // determine dest gain | |
| sF32 dgain = 1.0f; | |
| sF32 lvl = levels[i].ch[ch]; | |
| if (lvl >= 1.0f) | |
| dgain = 1.0f / (1.0f + ratio * (lvl - 1.0f)); | |
| // and compress | |
| gain += (dgain < gain ? attack : release) * (dgain - gain); | |
| buf[i].ch[ch] = v * gain; | |
| } | |
| curgain[ch] = gain; | |
| if (ch == 1) | |
| dbcnt = dbind; | |
| } | |
| } | |
| private: | |
| // level detection variants | |
| inline sF32 doPeak(sF32 in, sInt ch) | |
| { | |
| peakval[ch] = max(peakval[ch] * fccpdfalloff + fcdcoffset, fabsf(in)); | |
| return peakval[ch]; | |
| } | |
| inline sF32 doRMS(sF32 in, sInt ch) | |
| { | |
| sF32 insq = sqr(in + fcdcoffset); | |
| rmsval[ch] += insq - rmsbuf[rmscnt].ch[ch]; // add new sample, remove oldest | |
| rmsbuf[rmscnt].ch[ch] = insq; // keep track of value we added | |
| return sqrtf(rmsval[ch] / (sF32)RMSLEN); | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Stereo reverb | |
| // -------------------------------------------------------------------------- | |
| struct syVReverb | |
| { | |
| sF32 revtime; | |
| sF32 highcut; | |
| sF32 lowcut; | |
| sF32 vol; | |
| }; | |
| struct V2Reverb | |
| { | |
| sF32 gainc[4]; // feedback gain for comb filter delays 0-3 | |
| sF32 gaina[2]; // feedback gain for allpas delays 0-1 | |
| sF32 gainin; // input gain | |
| sF32 damp; // high cut (1-val^2) | |
| sF32 lowcut; // low cut (val^2) | |
| V2Delay combd[2][4]; // left/right comb filter delay lines | |
| sF32 combl[2][4]; // left/right comb delay filter buffers | |
| V2Delay alld[2][2]; // left/right allpass filters | |
| sF32 hpf[2]; // memory for low cut filters | |
| V2Instance *inst; | |
| // delay line buffers | |
| sF32 bcombl0[1309]; | |
| sF32 bcombl1[1635]; | |
| sF32 bcombl2[1811]; | |
| sF32 bcombl3[1926]; | |
| sF32 balll0[220]; | |
| sF32 balll1[74]; | |
| sF32 bcombr0[1327]; | |
| sF32 bcombr1[1631]; | |
| sF32 bcombr2[1833]; | |
| sF32 bcombr3[1901]; | |
| sF32 ballr0[205]; | |
| sF32 ballr1[77]; | |
| void init(V2Instance *instance) | |
| { | |
| // init filters | |
| combd[0][0].init(bcombl0); | |
| combd[0][1].init(bcombl1); | |
| combd[0][2].init(bcombl2); | |
| combd[0][3].init(bcombl3); | |
| alld[0][0].init(balll0); | |
| alld[0][1].init(balll1); | |
| combd[1][0].init(bcombr0); | |
| combd[1][1].init(bcombr1); | |
| combd[1][2].init(bcombr2); | |
| combd[1][3].init(bcombr3); | |
| alld[1][0].init(ballr0); | |
| alld[1][1].init(ballr1); | |
| inst = instance; | |
| reset(); | |
| } | |
| void reset() | |
| { | |
| for (sInt ch=0; ch < 2; ch++) | |
| { | |
| // comb | |
| for (sInt i=0; i < 4; i++) | |
| { | |
| combd[ch][i].reset(); | |
| combl[ch][i] = 0.0f; | |
| } | |
| // allpass | |
| for (sInt i=0; i < 2; i++) | |
| alld[ch][i].reset(); | |
| // low cut | |
| hpf[ch] = 0.0f; | |
| } | |
| } | |
| void set(const syVReverb *para) | |
| { | |
| static const sF32 gaincdef[4] = { | |
| 0.966384599f, 0.958186359f, 0.953783929f, 0.950933178f | |
| }; | |
| static const sF32 gainadef[2] = { | |
| 0.994260075f, 0.998044717f | |
| }; | |
| sF32 e = inst->SRfclinfreq * sqr(64.0f / (para->revtime + 1.0f)); | |
| for (sInt i=0; i < 4; i++) | |
| gainc[i] = powf(gaincdef[i], e); | |
| for (sInt i=0; i < 2; i++) | |
| gaina[i] = powf(gainadef[i], e); | |
| damp = inst->SRfclinfreq * (para->highcut / 128.0f); | |
| gainin = para->vol / 128.0f; | |
| lowcut = inst->SRfclinfreq * sqr(sqr(para->lowcut / 128.0f)); | |
| } | |
| void render(StereoSample *dest, sInt nsamples) | |
| { | |
| const sF32 *inbuf = inst->aux1buf; | |
| COVER("RVB render"); | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| sF32 in = inbuf[i] * gainin + fcdcoffset; | |
| for (sInt ch=0; ch < 2; ch++) | |
| { | |
| // parallel comb filters | |
| sF32 cur = 0.0f; | |
| for (sInt j=0; j < 4; j++) | |
| { | |
| sF32 dv = gainc[j] * combd[ch][j].fetch(); | |
| sF32 nv = (j & 1) ? (dv - in) : (dv + in); // alternate phase on combs | |
| sF32 lp = combl[ch][j] + damp * (nv - combl[ch][j]); | |
| combd[ch][j].feed(lp); | |
| cur += lp; | |
| } | |
| // serial allpass filters | |
| for (sInt j=0; j < 2; j++) | |
| { | |
| sF32 dv = alld[ch][j].fetch(); | |
| sF32 dz = cur + gaina[j] * dv; | |
| alld[ch][j].feed(dz); | |
| cur = dv - gaina[j] * dz; | |
| } | |
| // low cut and output | |
| hpf[ch] += lowcut * (cur - hpf[ch]); | |
| dest[i].ch[ch] += cur - hpf[ch]; | |
| } | |
| } | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Channel | |
| // -------------------------------------------------------------------------- | |
| struct syVChan | |
| { | |
| sF32 chanvol; | |
| sF32 auxarcv; // aux a receive | |
| sF32 auxbrcv; // aux b receive | |
| sF32 auxasnd; // aux a send | |
| sF32 auxbsnd; // aux b send | |
| sF32 aux1; | |
| sF32 aux2; | |
| sF32 fxroute; | |
| syVBoost boost; | |
| syVDist dist; | |
| syVModDel chorus; | |
| syVComp comp; | |
| }; | |
| struct V2Chan | |
| { | |
| enum FXRouting | |
| { | |
| FXR_DIST_THEN_CHORUS = 0, | |
| FXR_CHORUS_THEN_DIST, | |
| }; | |
| sF32 chgain; // channel gain | |
| sF32 a1gain; // aux1 gain | |
| sF32 a2gain; // aux2 gain | |
| sF32 aasnd; // aux a send gain | |
| sF32 absnd; // aux b send gain | |
| sF32 aarcv; // aux a receive gain | |
| sF32 abrcv; // aux b receive gain | |
| sInt fxr; | |
| V2DCFilter dcf1; | |
| V2Boost boost; | |
| V2Dist dist; | |
| V2DCFilter dcf2; | |
| V2ModDel chorus; | |
| V2Comp comp; | |
| V2Instance *inst; | |
| void init(V2Instance *instance, sF32 *delbuf1, sF32 *delbuf2, sInt buflen) | |
| { | |
| inst = instance; | |
| dcf1.init(inst); | |
| boost.init(inst); | |
| dist.init(inst); | |
| dcf2.init(inst); | |
| chorus.init(inst, delbuf1, delbuf2, buflen); | |
| comp.init(inst); | |
| } | |
| void set(const syVChan *para) | |
| { | |
| aarcv = para->auxarcv / 128.0f; | |
| abrcv = para->auxbrcv / 128.0f; | |
| aasnd = fcgain * (para->auxasnd / 128.0f); | |
| absnd = fcgain * (para->auxbsnd / 128.0f); | |
| chgain = fcgain * (para->chanvol / 128.0f); | |
| a1gain = chgain * fcgainh * (para->aux1 / 128.0f); | |
| a2gain = chgain * fcgainh * (para->aux2 / 128.0f); | |
| fxr = (sInt)para->fxroute; | |
| dist.set(¶->dist); | |
| chorus.set(¶->chorus); | |
| comp.set(¶->comp); | |
| boost.set(¶->boost); | |
| } | |
| void process(sInt nsamples) | |
| { | |
| StereoSample *chan = inst->chanbuf; | |
| // AuxA/B receive (stereo) | |
| accumulate(chan, inst->auxabuf, nsamples, aarcv); | |
| accumulate(chan, inst->auxbbuf, nsamples, abrcv); | |
| // Filters | |
| dcf1.renderStereo(chan, chan, nsamples); | |
| DEBUG_PLOT_STEREO(&dcf1, chan, nsamples); | |
| comp.render(chan, nsamples); | |
| boost.render(chan, nsamples); | |
| if (fxr == FXR_DIST_THEN_CHORUS) | |
| { | |
| dist.renderStereo(chan, chan, nsamples); | |
| dcf2.renderStereo(chan, chan, nsamples); | |
| chorus.renderChan(chan, nsamples); | |
| } | |
| else // FXR_CHORUS_THEN_DIST | |
| { | |
| chorus.renderChan(chan, nsamples); | |
| dist.renderStereo(chan, chan, nsamples); | |
| dcf2.renderStereo(chan, chan, nsamples); | |
| } | |
| // Aux1/2 send (mono) | |
| accumulateMonoMix(inst->aux1buf, chan, nsamples, a1gain); | |
| accumulateMonoMix(inst->aux2buf, chan, nsamples, a2gain); | |
| // AuxA/B send (stereo) | |
| accumulate(inst->auxabuf, chan, nsamples, aasnd); | |
| accumulate(inst->auxbbuf, chan, nsamples, absnd); | |
| // Channel buffer to mix buffer (stereo) | |
| accumulate(inst->mixbuf, chan, nsamples, chgain); | |
| DEBUG_PLOT_STEREO(this, chan, nsamples); | |
| } | |
| private: | |
| void accumulate(StereoSample *dest, const StereoSample *src, sInt nsamples, sF32 gain) | |
| { | |
| if (gain == 0.0f) | |
| return; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| dest[i].l += gain * src[i].l; | |
| dest[i].r += gain * src[i].r; | |
| } | |
| } | |
| void accumulateMonoMix(sF32 *dest, const StereoSample *src, sInt nsamples, sF32 gain) | |
| { | |
| if (gain == 0.0f) | |
| return; | |
| for (sInt i=0; i < nsamples; i++) | |
| dest[i] += gain * (src[i].l + src[i].r); | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Sound definitions | |
| // -------------------------------------------------------------------------- | |
| struct V2Mod | |
| { | |
| sU8 source; // source: vel/ctl1-7/aenv/env2/lfo1/lfo2 | |
| sU8 val; // 0=-1 .. 128=1 | |
| sU8 dest; // destination (index into V2Sound) | |
| }; | |
| struct V2Sound | |
| { | |
| sU8 voice[sizeof(syVV2) / sizeof(sF32)]; | |
| sU8 chan[sizeof(syVChan) / sizeof(sF32)]; | |
| sU8 maxpoly; | |
| sU8 modnum; | |
| V2Mod modmatrix[1]; // actually modnum entries! | |
| }; | |
| union V2PatchMap | |
| { | |
| sU32 offsets[128]; // offsets into raw_data[] | |
| sU8 raw_data[1]; // variable size | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // Ronan | |
| // -------------------------------------------------------------------------- | |
| struct syWRonan | |
| { | |
| sU8 mem[64*1024]; // "that should be enough" --synth.asm. :) | |
| }; | |
| #ifdef RONAN | |
| extern "C" | |
| { | |
| void __stdcall ronanCBInit(syWRonan *pthis); | |
| void __stdcall ronanCBTick(syWRonan *pthis); | |
| void __stdcall ronanCBNoteOn(syWRonan *pthis); | |
| void __stdcall ronanCBNoteOff(syWRonan *pthis); | |
| void __stdcall ronanCBSetCtl(syWRonan *pthis, sU32 ctl, sU32 val); | |
| void __stdcall ronanCBProcess(syWRonan *pthis, sF32 *buf, sU32 len); | |
| void __stdcall ronanCBSetSR(syWRonan *pthis, sInt samplerate); | |
| } | |
| #else | |
| static inline void ronanCBInit(syWRonan *) {} | |
| static inline void ronanCBTick(syWRonan *) {} | |
| static inline void ronanCBNoteOn(syWRonan *) {} | |
| static inline void ronanCBNoteOff(syWRonan *) {} | |
| static inline void ronanCBSetCtl(syWRonan *, sU32, sU32) {} | |
| static inline void ronanCBProcess(syWRonan *, sF32 *, sU32) {} | |
| static inline void ronanCBSetSR(syWRonan *, sInt) {} | |
| extern "C" void __stdcall synthSetLyrics(void *, const char **) {} | |
| #endif | |
| // -------------------------------------------------------------------------- | |
| // Synth | |
| // -------------------------------------------------------------------------- | |
| struct V2ChanInfo | |
| { | |
| sU8 pgm; // program | |
| sU8 ctl[7]; // controllers | |
| }; | |
| // V2Synth holds a V2Instance. | |
| // In the original code these are one and the same struct (SYN) but that | |
| // would turn out fairly awkward in this C++ version, hence the split. | |
| struct V2Synth | |
| { | |
| static const sInt POLY = 64; | |
| static const sInt CHANS = 16; | |
| const V2PatchMap *patchmap; | |
| sU32 mrstat; // running status in MIDI decoding | |
| sU32 curalloc; | |
| sInt samplerate; | |
| sInt chanmap[POLY]; // voice -> chan | |
| sU32 allocpos[POLY]; | |
| sInt voicemap[CHANS]; // chan -> choice | |
| sInt tickd; // number of finished samples left in mix buffer | |
| V2ChanInfo chans[CHANS]; | |
| syVV2 voicesv[POLY]; | |
| V2Voice voicesw[POLY]; | |
| syVChan chansv[CHANS]; | |
| V2Chan chansw[CHANS]; | |
| struct Globals | |
| { | |
| syVReverb rvbparm; | |
| syVModDel delparm; | |
| sF32 vlowcut; | |
| sF32 vhighcut; | |
| syVComp cprparm; | |
| sU8 guicolor; | |
| sU8 _pad[3]; | |
| } globals; | |
| V2Reverb reverb; | |
| V2ModDel delay; | |
| V2DCFilter dcf; | |
| V2Comp compr; | |
| sF32 lcfreq; // low cut freq | |
| sF32 lcbuf[2]; // low cut buf l/r | |
| sF32 hcfreq; // high cut freq | |
| sF32 hcbuf[2]; // high cut buf l/r | |
| bool initialized; | |
| // delay buffers | |
| sF32 maindelbuf[2][32768]; | |
| sF32 chandelbuf[CHANS][2][2048]; | |
| V2Instance instance; | |
| syWRonan ronan; | |
| void init(const void *patchmap, sInt samplerate) | |
| { | |
| // Ahem, so this is somewhat dubious, but we don't use | |
| // virtual functions or anything so it should be fine. Ahem. | |
| // Look away please :) | |
| memset(this, 0, sizeof(this)); | |
| // set sampling rate | |
| this->samplerate = samplerate; | |
| instance.calcNewSampleRate(samplerate); | |
| ronanCBSetSR(&ronan, samplerate); | |
| // patch map | |
| this->patchmap = (const V2PatchMap*)patchmap; | |
| // init voices | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| chanmap[i] = -1; | |
| voicesw[i].init(&instance); | |
| } | |
| // init channels | |
| for (sInt i=0; i < CHANS; i++) | |
| { | |
| chans[i].ctl[6] = 0x7f; | |
| chansw[i].init(&instance, chandelbuf[i][0], chandelbuf[i][1], COUNTOF(chandelbuf[i][0])); | |
| } | |
| // global filters | |
| reverb.init(&instance); | |
| delay.init(&instance, maindelbuf[0], maindelbuf[1], COUNTOF(maindelbuf[0])); | |
| ronanCBInit(&ronan); | |
| compr.init(&instance); | |
| dcf.init(&instance); | |
| // debug plots (uncomment the ones you want) | |
| sInt sr_plot = 44100/10; // plot rate | |
| sInt sr_lfo = 800; | |
| sInt w = 800, h = 150; | |
| //DEBUG_PLOT_OPEN(&voicesw[1].osc[0], "Voice 1 VCO 0", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(&voicesw[1].osc[1], "Voice 1 VCO 1", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(&voicesw[1].vcf[0], "Voice 1 VCF 0", sr_plot, w, h); | |
| DEBUG_PLOT_OPEN(&voicesw[1].env[0], "Voice 1 Env 0", sr_lfo, w, h); | |
| //DEBUG_PLOT_OPEN(&voicesw[1].lfo[0], "Voice 1 LFO 0", sr_lfo, w, h); | |
| //DEBUG_PLOT_OPEN(&voicesw[1].dist, "Voice 1 Dist", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(&voicesw[1], "Voice 1 final", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(DEBUG_PLOT_CHAN(&chansw[0].dcf1, 0), "Chan 0 DCF1 L", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(DEBUG_PLOT_CHAN(&chansw[0].dcf1, 1), "Chan 0 DCF1 R", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(DEBUG_PLOT_CHAN(&chansw[0], 0), "Channel 0 L", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(DEBUG_PLOT_CHAN(&chansw[0], 1), "Channel 0 R", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(DEBUG_PLOT_CHAN(&instance.mixbuf, 0), "Mix L", sr_plot, w, h); | |
| //DEBUG_PLOT_OPEN(DEBUG_PLOT_CHAN(&instance.mixbuf, 1), "Mix R", sr_plot, w, h); | |
| initialized = true; | |
| } | |
| void render(sF32 *buf, sInt nsamples, sF32 *buf2, bool add) | |
| { | |
| sInt todo = nsamples; | |
| // fragment loop - chunk everything into frames. | |
| while (todo) | |
| { | |
| // do we need to render a new frame? | |
| if (!tickd) | |
| tick(); | |
| // copy to dest buffer(s) | |
| const StereoSample *src = &instance.mixbuf[instance.SRcFrameSize - tickd]; | |
| sInt nread = min(todo, tickd); | |
| if (!buf2) // interleaved samples | |
| { | |
| if (!add) | |
| { | |
| COVER("OUT interleaved set"); | |
| memcpy(buf, src, nread * sizeof(StereoSample)); | |
| } | |
| else | |
| { | |
| COVER("OUT interleaved add"); | |
| for (sInt i=0; i < nread; i++) | |
| { | |
| buf[i*2+0] += src[i].l; | |
| buf[i*2+1] += src[i].r; | |
| } | |
| } | |
| buf += 2*nread; | |
| } | |
| else // buf = left, buf2 = right | |
| { | |
| if (!add) | |
| { | |
| COVER("OUT separate set"); | |
| for (sInt i=0; i < nread; i++) | |
| { | |
| buf[i] = src[i].l; | |
| buf2[i] = src[i].r; | |
| } | |
| } | |
| else | |
| { | |
| COVER("OUT separate add"); | |
| for (sInt i=0; i < nread; i++) | |
| { | |
| buf[i] += src[i].l; | |
| buf2[i] += src[i].r; | |
| } | |
| } | |
| buf += nread; | |
| buf2 += nread; | |
| } | |
| todo -= nread; | |
| tickd -= nread; | |
| } | |
| DEBUG_PLOT_UPDATE(); | |
| } | |
| void processMIDI(const sU8 *cmd) | |
| { | |
| while (*cmd != 0xfd) // until end of stream | |
| { | |
| if (*cmd & 0x80) // start of message | |
| mrstat = *cmd++; | |
| if (mrstat < 0x80) // we don't have a current message? uhm... | |
| break; | |
| sInt chan = mrstat & 0xf; | |
| switch ((mrstat >> 4) & 7) | |
| { | |
| case 1: // Note on | |
| if (cmd[1] != 0) // velocity==0 is actually a note off | |
| { | |
| COVER("MIDI note on"); | |
| if (chan == CHANS-1) | |
| ronanCBNoteOn(&ronan); | |
| // calculate current polyphony for this channel | |
| const V2Sound *sound = getpatch(chans[chan].pgm); | |
| sInt npoly = 0; | |
| for (sInt i=0; i < POLY; i++) | |
| npoly += (chanmap[i] == chan); | |
| // voice allocation. this is equivalent to the original V2 code, | |
| // but hopefully simpler to follow. | |
| sInt usevoice = -1; | |
| sInt chanmask, chanfind; | |
| if (!npoly || npoly < sound->maxpoly) // even if maxpoly is 0, allow at least 1. | |
| { | |
| // if we haven't reached polyphony limit yet, try to find a free voice | |
| // first. | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| if (chanmap[i] < 0) | |
| { | |
| usevoice = i; | |
| break; | |
| } | |
| } | |
| // okay, need to find a free voice. we'll take any channel. | |
| COVER("SYN find voice any"); | |
| chanmask = 0; | |
| chanfind = 0; | |
| } | |
| else | |
| { | |
| // if we're at polyphony limit, we know there's at least one voice | |
| // used by this channel, so we can limit ourselves to killing | |
| // voices from our own chan. | |
| COVER("SYN find voice channel"); | |
| chanmask = 0xf; | |
| chanfind = chan; | |
| } | |
| // don't have a voice yet? kill oldest eligible one with gate off. | |
| if (usevoice < 0) | |
| { | |
| COVER("SYN replace voice gate off"); | |
| sU32 oldest = curalloc; | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| if ((chanmap[i] & chanmask) == chanfind && !voicesw[i].gate && allocpos[i] < oldest) | |
| { | |
| oldest = allocpos[i]; | |
| usevoice = i; | |
| } | |
| } | |
| } | |
| // still no voice? okay, just take the oldest one we can find, period. | |
| if (usevoice < 0) | |
| { | |
| COVER("SYN replace voice oldest"); | |
| sU32 oldest = curalloc; | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| if ((chanmap[i] & chanmask) == chanfind && allocpos[i] < oldest) | |
| { | |
| oldest = allocpos[i]; | |
| usevoice = i; | |
| } | |
| } | |
| } | |
| // we have our voice - assign it! | |
| assert(usevoice >= 0); | |
| chanmap[usevoice] = chan; | |
| voicemap[chan] = usevoice; | |
| allocpos[usevoice] = curalloc++; | |
| // and note on! | |
| storeV2Values(usevoice); | |
| voicesw[usevoice].noteOn(cmd[0], cmd[1]); | |
| cmd += 2; | |
| break; | |
| } | |
| // fall-through (for when we had a note off) | |
| case 0: // Note off | |
| COVER("MIDI note off"); | |
| if (chan == CHANS-1) | |
| ronanCBNoteOff(&ronan); | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| if (chanmap[i] != chan) | |
| continue; | |
| V2Voice *voice = &voicesw[i]; | |
| if (voice->note == cmd[0] && voice->gate) | |
| voice->noteOff(); | |
| } | |
| cmd += 2; | |
| break; | |
| case 2: // Aftertouch | |
| COVER("MIDI aftertouch"); | |
| cmd++; // ignored | |
| break; | |
| case 3: // Control change | |
| COVER("MIDI controller change"); | |
| { | |
| sInt ctrl = cmd[0]; | |
| sU8 val = cmd[1]; | |
| if (ctrl >= 1 && ctrl <= 7) | |
| { | |
| chans[chan].ctl[ctrl - 1] = val; | |
| if (chan == CHANS-1) | |
| ronanCBSetCtl(&ronan, ctrl, val); | |
| } | |
| else if (ctrl == 120) // CC #120: all sound off | |
| { | |
| COVER("MIDI CC all sound off"); | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| if (chanmap[i] != chan) | |
| continue; | |
| voicesw[i].init(&instance); | |
| chanmap[i] = -1; | |
| } | |
| } | |
| else if (ctrl == 123) // CC #123: all notes off | |
| { | |
| COVER("MIDI CC all notes off"); | |
| if (chan == CHANS-1) | |
| ronanCBNoteOff(&ronan); | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| if (chanmap[i] == chan) | |
| voicesw[i].noteOff(); | |
| } | |
| } | |
| } | |
| cmd += 2; | |
| break; | |
| case 4: // Program change | |
| COVER("MIDI program change"); | |
| { | |
| sU8 pgm = *cmd++ & 0x7f; | |
| // did the program actually change? | |
| if (chans[chan].pgm != pgm) | |
| { | |
| COVER("MIDI program change real"); | |
| chans[chan].pgm = pgm; | |
| // need to turn all voices on this channel off. | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| if (chanmap[i] == chan) | |
| chanmap[i] = -1; | |
| } | |
| } | |
| // either way, reset controllers | |
| for (sInt i=0; i < 6; i++) | |
| chans[chan].ctl[i] = 0; | |
| } | |
| break; | |
| case 5: // Pitch bend | |
| COVER("MIDI pitch bend"); | |
| cmd += 2; // ignored | |
| break; | |
| case 6: // Poly Aftertouch | |
| COVER("MIDI poly aftertouch"); | |
| cmd += 2; // ignored | |
| break; | |
| case 7: // System | |
| COVER("MIDI system exclusive"); | |
| if (chan == 0xf) // Reset | |
| init(patchmap, samplerate); | |
| break; // rest ignored | |
| } | |
| } | |
| } | |
| void setGlobals(const sU8 *para) | |
| { | |
| if (!initialized) | |
| return; | |
| // copy over | |
| sF32 *globf = (sF32 *)&globals; | |
| for (sInt i=0; i < sizeof(globals)/sizeof(sF32); i++) | |
| globf[i] = para[i]; | |
| // set | |
| reverb.set(&globals.rvbparm); | |
| delay.set(&globals.delparm); | |
| compr.set(&globals.cprparm); | |
| lcfreq = sqr((globals.vlowcut + 1.0f) / 128.0f); | |
| hcfreq = sqr((globals.vhighcut + 1.0f) / 128.0f); | |
| } | |
| void getPoly(sInt *dest) | |
| { | |
| for (sInt i=0; i <= CHANS; i++) | |
| dest[i] = 0; | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| sInt chan = chanmap[i]; | |
| if (chan < 0) | |
| continue; | |
| dest[chan]++; | |
| dest[CHANS]++; | |
| } | |
| } | |
| void getPgm(sInt *dest) | |
| { | |
| for (sInt i=0; i < CHANS; i++) | |
| dest[i] = chans[i].pgm; | |
| } | |
| private: | |
| const V2Sound *getpatch(sInt pgm) const | |
| { | |
| assert(pgm >= 0 && pgm < 128); | |
| return (const V2Sound *)&patchmap->raw_data[patchmap->offsets[pgm]]; | |
| } | |
| sF32 getmodsource(const V2Voice *voice, sInt chan, sInt source) const | |
| { | |
| sF32 in = 0.0f; | |
| switch (source) | |
| { | |
| case 0: // velocity | |
| COVER("MOD src vel"); | |
| in = voice->velo; | |
| break; | |
| case 1: case 2: case 3: case 4: case 5: case 6: case 7: // controller value | |
| COVER("MOD src ctl"); | |
| in = chans[chan].ctl[source-1]; | |
| break; | |
| case 8: case 9: // EG output | |
| COVER("MOD src EG"); | |
| in = voice->env[source-8].out; | |
| break; | |
| case 10: case 11: // LFO output | |
| COVER("MOD src LFO"); | |
| in = voice->lfo[source-10].out; | |
| break; | |
| default: // note | |
| COVER("MOD src note"); | |
| in = 2.0f * (voice->note - 48.0f); | |
| break; | |
| } | |
| return in; | |
| } | |
| void storeV2Values(sInt vind) | |
| { | |
| assert(vind >= 0 && vind < POLY); | |
| sInt chan = chanmap[vind]; | |
| if (chan < 0) | |
| return; | |
| // get patch definition | |
| const V2Sound *patch = getpatch(chans[chan].pgm); | |
| // voice data | |
| syVV2 *vpara = &voicesv[vind]; | |
| sF32 *vparaf = (sF32 *)vpara; | |
| V2Voice *voice = &voicesw[vind]; | |
| // copy voice dependent data | |
| for (sInt i=0; i < COUNTOF(patch->voice); i++) | |
| vparaf[i] = (sF32)patch->voice[i]; | |
| // modulation matrix | |
| for (sInt i=0; i < patch->modnum; i++) | |
| { | |
| const V2Mod *mod = &patch->modmatrix[i]; | |
| if (mod->dest >= COUNTOF(patch->voice)) | |
| continue; | |
| sF32 scale = (mod->val - 64.0f) / 64.0f; | |
| vparaf[mod->dest] = clamp(vparaf[mod->dest] + scale*getmodsource(voice, chan, mod->source), 0.0f, 128.0f); | |
| } | |
| voice->set(vpara); | |
| } | |
| void storeChanValues(sInt chan) | |
| { | |
| assert(chan >= 0 && chan < CHANS); | |
| // get patch definition | |
| const V2Sound *patch = getpatch(chans[chan].pgm); | |
| // chan data | |
| syVChan *cpara = &chansv[chan]; | |
| sF32 *cparaf = (sF32 *)cpara; | |
| V2Chan *cwork = &chansw[chan]; | |
| V2Voice *voice = &voicesw[voicemap[chan]]; | |
| // copy channel dependent data | |
| for (sInt i=0; i < COUNTOF(patch->chan); i++) | |
| cparaf[i] = (sF32)patch->chan[i]; | |
| // modulation matrix | |
| for (sInt i=0; i < patch->modnum; i++) | |
| { | |
| const V2Mod *mod = &patch->modmatrix[i]; | |
| sInt dest = mod->dest - COUNTOF(patch->voice); | |
| if (dest < 0 || dest >= COUNTOF(patch->chan)) | |
| continue; | |
| sF32 scale = (mod->val - 64.0f) / 64.0f; | |
| cparaf[dest] = clamp(cparaf[dest] + scale*getmodsource(voice, chan, mod->source), 0.0f, 128.0f); | |
| } | |
| cwork->set(cpara); | |
| } | |
| void tick() | |
| { | |
| // voices | |
| for (sInt i=0; i < POLY; i++) | |
| { | |
| if (chanmap[i] < 0) | |
| continue; | |
| storeV2Values(i); | |
| voicesw[i].tick(); | |
| // if EG1 finished, turn off voice | |
| if (voicesw[i].env[0].state == V2Env::OFF) | |
| chanmap[i] = -1; | |
| } | |
| // chans | |
| for (sInt i=0; i < CHANS; i++) | |
| storeChanValues(i); | |
| ronanCBTick(&ronan); | |
| tickd = instance.SRcFrameSize; | |
| renderFrame(); | |
| #if COVERAGE | |
| // print coverage updates as they happen | |
| static int cur_frame; | |
| static const char *old_coverage[COUNTOF(code_coverage)]; | |
| sInt ccount = synthGetNumCoverage(); | |
| for (sInt i=0; i < ccount; i++) | |
| { | |
| if (old_coverage[i] != code_coverage[i]) | |
| { | |
| old_coverage[i] = code_coverage[i]; | |
| printf("[%5d,%3d] %s\n", cur_frame, i, code_coverage[i]); | |
| } | |
| } | |
| cur_frame++; | |
| #endif | |
| } | |
| void renderFrame() | |
| { | |
| sInt nsamples = instance.SRcFrameSize; | |
| // clear output buffer | |
| memset(instance.mixbuf, 0, nsamples * sizeof(StereoSample)); | |
| // clear aux buffers | |
| memset(instance.aux1buf, 0, nsamples * sizeof(sF32)); | |
| memset(instance.aux2buf, 0, nsamples * sizeof(sF32)); | |
| memset(instance.auxabuf, 0, nsamples * sizeof(StereoSample)); | |
| memset(instance.auxbbuf, 0, nsamples * sizeof(StereoSample)); | |
| // process all channels | |
| for (sInt chan=0; chan < CHANS; chan++) | |
| { | |
| // check if any voices are active on this channel | |
| sInt voice = 0; | |
| while (voice < POLY && chanmap[voice] != chan) | |
| voice++; | |
| if (voice == POLY) | |
| continue; | |
| // clear channel buffer | |
| memset(instance.chanbuf, 0, nsamples * sizeof(StereoSample)); | |
| // render all voices on this channel | |
| for (; voice < POLY; voice++) | |
| { | |
| if (chanmap[voice] != chan) | |
| continue; | |
| voicesw[voice].render(instance.chanbuf, nsamples); | |
| } | |
| // channel 15 -> Ronan | |
| if (chan == CHANS-1) | |
| ronanCBProcess(&ronan, &instance.chanbuf[0].l, nsamples); | |
| chansw[chan].process(nsamples); | |
| } | |
| // global filters | |
| StereoSample *mix = instance.mixbuf; | |
| reverb.render(mix, nsamples); | |
| delay.renderAux2Main(mix, nsamples); | |
| dcf.renderStereo(mix, mix, nsamples); | |
| // low cut/high cut | |
| sF32 lcf = lcfreq, hcf = hcfreq; | |
| for (sInt i=0; i < nsamples; i++) | |
| { | |
| for (sInt ch=0; ch < 2; ch++) | |
| { | |
| // low cut | |
| sF32 x = mix[i].ch[ch] - lcbuf[ch]; | |
| lcbuf[ch] += lcf * x; | |
| // high cut | |
| if (hcf != 1.0f) | |
| { | |
| hcbuf[ch] += hcf * (x - hcbuf[ch]); | |
| x = hcbuf[ch]; | |
| } | |
| mix[i].ch[ch] = x; | |
| } | |
| } | |
| // sum compressor | |
| compr.render(mix, nsamples); | |
| DEBUG_PLOT_STEREO(mix, mix, nsamples); | |
| } | |
| }; | |
| // -------------------------------------------------------------------------- | |
| // C-style interface | |
| // -------------------------------------------------------------------------- | |
| unsigned int __stdcall synthGetSize() | |
| { | |
| return sizeof(V2Synth); | |
| } | |
| void __stdcall synthInit(void *pthis, const void *patchmap, int samplerate) | |
| { | |
| ((V2Synth *)pthis)->init(patchmap, samplerate); | |
| } | |
| void __stdcall synthRender(void *pthis, void *buf, int smp, void *buf2, int add) | |
| { | |
| ((V2Synth *)pthis)->render((sF32 *)buf, smp, (sF32 *)buf2, add != 0); | |
| } | |
| void __stdcall synthProcessMIDI(void *pthis, const void *ptr) | |
| { | |
| ((V2Synth *)pthis)->processMIDI((const sU8 *)ptr); | |
| } | |
| void __stdcall synthSetGlobals(void *pthis, const void *ptr) | |
| { | |
| ((V2Synth *)pthis)->setGlobals((const sU8 *)ptr); | |
| } | |
| void __stdcall synthGetPoly(void *pthis, void *dest) | |
| { | |
| ((V2Synth *)pthis)->getPoly((sInt*)dest); | |
| } | |
| void __stdcall synthGetPgm(void *pthis, void *dest) | |
| { | |
| ((V2Synth *)pthis)->getPgm((sInt*)dest); | |
| } | |
| void __stdcall synthSetVUMode(void *, int) | |
| { | |
| // nyi | |
| } | |
| void __stdcall synthGetChannelVU(void *, int, float *, float *) | |
| { | |
| // nyi | |
| } | |
| void __stdcall synthGetMainVU(void *, float *, float *) | |
| { | |
| // nyi | |
| } | |
| long __stdcall synthGetFrameSize(void *pthis) | |
| { | |
| return ((V2Synth *)pthis)->instance.SRcFrameSize; | |
| } | |
| extern "C" void * __stdcall synthGetSpeechMem(void *pthis) | |
| { | |
| return &((V2Synth *)pthis)->ronan; | |
| } | |
| #if COVERAGE | |
| void synthPrintCoverage() | |
| { | |
| int end = synthGetNumCoverage(); | |
| printf("synth coverage:\n"); | |
| for (int i=0; i < end; i++) | |
| printf("[%3d] %s\n", i, code_coverage[i] ? code_coverage[i] : "<not hit>"); | |
| } | |
| static int synthGetNumCoverage() | |
| { | |
| return __COUNTER__; | |
| } | |
| #endif | |
| // vim: sw=2:sts=2:et:cino=\:0l1g0(0 |