cuneus 0.5.0

A WGPU-based shader development tool
Documentation
// Cuneus GPU Synth — a simple keyboard instrument written entirely in WGSL
// Enes Altun, 2025-2026; MIT License
// Press keys 1-9 to play notes (C4 through D5). Everything runs on the GPU:
// waveform generation, ADSR envelopes, lowpass filter, distortion, chorus,
// delay, reverb — all computed per-sample at 44.1kHz.
// It's not a Moog, but it's fun to play with :-)

struct TimeUniform { time: f32, delta: f32, frame: u32, _padding: u32 };
@group(0) @binding(0) var<uniform> u_time: TimeUniform;
@group(1) @binding(0) var output: texture_storage_2d<rgba16float, write>;
@group(1) @binding(1) var<uniform> params: SynthParams;
@group(2) @binding(0) var<storage, read_write> audio_buffer: array<f32>;

struct SynthParams {
    tempo: f32,
    waveform_type: u32,
    octave: f32,
    volume: f32,
    beat_enabled: u32,
    reverb_mix: f32,
    delay_time: f32,
    delay_feedback: f32,
    filter_cutoff: f32,
    filter_resonance: f32,
    distortion_amount: f32,
    chorus_rate: f32,
    chorus_depth: f32,
    attack_time: f32,
    decay_time: f32,
    sustain_level: f32,
    release_time: f32,
    sample_offset: u32,
    samples_to_generate: u32,
    sample_rate: u32,
    key_states: array<vec4<f32>, 3>,
    key_decay: array<vec4<f32>, 3>,
};

const PI: f32 = 3.14159265;
const TAU: f32 = 6.2831853;

fn get_note_frequency(idx: u32, octave: f32) -> f32 {
    let notes = array<f32, 9>(
        261.63, 293.66, 329.63, 349.23, 392.00,
        440.00, 493.88, 523.25, 587.33
    );
    return notes[idx] * pow(2.0, octave - 4.0);
}

// returns vec2(press_time, release_time) for voice i
fn get_key(i: u32) -> vec2<f32> {
    let vi = i / 4u;
    let ci = i % 4u;
    var press_t: f32 = 0.0;
    var release_t: f32 = 0.0;
    if (ci == 0u) { press_t = params.key_states[vi].x; release_t = params.key_decay[vi].x; }
    else if (ci == 1u) { press_t = params.key_states[vi].y; release_t = params.key_decay[vi].y; }
    else if (ci == 2u) { press_t = params.key_states[vi].z; release_t = params.key_decay[vi].z; }
    else { press_t = params.key_states[vi].w; release_t = params.key_decay[vi].w; }
    return vec2<f32>(press_t, release_t);
}

fn adsr_envelope(t: f32, press_time: f32, release_time: f32) -> f32 {
    if (press_time <= 0.0) { return 0.0; }
    let since_press = t - press_time;
    if (since_press < 0.0) { return 0.0; }

    let A = max(params.attack_time, 0.005); // min 5ms to avoid clicks
    let D = params.decay_time;
    let S = params.sustain_level;

    var level: f32;
    if (since_press < A) {
        level = smoothstep(0.0, A, since_press);
    } else if (since_press < A + D) {
        level = 1.0 - (1.0 - S) * (since_press - A) / D;
    } else {
        level = S;
    }

    if (release_time > 0.0) {
        let since_release = t - release_time;
        if (since_release < 0.0) { return level; }
        let R = max(params.release_time, 0.02);
        // figure out where the envelope was when the key was released
        let rsp = release_time - press_time;
        var release_level: f32;
        if (rsp < A) { release_level = rsp / A; }
        else if (rsp < A + D) { release_level = 1.0 - (1.0 - S) * (rsp - A) / D; }
        else { release_level = S; }
        level = release_level * exp(-since_release * 5.0 / R);
        if (level < 0.001) { return 0.0; }
    }

    return level;
}

fn generate_waveform(phase: f32, waveform_type: u32) -> f32 {
    switch waveform_type {
        case 0u: { return sin(phase); }
        case 1u: { return 2.0 * fract(phase / TAU) - 1.0; }
        case 2u: { return select(-1.0, 1.0, sin(phase) > 0.0); }
        case 3u: {
            let t = fract(phase / TAU);
            return select(4.0 * t - 1.0, 3.0 - 4.0 * t, t > 0.5);
        }
        case 4u: {
            return 2.0 * fract(sin(phase * 12.9898) * 43758.5453) - 1.0;
        }
        default: { return sin(phase); }
    }
}

fn lowpass(s: f32, cutoff: f32, resonance: f32, t: f32) -> f32 {
    if (cutoff > 0.95) { return s; }
    let freq = cutoff * cutoff * 0.8;
    return s * (0.3 + freq * 0.7) + s * sin(t * 50.0) * resonance * 0.1;
}

fn distort(s: f32, amount: f32) -> f32 {
    if (amount < 0.01) { return s; }
    let drive = 1.0 + amount * 5.0;
    let driven = s * drive;
    return mix(s, driven / (1.0 + abs(driven)), amount);
}

fn chorus(s: f32, t: f32, rate: f32, depth: f32) -> f32 {
    if (depth < 0.01) { return s; }
    let lfo1 = sin(t * rate) * depth;
    let lfo2 = sin(t * rate * 1.3 + 1.57) * depth;
    return (s + s * (1.0 + lfo1 * 0.5) * 0.4 + s * (1.0 + lfo2 * 0.3) * 0.3) / 1.7;
}

fn reverb(s: f32, mix_amt: f32, t: f32) -> f32 {
    if (mix_amt < 0.01) { return s; }
    let r = s * 0.7
        + s * sin(t * 100.0) * 0.15 * mix_amt
        + s * sin(t * 150.0 + 0.08) * 0.1 * mix_amt
        + s * sin(t * 80.0 + 0.15) * 0.08 * mix_amt;
    return mix(s, r, mix_amt);
}

fn delay_fx(s: f32, t: f32, del_time: f32, feedback: f32) -> f32 {
    if (feedback < 0.01) { return s; }
    let dt = t - del_time;
    return s + s * sin(dt * 10.0) * feedback * 0.6 + s * sin(dt * 15.0) * feedback * 0.12;
}

fn kick(t: f32, tempo: f32) -> f32 {
    let beat_t = fract(t / (60.0 / tempo));
    if (beat_t < 0.1) {
        let env = exp(-beat_t * 30.0);
        let freq = mix(40.0, 120.0, exp(-beat_t * 40.0));
        return sin(TAU * freq * beat_t) * env * 0.25;
    }
    return 0.0;
}

fn synthSample(t: f32) -> vec2<f32> {
    var sum: f32 = 0.0;
    var num_active: f32 = 0.0;

    for (var i = 0u; i < 9u; i++) {
        let k = get_key(i);
        let press_time = k.x;
        let release_time = k.y;

        let env = adsr_envelope(t, press_time, release_time);
        if (env > 0.0005) {
            let freq = get_note_frequency(i, params.octave);
            let detune = (f32(i) - 4.0) * 0.002;
            let adj_freq = freq * (1.0 + detune);
            let phase_offset = f32(i) * 0.61803;
            let phase = (t * adj_freq + phase_offset) * TAU;

            var s = generate_waveform(phase, params.waveform_type);
            s = lowpass(s, params.filter_cutoff, params.filter_resonance, t);
            s = distort(s, params.distortion_amount);
            s = chorus(s, t + f32(i) * 0.1, params.chorus_rate, params.chorus_depth);

            sum += s * env * 0.6;
            num_active += 1.0;
        }
    }

    if (num_active > 1.0) { sum /= sqrt(num_active); }

    if (params.beat_enabled > 0u) {
        sum += kick(t, params.tempo);
    }

    sum = delay_fx(sum, t, params.delay_time, params.delay_feedback);
    sum = reverb(sum, params.reverb_mix, t);

    sum *= params.volume;
    let clipped = sum / (1.0 + abs(sum));

    let stereo_offset = sin(t * params.chorus_rate * 0.7) * params.chorus_depth * 0.1;
    return vec2<f32>(clipped - stereo_offset, clipped + stereo_offset);
}

@compute @workgroup_size(16, 16, 1)
fn main(@builtin(global_invocation_id) g: vec3<u32>) {
    let dims = textureDimensions(output);
    if (g.x >= dims.x || g.y >= dims.y) { return; }

    if (g.x == 0u && g.y == 0u) {
        let sr = f32(params.sample_rate);
        let n = params.samples_to_generate;
        for (var i = 0u; i < n; i++) {
            let global_sample = params.sample_offset + i;
            let t = f32(global_sample) / sr;
            let stereo = synthSample(t);
            audio_buffer[i * 2u] = stereo.x;
            audio_buffer[i * 2u + 1u] = stereo.y;
        }
    }

    let uv = vec2<f32>(f32(g.x) / f32(dims.x), f32(g.y) / f32(dims.y));

    var color = vec3<f32>(0.02, 0.02, 0.1) * (1.0 - uv.y * 0.3);

    if (params.beat_enabled > 0u && uv.y > 0.98) {
        let beat_t = fract(u_time.time / (60.0 / params.tempo));
        let pulse = exp(-beat_t * 10.0) * 0.8;
        color = vec3<f32>(pulse, pulse * 0.5, pulse * 0.2);
    }

    let bar_top: f32 = 0.9;
    let bar_max_h: f32 = 0.6;
    let bar_w: f32 = 0.08;
    let bar_sp: f32 = 0.02;
    let total_w = 9.0 * bar_w + 8.0 * bar_sp;
    let start_x = (1.0 - total_w) * 0.5;

    for (var i = 0u; i < 9u; i++) {
        let bx = start_x + f32(i) * (bar_w + bar_sp);
        let k = get_key(i);
        let env = adsr_envelope(u_time.time, k.x, k.y);
        let is_held = k.x > 0.0 && k.y == 0.0;
        let intensity = max(0.1, env);
        let bar_h = bar_max_h * intensity;
        let bar_bot = bar_top - bar_h;

        if (uv.x >= bx && uv.x <= bx + bar_w && uv.y >= bar_bot && uv.y <= bar_top) {
            let hue = f32(i) / 8.0 * TAU;
            let rc = vec3<f32>(
                0.5 + 0.5 * sin(hue),
                0.5 + 0.5 * sin(hue + 2.094),
                0.5 + 0.5 * sin(hue + 4.188)
            );
            let grad = 1.0 - (bar_top - uv.y) / bar_h * 0.3;
            if (is_held) {
                let pulse = sin(u_time.time * 10.0) * 0.1 + 0.9;
                color = rc * intensity * grad * pulse;
            } else {
                color = rc * intensity * grad * 0.5;
            }
        }

        if (uv.x >= bx && uv.x <= bx + bar_w && uv.y >= 0.92 && uv.y <= 0.98) {
            color = vec3<f32>(0.8, 0.8, 0.9);
        }
    }

    if (uv.y < 0.05) {
        var wc = vec3<f32>(0.5);
        switch params.waveform_type {
            case 0u: { wc = vec3<f32>(0.3, 0.8, 0.3); }
            case 1u: { wc = vec3<f32>(0.8, 0.8, 0.3); }
            case 2u: { wc = vec3<f32>(0.8, 0.3, 0.3); }
            case 3u: { wc = vec3<f32>(0.3, 0.3, 0.8); }
            case 4u: { wc = vec3<f32>(0.8, 0.3, 0.8); }
            default: {}
        }
        color = wc;
    }

    textureStore(output, g.xy, vec4<f32>(color, 1.0));
}