Pitch & time-stretch
Two ways to change 'speed': cheap playback-rate (alters pitch + tempo) and granular time-stretch (preserves pitch).
TL;DR
The Web Audio API gives you a free way to change playback rate via
AudioBufferSourceNode.playbackRate — 1.5× makes the buffer
finish 50% faster and raises the pitch by a fifth. zvuk exposes
this as play({ pitch }).
For pitch-preserving tempo changes (slow it down without making it sound
underwater), use StretchProcessor — an offline granular
SOLA-style renderer that takes one AudioBuffer and returns a stretched copy
at the same pitch.
Live demo — A/B
Both modes on the same source. Twiddle and click both buttons; you should clearly hear the difference between "chipmunk" and "tape slowdown."
Pitch via play() — cheap, pitch + tempo
engine.sound('coin').play({
pitch: 1.5, // 1.5× = chipmunk pitch + faster
});
// With jitter — varies per voice, makes stacked SFX feel organic:
engine.sound('hit').play({
pitch: { jitter: 0.08 }, // ±8% playback rate
}); Use this for: SFX that should feel "smaller" or "bigger", random pitch variation on stacked one-shots, retro arcade effects.
Time-stretch via StretchProcessor — preserves pitch
import { StretchProcessor } from 'zvuk';
const original = await fetch('/music/bg.webm')
.then((r) => r.arrayBuffer())
.then((ab) => engine.context.decodeAudioData(ab));
// Render a slower copy at half-tempo, pitch unchanged.
const slowMo = StretchProcessor.stretchBuffer(engine.context, original, 2);
engine.createSound('bg-half', slowMo, { bus: 'music' });
engine.sound('bg-half').play({ loop: true }); Use this for: slowed-down music for boss intros, doubled-tempo loops for chase scenes, normalising vocal cadence across recordings.
Pre-render multiple speeds
The renderer is offline — call it once per buffer/factor pair, cache the result. Realtime tempo control needs an AudioWorklet implementation (planned).
// Wrap it for re-use.
const stretched = (factor: number) =>
StretchProcessor.stretchBuffer(engine.context, original, factor);
engine.createSound('bg-1.25', stretched(1.25), { bus: 'music' });
engine.createSound('bg-1.5', stretched(1.5), { bus: 'music' });
engine.createSound('bg-2.0', stretched(2.0), { bus: 'music' }); Quality notes
- StretchProcessor uses overlap-add granular synthesis with cross-correlation alignment for stretch factors below 2.5×, plus a high-frequency-restore filter to compensate for the comb filtering granular methods inevitably introduce. It's good for music and dialogue.
- For factors > 3× you'll hear granular artifacts. Pitch-shifting impressionistic ambience can hide them; clean dialogue can't.
- Factor < 1 (i.e. play faster while preserving
pitch) is not currently supported. Use
play({ pitch })instead and accept the pitch shift, or pre-render shorter versions.
Pitfalls
play({ pitch }) for SFX, StretchProcessor
for sustained sources.