Vocoder Happy Birthday

A vocoder singing Happy Birthday
Forked from: Vocoder Puccini

Log in to post a comment.

// Forked from "Vocoder Puccini" by athibaul
// https://dittytoy.net/ditty/6f30b0885d

function dbamp(dB) {
    return 10 ** (dB / 20);
}

const TWOPI = 2*Math.PI;

class Smoother {
    // We feed values to this object, and it smoothes them out
    constructor(v, dur=0.07) {
        this.v = v;
        this.a0 = clamp01(ditty.dt / dur);
    }
    update(target) { // should be called once per sample
        return this.v += this.a0 * (target - this.v);
    }
}

const formantSynth = synth.def(class {
    constructor(options) {
        this.phase = 0;
        let freq = midi_to_hz(options.note);
        this.freqsmoo = new Smoother(freq);
        let ffreq = options.ffreq;
        this.ffreqsmoo = new Smoother(ffreq);
        this.ampsmoo = new Smoother(dbamp(options.ampdb));
        this.bwsmoo = new Smoother(options.bw);
        let formratio = options.ffreq / freq;
        this.k = Math.floor(formratio);
        this.q = formratio - this.k;
    }
    process(note, env, tick, options) {
        // Movable ring modulation as explained here:
        // http://msp.ucsd.edu/techniques/latest/book-html/node95.html
        let freq = midi_to_hz(options.note);
        freq = this.freqsmoo.update(freq);
        let ffreq = this.ffreqsmoo.update(options.ffreq);
        let dphase = TWOPI*freq*ditty.dt;
        this.phase += dphase;
        if(this.phase > TWOPI) {
            this.phase -= TWOPI;
            // End of one period
            // We can change the formant ratio now!
            let formratio = ffreq / freq;
            this.k = Math.floor(formratio);
            this.q = formratio - this.k;
        }
        let carrier = (1-this.q) * Math.cos(this.k*this.phase) + this.q * Math.cos((this.k+1)*this.phase);
        
        let bw = this.bwsmoo.update(options.bw);
        let b = bw / freq;
        let a = 0.5*b*b;
        let modulator = 1/(1 + a * (1-Math.cos(this.phase)));
        
        let amp = this.ampsmoo.update(dbamp(options.ampdb));
        return carrier * modulator * amp * env.value;
    }
}, {ffreq:650, bw:80, env:one, ampdb:0});


// Formant values taken from
// https://www.classes.cs.uchicago.edu/archive/1999/spring/CS295/Computing_Resources/Csound/CsManual3.48b1.HTML/Appendices/table3.html
const table = {
    a: {
        freq: [650, 1080, 2650, 2900, 3250],
        amp: [0, -6, -7, -8, -22],
        bw: [80, 90, 120, 130, 140]
    },
    e: {
        freq: [400, 1700, 2600, 3200, 3580],
        amp: [0, -14, -12, -14, -20],
        bw: [70, 80, 100, 120, 120]
    },
    i: {
        freq: [290, 1870, 2800, 3250, 3540],
        amp: [0, -15, -18, -20, -30],
        bw: [40, 90, 100, 120, 120]
    },
    o: {
        freq: [400, 800, 2600, 2800, 3000],
        amp: [0, -10, -12, -12, -26],
        bw: [40, 80, 100, 120, 120]
    },
    u: {
        freq: [350, 600, 2700, 2900, 3300],
        amp: [0, -20, -17, -14, -26],
        bw: [40, 60, 100, 120, 120]
    },
    r: {
        freq: [400, 1700, 2600, 3200, 3580],
        amp: [-60, -60, -60, -60, -60],
        bw: [70, 80, 100, 120, 120]
    }
};

ditty.bpm = 60;

const nn = [
    // Happy birthday to you
    c4, c4, d4, c4, f4, e4,
    // Happy birthday to you
    c4, c4, d4, c4, g4, f4,
    // Happy birthday dear (name)
    c4, c4, c5, a4, f4, e4, d4,
    // Happy birthday to you
    as4, as4, a4, f4, g4, f4
];

const vowels = [
    // Happy birthday to you
    "a", "a", "a", "a", "o", "u",
    // Happy birthday to you
    "a", "a", "a", "a", "o", "u",
    // Happy birthday dear (name)
    "a", "a", "a", "a", "e", "o", "u",
    // Happy birthday to you
    "a", "a", "a", "a", "o", "u",
];

const durs = [
    // Happy birthday to you
    0.5, 0.5, 1.0, 1.0, 1.0, 2.0,
    // Happy birthday to you
    0.5, 0.5, 1.0, 1.0, 1.0, 2.0,
    // Happy birthday dear (name)
    0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0,
    // Happy birthday to you
    0.5, 0.5, 1.0, 1.0, 1.0, 2.0,
];
                
const totaldur = durs.reduce( (a,b) => a+b, 0);

loop( (k) => {
    if(k > 0) {sleep(1); return;}
    // Tenor part
    const smoo_note = new Smoother(c3, 0.1);
    const s = new Array(5);
    for(let ns = 0; ns < 5; ns++) {
        s[ns] = formantSynth.play(
            (tick, options) => smoo_note.update(options.nn) + clamp01(tick-options.lastnote-0.5) * (0.5* Math.sin(4.7*TWOPI*tick)),
            {
                env: adsr,
                attack:0.1,
                release:0.1,
                duration:totaldur,
                nn: nn[0],
                ffreq:table[vowels[0]].freq[ns],
                bw:table[vowels[0]].bw[ns],
                ampdb:-60,
                lastnote: 0,
            }
        );
    }
    for(let i=0; i<nn.length; i++) {
        for(let ns=0; ns<5; ns++) {
            s[ns].options.nn = nn[i];
            if(vowels[i] != "r") {
                s[ns].options.ffreq = table[vowels[i]].freq[ns];
                s[ns].options.bw    = table[vowels[i]].bw[ns];
            }
            s[ns].options.ampdb = table[vowels[i]].amp[ns];
            if(i>0) {
                s[ns].options.lastnote += durs[i-1];
            }
        }
        sleep(durs[i]);
    }
}, {name:"vocoder", amp:0.2});