How Tone.js Sampler Handles Pitch Shifting
This article explains how Tone.Sampler handles
pitch-shifting when triggering samples in the Tone.js web audio library.
You will learn how the sampler uses playback rate manipulation to alter
pitch, how it interpolates between mapped audio files, and how to
optimize your sample maps to maintain high-quality audio playback across
different notes.
The Core Mechanism: Playback Rate Manipulation
Unlike real-time pitch-shifting effects that alter pitch while
preserving the original duration (such as phase vocoders),
Tone.Sampler shifts pitch by adjusting the playback
rate of the audio buffer. This is the digital equivalent of
speeding up or slowing down a vinyl record or tape machine.
- Higher Pitches: To play a note higher than the original sample, Tone.js increases the playback rate. This makes the sample play faster, resulting in a higher pitch and a shorter duration.
- Lower Pitches: To play a note lower than the original sample, Tone.js decreases the playback rate. This makes the sample play slower, resulting in a lower pitch and a longer duration.
The relationship between the pitch shift in semitones (\(n\)) and the playback rate is exponential, calculated using the formula:
\[\text{Playback Rate} = 2^{(n / 12)}\]
For example, triggering a note one octave higher (+12 semitones) doubles the playback rate (2.0), while triggering a note one octave lower (-12 semitones) halves the playback rate (0.5).
Multisampling and Nearest-Neighbor Interpolation
To prevent the unnatural sound stretching (often called the “chipmunk
effect”) that occurs when a single sample is pitched too far from its
original frequency, Tone.Sampler utilizes
multisampling.
When you instantiate a Tone.Sampler, you pass it an
object mapping specific midi notes to audio files:
const sampler = new Tone.Sampler({
urls: {
A1: "A1.mp3",
A2: "A2.mp3",
A3: "A3.mp3",
A4: "A4.mp3",
},
baseUrl: "https://tonejs.github.io/audio/salamander/",
onload: () => {
sampler.triggerAttackRelease("C3", "4n");
}
}).toDestination();When you trigger a note that does not have an explicit sample mapped
to it (like C3 in the example above),
Tone.Sampler performs the following steps:
- Finds the Closest Sample: It looks through the
mapped keys and identifies the closest available sample (in this case,
A2orA3). - Calculates the Interval: It determines the interval
distance in semitones between the target note (
C3) and the source sample. - Applies the Playback Rate: It loads the closest
sample’s buffer and adjusts its playback rate based on that interval
distance. If it chooses
A2(110 Hz) to playC3(130.81 Hz), it speeds up the playback ofA2by 3 semitones.
Best Practices for Clean Pitch Shifting
Because Tone.Sampler relies on playback rate changes,
pitch-shifting over wide intervals can degrade audio quality. To achieve
the most natural sound:
- Map Samples at Close Intervals: Provide a sample at least every minor third (3 semitones) or fourth (4 semitones). The smaller the gap between your mapped samples, the less pitch-shifting Tone.js has to perform, resulting in a more realistic instrument sound.
- Use High-Quality Source Files: Because slowing down a sample stretches the digital samples further apart, using high-sample-rate source files (like 44.1kHz or 48kHz) helps prevent aliasing and high-frequency loss when pitching downwards.