Vocoder Puccini

A vocoder singing the aria "Nessun Dorma" from Puccini's opera "Turandot"

Log in to post a comment.

//input.formantFreq = 1300; // min=200, max=8000, step=1
//input.bandwidth = 200; // min=0, max=8000, step=1
//input.warp = 0; // min=0, max=1, step=0.01
//input.note = 0; // min=0, max=7, step=1
//input.vowel = 0; // min=0, max=4, step=1

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 = Math.exp(a * (Math.cos(this.phase) - 1));
        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;
// Play formant synths
// Adapted from the famous aria "Nessun Dorma" from Act III of the opera "Turandot" by Giacomo Puccini
const nn     = [d4,   d4,   d4,  e4,  e4, fs4,  e4,  d4,  d4,  e4,  e4,  e4, cs4,  b3,  b3,  e4, 
                e4,  fs4,  g4, fs4, fs4,  e4, fs4,  d4, cs4, cs4,  d4,  e4,  e4,  e4,  fs4, fs4,  g4,  g4,
                a4,   a4,  a4,  a4,  a4, fs4, fs4, fs4, fs4, fs4, fs4, fs4,  d4,  a3,  a3,  a3,
                a3,   a3,  a3,  e4, cs4,  d4, 
                b3, b3,d4, g4, d4,d4,b4,a4,a4
                ];
const vowels = ["r", "a", "i", "i", "o", "i", "e", "o", "e", "i", "u", "o", "i", "e", "r", "r",
                "i", "o", "e", "i", "o", "e", "u", "a", "a", "r", "o", "o", "r", "i", "e", "u", "a", "o",
                "o", "e", "a", "o", "a", "e", "e", "e", "r", "a", "o", "a", "e", "e", "e", "r",
                "a", "a", "a", "i", "e", "o",
                "r","i","e","o","r","i","e","e","o"];
const durs   = [0.5,  .3,  .2,  .1,  .4, 0.5, 0.5,  .1,  .4,  .1, .55,  .1, .25,   2, 0.25, 0.25,
                0.5, 0.5, 0.4, 0.2, 0.4, 0.5, .75, .25, 0.8, 0.2, 1.0, 0.2, 0.3, 0.5, 0.3, 0.2, 0.25, 0.25,
                1.0, 1.0, 0.5, 0.5, .75, .25, 1.0, 0.8, 0.2, 0.5, 0.5, .75, .25, 1.0, 0.2, 0.3,
                0.5, 0.5, 0.5, 1.7, 0.3, 2.5,
                0.5,0.75,0.25,2.5,0.5,1,2.7,0.3,3.5];
const totaldur = durs.reduce( (a,b) => a+b, 0);

const bsnn =  [ d2, fs2,  g2, fs2,  e2,  g2,  a2,  b2, cs3,  a1,  g1, fs1,  e1,  a1,   0,  g1,
                g1, d2, fs2, g2, fs2, e2, g2, a1, b1, cs2, a1, g1, d1];
const bsdur = [3.0, 1.0, 1.0, 1.0, 3.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.5, 4.0, 4.0, 3.5, 1.5, 4.0,
               4+3,3.0, 1.0, 1., 1.0, 3., 1., 1., 1., 1.0, .7, .7, 4];
const clnn  = [ a2,  d3,  g3,  d3,  e3,  e3,  e3,  a2,  g2, fs2,  e2,  a2,  a2,  d3,
               g2, d3, d3, d3, d3, d3, d3, a2, b2, cs3, a2, g2, d2];
const cldur = [3.0, 4.0, 2.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.5, 4.0, 4.0, 2.0, 3.0, 4.0,
               4+3, 3,   1, 1,  1,  3, 1,  1,  1,  1,   .7, .7, 4];
const rhnn  = [[a3,fs4,a4], [a3,a4], [b3,d4,b4], [b3,fs4,b4],[cs4,cs5], [a3,d4,a4], [a3,e4,a4],
               [a3,e4,a4], [fs4,cs4,fs5],[g4,cs5,g5],[a4,e5,a5],[a4,e5,a5],[fs4,cs5,fs5], [fs4,cs5,fs5],
               [fs4,b4,fs5], [fs4,d5,fs5],[d4,b4,d5],[a3,d4,fs4,a4],
               [d4,fs4,b4,d5],[cs4,e4,a4,cs5],[b3,d4,g4,b4],[a3,cs4,fs4,a4],[b3,cs4,e4], [],
               [d4,g4,b4,d5],
               [d5],[g4],[b4],[d5],[g5],[d5],[g5],[b5],
               [],[d5,d6],[e5,e6],[fs5,fs6],[e5,e6],[d5,d6],[e5,e6],[cs5,cs6],
               [b4,b5],[e5,e6],[fs5,fs6],[g5,g6],[fs5,fs6],[e5,e6],[fs5,fs6],[d5,d6],
               [cs5,e5,a5,cs6],[d5,a5,d6],[e5,a5,e6],[fs5,a5,fs6],[g5,b5,g6],[a5,d6,a6]];
const rhdur = [3.0,             1.0,        5.0,         1.0,      1.0,        1.0,      0.5,
                    0.5,           0.5,       0.5,      3.0,       0.75,       0.25,          2.5,
                    0.5,           0.75,        0.25,    1.0,
                    0.5,            0.5,            0.5,             0.5,         0.5,    1.5,
                    4.0,
                    0.5,0.5,0.5,0.5,0.5,0.5,1,3,
                    0.5,0.5,0.5,0.5,0.5,0.5,0.75,0.25,
                    2.5,0.5,0.5,.5,.5,.5,.75,.25,1,1,1,0.7,0.7,4];

const innn = [[], [fs3,a3], [], [g3,b3], [], [fs3], [g3], [],
                [],[d3,g3,b3],[g3,b3,d4],[b3,d4,g4],[d4,g4,b4],[g3,b3,d4],[b3,d4,g4],[d4,g4,b4],
                [g4,b4,d5],[b3,d4,g4],[d4,g4,b4],[g4,b4,d5],[g4,b4,d5,g5],[d4,g4,b4,d5],[g4,b4,d5,g5],
                [fs3,a3,d4,fs4,a4],[d3,a3,d4,fs4],
                [a3,d4,g4], [g3,d4,fs4], [e3,g3,b3,e4,g4], [b3,d4,g4],
                [e3,a3,cs4,e4,a4],[fs3,b3,d4,a4,b4],[e3,a3,cs4,e4,a4],[a3,d4,fs4,a4,d5],[g3,b3,d4,g4,b4,d5],
                [a2,d3,fs3,a3,fs4,a4]];
const indur = [14.5, 3.5, 0.5,  3.5,   0.5,   2.5,  0.5,  1.5,
               0.5,    0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
               0.5,0.5,0.5,0.5,0.5,0.5,1+3,
               3, 1,
               1, 1, 3, 1,
               1, 1, 1, 0.7, 0.7,
               4
               ];

// --------------------------------------  The usual reverb -------------------------

class Delayline {
    constructor(n) {
        this.n = n;
        this.p = 0;
        this.data = new Float32Array(n);
    }
    current() {
        return this.data[this.p]; // using lastOut results in 1 sample excess delay
    }
    clock(input) {
        this.data[this.p] = input;
        if(++this.p >= this.n) {
            this.p = 0;
        }
    }
}

const ISQRT2 = Math.sqrt(0.5);
const SQRT2 = Math.sqrt(2);
const SQRT8 = Math.sqrt(8);
const ISQRT8 = 1/SQRT8;
// const fibodelays = [1410,1662,1872,1993,2049,2114,2280,2610]; // X^8 = X+1
const fibodelays = [1467,1691,1932,2138,2286,2567,3141,3897]; // X^8 = X+2
const fiboshort = [465, 537, 617, 698, 742, 802, 963, 1215]; // X^8 = X+2
let meandelay = fibodelays[3] * ditty.dt;

const reverb = filter.def(class {
    constructor(options) {
        this.outgain = 0.3;
        
        this.dls = [];
        this.s0 = new Float32Array(8); // Lowpass filter memory
        for(let i=0; i<8; i++) {
            this.dls.push(new Delayline(fibodelays[i]));
            this.s0[i] = 0;
        }
        this.a0 = clamp01(2*Math.PI*options.cutoff*ditty.dt);
        
        this.dls_nr = []; // Non-recirculating delay lines
        for(let i=0; i<4; i++) {
            this.dls_nr.push(new Delayline(fiboshort[i*2]));
        }
    }
    process(input, options) {
        // First stage: non-recirculating delay lines
        let s0 = input[0], s1 = input[1];
        for(let i=0; i<4; i++) {
            let u0 = 0.6*s0 + 0.8*s1, u1 = 0.8*s0 - 0.6*s1;
            let u1p = this.dls_nr[i].current();
            this.dls_nr[i].clock(u1);
            s0 = u0;
            s1 = u1p;
        }
        let v0 = (s0 + s1);
        let v1 = (s0 - s1);
        //let s0 = -0.6*input[0] + 0.8*input[1];
        //let s1 = 0.8*input[0] + 0.6*input[1];
        
        // Second stage: recirculating delay lines
        let rt60 = options.rt60;
        let loopgain = 10 ** (-3*meandelay / rt60) * ISQRT8;
        let higain = 10 ** (-3*meandelay / options.rtHi) * ISQRT8;
        let v = this.dls.map( (dl) => dl.current());
        // Fast Walsh-Hadamard transform
        // https://formulasearchengine.com/wiki/Fast_Walsh%E2%80%93Hadamard_transform
        let w = [v[0]+v[4], v[1]+v[5], v[2]+v[6], v[3]+v[7],
                 v[0]-v[4], v[1]-v[5], v[2]-v[6], v[3]-v[7]];
        let x = [w[0]+w[2], w[1]+w[3], w[0]-w[2], w[1]-w[3],
                 w[4]+w[6], w[5]+w[7], w[4]-w[6], w[5]-w[7]];
        let y = [x[0]+x[1], x[0]-x[1], x[2]+x[3], x[2]-x[3],
                 x[4]+x[5], x[4]-x[5], x[6]+x[7], x[6]-x[7]];
        y[0] += v0;
        y[2] += v1;
        for(let i=0; i<8; i++) {
            let hipass = y[i] - this.s0[i];
            this.dls[i].clock(this.s0[i] * loopgain + hipass * higain); // Mix lowpass and hipass in different amounts
            this.s0[i] += this.a0 * hipass;
        }
        return [lerp(input[0], v[0], options.mix),
                lerp(input[1], v[2], options.mix)];
    }
}, {mix:0.2, rt60:1, cutoff:5000, rtHi:0.8});

const blank = filter.def(class {constructor(options){} filter(input, options){return input;}}); // For debug purposes

const hallverb = reverb.createShared({mix:0.6, rt60: 2.5, rtHi:1.5});

// --------------------------------------  End usual reverb -------------------------





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}).connect(hallverb);


// Bass part

function tri(t) {
    return -1 + 4*Math.abs(t%1 - 0.5);
}

const varsaw = synth.def(
    (phase, env, tick) => { let x = (phase%1); return (x-0.5) * clamp01(15*x*(1-x)*(1 + 0.1*tri(tick))) * env.value; },
    {attack:0.2, release:0.1, decay:1, sustain: 0.8, duration:1}
);

loop( (i) => {
    if(i < bsnn.length) {
        if(bsnn[i]){
            varsaw.play(
                bsnn[i],
                {
                    duration:bsdur[i],
                    decay:Math.min(1, bsdur[i]-0.2),
                    amp:2**((48-bsnn[i])/24),
                    pan: 0.6
                }
            );
        }
        sleep(bsdur[i]);
    } else {
        sleep(1);
    }
}, {name: "bass", amp:0.2}).connect(hallverb);

loop( (i) => {
    if(i < clnn.length) {
        varsaw.play(clnn[i], {duration:cldur[i], decay:Math.min(1, cldur[i]-0.2), amp:2**((48-clnn[i])/24), pan:0.4});
        sleep(cldur[i]);
    } else {
        sleep(1);
    }
}, {name:"cello", amp:0.2}).connect(hallverb);


loop( (i) => {
    if(i < rhnn.length) {
        for(let j = 0; j<rhnn[i].length; j++) {
            varsaw.play(rhnn[i][j], {duration:rhdur[i], decay:Math.min(1, rhdur[i]-0.2), 
                                     amp:2**((48-rhnn[i][j])/24), pan:Math.random()-0.8});
        }
        sleep(rhdur[i]);
    } else {
        sleep(1);
    }
}, {name:"violins", amp:0.2}).connect(hallverb);


loop( (i) => {
    if(i < innn.length) {
        for(let j = 0; j<innn[i].length; j++) {
            varsaw.play(innn[i][j], {duration:indur[i], decay:Math.min(1, indur[i]-0.2), 
                                     amp:2**((48-innn[i][j])/24), pan:-0.2});
        }
        sleep(indur[i]);
    } else {
        sleep(1);
    }
}, {name:"viola", amp:0.2}).connect(hallverb);