Tuomas Siipola Articles Projects

Pitch shifting in Web Audio API

Using samples is a quick and easy way to create realistic sounding virtual instruments. We only need a couple of samples, typically recordings of individual notes played on a real instrument, which can then be pitch shifted to play any note. This article will teach you how to create a sample-based instrument using JavaScript and Web Audio API.

Changing the pitch

Let's begin by loading a sample and playing it. We'll use a recording of note C4 played on harpsichord by pjcohen licensed under CC0 1.0.

const context = new AudioContext();

function loadSample(url) {
  return fetch(url)
    .then(response => response.arrayBuffer())
    .then(buffer => context.decodeAudioData(buffer));
}

function playSample(sample) {
  const source = context.createBufferSource();
  source.buffer = sample;
  source.connect(context.destination);
  source.start(0);
}

loadSample('harpsichord-c4.wav')
  .then(sample => playSample(sample));

How can we modify the pitch of the sample? When looking for available options, detune property of AudioBufferSourceNode seems to be the obvious answer. However, it has a couple of major drawbacks:

Instead of using detune, we'll use the better supported playbackRate property for pitch shifting. For example, setting playbackRate to 2 will play the sample twice as fast and 0.5 will halve the speed.

function playSample(sample, rate) {
  const source = context.createBufferSource();
  source.buffer = sample;
  source.playbackRate.value = rate;
  source.connect(context.destination);
  source.start(0);
}
playSample(sample, 0.5);

Test different playback rates below:

Make it musical

We can now change the pitch of the sample by changing its playback rate. But which playback rate should we use to play a specific musical note? To answer this, we need to understand how the frequencies of pitches are related to each other.

Let's start with the fundamental relationship between frequencies: an octave. An octave means that the ratio between two frequencies is 2:1. In other words the playback rate for one octave higher is 2 and for two octaves higher it's 4. Likewise, the playback rate for one octave lower is 0.5 and for two octaves lower it's 0.25. We can express this as the following function:

\[\text{playback-rate}(\text{octaves}) = 2^{\text{octaves}}\]

In western music each octave is divided into 12 equal parts called semitones. This can be expressed with a small modification to the previous function:

\[\text{playback-rate}(\text{semitones}) = 2^{\text{semitones}/12}\]

Typically, we think in terms of absolute pitches like C4 or D4. We can represent these pitches as MIDI notes 60 and 62. Now it's easy to calculate their difference of 2 semitones. In general the playback rate required to play MIDI note \(a\) using a sample of MIDI note \(b\) is the following:

\[\text{playback-rate}(a,b) = 2^{(a-b)/12}\]

Let's turn this into code:

function playSample(sample, sampleNote, noteToPlay) {
  const source = context.createBufferSource();
  source.buffer = sample;
  source.playbackRate.value = 2 ** ((noteToPlay - sampleNote) / 12);
  source.connect(context.destination);
  source.start(0);
}
playSample(sample, 60, 62);

Now we can use this function to build complete musical instruments. Try out the harpsichord below by clicking or tapping the keys or by using your computer's keyboard (first move focus by clicking or tabbing, then play starting from Q or Z keys on QWERTY keyboard):

If you're interested, check out the source code for the harpsichord.

Sometimes we need smaller intervals than a semitone, for example to implement vibrato or pitch bending. A commonly used unit for this is cent which is also used by the detune property. There are 100 cents in a semitone, so calculating the playback rate is straightforward:

\[\text{playback-rate}(\text{cents}) = 2^{\text{cents}/1200}\]

However, it's easy to convert cents to (decimal) semitones, so we don't need to rewrite our code in order to use cents:

let bend = 10; // 10 cents = 0.1 semitones
playSample(sample, 60, 62 + bend / 100);

Development ideas

There are many ways to improve upon the implementation presented here:

Finally, there are many sound synthesis techniques like filters and envelopes which can be used, with or without samples, to create more varied and interesting sounds.