bungee-pitch-shift
v1.0.8
Published
High-quality, low-latency pitch shifting and time stretching for Web Audio API using WebAssembly
Maintainers
Readme
Bungee Pitch Shift
High-quality, low-latency pitch shifting and time stretching for Web Audio API using WebAssembly.
Built on the Bungee phase vocoder library, this package provides both a framework-agnostic class-based API and optional React hooks for easy integration.
Features
- High Quality: Phase vocoder algorithm for natural-sounding pitch shifts
- Low Latency: ~40ms latency using AudioWorklet and WebAssembly
- Independent Control: Separate pitch shifting (-12 to +12 semitones) and time stretching (0.5x to 2x speed)
- Wet/Dry Mix: Blend processed and original signals
- Web Worker Support: Offline processing, file loading, and waveform generation in background threads
- Framework Agnostic: Works with vanilla JavaScript or any framework
- React Support: Optional React hooks included
- TypeScript: Full TypeScript support with type definitions
Installation
npm install bungee-pitch-shiftSetup
After installation, copy the bundled processor to your public directory:
# Copy the bundled processor (includes embedded WASM)
cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/That's it! The bundled processor is completely self-contained:
- ✅ Includes the AudioWorklet processor
- ✅ Includes the Emscripten WASM glue code
- ✅ Includes the embedded WASM binary
- ✅ No additional files needed
Note: The path assumes your public directory is at public/. Adjust as needed for your project structure (e.g., public/, static/, dist/).
Build Tool Integration
For automated copying during builds, add a postinstall script to your package.json:
{
"scripts": {
"postinstall": "cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/"
}
}Using a CDN
Load the bundled processor from a CDN or remote server:
const pitchShift = await BungeePitchShift.create(audioContext, {
workletPath: 'https://cdn.example.com/bungee-processor-bundled.js'
});Benefits:
- No need to copy files to your public directory
- Better caching and performance via CDN
- Simplified deployment
- Share across multiple projects
CORS Note: When loading from a different domain, ensure the server sends appropriate CORS headers.
Next.js / Vite / Other Build Tools
Copy the file to your static assets directory:
Next.js:
cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/Vite:
cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/Create React App:
cp node_modules/bungee-pitch-shift/dist/bungee-processor-bundled.js public/Quick Start
Vanilla JavaScript / TypeScript
import { BungeePitchShift } from 'bungee-pitch-shift';
// Create audio context
const audioContext = new AudioContext();
// Initialize pitch shifter (uses bundled processor by default)
const pitchShift = await BungeePitchShift.create(audioContext);
// Or specify custom path:
// const pitchShift = await BungeePitchShift.create(audioContext, {
// workletPath: '/bungee-processor-bundled.js'
// });
// Connect to audio graph
sourceNode.connect(pitchShift.node);
pitchShift.connect(audioContext.destination);
// Control parameters
pitchShift.setPitch(5); // +5 semitones (perfect fourth up)
pitchShift.setSpeed(1.2); // 20% faster
pitchShift.setMix(0.8); // 80% wet, 20% dry
// Cleanup when done
pitchShift.dispose();React
import { usePitchShift } from 'bungee-pitch-shift/react';
import { useRef, useEffect } from 'react';
function AudioPlayer() {
const audioContext = useRef(new AudioContext()).current;
const {
pitchShift,
initialized,
updatePitch,
updateSpeed,
updateMix,
info
} = usePitchShift(audioContext);
useEffect(() => {
if (!pitchShift || !initialized) return;
// Connect to audio graph
sourceNode.connect(pitchShift.node);
pitchShift.connect(audioContext.destination);
return () => {
pitchShift.disconnect();
};
}, [pitchShift, initialized]);
return (
<div>
<h3>Bungee v{info.version}</h3>
<label>
Pitch:
<input
type="range"
min="-12"
max="12"
step="1"
onChange={(e) => updatePitch(Number(e.target.value))}
/>
</label>
<label>
Speed:
<input
type="range"
min="0.5"
max="2"
step="0.1"
onChange={(e) => updateSpeed(Number(e.target.value))}
/>
</label>
<label>
Mix:
<input
type="range"
min="0"
max="1"
step="0.01"
onChange={(e) => updateMix(Number(e.target.value))}
/>
</label>
</div>
);
}Web Worker Utilities
The bungee-pitch-shift/worker module provides Web Worker-based utilities for CPU-intensive audio operations. These tools keep your main thread responsive during heavy processing tasks.
Features
- Offline Processing: Process entire audio files with pitch shifting
- Audio File Loading: Load and decode audio files off the main thread
- Waveform Generation: Generate visualization data without blocking UI
- Fully Typed: Complete TypeScript support
- Transferable Objects: Efficient zero-copy data transfer
Setup
Copy the bundled worker file to your public directory:
cp node_modules/bungee-pitch-shift/dist/worker/audio-processor.worker.bundle.js public/Important: Use the .bundle.js file (not .worker.js). The bundle file is self-contained and has all dependencies included.
OfflineProcessor - Batch Audio Processing
Process entire audio files with pitch shifting in a worker thread:
import { OfflineProcessor } from 'bungee-pitch-shift/worker';
const processor = new OfflineProcessor({
workerPath: '/audio-processor.worker.bundle.js',
workletPath: '/bungee-processor-bundled.js'
});
// Process audio buffer
const audioContext = new AudioContext();
const processed = await processor.processWithContext(
audioBuffer,
audioContext,
{
pitch: 5, // +5 semitones
speed: 1.2, // 20% faster
mix: 1.0, // 100% wet
onProgress: (progress) => {
console.log(`Progress: ${Math.round(progress * 100)}%`);
}
}
);
// Cleanup
processor.dispose();Key Benefits:
- Non-blocking: UI remains responsive during processing
- Progress tracking: Get real-time updates on processing status
- Faster than real-time: No playback constraints
AudioFileLoader - Load Audio Files in Workers
Load and decode audio files without blocking the main thread:
import { AudioFileLoader } from 'bungee-pitch-shift/worker';
const loader = new AudioFileLoader({
workerPath: '/audio-processor.worker.bundle.js'
});
// Load from URL
const audioContext = new AudioContext();
const audioBuffer = await loader.loadWithContext(
'https://example.com/audio.mp3',
audioContext,
{
onProgress: (progress) => {
console.log(`Loading: ${Math.round(progress * 100)}%`);
}
}
);
// Or decode raw data
const response = await fetch('audio.mp3');
const arrayBuffer = await response.arrayBuffer();
const decoded = await loader.decodeWithContext(arrayBuffer, audioContext);
// Cleanup
loader.dispose();Use Cases:
- Large file loading without UI freezing
- Batch loading multiple files
- Pre-loading audio for web apps
WaveformGenerator - Visualization Data
Generate waveform data for audio visualizations:
import { WaveformGenerator } from 'bungee-pitch-shift/worker';
const generator = new WaveformGenerator({
workerPath: '/audio-processor.worker.bundle.js'
});
// Generate waveform with 1000 samples
const waveform = await generator.generate(audioBuffer, {
samples: 1000,
channel: 0, // Left channel (or -1 for average)
useRMS: false // Use peak values
});
// Use waveform.data for rendering
for (let i = 0; i < waveform.data.length; i++) {
const amplitude = waveform.data[i];
// Draw visualization bar
}
// Generate for multiple channels
const multiChannel = await generator.generateMultiChannel(audioBuffer, {
samples: 1000
});
// Quick preview (low resolution)
const preview = await generator.generatePreview(audioBuffer, 100);
// High resolution
const hires = await generator.generateHighRes(audioBuffer);
// Cleanup
generator.dispose();Waveform Options:
samples: Number of samples in output (default: 1000)channel: Which channel to use (0 = left, 1 = right, -1 = average all)useRMS: Use RMS values instead of peaks (default: false)
Worker Utilities
The worker module also exports useful utility functions:
import {
serializeAudioBuffer,
deserializeAudioBuffer,
getTransferables,
downsampleAudioBuffer,
validateAudioBuffer,
formatDuration,
formatBytes
} from 'bungee-pitch-shift/worker';
// Serialize for efficient transfer
const serialized = serializeAudioBuffer(audioBuffer);
const transferables = getTransferables(serialized);
worker.postMessage({ audioData: serialized }, transferables);
// Downsample for visualization
const waveformData = downsampleAudioBuffer(audioBuffer, 1000);
// Format helpers
console.log(formatDuration(audioBuffer.duration)); // "2:30"
console.log(formatBytes(arrayBuffer.byteLength)); // "1.5 MB"Worker Configuration
All worker classes accept configuration options:
interface WorkerConfig {
workerPath?: string; // Path to worker script
workletPath?: string; // Path to bundled AudioWorklet processor
timeout?: number; // Operation timeout (ms)
// Deprecated - no longer needed with bundled processor:
wasmPath?: string; // @deprecated
wasmBinaryPath?: string; // @deprecated
}Best Practices
- Reuse Worker Instances: Create once, use multiple times
- Dispose When Done: Always call
.dispose()to clean up - Progress Callbacks: Use for better UX on long operations
- Error Handling: Wrap worker calls in try-catch blocks
const processor = new OfflineProcessor();
try {
const result = await processor.processWithContext(
audioBuffer,
audioContext,
{ pitch: 5, speed: 1.0 }
);
// Use result...
} catch (error) {
console.error('Processing failed:', error);
} finally {
processor.dispose();
}API Reference
BungeePitchShift Class
Static Methods
create(audioContext, config?)
Create and initialize a new pitch shifter instance.
// Simplest usage (uses defaults)
const pitchShift = await BungeePitchShift.create(audioContext);
// With configuration
const pitchShift = await BungeePitchShift.create(audioContext, {
workletPath: '/bungee-processor-bundled.js', // Path or URL to bundled processor
initialPitch: 0, // Initial pitch in semitones
initialSpeed: 1.0, // Initial speed multiplier
initialMix: 1.0 // Initial wet/dry mix
});Note: wasmPath and wasmBinaryPath options are deprecated. The bundled processor has the WASM binary embedded, so these are no longer needed.
Instance Properties
node: AudioWorkletNode- The AudioWorkletNode for connecting to audio graphinitialized: boolean- Whether the processor is initializedparams: PitchShiftParams- Current parameters (read-only)info: BungeeInfo- Information about the Bungee processor
Instance Methods
setPitch(semitones: number)
Set pitch shift in semitones.
pitchShift.setPitch(7); // Perfect fifth up
pitchShift.setPitch(-12); // One octave downsetSpeed(speed: number)
Set playback speed multiplier.
pitchShift.setSpeed(1.5); // 50% faster
pitchShift.setSpeed(0.75); // 25% slowersetMix(mix: number)
Set wet/dry mix (0 = fully dry, 1 = fully wet).
pitchShift.setMix(1.0); // 100% processed
pitchShift.setMix(0.5); // 50/50 blend
pitchShift.setMix(0.0); // 100% dry (bypass)reset()
Reset internal processor state. Call this when seeking in audio playback.
pitchShift.reset();connect(destination: AudioNode)
Connect to another audio node.
pitchShift.connect(audioContext.destination);disconnect(destination?: AudioNode)
Disconnect from audio nodes.
pitchShift.disconnect();addEventListener(listener: EventListener)
Add an event listener for processor events.
pitchShift.addEventListener((event) => {
if (event.type === 'initialized') {
console.log('Version:', event.info.version);
}
});removeEventListener(listener: EventListener)
Remove an event listener.
dispose()
Clean up and release resources. Always call this when done.
pitchShift.dispose();React Hook: usePitchShift
const {
pitchShift, // BungeePitchShift instance
initialized, // boolean
params, // Current parameters
info, // Processor info
updatePitch, // (semitones: number) => void
updateSpeed, // (speed: number) => void
updateMix, // (mix: number) => void
setEnabled, // (enabled: boolean) => void
reset // () => void
} = usePitchShift(audioContext, config);TypeScript Types
interface BungeePitchShiftConfig {
/** Path to bundled processor (default: '/bungee-processor-bundled.js') */
workletPath?: string;
/** @deprecated - No longer needed, WASM is embedded in bundled processor */
wasmPath?: string;
/** @deprecated - No longer needed, WASM is embedded in bundled processor */
wasmBinaryPath?: string;
/** Initial pitch shift in semitones (default: 0) */
initialPitch?: number;
/** Initial playback speed (default: 1.0) */
initialSpeed?: number;
/** Initial wet/dry mix (default: 1.0) */
initialMix?: number;
}
interface PitchShiftParams {
pitchSemitones: number;
speed: number;
mix: number;
}
interface BungeeInfo {
version?: string;
edition?: string;
latency?: number;
}Examples
Audio File Player with Pitch Shift
import { BungeePitchShift } from 'bungee-pitch-shift';
const audioContext = new AudioContext();
const pitchShift = await BungeePitchShift.create(audioContext);
// Load audio file
const response = await fetch('audio.mp3');
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// Create source
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
// Connect: source → pitch shift → destination
source.connect(pitchShift.node);
pitchShift.connect(audioContext.destination);
// Play with pitch shift
pitchShift.setPitch(3);
source.start();Real-time Microphone Processing
import { BungeePitchShift } from 'bungee-pitch-shift';
const audioContext = new AudioContext();
const pitchShift = await BungeePitchShift.create(audioContext);
// Get microphone input
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioContext.createMediaStreamSource(stream);
// Connect: mic → pitch shift → destination
source.connect(pitchShift.node);
pitchShift.connect(audioContext.destination);
// Apply pitch shift
pitchShift.setPitch(-5);Integration with Existing Effects Chain
const reverb = audioContext.createConvolver();
const delay = audioContext.createDelay();
const gain = audioContext.createGain();
// Complex chain: source → pitch → reverb → delay → gain → destination
source.connect(pitchShift.node);
pitchShift.connect(reverb);
reverb.connect(delay);
delay.connect(gain);
gain.connect(audioContext.destination);Browser Compatibility
Requires:
- Web Audio API with AudioWorklet support
- WebAssembly support
Supported browsers:
- Chrome/Edge 66+
- Firefox 76+
- Safari 14.1+
Performance
- Latency: ~40ms (depending on buffer size and sample rate)
- CPU Usage: Efficient WASM implementation
- Automatic Bypass: When pitch=0 and speed=1.0, processing is bypassed for maximum performance
License
MIT
Credits
Built on Bungee by Signalsmith Audio.
