// Crusher: the mix through an old sampler and a broken radio: fewer bits,
// a lower sample rate, drive, a tone control and hiss.
//   1 Bits      16 bits (clean) down to 3
//   2 Rate      44 kHz (clean) down to under 1.4 kHz
//   3 Drive     clean to hard clipping
//   4 Tone      dark to open
//   5 Hiss      none to a lot, rising and falling with the sound (silence stays silent)
//   6 Width     mono, as it came in (the middle), twice as wide
//   7 Mix       none, half and half (the middle), crushed only
// As a template: the sound is taken to the working level (fx::level), changed
// there with fx::gain and fx::times, and put back with fx::blend. Only what the
// knobs ask for is crushed: everything else keeps every bit.
#include "../fx.h"

using fx::i32;
using fx::u32;

namespace {
// The state's words: the hold's countdown and what it holds, the tone filter,
// the hiss's random number and how loud the sound is.
enum { kHold, kHeldL, kHeldR, kLowL, kLowR, kSeed, kLevel };
// Fewer bits: a level rounded to steps of 2^shift (full scale is 2^27, so a
// shift of 12 is 16 bits). Rounded to the nearest step, so silence stays silent.
inline i32 crush(i32 v, u32 shift) { return ((v + (1 << (shift - 1))) >> shift) << shift; }
// Drive: into the soft clip `into` (Q15, times 8: up to 8x), then `after` (Q15) of it back.
inline i32 drive(i32 v, i32 into, i32 after) {
    const i32 hot = fx::clamp(fx::gain(v, into), -(fx::full >> 2), fx::full >> 2) * 8;   // within 2x full
    return fx::gain(fx::soft(hot), after);
}
}

extern "C" void hd_start(u32* state) { state[kSeed] = 0x2545f491u; }

extern "C" void hd_render(u32* state, i32* left, i32* right, const i32* parameters) {
    // The knobs (Q15, 0 to 32767), turned into what the sound needs.
    const i32 bits = fx::knob(parameters, 0), rate = fx::knob(parameters, 1), hard = fx::knob(parameters, 2);
    const i32 tone = 1200 + ((fx::knob(parameters, 3) * 31500) >> 15);       // the tone's low-pass step
    const i32 hiss = (fx::knob(parameters, 4) * 2400) >> 15;                   // up to 7% of full scale
    const i32 width = fx::knob(parameters, 5), m = fx::knob(parameters, 6);
    const i32 curve = (hard * hard) >> 15;                                     // the drive's curve
    const i32 into = static_cast<i32>(fx::exp2_q12((curve * 3) >> 3));        // 2^(3d) / 8, Q15
    const i32 after = fx::clamp(static_cast<i32>(fx::exp2_q12(-((curve * 19) >> 6))) * 8, 0, 32767);   // 2^(-2.4d)
    const u32 drop = static_cast<u32>((bits * 14) >> 15);                     // bits dropped of 16: 0 is clean
    const u32 hold = 1 + static_cast<u32>(((rate * rate) >> 15) * 31 >> 15);  // samples held: 1 (clean) to 32
    u32 count = state[kHold], seed = state[kSeed];
    i32 held_l = static_cast<i32>(state[kHeldL]), held_r = static_cast<i32>(state[kHeldR]);
    i32 low_l = static_cast<i32>(state[kLowL]), low_r = static_cast<i32>(state[kLowR]);
    i32 level = static_cast<i32>(state[kLevel]);                               // how loud the sound is, a level
    for (unsigned i = 0; i < 64; ++i) {
        i32 l = fx::level(left[i]), r = fx::level(right[i]);
        fx::widen(l, r, width);
        // Drive: up to 8x into the clip, and most of that taken back after, so it
        // gets harder and grittier rather than much louder.
        if (hard) { l = drive(l, into, after); r = drive(r, into, after); }
        // A lower rate: hold a sample for `hold` samples, fewer bits as it's taken.
        if (!count) {
            held_l = drop ? crush(l, 12 + drop) : l;
            held_r = drop ? crush(r, 12 + drop) : r;
            count = hold;
        }
        --count;
        // Hiss: white noise as loud as the sound (quick to rise, slow to fall),
        // opposite in the two sides so it sounds wide.
        seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5;
        const i32 loud = (l < 0 ? -l : l) + (r < 0 ? -r : r);
        level += loud > level ? (loud - level) >> 6 : -(((level - loud) >> 10) + (level > loud));   // the +1 lets it reach 0
        const i32 white = fx::gain(static_cast<i32>(seed) >> 4, hiss);        // within +-full x hiss
        const i32 noise = fx::times(white, fx::clamp(level * 2, 0, fx::full));
        // Tone: a one-pole low-pass, nearly open at the top.
        low_l = fx::follow(low_l, held_l + noise, tone);
        low_r = fx::follow(low_r, held_r - noise, tone);
        left[i] = fx::blend(left[i], low_l, m);
        right[i] = fx::blend(right[i], low_r, m);
    }
    state[kHold] = count; state[kSeed] = seed; state[kLevel] = static_cast<u32>(level);
    state[kHeldL] = static_cast<u32>(held_l); state[kHeldR] = static_cast<u32>(held_r);
    state[kLowL] = static_cast<u32>(low_l); state[kLowR] = static_cast<u32>(low_r);
}
