desc:HIBIKI — Subtractive Synthesizer
//tags: synth subtractive polyphonic
//author: Sammy Jankis (sammyjankis.com)
//
// Port of hibiki.html to JSFX for REAPER.
// 2-oscillator FM/subtractive synth with filter, envelopes, LFOs, effects.
// 16-voice polyphony, MIDI-driven, velocity-sensitive.
//
// Based on the web synth Amy called "Truly awesome. Terrifying."

// --- OSCILLATORS ---
slider1:2<0,3,1{Sine,Triangle,Saw,Square}>OSC1 Wave
slider2:0<-2,2,1>OSC1 Octave
slider3:0<-50,50,1>OSC1 Fine (cents)
slider4:80<0,100,1>OSC1 Level (%)
slider5:2<0,3,1{Sine,Triangle,Saw,Square}>OSC2 Wave
slider6:0<-2,2,1>OSC2 Octave
slider7:7<-50,50,1>OSC2 Fine (cents)
slider8:60<0,100,1>OSC2 Level (%)

// --- MODULATION ---
slider9:0<0,1000,1>FM Amount (OSC2→OSC1)
slider10:0<0,100,1>Noise (%)
slider11:0<0,1,1{Off,On}>Ring Mod

// --- FILTER ---
slider12:0<0,2,1{Lowpass,Highpass,Bandpass}>Filter Type
slider13:8000<20,20000,1>Filter Cutoff (Hz)
slider14:1<0.1,30,0.1>Filter Q

// --- FILTER ENVELOPE ---
slider15:10<3,2000,1>Flt Atk (ms)
slider16:300<10,3000,1>Flt Dec (ms)
slider17:70<0,100,1>Flt Sus (%)
slider18:300<10,5000,1>Flt Rel (ms)
slider19:4000<0,8000,1>Flt Env Amt (Hz)

// --- AMP ENVELOPE ---
slider20:10<3,2000,1>Amp Atk (ms)
slider21:300<10,3000,1>Amp Dec (ms)
slider22:70<0,100,1>Amp Sus (%)
slider23:300<10,5000,1>Amp Rel (ms)

// --- LFO1 → FILTER ---
slider24:0<0,3,1{Sine,Triangle,Saw,Square}>LFO1 Wave
slider25:2<0.1,20,0.1>LFO1 Rate (Hz)
slider26:0<0,4000,1>LFO1 Depth (Hz)

// --- LFO2 → AMP ---
slider27:0<0,3,1{Sine,Triangle,Saw,Square}>LFO2 Wave
slider28:5<0.1,20,0.1>LFO2 Rate (Hz)
slider29:0<0,100,1>LFO2 Depth (%)

// --- DELAY ---
slider30:0<0,1,1{Off,On}>Delay
slider31:350<50,1000,1>Delay Time (ms)
slider32:40<0,90,1>Delay Feedback (%)
slider33:30<0,100,1>Delay Mix (%)

// --- CHORUS ---
slider34:0<0,1,1{Off,On}>Chorus
slider35:0.8<0.1,5,0.1>Chorus Rate (Hz)
slider36:5<0,20,0.1>Chorus Depth (ms)
slider37:30<0,100,1>Chorus Mix (%)

// --- MASTER ---
slider38:0<0,500,1>Glide (ms)
slider39:70<0,100,1>Master Volume (%)

// --- PRESETS ---
slider40:0<0,17,1{--,INIT,PAD,BASS,LEAD,PLUCK,FM BELL,STRINGS,ACID,ORGAN,BRASS,NOISE HIT,SHIMMER,SUB BASS,WOBBLE,RING MOD,DARK PAD,CUSTOM}>Preset

in_pin:none
out_pin:Hibiki L
out_pin:Hibiki R

@init
TWO_PI = 2 * $pi;
MAX_POLY = 16;
VOICE_SIZE = 64;
prev_preset = 0;

// Memory layout:
// Voice state: 0..MAX_POLY*VOICE_SIZE
// Delay buffer: after voices
// Chorus buffer: after delay
VOICE_MEM = 0;
DELAY_BUF = MAX_POLY * VOICE_SIZE;
DELAY_MAX = 48000; // 1s at 48kHz
CHORUS_BUF = DELAY_BUF + DELAY_MAX * 2; // stereo
CHORUS_MAX = 4800; // ~100ms at 48kHz

// Voice offsets:
// 0: active (0=free, 1=attack, 2=decay, 3=sustain, 4=release)
// 1: midi note
// 2: velocity (0-1)
// 3: osc1_phase
// 4: osc2_phase
// 5: target_freq (Hz)
// 6: current_freq (Hz, for glide)
// 7: amp_env (0-1)
// 8: amp_stage (0=off,1=atk,2=dec,3=sus,4=rel)
// 9: amp_time (samples in current stage)
// 10: flt_env (0-1)
// 11: flt_stage
// 12: flt_time
// 13-18: biquad state (x1,x2,y1,y2)
// 19: osc1_out (for FM)
// 20: osc2_out
// 21-26: biquad coefficients (b0,b1,b2,a1,a2)

// Init voices
i = 0;
while (i < MAX_POLY) (
  base = VOICE_MEM + i * VOICE_SIZE;
  base[0] = 0; // inactive
  i += 1;
);

// Init delay buffer
i = 0;
while (i < DELAY_MAX * 2) (
  (DELAY_BUF + i)[0] = 0;
  i += 1;
);
delay_pos = 0;

// Init chorus buffer
i = 0;
while (i < CHORUS_MAX * 2) (
  (CHORUS_BUF + i)[0] = 0;
  i += 1;
);
chorus_pos = 0;

// LFO phases
lfo1_phase = 0;
lfo2_phase = 0;

// Generate waveform sample
function gen_wave(wave, phase) (
  wave == 0 ? sin(phase) :
  wave == 1 ? 2 * abs(2 * ((phase / TWO_PI) - floor(phase / TWO_PI + 0.5))) - 1 :
  wave == 2 ? 2 * ((phase / TWO_PI) - floor(phase / TWO_PI)) - 1 :
  /* square */ (sin(phase) >= 0 ? 1 : -1);
);

// Compute biquad coefficients
function calc_biquad(type, fc, q) local(w0, alpha, b0, b1, b2, a0, a1, a2, cosw, sinw) (
  fc = min(max(fc, 20), srate * 0.45);
  q = max(q, 0.1);
  w0 = TWO_PI * fc / srate;
  cosw = cos(w0);
  sinw = sin(w0);
  alpha = sinw / (2 * q);

  type == 0 ? ( // lowpass
    b0 = (1 - cosw) / 2;
    b1 = 1 - cosw;
    b2 = (1 - cosw) / 2;
  ) : type == 1 ? ( // highpass
    b0 = (1 + cosw) / 2;
    b1 = -(1 + cosw);
    b2 = (1 + cosw) / 2;
  ) : ( // bandpass
    b0 = alpha;
    b1 = 0;
    b2 = -alpha;
  );
  a0 = 1 + alpha;
  a1 = -2 * cosw;
  a2 = 1 - alpha;

  // Normalize
  bq_b0 = b0 / a0;
  bq_b1 = b1 / a0;
  bq_b2 = b2 / a0;
  bq_a1 = a1 / a0;
  bq_a2 = a2 / a0;
);

// Process biquad filter (state stored at voice+offset)
function proc_biquad(x, base, off) local(y) (
  y = bq_b0 * x + bq_b1 * (base + off)[0] + bq_b2 * (base + off + 1)[0]
    - bq_a1 * (base + off + 2)[0] - bq_a2 * (base + off + 3)[0];
  (base + off + 1)[0] = (base + off)[0];
  (base + off)[0] = x;
  (base + off + 3)[0] = (base + off + 2)[0];
  (base + off + 2)[0] = y;
  y;
);

// MIDI note to frequency
function midi2freq(note) (
  440 * pow(2, (note - 69) / 12);
);

@slider
// Preset loading
slider40 != prev_preset && slider40 > 0 && slider40 <= 16 ? (
  // Wave: sine=0, triangle=1, saw=2, square=3
  // Filter: lp=0, hp=1, bp=2
  // Envelope times in ms (web values * 1000)
  // Bool: off=0, on=1
  slider40 == 1 ? ( // INIT
    slider1=2; slider2=0; slider3=0; slider4=80;
    slider5=2; slider6=0; slider7=7; slider8=60;
    slider9=0; slider10=0; slider11=0;
    slider12=0; slider13=8000; slider14=1;
    slider15=10; slider16=300; slider17=70; slider18=300; slider19=4000;
    slider20=10; slider21=300; slider22=70; slider23=300;
    slider24=0; slider25=2; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=0; slider31=350; slider32=40; slider33=30;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=0; slider39=70;
  ) : slider40 == 2 ? ( // PAD
    slider1=2; slider2=0; slider3=-7; slider4=70;
    slider5=2; slider6=0; slider7=7; slider8=70;
    slider9=0; slider10=5; slider11=0;
    slider12=0; slider13=2000; slider14=2;
    slider15=500; slider16=2000; slider17=40; slider18=1500; slider19=3000;
    slider20=800; slider21=1000; slider22=80; slider23=2000;
    slider24=1; slider25=0.3; slider26=500;
    slider27=0; slider28=4; slider29=0;
    slider30=1; slider31=500; slider32=50; slider33=25;
    slider34=1; slider35=0.5; slider36=8; slider37=40;
    slider38=0; slider39=60;
  ) : slider40 == 3 ? ( // BASS
    slider1=2; slider2=-1; slider3=0; slider4=90;
    slider5=3; slider6=-1; slider7=0; slider8=50;
    slider9=0; slider10=0; slider11=0;
    slider12=0; slider13=800; slider14=5;
    slider15=5; slider16=300; slider17=10; slider18=200; slider19=3000;
    slider20=5; slider21=200; slider22=60; slider23=150;
    slider24=0; slider25=2; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=0; slider31=350; slider32=40; slider33=30;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=30; slider39=80;
  ) : slider40 == 4 ? ( // LEAD
    slider1=3; slider2=0; slider3=0; slider4=80;
    slider5=2; slider6=0; slider7=5; slider8=60;
    slider9=0; slider10=0; slider11=0;
    slider12=0; slider13=3000; slider14=8;
    slider15=5; slider16=200; slider17=50; slider18=300; slider19=5000;
    slider20=5; slider21=150; slider22=80; slider23=200;
    slider24=0; slider25=5; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=1; slider31=250; slider32=35; slider33=20;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=50; slider39=70;
  ) : slider40 == 5 ? ( // PLUCK
    slider1=1; slider2=0; slider3=0; slider4=80;
    slider5=0; slider6=1; slider7=0; slider8=40;
    slider9=0; slider10=10; slider11=0;
    slider12=0; slider13=1000; slider14=3;
    slider15=3; slider16=200; slider17=0; slider18=200; slider19=6000;
    slider20=3; slider21=400; slider22=0; slider23=300;
    slider24=0; slider25=2; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=1; slider31=300; slider32=45; slider33=35;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=0; slider39=75;
  ) : slider40 == 6 ? ( // FM BELL
    slider1=0; slider2=0; slider3=0; slider4=80;
    slider5=0; slider6=1; slider7=2; slider8=0;
    slider9=400; slider10=0; slider11=0;
    slider12=0; slider13=12000; slider14=1;
    slider15=3; slider16=1000; slider17=10; slider18=1000; slider19=2000;
    slider20=3; slider21=1500; slider22=0; slider23=2000;
    slider24=0; slider25=0.5; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=1; slider31=400; slider32=55; slider33=30;
    slider34=1; slider35=0.3; slider36=10; slider37=25;
    slider38=0; slider39=65;
  ) : slider40 == 7 ? ( // STRINGS
    slider1=2; slider2=0; slider3=-12; slider4=60;
    slider5=2; slider6=0; slider7=12; slider8=60;
    slider9=0; slider10=0; slider11=0;
    slider12=0; slider13=4000; slider14=1;
    slider15=300; slider16=1000; slider17=60; slider18=800; slider19=2000;
    slider20=500; slider21=500; slider22=90; slider23=1000;
    slider24=1; slider25=0.2; slider26=300;
    slider27=0; slider28=5.5; slider29=8;
    slider30=0; slider31=350; slider32=40; slider33=30;
    slider34=1; slider35=0.6; slider36=6; slider37=50;
    slider38=0; slider39=60;
  ) : slider40 == 8 ? ( // ACID
    slider1=2; slider2=-1; slider3=0; slider4=90;
    slider5=3; slider6=-1; slider7=0; slider8=30;
    slider9=0; slider10=0; slider11=0;
    slider12=0; slider13=400; slider14=18;
    slider15=3; slider16=150; slider17=0; slider18=100; slider19=7000;
    slider20=3; slider21=150; slider22=0; slider23=100;
    slider24=0; slider25=2; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=1; slider31=200; slider32=50; slider33=25;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=60; slider39=75;
  ) : slider40 == 9 ? ( // ORGAN
    slider1=0; slider2=0; slider3=0; slider4=80;
    slider5=0; slider6=1; slider7=0; slider8=50;
    slider9=0; slider10=0; slider11=0;
    slider12=0; slider13=6000; slider14=1;
    slider15=5; slider16=100; slider17=80; slider18=100; slider19=1000;
    slider20=5; slider21=50; slider22=95; slider23=50;
    slider24=0; slider25=6; slider26=200;
    slider27=0; slider28=6; slider29=8;
    slider30=0; slider31=350; slider32=40; slider33=30;
    slider34=1; slider35=1.2; slider36=4; slider37=35;
    slider38=0; slider39=65;
  ) : slider40 == 10 ? ( // BRASS
    slider1=2; slider2=0; slider3=0; slider4=80;
    slider5=2; slider6=0; slider7=10; slider8=70;
    slider9=0; slider10=3; slider11=0;
    slider12=0; slider13=1500; slider14=3;
    slider15=60; slider16=300; slider17=40; slider18=200; slider19=5000;
    slider20=80; slider21=200; slider22=85; slider23=150;
    slider24=0; slider25=5; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=0; slider31=350; slider32=40; slider33=30;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=20; slider39=70;
  ) : slider40 == 11 ? ( // NOISE HIT
    slider1=3; slider2=0; slider3=0; slider4=30;
    slider5=2; slider6=1; slider7=0; slider8=20;
    slider9=0; slider10=80; slider11=0;
    slider12=2; slider13=3000; slider14=8;
    slider15=3; slider16=60; slider17=0; slider18=50; slider19=5000;
    slider20=3; slider21=80; slider22=0; slider23=50;
    slider24=0; slider25=2; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=1; slider31=180; slider32=60; slider33=35;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=0; slider39=75;
  ) : slider40 == 12 ? ( // SHIMMER
    slider1=0; slider2=1; slider3=0; slider4=70;
    slider5=1; slider6=2; slider7=3; slider8=40;
    slider9=120; slider10=5; slider11=0;
    slider12=1; slider13=800; slider14=2;
    slider15=500; slider16=1500; slider17=20; slider18=2000; slider19=1500;
    slider20=300; slider21=1000; slider22=40; slider23=3000;
    slider24=1; slider25=0.15; slider26=600;
    slider27=0; slider28=3; slider29=5;
    slider30=1; slider31=450; slider32=65; slider33=40;
    slider34=1; slider35=0.4; slider36=12; slider37=45;
    slider38=0; slider39=55;
  ) : slider40 == 13 ? ( // SUB BASS
    slider1=0; slider2=-2; slider3=0; slider4=100;
    slider5=0; slider6=-1; slider7=0; slider8=30;
    slider9=0; slider10=0; slider11=0;
    slider12=0; slider13=300; slider14=2;
    slider15=10; slider16=200; slider17=80; slider18=100; slider19=500;
    slider20=10; slider21=100; slider22=90; slider23=100;
    slider24=0; slider25=2; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=0; slider31=350; slider32=40; slider33=30;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=30; slider39=85;
  ) : slider40 == 14 ? ( // WOBBLE
    slider1=2; slider2=-1; slider3=0; slider4=80;
    slider5=3; slider6=-1; slider7=0; slider8=60;
    slider9=0; slider10=0; slider11=0;
    slider12=0; slider13=600; slider14=12;
    slider15=10; slider16=500; slider17=30; slider18=300; slider19=4000;
    slider20=10; slider21=200; slider22=80; slider23=200;
    slider24=0; slider25=3; slider26=3000;
    slider27=0; slider28=5; slider29=0;
    slider30=0; slider31=350; slider32=40; slider33=30;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=40; slider39=70;
  ) : slider40 == 15 ? ( // RING MOD
    slider1=0; slider2=0; slider3=0; slider4=80;
    slider5=0; slider6=1; slider7=30; slider8=80;
    slider9=0; slider10=0; slider11=1;
    slider12=0; slider13=10000; slider14=1;
    slider15=3; slider16=500; slider17=20; slider18=1000; slider19=3000;
    slider20=3; slider21=800; slider22=0; slider23=1500;
    slider24=0; slider25=0.3; slider26=0;
    slider27=0; slider28=5; slider29=0;
    slider30=1; slider31=500; slider32=60; slider33=35;
    slider34=0; slider35=0.8; slider36=5; slider37=30;
    slider38=0; slider39=60;
  ) : slider40 == 16 ? ( // DARK PAD
    slider1=2; slider2=-1; slider3=-5; slider4=60;
    slider5=3; slider6=0; slider7=5; slider8=50;
    slider9=0; slider10=8; slider11=0;
    slider12=0; slider13=800; slider14=4;
    slider15=1000; slider16=2000; slider17=20; slider18=2000; slider19=1500;
    slider20=1500; slider21=1000; slider22=70; slider23=3000;
    slider24=1; slider25=0.1; slider26=400;
    slider27=0; slider28=0.3; slider29=12;
    slider30=1; slider31=600; slider32=55; slider33=30;
    slider34=1; slider35=0.3; slider36=10; slider37=40;
    slider38=0; slider39=55;
  );
);
prev_preset = slider40;

// Convert ms to sample counts
amp_atk_samps = max(1, slider20 * srate / 1000);
amp_dec_samps = max(1, slider21 * srate / 1000);
amp_sus_level = slider22 / 100;
amp_rel_samps = max(1, slider23 * srate / 1000);

flt_atk_samps = max(1, slider15 * srate / 1000);
flt_dec_samps = max(1, slider16 * srate / 1000);
flt_sus_level = slider17 / 100;
flt_rel_samps = max(1, slider18 * srate / 1000);
flt_env_amt = slider19;

osc1_level = slider4 / 100;
osc2_level = slider8 / 100;
fm_amount = slider9;
noise_level = slider10 / 100;

delay_samps = slider31 * srate / 1000;
delay_fb = slider32 / 100;
delay_mix = slider33 / 100;

glide_samps = max(1, slider38 * srate / 1000);
master_vol = slider39 / 100;

@block
// Process MIDI events
while (midirecv(ts, msg1, msg2, msg3)) (
  status = msg1 & 0xF0;

  status == 0x90 && msg3 > 0 ? ( // Note On
    note = msg2;
    vel = msg3 / 127;

    // Find free voice or steal oldest
    best = -1;
    best_time = 0;
    i = 0;
    while (i < MAX_POLY) (
      base = VOICE_MEM + i * VOICE_SIZE;
      base[0] == 0 ? (
        best = i;
        i = MAX_POLY; // break
      ) : (
        // Track longest-running voice for stealing
        base[9] > best_time ? (
          best_time = base[9];
          best = i;
        );
      );
      i += 1;
    );

    best >= 0 ? (
      base = VOICE_MEM + best * VOICE_SIZE;
      old_freq = base[6]; // save for glide
      base[0] = 1; // active
      base[1] = note;
      base[2] = vel;
      // Keep phases for smooth transition if voice was active
      base[0] == 0 ? (
        base[3] = 0; // osc1_phase
        base[4] = 0; // osc2_phase
      );
      target = midi2freq(note);
      base[5] = target;
      // Glide: start from old freq if slider38 > 0 and old voice was playing
      slider38 > 0 && old_freq > 0 ? (
        base[6] = old_freq;
      ) : (
        base[6] = target;
      );
      // Amp envelope: attack
      base[7] = 0; // amp_env starts at 0
      base[8] = 1; // attack stage
      base[9] = 0; // time counter
      // Filter envelope: attack
      base[10] = 0;
      base[11] = 1;
      base[12] = 0;
      // Clear biquad state
      (base + 13)[0] = 0;
      (base + 14)[0] = 0;
      (base + 15)[0] = 0;
      (base + 16)[0] = 0;
    );
  );

  status == 0x80 || (status == 0x90 && msg3 == 0) ? ( // Note Off
    note = msg2;
    i = 0;
    while (i < MAX_POLY) (
      base = VOICE_MEM + i * VOICE_SIZE;
      base[0] > 0 && base[0] < 4 && base[1] == note ? (
        base[8] = 4; // release stage
        base[9] = 0;
        base[11] = 4; // filter release
        base[12] = 0;
      );
      i += 1;
    );
  );

  // Pass through other MIDI
  midisend(ts, msg1, msg2, msg3);
);

@sample
mix_l = 0;
mix_r = 0;

// Advance LFOs
lfo1_inc = TWO_PI * slider25 / srate;
lfo2_inc = TWO_PI * slider28 / srate;
lfo1_phase += lfo1_inc;
lfo1_phase >= TWO_PI ? lfo1_phase -= TWO_PI;
lfo2_phase += lfo2_inc;
lfo2_phase >= TWO_PI ? lfo2_phase -= TWO_PI;

lfo1_val = gen_wave(slider24, lfo1_phase) * slider26;
lfo2_val = gen_wave(slider27, lfo2_phase) * (slider29 / 100);

// Process each voice
i = 0;
while (i < MAX_POLY) (
  base = VOICE_MEM + i * VOICE_SIZE;

  base[0] > 0 ? (
    vel = base[2];

    // --- Glide ---
    base[6] != base[5] ? (
      glide_rate = pow(base[5] / base[6], 1 / glide_samps);
      base[6] *= glide_rate;
      abs(base[6] - base[5]) < 0.01 ? base[6] = base[5];
    );
    freq = base[6];

    // --- Amp Envelope ---
    base[8] == 1 ? ( // attack
      base[7] += 1 / amp_atk_samps;
      base[7] >= 1 ? (
        base[7] = 1;
        base[8] = 2; // decay
        base[9] = 0;
      );
    ) : base[8] == 2 ? ( // decay
      base[7] -= (1 - amp_sus_level) / amp_dec_samps;
      base[7] <= amp_sus_level ? (
        base[7] = amp_sus_level;
        base[8] = 3; // sustain
      );
    ) : base[8] == 4 ? ( // release
      base[7] -= base[7] / amp_rel_samps;
      base[7] < 0.0001 ? (
        base[7] = 0;
        base[0] = 0; // voice off
      );
    );
    amp_env = base[7];

    // --- Filter Envelope ---
    base[11] == 1 ? (
      base[10] += 1 / flt_atk_samps;
      base[10] >= 1 ? (
        base[10] = 1;
        base[11] = 2;
        base[12] = 0;
      );
    ) : base[11] == 2 ? (
      base[10] -= (1 - flt_sus_level) / flt_dec_samps;
      base[10] <= flt_sus_level ? (
        base[10] = flt_sus_level;
        base[11] = 3;
      );
    ) : base[11] == 4 ? (
      base[10] -= base[10] / flt_rel_samps;
      base[10] < 0.0001 ? base[10] = 0;
    );
    flt_env = base[10];

    // --- OSC2 ---
    osc2_freq = freq * pow(2, slider6) * pow(2, slider7 / 1200);
    osc2_inc = TWO_PI * osc2_freq / srate;
    base[4] += osc2_inc;
    base[4] >= TWO_PI ? base[4] -= TWO_PI;
    osc2_out = gen_wave(slider5, base[4]) * osc2_level;

    // --- OSC1 (with FM from OSC2) ---
    osc1_freq = freq * pow(2, slider2) * pow(2, slider3 / 1200);
    fm_amount > 0 ? (
      osc1_freq = osc1_freq * (1 + fm_amount * osc2_out);
    );
    osc1_inc = TWO_PI * osc1_freq / srate;
    base[3] += osc1_inc;
    base[3] >= TWO_PI ? base[3] -= TWO_PI;
    // Clamp phase for FM stability
    base[3] > 100 * TWO_PI ? base[3] -= floor(base[3] / TWO_PI) * TWO_PI;
    base[3] < -100 * TWO_PI ? base[3] += floor(-base[3] / TWO_PI) * TWO_PI;
    osc1_out = gen_wave(slider1, base[3]) * osc1_level;

    // --- Mix oscillators ---
    slider11 ? ( // Ring mod
      sig = osc1_out * osc2_out;
    ) : (
      sig = osc1_out + osc2_out;
    );

    // --- Noise ---
    noise_level > 0 ? (
      sig += (rand(2) - 1) * noise_level;
    );

    // --- Filter ---
    cutoff = slider13 + flt_env * flt_env_amt + lfo1_val;
    cutoff = min(max(cutoff, 20), srate * 0.45);
    calc_biquad(slider12, cutoff, slider14);
    sig = proc_biquad(sig, base, 13);

    // --- Apply envelopes ---
    sig *= amp_env * vel;

    // --- LFO2 tremolo ---
    lfo2_val > 0 ? (
      sig *= 1 - lfo2_val * 0.5;
    );

    mix_l += sig;
    mix_r += sig;

    // Increment time
    base[9] += 1;
  );

  i += 1;
);

// --- Delay Effect ---
slider30 ? (
  delay_read = delay_pos - floor(delay_samps);
  delay_read < 0 ? delay_read += DELAY_MAX;

  dl_l = (DELAY_BUF + delay_read * 2)[0];
  dl_r = (DELAY_BUF + delay_read * 2 + 1)[0];

  (DELAY_BUF + delay_pos * 2)[0] = mix_l + dl_l * delay_fb;
  (DELAY_BUF + delay_pos * 2 + 1)[0] = mix_r + dl_r * delay_fb;

  mix_l = mix_l * (1 - delay_mix) + dl_l * delay_mix;
  mix_r = mix_r * (1 - delay_mix) + dl_r * delay_mix;

  delay_pos += 1;
  delay_pos >= DELAY_MAX ? delay_pos = 0;
);

// --- Chorus Effect ---
slider34 ? (
  chorus_depth_samps = slider36 * srate / 1000;
  chorus_lfo = (sin(TWO_PI * slider35 * chorus_pos / srate) + 1) * 0.5;
  chorus_offset = floor(chorus_lfo * chorus_depth_samps);
  chorus_read = chorus_pos - chorus_offset;
  chorus_read < 0 ? chorus_read += CHORUS_MAX;

  ch_l = (CHORUS_BUF + (chorus_read % CHORUS_MAX) * 2)[0];
  ch_r = (CHORUS_BUF + (chorus_read % CHORUS_MAX) * 2 + 1)[0];

  (CHORUS_BUF + (chorus_pos % CHORUS_MAX) * 2)[0] = mix_l;
  (CHORUS_BUF + (chorus_pos % CHORUS_MAX) * 2 + 1)[0] = mix_r;

  ch_mix = slider37 / 100;
  mix_l = mix_l * (1 - ch_mix) + ch_l * ch_mix;
  mix_r = mix_r * (1 - ch_mix) + ch_r * ch_mix;

  chorus_pos += 1;
);

// --- Master output ---
spl0 = mix_l * master_vol;
spl1 = mix_r * master_vol;
