All I Want For Christmas

...is some activity on dittytoy!

Log in to post a comment.

// ========================================================
//     All I Want for Christmas Is You --- Mariah Carey
//          adapted to DittyToy by Alexis THIBAULT      
// ========================================================
//
//
// This was a fun arrangement to make!
//
// As in many of my ditties, most of the sound is generated using 
// frequency modulation synthesis. There was also a little bit of
// sound design involved, especially for the sleigh bells.
//
// Since JS accepts arbitrary emoji characters in strings, that's
// what I used as the name of the loops, so that they appear in the
// player interface on the left... Lots of fun, snow and Christmas trees!
//
// The instrument list is as follows:
//   - bass
//   - voice
//   - sleigh bells
//   - piano
//   - backing vocals
//   - snare drum
//
// The interesting ones are the voice and the sleigh bells.
// The piano and snare drum were directly reused from other code snippets of mine,
// and are interesting as well.
// 
// Regarding the rhythm, the fun part of this song is the "swing" feel, which means
// that the offbeat is slightly delayed - in fact, almost as much as if the first
// part of the beat was worth two thirds of the beat duration. This meant that I had to
// be careful with the durations of the notes in the vocal line, to avoid
// messing it all up.



// Quite fast tempo
ditty.bpm = 150;
// Long half / short half of the beat
// Setting it to 0.5 makes a straight rhythm, higher values mean more swing.
const lh = 0.6, sh = 1 - lh;



// ------ Some constants and utility functions ------
const TWOPI = 2*Math.PI;
 // "Modulated sine"
 // Useful for phase modulation synthesis.
 // I explain some techniques for sound design using FM/PM in this tutorial:
 // https://www.youtube.com/watch?v=CqDrw0l0tas
const msin  = (p,m) => Math.sin(TWOPI*p + m);
// "Formant sine"
// Compresses the waveform in time but keeps the cycle-time intact.
// Useful for wide spectral enrichment with some control.
// Inspired by the Surge wavetable synthesizer
// https://surge-synthesizer.github.io/
const fsin  = (p,f) => Math.sin(TWOPI*clamp01((p%1)*f));
// Uniformly distributed random number
const Rand  = (a,b) => a + Math.random() * (b-a);
// Exponentially distributed random number
const XRand = (a,b) => a * (b/a)**Math.random();
const smoothstep = (a,b,x) => clamp01((x-a)/(b-a));
const exp = Math.exp;
const sin = Math.sin;
const abs = Math.abs;
const mix = (a,b,x) => a + x * (b-a);

// "Band-limited noise"
// This class generates a noise signal which contains a wide range of frequencies
// approximately contained in the [fmin,fmax] range.
// It does so by generating a sine wave, but varying its frequency randomly from
// one period to the next. Some smoothing is applied to the instantaneous
// frequency to avoid slope discontinuities.
// In terms of frequency it produces a more or less wide "peak", rather than a perfectly
// flat distribution. This is fine for our purpose.
class BandlimitedNoise {
    constructor(fmin, fmax) {
        this.phase = 0;
        this.fmin = fmin;
        this.fmax = fmax;
        this.fcur = XRand(fmin,fmax);
        this.newTarget();
    }
    newTarget() {
        this.ftarget = XRand(this.fmin,this.fmax);
        this.a0 = clamp01(2 * Math.PI * this.ftarget * ditty.dt);
    }
    process() {
        this.fcur += this.a0 * (this.ftarget - this.fcur);
        let oldphase = this.phase;
        this.phase += ditty.dt * this.fcur;
        if(Math.floor(this.phase) != Math.floor(oldphase)) {
            this.newTarget();
        }
        return Math.sin(TWOPI*this.phase);
    }
}
// --------------------------------------------------


// ====== BASS ======

// The sound design of the bass is original, though not very surprising.
// I needed a basic sound, with just a bit of timbre, which is why I turned to
// FM synthesis with a single carrier and modulator.
// As it played some of the higher notes of the melody, it started sounding too
// "muddy"; I solved this by lowering the index of modulation for higher pitches.

const bass = synth.def(class {
    constructor(options) {
        this.phase = 0;
        this.freq = midi_to_hz(options.note);
    }
    process(note, env, tick, options) {
        this.phase += ditty.dt * this.freq;
        let sig = 0;
        let iom = 120 / this.freq; // index of modulation: controls the number of harmonics
        sig += msin(this.phase, iom * msin(this.phase,0));
        // Add a wideband "pluck" at the beginning of the note
        let formant = (1 + 1*(0.01)**tick) * (2000/this.freq);
        sig += fsin(this.phase, formant) * 0.1 * (0.01)**tick;
        return sig * env.value;
    }
},
{attack:0.01, decay:0.25, sustain:0.8, release:0.1, duration:0.25, amp:0.4});

// In the original song, the bass plays quarter notes most of the time,
// with a few syncopated rhythms here and there.
// For simplicity, I gave up on the syncopated rhythms and did only the quarter notes.
// Note that it contains some "walking bass" patterns here and there, hence 
// the need to code notes individually (rather than per bar).
const bassNotes = [
    g1, g1, g1, g1, g1, g1, g1, g1,
    g1, g1, g1, g1, g1, a1,bb1, b1,
    c2, c2, c2, c2, c2, c2, c2, c2,
    eb2,eb2,eb2,eb2,eb2,eb2,eb2,eb2,
    //g1, g1, g1, g1, g1, g1, g1, g1,
    //g1, g1, g1, g1, g1, a1,bb1, b1,
    //c2, c2, c2, c2, c2, c2, c2, d2,
    //eb2,eb2,eb2,eb2,eb2,eb2,eb2,eb2,
    g1, g1, g1, g1, g1, g1, b1, b1,
    e2, e2, e2, e2, e2, e2,eb2, eb2,
    d2, d2, d2, d2, e2, e2, e2, e2,
    a1, a1, a1, a1, d2, d2, d2, d2,
    g1, g1, g1, g1, e2, e2, e2, e2,
    a1, a1, a1, a1,
    // For the last bar, I wanted to do something different,
    // so I used a special value, which is treated by an independent piece of code.
    1000
];

loop((i) => {
    let note = bassNotes.ring(i);
    if(note == 1000) {
        // Special case: play the final pickup bar instead of a single note.
        // It goes: eighth-note silence, then seven times D2 as eighth notes.
        // However, since the rhythm is syncopated, we must differentiate the
        // long and short halves of the beat.
        
        // First beat
        sleep(lh);
        bass.play(d2, {duration:sh, decay:0.1});
        sleep(sh);
        for(let i=0; i<3; i++) {
            // Second, third, and fourth beats
            bass.play(d2, {duration:sh, decay:0.1});
            sleep(lh);
            bass.play(d2, {duration:sh, decay:0.1});
            sleep(sh);
        }
    } else {
        // General case: simply play the note for one beat
        bass.play(note);
        sleep(1);
    }
}, {name:"πŸŒ™πŸŒŸ"});



// ====== VOICE ======

// For the lead sound, I tried to design a synth with some "voice-like" qualities.
// I did this by letting the pitch vary around the desired note, with a random slide 
// at the beginning of the note, and using vibrato afterwards.
//
// I also used FM to design a voice-like timbre, with randomized formant frequencies
// to mimic the resonances of the vocal tracts corresponding to different vowels.
// Only two formants are used, which means a rather limited timbre and poor vowel
// identification: a more flexible option would be to use formant tables, as I did
// in "Vocoder Puccini": https://dittytoy.net/ditty/6f30b0885d
// Anyway, I like how this synth sounds, it reminds me of Animal Crossing characters.

const lead = synth.def(class {
    constructor(options) {
        this.phase = 0;
        this.initDetune = Math.random()*2-1; // Slightly out of tune start of note
        this.freq0 = midi_to_hz(options.note);
        // Synth with two random formants, generated using frequency modulation
        let f1 = Rand(270,800); // formant frequencies
        let f2 = Rand(700, 2100);
        // We cannot use carrier frequencies at f1 and f2, because that would give
        // rise to an inharmonic timbre. Instead we round them to the closest
        // multiples of freq0.
        // Determine the integer factor to use.
        this.i1 = Math.max(Math.round(f1 / this.freq0), 1);
        this.i2 = Math.round(f2 / this.freq0);
        let bw1 = 60; // bandwidth
        let bw2 = 50 + 40 * (f2 / 1500);
        this.iom1 = 0.5 + bw1 / this.freq0; // index of modulation
        this.iom2 = 0.5 + bw2 / this.freq0;
    }
    process(note, env, tick, options) {
        let sec = tick_to_second(tick);
        // How much should we detune the note?
        let vib = clamp01(tick-1) * msin(5*sec,0)  // 5 Hertz vibrato with a fade-in
                    + 60*clamp01(0.25-tick) * this.initDetune; // initial detune, with a quick fade-out
        let freq = midi_to_hz(note + 0.2*vib);
        this.phase += ditty.dt * freq;
        let sig = 0;
        sig += msin(this.i1 * this.phase, msin(this.phase, 0) * this.iom1);
        sig += msin(this.i2 * this.phase, msin(this.phase, 0) * this.iom2) * 0.2;
        return sig * env.value;
    }
}, {attack:0.05, release:0.1, amp:0.3});

// Since the melody uses notes of varying duration, I decided to code it
// using an array of [note number, duration in ticks].
// For simplicity, I did not bother entering the vowels, gliding from one note to the next,
// adding volume changes, or any kind of finer expressivity.
const leadNotes = [
    [g3,1], [b3,1], [d4,lh], [fs4,1], [g4,1], [fs4,1], [e4,1], [d4,1], [0,sh],
    [a4,1], [g4,1], [g4,lh], [fs4,1], [g4,1], [fs4,1], [e4,sh], [d4,1+lh], [0,sh],
    [c4,1], [e4,1], [g4,1], [a4,lh], [b4,1], [a4,1], [g4,1], [e4,1], [0,sh],
    [c4,1], [eb4,lh], [g4,sh+1], [a4,lh], [bb4,1], [a4,1], [f4,sh], [eb4,1+lh], [0,sh],
    //[a4,1], [g4,lh], [g4,1], [fs4,1], [g4,1], [fs4,1], [e4,1], [d4,1], [0,sh],
    //[g3,1], [b3,1], [d4,lh], [fs4,1], [g4,1], [fs4,1], [e4,1], [d4,1], [0,sh],
    //[c4,1], [e4,1], [g4,1], [a4,lh], [b4,1], [a4,1], [g4,1], [e4,1], [0,sh],
    //[c4,1], [eb4,lh], [g4,sh+1], [a4,lh], [bb4,1], [a4,1], [f4,sh], [eb4,1+lh], [0,sh],
    
    [g4,1], [a4,1], [fs4,1], [g4,lh], [e4,1], [fs4,1], [e4,sh], [eb4,1], [0,1],
    [g4,1], [a4,1], [fs4,1], [g4,lh], [e4,1], [fs4,1], [e4,sh], [eb4,1], [0,1],
    [d4,1], [e4,1], [g4,lh], [d5,1], [c5,sh+2], [0,2],
    [b4,1], [a4,1], [g4,1], [e4,1], [eb4,1], [a4,2], [b4,1],
    [g4,4], [0,4+8]
];

loop((i) => {
    let note = leadNotes.ring(i);
    if(note[0]) {
        lead.play(note[0], {duration:note[1]});
    }
    sleep(note[1]);
}, {name:"πŸŽ…β˜ƒοΈ", amp:0.7});



// ====== SLEIGH BELLS ======

// Trying to sound design sleigh bells... wasn't easy!
// I used a sleigh bell sample I had on my computer, and tried to
// more or less match the envelope and spectral content using
// additive synthesis (I use Audacity to record the output
// of Dittytoy and display the spectrum of different sounds).
//
// I noticed that the sample had several noise peaks, hence the use of 
// band-limited noise sources. I could have mixed them with different
// amplitudes, but that turned out not to be necessary.
//
// Once I had matched the envelope and spectrum, I still wasn't happy about
// my sound, so I added some amplitude modulation on top, to mimic the
// "noise bursts" coming out of the small cymbals crashing against each other.

let sleighBells = synth.def(class {
    constructor(options) {
        // Initially I tried using pure sine sources, but the resulting sound
        // was not convincing at all.
        //this.freqs = [2320, 2958, 3914, 5462, 5729, 6634, 7545, 8467, 9435, 10878, 11578, 12307];
        //this.phases = this.freqs.map(v => 0.0);
        
        // Use slightly different values for the left and right sleigh bells
        if(options.model == 1) {
            this.blNoises = [
                new BandlimitedNoise(2590,2890),
                new BandlimitedNoise(2200,4600),
                new BandlimitedNoise(5120,7300),
                new BandlimitedNoise(9000, 11000),
                new BandlimitedNoise(11000, 17000),
            ];
        } else {
            this.blNoises = [
                new BandlimitedNoise(2390,2590),
                new BandlimitedNoise(2200,4600),
                new BandlimitedNoise(6120,8300),
                new BandlimitedNoise(9500, 11500),
                new BandlimitedNoise(10000, 16000),
            ];
        }
        this.envPhase = 0;
        // Frequency of the amplitude modulation.
        // The secret to making something synthetic sound natural is randomization!
        this.envFreq = Rand(30,100);
    }
    process(note, env, tick, options) {
        let sig = 0;
        for(let i=0; i<this.blNoises.length; i++) {
            //this.phases[i] += ditty.dt * this.freqs[i]; // For using pure sine sources
            //sig += Math.sin(TWOPI*this.phases[i]);
            sig += this.blNoises[i].process();
        }
        this.envPhase += this.envFreq * ditty.dt;
        // Lots of amplitude modulation at the start, less and less as the note goes on.
        let surEnv = mix(1.5 - (this.envPhase%1), 1, clamp01(3*tick));
        return 0.05 * sig * env.value * surEnv;
    }
}, {attack:0.03, release:0.3, curve:-4, model:1});

// Play the sleigh bells slightly off the beat, and slightly offset from each other,
// one on the left, one on the right.
loop( () => {
    // Delays up to 30ms are hardly detectable for most listeners (at least they don't sound like being late).
    // Randomize delay and amplitude for variety!
    let dly = second_to_tick(Rand(0.0,0.03));
    sleep(0.5*dly);
    sleighBells.play(0, {pan:-0.5, amp:Rand(0.3,1), model:1});
    sleep(0.5*dly);
    sleighBells.play(0, {pan:0.5, amp:Rand(0.3,1), model:2});
    sleep(1-dly);
}, {name:"πŸ¦ŒπŸ¦ŒπŸ””πŸ›·πŸ””", amp:0.7});


// ====== ELECTRIC PIANO ======

// By this point, we already have all the important elements of the song.
// However, it's still missing some accompaniment.
// In the original song, the chords are played on a piano, at low volume in the mix,
// with rather high notes.
// Since I had previously designed an electric piano sound using phase modulation
// ( see https://dittytoy.net/ditty/5f66552091 ), I simply reused it.

const ep1 = synth.def( (phase, env, tick, options) => { 
    let sig = msin(phase, msin(11*phase,0) * clamp01(1 - 10*tick)*options.amp);
    sig = sig + msin(phase, msin(phase,0) * 2 * options.amp);
    return sig * env.value;
}, {attack:0.01, release: 0.3, decay: 0.3, env: adsr2}
);

// The way the piano plays the chords is quite interesting: it plays triplets
// (i.e. 3 notes per tick), but instead of naΓ―vely playing arpeggios,
// we hear it alternating between the upper notes of the chord and the lowest note.
// Therefore this short pattern takes up 2/3rds of a tick,
// and is repeated three times, making up two ticks.

// Following this observation, I chose to structure the data in the following manner:
// [number of repetitions of the two-tick pattern, note 1, note 2, note 3].
// Observe that the notes played are quite high, thus they don't clash with the vocals.
// Also observe that inversions are used for the chords (the lowest note is different from
// the "root" played by the bass).
let pianoChords = [
    [8,d5,g5,b5], // G major
    [4,e5,g5,c6], // C major
    [4,eb5,g5,c6], // C minor
    [3,d5,g5,b5],[1,ds5,fs5,b5], // G major, B major to transition
    [3,e5,g5,b5],[1,eb5,g5,b5], // E minor, G augmented to transition
    [2,d5,g5,b5],[2,e5,gs5,b5], // G major, E major
    [2,cs5,g5,a5],[2,c5,fs5,a5], // A minor 7, D dominant 7
    [2,d5,g5,b5],[2,e5,g5,b5], // G major, E minor
    [2,c5,g5,a5], // A minor 7
    // Again, do something different for the last bar.
    1000
];

loop( (i) => {
    let ch = pianoChords.ring(i);
    if(ch != 1000) {
        // Play 3*N times the short pattern [upper notes, bottom note], making up 2*N beats in total.
        for(let j=0; j<3*ch[0]; j++) {
            ep1.play(ch[2], {amp:Rand(0.7,1)});
            ep1.play(ch[3], {amp:Rand(0.7,1)});
            sleep(1/3);
            ep1.play(ch[1], {amp:Rand(0.7,1)});
            sleep(1/3);
        }
    } else {
        // Play pickup bar: silence for the first eighth note, and D major chords afterwards.
        sleep(lh);
        ep1.play(d3);ep1.play(d4);ep1.play(d5);ep1.play(fs5);ep1.play(a5);ep1.play(d6);
        sleep(sh);
        for(let i=0; i<3; i++) {
            ep1.play(d3);ep1.play(d4);ep1.play(d5);ep1.play(fs5);ep1.play(a5);ep1.play(d6);
            sleep(lh);
            ep1.play(d3);ep1.play(d4);ep1.play(d5);ep1.play(fs5);ep1.play(a5);ep1.play(d6);
            sleep(sh);
        }
    }
}, {name:"β„οΈβ›ΈοΈπŸ’–", amp:0.03});


// ====== BACKING VOCALS ======

// For the backing vocals, I use a very simple stereo synth, with detuning between the left
// and right channels.
// The "timbre" option controls the relative amount of upper harmonics: for lower notes, it is
// nice to have more harmonics, so that they sound "full" rather than "muddy".
const backs = synth.def(
    (phase, env, tick, options) => [fsin(phase*1.005,2.5*options.timbre)*env.value, fsin(phase*0.995, 3.01*options.timbre)*env.value],
    // slow attack and release: this is not one of the main instruments so it shouldn't be too prominent. 
    // Plus, it makes it sound like an ensemble, where not everyone starts and stops at the same time.
    {attack:0.2, release:0.5, amp:0.1, timbre:1});

// Array of [note number, duration in ticks].
// Zero is used to signify rests.
// Once again I went for simplicity: the backing vocals have a more complicated
// melody in the original song, but here I gave them only the
// held notes, and the final line.
let backNotes = [
    [0, 16], 
    [c4,8], [eb4,8],
    [0, 4], [e4,2], [ds4, 2],
    [e4, 2], [0, 2], [e4,2], [ds4, 2], 
    [d4, 4], [e4, 3], [0, 1],
    [b4, 1], [a4,1], [g4,1], [e4,1], [eb4,1], [a4,2], [b4,1],
    [g4,4], [0,4+8]
];

loop( (i) => {
    let note = backNotes.ring(i);
    if(note[0]) {
        // Play two notes: one in the lower octave and one in the upper octave, with slight detuning.
        // Since the left and right channels are also detuned, this sounds like four voices in total.
        backs.play(note[0]-12, {duration:note[1], timbre:1.8});
        backs.play(note[0] + Math.random()*0.02-0.01, {duration:note[1]});
    }
    sleep(note[1]);
}, {name:"πŸ€ΆπŸ»β›„"});


// ====== SNARE DRUM ======

// Finally, for the pop-rock drive of the song, a snare drum is played on every second beat.
// I reused a snare drum sound of mine, which is very interesting from the sound design point of view:
// this one contains three layers: 
//   - the "body", which is the more-or-less sinusoidal vibration of the drum,
//     corresponding to its first resonance mode.
//   - the "noise", which is produced by the snares rattling against the back of the drum, and is
//     influenced by the resonances of the drum itself.
//   - the "click", which is the strong transient noise emitted during the first few milliseconds
//     after the stick hits the drumhead.
//
const snare = synth.def(class {
    process(ph, env,tick,options) {
        let t = tick_to_second(tick);
        // Snare is "body" + "white noise" + "click"
        // "Noise"
        // Previously I used colored (band-limited) noise, this time I didn't bother.
        let wnoise = Math.random()*2-1; //  + coloredNoise(t, 6500.0,1000.0)*0.3
        let nenv = exp(-15.0*t) * smoothstep(0.160,0.0,t) * 0.5; // Noise envelope
        
        // "Body"
        let spd = 200.0; // Pitch decay speed
        // The instantaneous frequency is 197 + 80*exp(-t*spd)
        // i.e. it goes down from 277 Hz to 197 Hz.
        // This code was adapted from sound generation code I used on shadertoy.com, 
        // where samples are generated in parallel, hence the use of an explicit antiderivative formula for the phase.
        let phase = TWOPI*197.0*t + TWOPI*80.0/spd * (1.0-exp(-t*spd));
        let body = sin(phase) * 1.5 * smoothstep(0.0,0.005,t) * smoothstep(0.035,0.0,t); // fade it in and out quickly
        let v = wnoise*nenv + body;
        // In the physical world, the resonance and the noise production are connected,
        // so let's add some distortion to fake this interaction.
        v /= 1.0 + abs(v);
        
        // "Click"
        // Just a very short noise burst.
        let click = (Math.random()*2-1) * exp(-600.0*t);
        v += click * 0.4;
        
        return v;
    }
});

loop( (i) => {
    sleep(1);
    snare.play(0);
    sleep(1);
}, {name:"πŸŽ„πŸŽ„πŸŽπŸŽπŸŽπŸŽ„πŸŽ„", amp:0.5});