#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(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(&para->osc[i]);

    for (sInt i=0; i < syVV2::NENV; i++)
      env[i].set(&para->env[i]);

    for (sInt i=0; i < syVV2::NFLT; i++)
      vcf[i].set(&para->flt[i]);

    for (sInt i=0; i < syVV2::NLFO; i++)
      lfo[i].set(&para->lfo[i]);

    dist.set(&para->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(&para->dist);
    chorus.set(&para->chorus);
    comp.set(&para->comp);
    boost.set(&para->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
}

int __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
