WebAudio Deep Note, part 3: loop and change pitch

October 11th, 2019. Tagged: JavaScript, WebAudio

This journey started here, then continued, then took a slight turn, just for giggles, and now we're back.

After you learned how to play a sound, now let's loop it, because the DeepNote goes on for about 25 seconds and we play the exact same sample of a cello that is under a second long.

Loop

Luckily, looping is the easiest thing to do in WebAudio. After you've created a buffer source object, you simply set its loop property to true. That simple. Then it would loop forever until you stop() it.

sample = audioContext.createBufferSource();
sample.buffer = audioBuffer;
sample.loop = true;
sample.connect(audioContext.destination);
sample.start();

Here, here, hear the result.

A note on looping a source

Your source sound is a wave (imagine a sine wave). If you cut the end of the sine exactly where the beginning is, you're golden. Here's a clumsy illustration:

loop cut

So, a bit of attention is required. Otherwise you hear clicks and pops when the end doesn't flow nicely into the beginning. In my case I tried, but not too hard. Because we'll have 30 of those looped samples and when they are of different length, due to repitching (see next), it's not that noticeable. But you should be careful when cutting things up in your favorite DAW (Digital Audio Workstation). BTW, mine (favorite DAW, that is) is Reaper.

Changing pitch

You can make the same sound sound higher (like a chipmunk) or lower (like Darth Vader). The easiest way is to speed up the playback. Imagine playing a tape (a cassette tape too) at a higher speed. Or putting your finger on a vinyl record while it's playing to slow it down.

That's what WebAudio gives you out of the box, a playback rate. Playing faster sounds higher. But it's also faster. If you play a 1 second sound at twice the speed, it sounds twice as high, but also ends quicker. The harder thing to do is sound higher or lower but not change the speed. This is what, now too famous, unfortunately, Autotune does. Pitch correction. When the singer is flat you raise the pitch but retain the speed (tempo). This is what WebAudio doesn't give you out of the box. For now. It's possible but it's not trivial. Luckily for our exercise, we can do with the speed increase without any troubles.

So in addition to the loop property of the buffer source, you get a playbackRate too. Like so:

sample.playbackRate.value = 2;

(Why .playbackRate.value = 2 and not .playbackRate = 2, let's leave for later. It has to do with the concept of audio parameters, a nice API actually)

Playback rate 1 is the same as original sound. 2 is twice as fast and an octave higher. A 440Hz sound played twice as fast will sound like 880Hz. This is the same A note but an octave above.

150Hz

Alright, now back to the DeepNote and the whole D is 150Hz.

If you look at the note frequencies, D3 is 146.83Hz. But in DeepNote they decided that D3 should be 150Hz. Cool. We can speed up our sample.

Additionally we don't have a D3 cello sample, but a C3 one. Because we got it for free on the Interwebs and we can't be picky with free. So we need to make our C3 (which is 130.81Hz) sound like a DeepNote's D3. It's a simple ratio. Check it out:

const C3 = 130.81;
const c3d150 = 150 / C3; // 1.1467013225;

So we need to speed up the playback rate with a measly 1.something to make a desired Deep D out of a rando C3. Not bad. We'll need to play a lot more notes later, but having a start point is good. All other notes are straight multiples of this c3d150 baseline due to the just tuning. Go back to part 1 of this blog series if just tuning sounded weird to you.

To hear the repitching in action, go to the example. I even added a wee checkbox you can check to hear the difference between the original C3 (rate 1) and the fancy D3 (rate c3d150)

The code is:

const C3 = 130.81;
const c3d150 = 150 / C3; // 1.1467013225;

function play() {
  fetch('Roland-SC-88-Cello-C3-glued-01.wav')
    .then(response => response.arrayBuffer())
    .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
    .then(audioBuffer => {
      sample = audioContext.createBufferSource();
      sample.buffer = audioBuffer;
      sample.loop = true;
      sample.playbackRate.value = repitch.checked ? c3d150 : 1;
      sample.connect(audioContext.destination);
      sample.start();
    })
    .catch(e => console.log('uff'));
}

WebMIDI Keyboard

One last thing: checkout the power of repitching by going to my WebMIDI keyboard. You can play a whole buncha notes and they are all a single repitched C4 sample.

Look at that waterfall, just 4k of (pretty) HTML and inline JS and a single mp3 sample. And it supports MIDI. (WebMIDI is a question for another time, or just view source if you're curious)

webmidikeyboard waterfall

Comments? Feedback? Find me on Twitter, Mastodon, Bluesky, LinkedIn, Threads