Hardsynced Piano

Saw wave with a hard sync, kinda sounds like an actual piano when tuned right

Log in to post a comment.

// inspired by https://www.youtube.com/watch?v=Aktb_dmY4vk
// essentially, pass a bandpassed saw wave with vibrato and a bit of noise into a short reverb
// 
// It is possible to not do the reverb, as the diffusion it does that creates the sound
// is essentially a number of parallel delays. A chorus of saw waves with random phases
// gives the same effect.

// bit slower, original is 210
ditty.bpm = 140;

input.tuning = 0.5; //min=0.125, max=2, step=0.125
input.reset = 0.45; //min=0, max=1, step=0.01


function softclip(x) {
    return x < -1 ? -1 : x > 1 ? 1 : 1.5*(1-x*x/3)*x;
}

function varsaw(p, formant) {
    let x = p-~~p;
    return (x - 0.5) * softclip(formant*x*(1-x)) * 2;
}

const osc = synth.def(
    class {
        constructor(options) {
            // frequencies
            this.freq = midi_to_hz(options.note) * ditty.dt;
            
            // phases
            this.time1 = 0;
            this.time2 = Math.random();
            
            // filter
            this.lowp = 0;
        }
        process(note, env, tick, options) {
            // phase times
            this.time1 += this.freq * input.tuning;
            this.time1 -= this.time1|0;
            
            this.time2 += this.freq * input.reset * input.tuning;
            const reset = this.time2 > 1;
            this.time2 -= this.time2|0;
            
            // hard sync
            if (reset) this.time1 = 0;
            
            // saw wave
            const saw = varsaw(this.time1, 20) * env.value;
            
            // lowpass
            const a = Math.exp(-Math.PI * 1000 * env.value * env.value * ditty.dt);
            this.lowp += (saw - this.lowp) * (1 - a);
            
            return this.lowp * 2;
        }
    }, {
        // attack parameters
        levels: [0, 1, 0, 0],
        times: [0.0005, 4, 1],
        env: segenv,
    }
);

// === reverb ===
input.body_length = 0.6; //min=0, max=1, step=0.01
input.body_diffuse = 0.6; //min=0, max=1, step=0.01
input.body_feedback = 0.3; //min=0, max=1, step=0.01
input.body_damping = 2200; //min=10, max=10000, step=1
input.body_wet = 0.8; //min=0, max=1, step=0.01

// from struss
class Delayline {
    constructor(n) {
        this.n = ~~n;
        this.p = 0;
        this.lastOut = 0;
        this.data = new Float32Array(n);
    }
    clock(input) {
        this.lastOut = this.data[this.p];
        this.data[this.p] = input;
        if (++this.p >= this.n) this.p = 0;
    }
    tap(offset) {
        let x = this.p - (offset|0) - 1;
        x %= this.n;
        if (x < 0) x += this.n;
        return this.data[x];
    }
}

function newdelay(count) {
    return new Array(count)
        .fill(null)
        .map(_ => new Delayline(ditty.sampleRate * 0.01))
}

function newlens(count, max) {
    return new Array(count)
        .fill(1)
        .map(_ => Math.random() * max * 0.01);
}

function hada8([c0, c1, c2, c3, c4, c5, c6, c7]) {
    // shuffle
    [c0, c1, c2, c3, c4, c5, c6, c7] = [c3, -c7, c1, -c4, -c5, c6, c0, c2];
    
    // 8x8 hadamard matrix
    return [
        (c0 + c1 + c2 + c3 + c4 + c5 + c6 + c7) * Math.sqrt(1 / 8),
        (c0 - c1 + c2 - c3 + c4 - c5 + c6 - c7) * Math.sqrt(1 / 8),
        (c0 + c1 - c2 - c3 + c4 + c5 - c6 - c7) * Math.sqrt(1 / 8),
        (c0 - c1 - c2 + c3 + c4 - c5 - c6 + c7) * Math.sqrt(1 / 8),
        (c0 + c1 + c2 + c3 - c4 - c5 - c6 - c7) * Math.sqrt(1 / 8),
        (c0 - c1 + c2 - c3 - c4 + c5 - c6 + c7) * Math.sqrt(1 / 8),
        (c0 + c1 - c2 - c3 - c4 - c5 + c6 + c7) * Math.sqrt(1 / 8),
        (c0 - c1 - c2 + c3 - c4 + c5 + c6 - c7) * Math.sqrt(1 / 8),
    ];
}

function house8([c0, c1, c2, c3, c4, c5, c6, c7]) {
    const sum = c0 + c1 + c2 + c3 + c4 + c5 +  c6 + c7;
    return [c0, c1, c2, c3, c4, c5, c6, c7].map(x => x - sum * 0.25);
}

// geraint luff reverberator
// https://signalsmith-audio.co.uk/writing/2021/lets-write-a-reverb/
class Reverb {
    constructor(n) {
        // diffusors
        this.diff1 = newdelay(8);
        this.diff2 = newdelay(8);
        this.diff3 = newdelay(8);
        this.diff4 = newdelay(8);
        
        // length of diffusors
        this.lens1 = newlens(8, ditty.sampleRate);
        this.lens2 = newlens(8, ditty.sampleRate);
        this.lens3 = newlens(8, ditty.sampleRate);
        this.lens4 = newlens(8, ditty.sampleRate);
        
        // reverb loop
        this.reverb = newdelay(8);
        this.rvlens = newlens(8, ditty.sampleRate);
        this.rvlp = new Array(8).fill(0);
    }
    tick(inp) {
        // split into channels
        const channels = new Array(8).fill(inp);
        
        // diffuser 1
        const diff1 = hada8(this.diff1.map((d, i) => {
            d.clock(channels[i]);
            return d.tap(this.lens1[i] * input.body_diffuse);
        }));
        
        // diffuser 2
        const diff2 = hada8(this.diff2.map((d, i) => {
            d.clock(diff1[i]);
            return d.tap(this.lens2[i] * input.body_diffuse);
        }));
        
        // diffuser 3
        const diff3 = hada8(this.diff3.map((d, i) => {
            d.clock(diff2[i]);
            return d.tap(this.lens3[i] * input.body_diffuse);
        }));
        
        // diffuser 4
        const diff4 = hada8(this.diff4.map((d, i) => {
            d.clock(diff3[i]);
            return d.tap(this.lens4[i] * input.body_diffuse);
        }));
        
        // reverb loop
        const fb = house8(this.reverb.map((d, i) => {
            const tap = d.tap(this.rvlens[i] * input.body_length);
            return tap * input.body_feedback + diff4[i];
        }));
        
        // filter
        const damped = fb.map((x, i) => {
            // one pole
            this.rvlp[i] 
                += (x - this.rvlp[i]) 
                * (1 - Math.exp(-input.body_damping * ditty.dt * Math.PI * 2));
            
            return this.rvlp[i];
        });
        
        // feedback
        this.reverb.forEach((d, i) => d.clock(damped[i]));
        
        // and out again
        return fb.reduce((a, d) => a + d, 0) * Math.sqrt(1 / channels.length);
    }
}

// Simple allpass reverberator, based on this article:
// http://www.spinsemi.com/knowledge_base/effects.html
const reverb = filter.def(class {
    constructor(options) {
        this.l = new Reverb(1);
        this.r = new Reverb(1);
    }
    process(inp, options) {
        const [l, r] = inp;
        return [
            this.l.tick(l) * input.body_wet
            + l * (1 - input.body_wet),
            this.r.tick(r) * input.body_wet
            + r * (1 - input.body_wet)
        ];
    }
});

// === original ===
// Forked from "Wizards & Warriors (main menu)" by romaindurand
// https://dittytoy.net/ditty/3066954356

function melodyPattern(notes, baseNote) {
    return () => {
        for(i = 0; i < notes.length; i++) {
            if (notes[i] != 0) osc.play(notes[i], { duration: 0.5, pan: 0.2 - Math.random() * 0.1 });
            sleep(0.5);
            if (baseNote != 0) osc.play(baseNote, { duration: 0.5, pan: 0.2 - Math.random() * 0.1 });
            sleep(0.5);
        }
    };
}

function simpleMelodyPattern(notes) {
    // calculate length for it to sound nice
    let lens = new Array(notes.length).fill(0.45);
    let last = 0;
    
    // every off note adds to the on length of the last on note
    for (let i = 0; i < lens.length; i++) {
        if (notes[i] === 0) lens[last] += 0.5;
        else last = i;
    }
    
    return () => {
        for(let i = 0; i < notes.length; i++) {
            if (notes[i] != 0) osc.play(notes[i], { duration: lens[i], pan: -0.2 - Math.random() * 0.1 });
            sleep(0.5);
        }
    };
}

const melodySeq0 = () => {
    melodyPattern([d5, e5, f5, g5], a4)();
    melodyPattern([bb4, d5, g5, f5], g4)();
    melodyPattern([e5, f5], c5)();
    melodyPattern([g5, c5], g4)();
    melodyPattern([bb4, a4, f5, e5], f4)();
    
    const notes0 = [d5, e5, f5, d5];
    melodyPattern(notes0, bb4)();
    //same as previous pattern with another base note
    melodyPattern(notes0, g4)();
    
    melodyPattern([e5, cs5, e5, cs5], a4)();
    melodyPattern([a5, cs5, a5, a5], a4)();
};

const melodySeq1 = () => {
    melodyPattern([f5, d5], a4)();
    simpleMelodyPattern([f5, g5, a5, a4])();
    simpleMelodyPattern([
        a5, bb4, d5, bb5, a5, bb4, d5, bb5,
        g5, g4, c5, g4, e5, f5, g5, c5,
        g5, a4, c5, a5, f5, a4, e5, c5,
        f5, bb4, d5, bb4, d5, e5, f5, bb4,
        f5, g4, bb4, g4, d5, e5, f5, g4
    ])();
    melodyPattern([f5, cs5, e5, cs5, a5, cs5], a4)();
    melodyPattern([a5, a5], cs5)();
};

const melodySeqEnd = simpleMelodyPattern([d5, a4, e5, f5]);

const bassPattern = simpleMelodyPattern([
        d3, 0, 0, 0, 0, d3, e3, f3,
        g3, 0, 0, 0, 0, g3, a3, bb3,
        c4, 0, 0, 0, 0, bb3, a3, g3,
        f3, 0, g3, 0, a3, 0, f3, 0,
        bb3, 0, 0, 0, 0, c4, bb3, a3,
        g3, 0, 0, 0, 0, a3, bb3, g3,
        a3, 0, 0, 0, a3, 0, 0, 0,
        a3, 0, g3, 0, f3, 0, e3, 0
    ]);

loop(() => {
    melodySeq0();
    melodySeq0();
    melodySeq1();
    melodySeq1();
    melodySeqEnd();
    melodySeqEnd();
    melodySeqEnd();
    simpleMelodyPattern([d5, 0, 0, 0])();
}, { name: 'melody', amp: 0.3 }).connect(reverb.create());

loop(() => {
    sleep(32);
    bassPattern();
    bassPattern();
    bassPattern();
    simpleMelodyPattern([d3, 0, 0, 0, 0, 0, 0, 0])();
    sleep(4);
}, { name: 'bass', amp: 0.5 }).connect(reverb.create());