Skip to content

Tempo Change

Patterns for tempo changes and monitoring buffering state.

Modules used: play, stretcher

Tempo Change Demo

A demo of WSOLA-based pitch-preserving time-stretch. Use the tempo slider to change playback speed in real-time.

Sound Source

Use a longer sound source to experience pitch-preserving time-stretch.

or

Chunk Buffering Demo

A demo to observe the chunk buffering mechanism of the WSOLA stretcher engine. Monitor chunk processing states and events in real-time.

Sound Source

Use a longer sound source to observe chunk splitting behavior.

or

Code Examples

1. Pitch-Preserving Tempo Change

Since preservePitch defaults to true, pitch is preserved by default in normal play() calls. For regular speed changes (where pitch also changes), explicitly set preservePitch: false.

import { WaaPlayer } from "waa-play";
const waa = new WaaPlayer();
await waa.ensureRunning();
const buffer = await waa.load("/audio/track.mp3");
// Change tempo while preserving pitch (default behavior)
const playback = waa.play(buffer, { playbackRate: 0.8 });
// Change tempo during playback
playback.setPlaybackRate(1.2); // 1.2x speed, pitch maintained
// To change pitch along with tempo, set preservePitch: false
const playback2 = waa.play(buffer, {
playbackRate: 1.5,
preservePitch: false,
});

The WSOLA algorithm allows you to change tempo without changing pitch. Setting preservePitch: true (default) enables WSOLA-based time stretching.

Function API
import { createContext, ensureRunning } from "waa-play/context";
import { loadBuffer } from "waa-play/buffer";
import { play } from "waa-play/play";
const ctx = createContext();
await ensureRunning(ctx);
const buffer = await loadBuffer(ctx, "/audio/track.mp3");
// Change tempo while preserving pitch (default behavior)
const playback = play(ctx, buffer, { playbackRate: 0.8 });
// Change tempo during playback
playback.setPlaybackRate(1.2);
// To change pitch along with tempo, set preservePitch: false
const playback2 = play(ctx, buffer, {
playbackRate: 1.5,
preservePitch: false,
});

2. Monitoring Buffering State

Since WSOLA processing is asynchronous, buffering may occur when changing tempo.

const playback = waa.play(buffer, { playbackRate: 0.8 });
// Monitor buffering start
playback.on("buffering", ({ reason }) => {
console.log(`Buffering... (reason: ${reason})`);
// Show loading UI
});
// Monitor buffering completion
playback.on("buffered", ({ stallDuration }) => {
console.log(`Buffering complete (${stallDuration.toFixed(0)}ms)`);
// Hide loading UI
});
// Check stretcher state via snapshot
const snapshot = waa.getSnapshot(playback);
if (snapshot.stretcher) {
console.log(`Tempo: ${snapshot.stretcher.tempo}`);
console.log(`Buffer health: ${snapshot.stretcher.bufferHealth}`);
console.log(`Converting: ${snapshot.stretcher.converting}`);
console.log(`Conversion progress: ${(snapshot.stretcher.conversionProgress * 100).toFixed(0)}%`);
}

The reason in the buffering event is one of "initial" | "seek" | "tempo-change" | "underrun". You can get detailed state information via the stretcher field in getSnapshot().

Function API
import { getSnapshot } from "waa-play/adapters";
const playback = play(ctx, buffer, { playbackRate: 0.8 });
playback.on("buffering", ({ reason }) => {
console.log(`Buffering... (reason: ${reason})`);
});
playback.on("buffered", ({ stallDuration }) => {
console.log(`Buffering complete (${stallDuration.toFixed(0)}ms)`);
});
const snapshot = getSnapshot(playback);
if (snapshot.stretcher) {
console.log(`Tempo: ${snapshot.stretcher.tempo}`);
console.log(`Buffer health: ${snapshot.stretcher.bufferHealth}`);
console.log(`Converting: ${snapshot.stretcher.converting}`);
console.log(`Conversion progress: ${(snapshot.stretcher.conversionProgress * 100).toFixed(0)}%`);
}