// ======================================================== // 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});