const WARP_MODES = {
  Off: WavetableWarpMode.Off,
  Bend: WavetableWarpMode.Bend,
  Sync: WavetableWarpMode.Sync,
}

const MODULATION_MODES = ['Off', 'FM', 'AM', 'Ring'];

const SUB_WAVEFORMS = {
  'Saw': Waveform.Sawtooth,
  'Sine': Waveform.Sine,
  'Square': Waveform.Square,
}

const FILTER_TYPES = {
  'Low Pass': BiQuadFilterMode.LowPass,
  'High Pass': BiQuadFilterMode.HighPass,
  'Band Pass': BiQuadFilterMode.BandPass,
  'Notch': BiQuadFilterMode.Notch,
}

const usePitchModulator = (pitch, minPitch, maxPitch) => {
  const result = useMonoSignal();
  const range = useMonoSignal();
  subtract(pitch, minPitch, result);
  subtract(maxPitch, minPitch, range);
  divide(result, range, result);
  return result;
}

const useVelocityModulator = (velocity, velocityCurve) => {
  const result = useMonoSignal();
  for (let i = 0; i < velocity.length; i ++) {
    result[i] = Math.pow(velocity[i], Math.pow(2, velocityCurve[i] * -2))
  }
  return result;
}

const useMultipliedEnvelope = (curve, amplitude, trigger, gate, attack, decay, sustain, release) => {
  return multiply(
    amplitude,
    useEnvelope(curve === 'Linear' ? Curve.Linear : Curve.Log2)(trigger, gate, attack, decay, sustain, release),
    useMonoSignal()
  );
}

const LFO_BEAT_FRACTIONS = [
  32,
  24,
  16,
  12,
  8,
  6,
  4,
  3,
  2,
  1.5,
  4/3,
  1,
  3/4,
  2/3,
  1/2,
  1/3,
  1/4,
  1/6,
  1/8,
  1/12,
  1/16,
  1/24,
  1/32
];

const useSyncFrequency = (frequency, bpm) => {
  const syncedFrequency = useMonoSignal(frequency.length);
  for (let i = 0; i < syncedFrequency.length; i ++) {
    syncedFrequency[i] = (1/frequency[i]) * (bpm[i] / 60);
  }
  return syncedFrequency;
}

const useMultipliedLFO = (amplitude, waveform, frequency, phase, trigger) => {
  return multiply(
    amplitude,
    useOscillator(waveform)(frequency, phase, trigger),
    useMonoSignal()
  );
}

const usePortamento = (pitch, portamento, trigger, gate) => {
  const result = useMonoSignal();
  if (result[0] === 0) {
    result.fill(pitch[0]);
  }
  const lastGate = useVal(gate[0]);
  for (let i = 0; i < pitch.length; i ++) {
    if (trigger[i] > 0.5 && lastGate.current < 0.5) {
      result.fill(pitch[i], i);
    } else {
      const curvedPortamento = Math.pow(portamento[i], .25);
      result[i] = (
        (result[i] * curvedPortamento) + (pitch[i] * (1 - curvedPortamento))
      );
    }
    lastGate.current = gate[i];
  }
  return result;
}

const useOscillatorSection = (
  focal,
  retrigger,
  trigger,
  pitch,
  unison,
  spread,
  width,
  wavetable,
  wtPos,
  warpMode,
  warp,
  phase,
  phaseRand,
  transpose,
  pan,
  level,
  feedback
) => {
  const output = useStereoSignal();

  output[0].fill(0);
  output[1].fill(0);

  const randomPhases = [];
  for (let u = 0; u < unison; u ++) {
    randomPhases[u] = useMonoSignal();
    randomPhases[u].fill(randomPhases[u][randomPhases[u].length - 1]);
  }

  for (let i = 0; i < trigger.length; i ++) {
    if (trigger[i] > 0.5) {
      for (let u = 0; u < randomPhases.length; u ++) {
        randomPhases[u].fill(((Math.random() * 2) - 1) * Math.PI, i);
      }
    }
  }

  for (let u = 0; u < unison; u ++) {
    const voicePosition = unison === 1
      ? 0
      : (((u / (unison - 1)) * 2) - 1);
    const voicePitchPosition = Math.floor(unison/2) % 2 === 0 ? voicePosition : -voicePosition;
    const voiceFrequency = useMonoSignal();

    for (let i = 0; i < pitch.length; i ++) voiceFrequency[i] = pitchToHz(pitch[i] + (voicePitchPosition * spread[i]) + transpose[i]);

    const voicePhase = useMonoSignal();
    for (let i = 0; i < phase.length; i ++) voicePhase[i] = phase[i] + (randomPhases[u][i] * phaseRand[i]);

    const oscillator = useWavetableReader(wavetable, warpMode, feedback);
    const monoSignal = oscillator(voiceFrequency, wtPos, warp, voicePhase, retrigger ? trigger : useMonoSignal(), focal && u === 0);
    const leftLevel = useMonoSignal();
    const rightLevel = useMonoSignal();

    for (let i = 0; i < monoSignal.length; i ++) {
      const pRange = (1 - Math.abs(pan[i]));
      const effectivePan = pan[i] + ((voicePosition * width[i]) * pRange);
      leftLevel[i] = Math.min(1, (-1 * (effectivePan - 1)))
      rightLevel[i] = Math.min(1, (effectivePan + 1))
    }

    const leftOutput = useMonoSignal();
    const rightOutput = useMonoSignal();

    multiply(monoSignal, leftLevel, leftOutput);
    multiply(monoSignal, rightLevel, rightOutput);
    multiply(leftOutput, level, leftOutput);
    multiply(rightOutput, level, rightOutput);
    add(output[0], leftOutput, output[0]);
    add(output[1], rightOutput, output[1]);
  }
  return output;
}

const getUsedModulators = (matrix) => {
  // envelope1 is hard-coded to control voice amplitude.
  const usedModulators = {
    envelope1: true
  };
  Object.keys(matrix).forEach((destination) => {
    Object.keys(matrix[destination]).forEach((k) => {
      usedModulators[k] = true;
    });
  });
  return usedModulators;
}

const Wavetable = (s, g) => {
  const usedModulators = useReturnedConst(getUsedModulators, s.matrix)
  for (let c = 0; c < s.output.length; c ++) s.output[c].fill(0);

  const voices = useRoundRobinVoicing(s.input, s.voiceCount, s.retrigger);
  for (let v = 0; v < voices.length; v ++) {
    const exit = enterChildScope();

    // Do the once-per-voice things.
    const voice = voices[v];
    const focal = s.interfaceVisible && voice.focal;
    const trigger = voice.trigger;
    const gate = voice.gate;
    const portamentoPitch = usePortamento(voice.pitch, s.portamento, trigger, gate);

    const modulators = {};

    modulators.velocity = useVelocityModulator(voice.velocity, s.velocityCurve);
    modulators.pitch = usePitchModulator(portamentoPitch, s.minPitch, s.maxPitch);
    modulators.modWheel = s.input.ccs.modulationWheel;

    const mPre1 = useModulationMatrix(
      s.matrix,
      modulators,
      s
    );

    // envelope1 is always active.
    modulators.envelope1 = useMultipliedEnvelope(s.env2Curve, mPre1.envAmplitude1, trigger, gate, mPre1.attack1, mPre1.decay1, mPre1.sustain1, mPre1.release1);

    if (voice.alive && allEqual(modulators.envelope1, 0)) {
      voice.alive = false;
    }

    if (!voice.alive && !focal) {
      exit();
      continue;
    }

    modulators.envelope2 = usedModulators.envelope2 ? useMultipliedEnvelope(s.env2Curve, mPre1.envAmplitude2, trigger, gate, mPre1.attack2, mPre1.decay2, mPre1.sustain2, mPre1.release2) : useMonoSignal();

    modulators.envelope3 = usedModulators.envelope3 ? useMultipliedEnvelope(s.env3Curve, mPre1.envAmplitude3, trigger, gate, mPre1.attack3, mPre1.decay3, mPre1.sustain3, mPre1.release3) : useMonoSignal();

    modulators.envelope4 = usedModulators.envelope4 ? useMultipliedEnvelope(s.env4Curve, mPre1.envAmplitude4, trigger, gate, mPre1.attack4, mPre1.decay4, mPre1.sustain4, mPre1.release4) : useMonoSignal();

    const mPre2 = useModulationMatrix(
      s.matrix,
      modulators,
      s
    );

    const frequency1 = s.sync1 === 'Sync' ? useSyncFrequency(mPre2.frequency1, g.bpm) : mPre2.frequency1;
    const frequency2 = s.sync2 === 'Sync' ? useSyncFrequency(mPre2.frequency2, g.bpm) : mPre2.frequency2;
    const frequency3 = s.sync3 === 'Sync' ? useSyncFrequency(mPre2.frequency3, g.bpm) : mPre2.frequency3;

    modulators.lfo1 = usedModulators.lfo1 ? useMultipliedLFO(mPre2.lfoAmplitude1, SUB_WAVEFORMS[s.waveform1], frequency1, mPre2.phase1, trigger, g.bpm, s.sync1) : useMonoSignal();
    modulators.lfo2 = usedModulators.lfo2 ? useMultipliedLFO(mPre2.lfoAmplitude2, SUB_WAVEFORMS[s.waveform2], frequency2, mPre2.phase2, trigger, g.bpm, s.sync2) : useMonoSignal();
    modulators.lfo3 = usedModulators.lfo3 ? useMultipliedLFO(mPre2.lfoAmplitude3, SUB_WAVEFORMS[s.waveform3], frequency3, mPre2.phase3, trigger, g.bpm, s.sync3) : useMonoSignal();

    const m = useModulationMatrix(
      s.matrix,
      modulators,
      s
    );

    const allOnes = useMonoSignal();
    allOnes.fill(1);

    const bOutput = s.oscBEnabled ? useOscillatorSection(
      focal,
      s.retrigger,
      trigger,
      portamentoPitch,
      s.unisonB,
      m.spreadB,
      m.widthB,
      s.wavetableB,
      m.wtPosB,
      WARP_MODES[s.warpModeB],
      m.warpB,
      m.phaseB,
      m.phaseRandB,
      m.transposeB,
      m.panB,
      s.modulationMode === 'Off' ? m.levelB : allOnes,
      s.waveformB,
    ) : useStereoSignal();

    const fmPitch = useMonoSignal();
    const amLevel = useMonoSignal();
    const bOutputMono = useMonoSignal();

    if (s.modulationMode !== 'Off') {
      add(bOutput[0], bOutput[1], bOutputMono);
    }

    if (s.modulationMode === 'FM') {
      const fmScale = useMonoSignal();
      fmScale.fill(60);
      const fmModulator = useMonoSignal();
      multiply(fmScale, m.modulationAmount, fmScale);
      multiply(bOutputMono, fmScale, fmModulator);
      add(portamentoPitch, fmModulator, fmPitch);
    } else {
      fmPitch.set(portamentoPitch);
    }

    if (s.modulationMode === 'AM') {
      const amModulator = useMonoSignal();
      const offsetModulator = useMonoSignal();
      const offset = useMonoSignal();
      offset.fill(1);
      const multiplier = useMonoSignal();
      multiplier.fill(0.5);
      add(bOutputMono, offset, offsetModulator); // 0 - 2
      multiply(offsetModulator, multiplier, amModulator); // 0 - 1
      multiply(m.modulationAmount, amModulator, amModulator);
      add(m.levelA, amModulator, amLevel);
    } else if (s.modulationMode === 'Ring') {
      const amModulator = useMonoSignal();
      multiply(m.modulationAmount, bOutputMono, amModulator);
      multiply(m.levelA, amModulator, amLevel);
    } else {
      amLevel.set(m.levelA);
    }

    const aOutput = s.oscAEnabled ? useOscillatorSection(
      focal,
      s.retrigger,
      trigger,
      fmPitch,
      s.unisonA,
      m.spreadA,
      m.widthA,
      s.wavetableA,
      m.wtPosA,
      WARP_MODES[s.warpModeA],
      m.warpA,
      m.phaseA,
      m.phaseRandA,
      m.transposeA,
      m.panA,
      amLevel,
      s.waveformA,
    ) : useStereoSignal();

    const subFrequency = useMonoSignal();
    const totalSubTransposition = add(portamentoPitch, m.transposeSub, useMonoSignal());
    for (let i = 0; i < subFrequency.length; i ++) subFrequency[i] = pitchToHz(totalSubTransposition[i]);

    const subOscillator = useOscillator(SUB_WAVEFORMS[s.subWaveform], s.subEnabled);
    const subOutput = subOscillator(subFrequency, useConstantBuffer(0), trigger);
    const multipliedSubOutput = s.subEnabled ? multiply(subOutput, m.subLevel, useMonoSignal()) : useMonoSignal();

    const leftFilter = useBiQuadFilter();
    const rightFilter = useBiQuadFilter();

    const preFilterOutput = useStereoSignal();
    const filterBypassOutput = useStereoSignal();

    preFilterOutput[0].fill(0);
    preFilterOutput[1].fill(0);

    filterBypassOutput[0].fill(0);
    filterBypassOutput[1].fill(0);

    if (s.filterA) {
      add(preFilterOutput[0], aOutput[0], preFilterOutput[0]);
      add(preFilterOutput[1], aOutput[1], preFilterOutput[1]);
    } else {
      add(filterBypassOutput[0], aOutput[0], filterBypassOutput[0]);
      add(filterBypassOutput[1], aOutput[1], filterBypassOutput[1]);
    }

    if (s.modulationMode === 'Off') {
      if (s.filterB) {
        add(preFilterOutput[0], bOutput[0], preFilterOutput[0]);
        add(preFilterOutput[1], bOutput[1], preFilterOutput[1]);
      } else {
        add(filterBypassOutput[0], bOutput[0], filterBypassOutput[0]);
        add(filterBypassOutput[1], bOutput[1], filterBypassOutput[1]);
      }
    }

    if (s.filterSub) {
      add(preFilterOutput[0], multipliedSubOutput, preFilterOutput[0]);
      add(preFilterOutput[1], multipliedSubOutput, preFilterOutput[1]);
    } else {
      add(filterBypassOutput[0], multipliedSubOutput, filterBypassOutput[0]);
      add(filterBypassOutput[1], multipliedSubOutput, filterBypassOutput[1]);
    }

    const filterOutput = useStereoSignal();

    filterOutput[0].set(leftFilter(
      preFilterOutput[0],
      FILTER_TYPES[s.filterType],
      m.filterCutoff,
      m.filterResonance,
      useConstantBuffer(0)
    ));

    filterOutput[1].set(rightFilter(
      preFilterOutput[1],
      FILTER_TYPES[s.filterType],
      m.filterCutoff,
      m.filterResonance,
      useConstantBuffer(0)
    ));

    const voiceOutput = useStereoSignal();

    const totalAmplitude = multiply(m.master, modulators.envelope1, useMonoSignal());

    add(filterOutput[0], filterBypassOutput[0], voiceOutput[0]);
    add(filterOutput[1], filterBypassOutput[1], voiceOutput[1]);
    multiply(voiceOutput[0], totalAmplitude, voiceOutput[0]);
    multiply(voiceOutput[1], totalAmplitude, voiceOutput[1]);
    add(s.output[0], voiceOutput[0], s.output[0]);
    add(s.output[1], voiceOutput[1], s.output[1]);

    const oscIndex = useVal(0);
    const lastSubOutput = useVal(subOutput[0]);
    if (focal) {
      s.modulatedCutoff[0] = m.filterCutoff[0];
      s.modulatedResonance[0] = m.filterResonance[0];
      for (let i = 0; i < voiceOutput[0].length; i ++) {
        if (i % 2 !== 0) continue;
        if (oscIndex.current < s.oscilloscope.length) {
          s.oscilloscope[oscIndex.current] = voiceOutput[0][i] + voiceOutput[1][i];
          oscIndex.current ++;
        } else if (subOutput[i] > 0 && lastSubOutput.current < 0) {
          oscIndex.current = 0;
        }
        lastSubOutput.current = subOutput[i];
      }
    }
    exit();
  }
}

const WaveformRenderer = (ctx, settings) => {
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;

  ctx.clearRect(0, 0, w, h);
  if (!settings.enabled) {
    ctx.fillStyle = gray400;
    ctx.fillRect(0, 0, w, h);
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.fillRect(0, h/2, w, h/2);

    ctx.fillStyle = gray200;
    ctx.font = `${h/5}px Arial`;
    ctx.fillText(settings.text, h/8, h/4);
    return;
  }
  ctx.strokeStyle = 'white';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(-w, h/2);
  for (let i = 0; i < settings.waveform.length; i ++) {
    ctx.lineTo(
      (i/(settings.waveform.length - 1)) * w,
      (h/2) - (settings.waveform[i] * h/2)
    );
  }
  ctx.lineTo(2 * w, h/2);
  ctx.stroke();
  ctx.closePath();

  ctx.save();
  ctx.clip();
  ctx.fillStyle = 'rgba(255,255,255,0.1)';
  ctx.fillRect(0, 0, w, h/2);
  ctx.fillStyle = 'rgba(0,0,0,0.1)';
  ctx.fillRect(0, h/2, w, h/2);
  ctx.restore();

  ctx.fillStyle = gray200;
  ctx.font = `${h/5}px Arial`;
  ctx.fillText(settings.text, h/8, h/4);
}

const OscillatorAWaveformRenderer = (ctx, settings) => WaveformRenderer(ctx, { ...settings, waveform: settings.waveformA, enabled: settings.oscAEnabled, text: 'A' });
const OscillatorBWaveformRenderer = (ctx, settings) => WaveformRenderer(ctx, { ...settings, waveform: settings.waveformB, enabled: settings.oscBEnabled, text: 'B' });

const FilterRenderer = (ctx, settings) => {
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;
  const transferFunction = getBiQuadFilterTransferFunction(
    sampleRate,
    FILTER_TYPES[settings.filterType],
    settings.modulatedCutoff,
    settings.modulatedResonance,
    0
  );
  const range = 36;

  ctx.clearRect(0, 0, w, h);
  ctx.save();
  roundedRect(ctx, 0, 0, w, h, 4);
  ctx.clip();
  ctx.beginPath();
  ctx.moveTo(-w, h);
  ctx.lineTo(-w, h/2);
  ctx.strokeStyle = 'white';

  for (let i = 0; i < transferFunction.length; i ++) {
    const gain = transferFunction[i][1]
    ctx.lineTo(
      (i / (transferFunction.length - 1)) * w,
      h - (((gain + range) / (2 * range)) * h)
    )
  }
  ctx.lineTo(w * 2, h);
  ctx.stroke();
  ctx.closePath();

  ctx.save();
  ctx.clip();
  ctx.fillStyle = 'rgba(255,255,255,0.1)';
  ctx.fillRect(0, 0, w, h/2);
  ctx.restore();

  ctx.restore();
}

const Oscilloscope = (ctx, settings) => {
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;

  ctx.clearRect(0, 0, w, h);

  ctx.strokeStyle = '#fff';
  ctx.beginPath();
  ctx.moveTo(-w, h/2);
  for (let i = 0; i < settings.oscilloscope.length; i ++) {
    ctx.lineTo((i/settings.oscilloscope.length) * w, h/2 + (settings.oscilloscope[i] * h/4));
  }
  ctx.lineTo(2 * w, h/2);
  ctx.stroke();
  ctx.closePath();
}

const oscillatorLayout = (suffix, renderFunction) => box(
  col([
    box(
      canvas(`waveform${suffix}`, renderFunction, 290, 100),
      'auto',
      'auto',
      `${blue500}aa`
    ),
    spacer(8),
    row([
      box(row([
        label('Enabled'),
        spacer(5),
        box(checkbox(`osc${suffix}Enabled`), 24, 24, gray100, 8),
      ]), 90, 20),
      spacer(5),
      box(
        wavetablePicker(`wavetable${suffix}`),
        90,
        20,
        blue500,
        4
      ),
      spacer(5),
      box(selector(`warpMode${suffix}`), 90, 20),
    ]),
    spacer(10),
    row([
      box(row([
        box(label('Unison'), 42, 20),
        box(number(`unison${suffix}`), 43, 20),
      ]), 90, 20, gray100, 4),
      spacer(5),
      box(linear(`wtPos${suffix}`), 90, 20, gray300, 4),
      spacer(5),
      box(linear(`warp${suffix}`), 90, 20, gray300, 4),
    ]),
    spacer(8),
    row([
      spacer(5),
      col([
        spacer(15),
        knob(`phase${suffix}`, { size: 25 })
      ]),
      col([
        knob(`phaseRand${suffix}`, { size: 25 }),
        spacer(15),
      ]),
      spacer(5),
      col([
        spacer(15),
        knob(`spread${suffix}`, { size: 25 })
      ]),
      col([
        knob(`width${suffix}`, { size: 25 }),
        spacer(15),
      ]),
      spacer(5),
      box(knob(`transpose${suffix}`), 56),
      spacer(5),
      box(knob(`pan${suffix}`), 54),
      box(knob(`level${suffix}`), 54),
    ]),
    spacer(5),
    row([
      box(
        label('Θ/Rand'),
        54,
        20,
        gray100,
        4
      ),
      spacer(5),
      box(
        label('Det/Wid'),
        54,
        20,
        gray100,
        4
      ),
      spacer(5),
      box(
        box(label('Transp'), 56),
        56,
        20,
        gray100,
        4
      ),
      spacer(5),
      box(
        row([
          box(label('Pan'), 54),
          box(label('Level'), 54),
        ]),
        54 * 2,
        20,
        gray100,
        4
      )
    ]),
    spacer(5),
  ]),
  'auto',
  'auto',
  gray200,
  8
);

const filterLayout = () => box(
  col([
    box(
      canvas('filter', FilterRenderer, 122, 50),
      'auto',
      'auto',
      `${blue500}aa`
    ),
    spacer(8),
    box(selector('filterType'), 120, 20),
    spacer(8),
    row([
      label('A'),
      spacer(3),
      box(checkbox('filterA'), 24, 24, gray100, 8),
      spacer(3),
      label('B'),
      spacer(3),
      box(checkbox('filterB'), 24, 24, gray100, 8),
      spacer(3),
      label('S'),
      spacer(3),
      box(checkbox('filterSub'), 24, 24, gray100, 8),
    ]),
    spacer(6),
    row([
      box(knob(`filterCutoff`), 56),
      box(knob(`filterResonance`), 56),
    ]),
    spacer(5),
    box(
      row([
        box(label('Freq'), 56),
        box(label('Res'), 56)
      ]),
      'auto',
      20,
      gray100,
      4
    ),
    spacer(5)
  ]),
  'auto',
  'auto',
  gray200,
  8
);

const modulationLayout = () => box(
  row([
    spacer(5),
    col([
      spacer(3),
      box(selector('modulationMode'), 72, 20),
      spacer(3),
      box(label('Mod B → A'), 65, 20, gray100, 4),
      spacer(3),
    ]),
    spacer(5),
    conditional('modulationMode', {
      'Off': box(spacer(35), 35, 35, gray300, 17.5),
      _other: knob('modulationAmount', { size: 35 })
    }),
    spacer(5),
  ]),
  'auto',
  'auto',
  gray200,
  8
);

const subLayout = () => box(
  col([
    spacer(5),
    box(row([
      label('Sub Enabled'),
      spacer(5),
      box(checkbox(`subEnabled`), 24, 24, gray100, 8),
    ]), 'auto', 33),
    spacer(5),
    box(selector('subWaveform'), 2 * 56, 20),
    spacer(5),
    row([
      box(knob(`subLevel`), 56),
      box(knob(`transposeSub`), 56),
    ]),
    spacer(5),
    box(
      row([
        box(label('Level'), 56),
        box(label('Transp'), 56)
      ]),
      'auto',
      20,
      gray100,
      4
    ),
    spacer(5)
  ]),
  155,
  'auto',
  gray200,
  8
);

const envelopeLayout = (suffix) => box(
  col([
    spacer(5),
    row([
      box(selector(`env${suffix}Curve`), 56 * 2, 20),
    ]),
    spacer(5),
    row([
      box(knob(`attack${suffix}`), 56),
      box(knob(`decay${suffix}`), 56),
      box(knob(`sustain${suffix}`), 56),
      box(knob(`release${suffix}`), 56),
      box(knob(`envAmplitude${suffix}`), 56),
    ]),
    spacer(5),
    row([
      box(
        row([
          box(label('Attack'), 56),
          box(label('Decay'), 56),
          box(label('Sustain'), 56),
          box(label('Release'), 56),
          box(label('Level'), 56)
        ]),
        'auto',
        20,
        gray100,
        4
      ),
    ]),
    spacer(5),
  ]),
  'auto',
  'auto',
  gray200,
  8
);

const lfoLayout = (suffix) => box(
  col([
    spacer(5),
    row([
      box(selector(`sync${suffix}`, { options: [ 'Sync', 'Free' ] }), 56 * 2, 20),
      box(selector(`waveform${suffix}`), 56 * 2, 20),
    ]),
    spacer(5),
    row([
      box(
        conditional(
          `sync${suffix}`,
          {
            Sync: knob(`frequency${suffix}`, { options: LFO_BEAT_FRACTIONS }),
            Free: knob(`frequency${suffix}`)
          }
        ),
        56
      ),
      box(knob(`phase${suffix}`), 56),
      box(knob(`lfoAmplitude${suffix}`), 56),
    ]),
    spacer(5),
    row([
      box(
        row([
          box(
            conditional(`sync${suffix}`, { Sync: label('Beats'), Free: label('Freq') }),
            56
          ),
          box(label('Phase'), 56),
          box(label('Level'), 56),
        ]),
        'auto',
        20,
        gray100,
        4
      ),
    ]),
    spacer(5),
  ]),
  'auto',
  'auto',
);

const globalsLayout = () => box(
  row([
    spacer(5),
    col([
      spacer(14),
      row([
        box(box(checkbox('retrigger'), 24, 24, gray100, 8), 56),
        spacer(3),
        box(number('voiceCount'), 54, 20, gray300, 4),
      ]),
      spacer(10),
      box(
        row([
          box(label('Retrig'), 56),
          box(label('Voices'), 56),
        ]),
        'auto',
        20,
        gray100,
        4
      ),
      spacer(5),
    ]),
    spacer(5),
    box(
      canvas(
        'oscilloscope',
        Oscilloscope,
        158,
        64
      ),
      'auto',
      'auto',
      gray100,
      4
    ),
    spacer(5),
    col([
      spacer(5),
      row([
        box(modulationAssigner('modWheel'), 56),
        spacer(5),
        box(knob('portamento'), 56),
        spacer(5),
        box(row([
          col([
            spacer(15),
            knob('minPitch', { size: 25 })
          ]),
          col([
            knob('maxPitch', { size: 25 }),
            spacer(15)
          ]),
        ]), 56),
        box(modulationAssigner('pitch'), 56),
        spacer(5),
        box(knob('velocityCurve'), 56),
        box(modulationAssigner('velocity'), 56),
        spacer(5),
        box(knob('master'), 56)
      ]),
      spacer(4),
      row([
        box(label('Mod'), 56, 20, gray100, 4),
        spacer(5),
        box(label('Porta'), 56, 20, gray100, 4),
        spacer(5),
        box(
          row([
            box(label('Range'), 56, 20),
            box(label('Pitch'), 56, 20),
          ]),
          'auto',
          'auto',
          gray100,
          4
        ),
        spacer(5),
        box(
          row([
            box(label('V Curve'), 56, 20),
            box(label('Velocity'), 56, 20),
          ]),
          'auto',
          'auto',
          gray100,
          4
        ),
        spacer(5),
        box(label('Master'), 56, 20, gray100, 4),
      ]),
      spacer(5),
    ]),
    spacer(5),
  ]),
  'auto',
  'auto',
  gray200,
  8
);
const lfoTabLayout = (suffix) => {
  const tabLayout = (color) => box(
    constantSetter('focusedLFO', `LFO ${suffix}`, col([
      spacer(4),
      row([
        spacer(7),
        box(label(`LFO${suffix}`), 30, 20),
        spacer(5),
        modulationAssigner(`lfo${suffix}`, { size: 30 }),
        spacer(6)
      ]),
      spacer(4),
    ])),
    'auto',
    'auto',
    color,
    0
  );
  return conditional('focusedLFO', { [`LFO ${suffix}`]: tabLayout(gray200), _other: tabLayout(gray125) })
}

const envTabLayout = (suffix) => {
  const tabLayout = (color) => box(
    constantSetter('focusedEnvelope', `Env ${suffix}`, col([
      spacer(4),
      row([
        spacer(7),
        box(label(`Env${suffix}`), 30, 20),
        spacer(5),
        modulationAssigner(`envelope${suffix}`, { size: 30 }),
        spacer(6)
      ]),
      spacer(4),
    ])),
    'auto',
    'auto',
    color,
    0
  );
  return conditional('focusedEnvelope', { [`Env ${suffix}`]: tabLayout(gray200), _other: tabLayout(gray125) })
}

this.constructDevice = () => constructPerBufferDevice({
  implementation: Wavetable,
  inputs: {
    input: midi('In'),

    portamento: numberSetting('Portamento', { defaultValue: 0, min: 0, max: 1 }),
    minPitch: numberSetting('Minimum Pitch', { defaultValue: 0, min: 0, max: 127 }),
    maxPitch: numberSetting('Maximum Pitch', { defaultValue: 127, min: 0, max: 127 }),
    velocityCurve: numberSetting('Velocity Curve', { defaultValue: 0, min: -1, max: 1 }),

    spreadA: numberSetting('Oscillator A Spread', { defaultValue: 0, min: 0, max: 2 }),
    widthA: numberSetting('Oscillator A Width', { defaultValue: 1, min: 0, max: 1 }),
    wtPosA: numberSetting('Oscillator A WT Pos', { defaultValue: 0, min: 0, max: 1 }),
    warpA: numberSetting('Oscillator A Warp', { defaultValue: 0, min: -1, max: 1 }),
    transposeA: numberSetting('Oscillator A Transpose', { defaultValue: 0, min: -12, max: 12 }),
    phaseA: numberSetting('Oscillator A Base Phase', { defaultValue: 0, min: -Math.PI, max: Math.PI }),
    phaseRandA: numberSetting('Oscillator A Random Phase', { defaultValue: Math.PI, min: 0, max: Math.PI }),
    panA: numberSetting('Oscillator A Pan', { defaultValue: 0, max: 1, min: -1 }),
    levelA: dbSetting('Oscillator A Level', { defaultValue: -6, max: 0 }),

    modulationAmount: numberSetting('Modulation Amount', { defaultValue: 0, max: 1, min: 0 }),

    spreadB: numberSetting('Oscillator B Spread', { defaultValue: 0, min: 0, max: 2 }),
    widthB: numberSetting('Oscillator B Width', { defaultValue: 1, min: 0, max: 1 }),
    wtPosB: numberSetting('Oscillator B WT Pos', { defaultValue: 0, min: 0, max: 1 }),
    warpB: numberSetting('Oscillator B Warp', { defaultValue: 0, min: -1, max: 1 }),
    transposeB: numberSetting('Oscillator B Transpose', { defaultValue: 0, min: -12, max: 12 }),
    phaseB: numberSetting('Oscillator B Base Phase', { defaultValue: 0, min: -Math.PI, max: Math.PI }),
    phaseRandB: numberSetting('Oscillator B Random Phase', { defaultValue: Math.PI, min: 0, max: Math.PI }),
    panB: numberSetting('Oscillator B Pan', { defaultValue: 0, max: 1, min: -1 }),
    levelB: dbSetting('Oscillator B Level', { defaultValue: -6, max: 0 }),

    subLevel: dbSetting('Sub Level', { defaultValue: -6, max: 0 }),
    transposeSub: numberSetting('Sub Transposition', { defaultValue: -12, max: 0, min: -24 }),

    filterCutoff: frequencySetting('Filter Cutoff Frequency', 20000),
    filterResonance: numberSetting('Filter Resonance', { defaultValue: Math.sqrt(.5), min: .1, max: 18, curve: Curve.Log2 }),

    attack1: adsrTimeSetting('Envelope 1 Attack', 1),
    decay1: adsrTimeSetting('Envelope 1 Decay', 300),
    sustain1: zeroOneSetting('Envelope 1 Sustain', 1),
    release1: adsrTimeSetting('Envelope 1 Release', 100),
    envAmplitude1: zeroOneSetting('Envelope 1 Level', 1),

    attack2: adsrTimeSetting('Envelope 2 Attack', 1),
    decay2: adsrTimeSetting('Envelope 2 Decay', 300),
    sustain2: zeroOneSetting('Envelope 2 Sustain', 0),
    release2: adsrTimeSetting('Envelope 2 Release', 100),
    envAmplitude2: zeroOneSetting('Envelope 2 Level', 1),

    attack3: adsrTimeSetting('Envelope 3 Attack', 1),
    decay3: adsrTimeSetting('Envelope 3 Decay', 300),
    sustain3: zeroOneSetting('Envelope 3 Sustain', 0),
    release3: adsrTimeSetting('Envelope 3 Release', 100),
    envAmplitude3: zeroOneSetting('Envelope 3 Level', 1),

    attack4: adsrTimeSetting('Envelope 4 Attack', 1),
    decay4: adsrTimeSetting('Envelope 4 Decay', 300),
    sustain4: zeroOneSetting('Envelope 4 Sustain', 0),
    release4: adsrTimeSetting('Envelope 4 Release', 100),
    envAmplitude4: zeroOneSetting('Envelope 4 Level', 1),

    frequency1: numberSetting('LFO 1 Frequency', { defaultValue: 1, min: 0.001, max: 100, curve: Curve.Log2 }),
    phase1: numberSetting('Phase 1', { defaultValue: 0, min: -Math.PI, max: Math.PI }),
    lfoAmplitude1: zeroOneSetting('LFO Level 1', 0.5),

    frequency2: numberSetting('LFO 2 Frequency', { defaultValue: 1, min: 0.001, max: 100, curve: Curve.Log2 }),
    phase2: numberSetting('Phase 2', { defaultValue: 0, min: -Math.PI, max: Math.PI }),
    lfoAmplitude2: zeroOneSetting('LFO Level 2', 0.5),

    frequency3: numberSetting('LFO 3 Frequency', { defaultValue: 1, min: 0.001, max: 100, curve: Curve.Log2 }),
    phase3: numberSetting('Phase 3', { defaultValue: 0, min: -Math.PI, max: Math.PI }),
    lfoAmplitude3: zeroOneSetting('LFO Level 3', 0.5),

    master: dbSetting('Master', { defaultValue: -6, max: 0 }),
  },

  modulators: {
    envelope1: modulator('Envelope 1', 0, 1),
    envelope2: modulator('Envelope 2', 0, 1),
    envelope3: modulator('Envelope 3', 0, 1),
    envelope4: modulator('Envelope 4', 0, 1),

    lfo1: modulator('LFO 1', -1, 1),
    lfo2: modulator('LFO 2', -1, 1),
    lfo3: modulator('LFO 3', -1, 1),

    pitch: modulator('Pitch', 0, 127),
    velocity: modulator('Velocity', 0, 1),
    modWheel: modulator('Mod Wheel', 0, 1),
  },

  constants: {
    oscAEnabled: booleanConstant('Oscillator A Enabled', true),
    oscBEnabled: booleanConstant('Oscillator B Enabled', false),
    subEnabled: booleanConstant('Sub Enabled', false),

    warpModeA: enumConstant('Oscillator A Warp Mode', Object.keys(WARP_MODES)),
    warpModeB: enumConstant('Oscillator B Warp Mode', Object.keys(WARP_MODES)),
    modulationMode: enumConstant('Modulation Mode', MODULATION_MODES),

    wavetableA: wavetableConstant('Oscillator A Wavetable'),
    wavetableB: wavetableConstant('Oscillator B Wavetable'),

    unisonA: numberConstant('Oscillator A Unison Count', { defaultValue: 1, min: 1, max: 8 }),
    unisonB: numberConstant('Oscillator B Unison Count', { defaultValue: 1, min: 1, max: 8 }),

    filterType: enumConstant('Filter Type', Object.keys(FILTER_TYPES)),
    subWaveform: enumConstant('Sub Waveform', Object.keys(SUB_WAVEFORMS)),

    filterA: booleanConstant('Oscillator A to Filter', true),
    filterB: booleanConstant('Oscillator B to Filter', true),
    filterSub: booleanConstant('Sub to Filter', true),

    focusedLFO: enumConstant('Focused LFO', ['LFO 1', 'LFO 2', 'LFO 3'], 'LFO 1'),
    focusedEnvelope: enumConstant('Focused Envelope', ['Env 1', 'Env 2', 'Env 3', 'Env 4'], 'Env 1'),

    env1Curve: enumConstant('Envelope 1 Curve', ['Linear', 'Exponential']),
    env2Curve: enumConstant('Envelope 2 Curve', ['Linear', 'Exponential']),
    env3Curve: enumConstant('Envelope 3 Curve', ['Linear', 'Exponential']),
    env4Curve: enumConstant('Envelope 4 Curve', ['Linear', 'Exponential']),

    waveform1: enumConstant('LFO 1 Waveform', Object.keys(SUB_WAVEFORMS), 'Sine'),
    waveform2: enumConstant('LFO 2 Waveform', Object.keys(SUB_WAVEFORMS), 'Sine'),
    waveform3: enumConstant('LFO 3 Waveform', Object.keys(SUB_WAVEFORMS), 'Sine'),

    sync1: enumConstant('LFO 1 Tempo Sync', ['Sync', 'Free']),
    sync2: enumConstant('LFO 2 Tempo Sync', ['Sync', 'Free']),
    sync3: enumConstant('LFO 3 Tempo Sync', ['Sync', 'Free']),

    retrigger: booleanConstant('Retrigger', true),
    voiceCount: numberConstant('Voice Count', { defaultValue: 4, min: 1, max: 16 })
  },

  outputs: {
    output: stereo('Out')
  },

  sharedArrays: {
    waveformA: 1024,
    waveformB: 1024,
    modulatedCutoff: 1,
    modulatedResonance: 1,
    oscilloscope: 512,
  },

  presetTags: {
    'Instrument': [
      'Bass',
      'Synth Lead',
      'Synth Pad',
      'Piano/Keys',
      'Percussion',
      'Plucked Strings',
      'Bowed Strings',
      'Brass',
      'Woodwinds',
      'Sound Effect',
    ],
    'Genre': [
      'Ambient',
      'Chill',
      'Dubstep',
      'EDM',
      'Funk',
      'Hip Hop',
      'House',
      'Jazz',
      'Lo-Fi',
      'Soundtrack',
      'Metal',
      'Pop',
      'R&B',
      'Rock',
      'Techno',
      'Trance',
    ],
    'Playback': [
      'Monophonic',
      'Polyphonic',
      'Portamento',
      'Arpeggiated',
      'Long/Evolving',
      'Chord',
      'Slow Attack',
      'Long Release'
    ]
  },

  layout: col([
    spacer(5),
    row([
      spacer(5),
      oscillatorLayout('A', OscillatorAWaveformRenderer),
      spacer(1),
      col([
        modulationLayout(),
        spacer(1),
        filterLayout(),
      ]),
      spacer(1),
      oscillatorLayout('B', OscillatorBWaveformRenderer),
      spacer(5),
    ]),
    spacer(1),
    row([
      spacer(5),
      box(
        col([
          row([
            lfoTabLayout('1'),
            lfoTabLayout('2'),
            lfoTabLayout('3'),
          ]),
          conditional('focusedLFO', {
            'LFO 1': lfoLayout('1'),
            'LFO 2': lfoLayout('2'),
            'LFO 3': lfoLayout('3'),
          }),
        ]),
        'auto',
        'auto',
        gray200,
        8
      ),
      spacer(1),
      subLayout(),
      spacer(1),
      box(
        col([
          row([
            envTabLayout('1'),
            envTabLayout('2'),
            envTabLayout('3'),
            envTabLayout('4'),
          ]),
          conditional('focusedEnvelope', {
            'Env 1': envelopeLayout('1'),
            'Env 2': envelopeLayout('2'),
            'Env 3': envelopeLayout('3'),
            'Env 4': envelopeLayout('4'),
          }),
        ]),
        'auto',
        'auto',
        gray200,
        8
      ),
      spacer(5),
    ]),
    spacer(1),
    globalsLayout(),
    spacer(5)
  ])
});
