@soundtouchjs/phase-vocoder-worklet

May 15, 2026 · View on GitHub

An AudioWorklet integration that uses the phase vocoder time-stretch algorithm from @soundtouchjs/stretch-phase-vocoder. Provides PhaseVocoderNode as a drop-in replacement for SoundTouchNode when smoother time-stretching at extreme ratios is required.

I accept cash if you like what's been done.

Part of the SoundTouchJS monorepo — for more information and so much more.

Installation

npm install @soundtouchjs/phase-vocoder-worklet @soundtouchjs/stretch-phase-vocoder

When to use this package

  • Playback at extreme ratios (< 0.5× or > 2×) where the default WSOLA algorithm produces audible artifacts.
  • Smooth slow-motion or fast-forward effects where "phasiness" is acceptable.
  • You want the same AudioWorkletNode API as SoundTouchNode with a different internal stretch algorithm.

For moderate ratios (0.5–2×) the default @soundtouchjs/audio-worklet typically sounds better.

Setup

Resolving processorUrl

Vite

import processorUrl from '@soundtouchjs/phase-vocoder-worklet/processor?url';

webpack 5

const processorUrl = new URL(
  '@soundtouchjs/phase-vocoder-worklet/processor',
  import.meta.url,
).href;

Static / CDN

Copy .dist/phase-vocoder-processor.js to your public directory and reference it by path:

const processorUrl = '/phase-vocoder-processor.js';

Usage

import { PhaseVocoderNode } from '@soundtouchjs/phase-vocoder-worklet';

const audioCtx = new AudioContext();
await PhaseVocoderNode.register(audioCtx, processorUrl);

const node = new PhaseVocoderNode({
  context: audioCtx,
  fftSize: 2048,      // optional, default 2048
  overlapFactor: 4,   // optional, default 4
});

node.pitch.value = 1.5;        // pitch up 50 %
node.pitchSemitones.value = 3; // or shift by semitones
node.playbackRate.value = 0.5; // slow down 2×

node.connect(audioCtx.destination);

// Connect your source:
sourceNode.connect(node);

Offline processing

Use processOffline() when you want the same phase-vocoder pipeline in an OfflineAudioContext (no live audio device needed):

import { processOffline } from '@soundtouchjs/phase-vocoder-worklet';

const processed = await processOffline({
  input: audioBuffer,
  processorUrl,
  playbackRate: 0.5,
  pitchSemitones: 3,
  fftSize: 1024,
  overlapFactor: 8,
});

API

Top-level exports:

ExportDescription
PhaseVocoderNodeMain-thread AudioWorkletNode wrapper
PROCESSOR_NAMEProcessor registration id
processOffline(options)Offline rendering helper using PhaseVocoderNode

PhaseVocoderNode

Extends AudioWorkletNode. Same API as SoundTouchNode with additional fftSize / overlapFactor options.

Static methods

MethodDescription
PhaseVocoderNode.register(context, processorUrl)Registers the processor module. Must be called before constructing nodes.
PhaseVocoderNode.registerStrategyModule(context, moduleUrl)Loads an interpolation strategy plugin into worklet scope.
PhaseVocoderNode.processorNameThe registered processor identifier string.

Constructor options

OptionTypeDefaultDescription
contextBaseAudioContextrequiredAudioContext or OfflineAudioContext
fftSize512 | 1024 | 2048 | 40962048FFT frame size — larger = better frequency resolution, higher latency
overlapFactor2 | 4 | 84Overlap factor — higher = smoother output, more computation
outputChannelCount1 | 22Output channel count (set to 1 for mono destinations)
sampleBufferType'circular' | 'fifo''circular'Internal buffer strategy
interpolationStrategyRateTransposerInterpolationStrategy'lanczos'Initial rate-transposer interpolation strategy

AudioParams

ParamDefaultRangeDescription
pitch1.00.1–8.0Pitch multiplier (k-rate)
pitchSemitones0-24–24Pitch shift in semitones, combined with pitch (k-rate)
playbackRate1.00.1–8.0Playback rate multiplier — set this to match the source node's playbackRate (k-rate)

Methods

MethodDescription
setInterpolationStrategy(strategy)Switches interpolation strategy at runtime.
setInterpolationStrategyParams(params)Updates parameters for the active strategy.
setStretchParameters(params)No-op for the phase vocoder (accepted for API parity with SoundTouchNode).

Processor observability

The processor posts metrics every 100 render blocks. Access them via the metrics getter or the metrics CustomEvent:

// Getter
const m = node.metrics; // ProcessorMetrics | null

// Event
node.addEventListener('metrics', (e) => {
  const { framesBuffered, underrunCount, blockCount } = (e as CustomEvent<ProcessorMetrics>).detail;
  console.log('underruns:', underrunCount);
});

ProcessorMetrics shape:

FieldDescription
framesBufferedOutput frames available at the last render block
underrunCountCumulative render blocks with fewer output frames than requested
blockCountTotal render blocks processed
timestampperformance.now() when the metrics arrived on the main thread

Trade-offs vs SoundTouchNode

SoundTouchNode (WSOLA)`PhaseVocoderNode$
\text{Quality} \text{at} \text{extreme} \text{ratios}\text{Artifacts} \text{above} 2 \times\text{Smooth} \text{at} \text{all} \text{ratios}
\text{Transient} \text{preservation}\text{Better} (\text{time}-\text{domain})\text{Worse} (\text{frequency} \text{smearing})
\text{Computation}\text{Lower}\text{Higher} (\text{FFT} \text{per} \text{hop})
\text{Startup} \text{latency}\text{Lower}$fftSize` samples
ArtifactsClicks / repeats"Phasiness" / smearing

Architecture

PhaseVocoderProcessor extends SoundTouchProcessorBase from @soundtouchjs/worklet-base, sharing the DSP pipeline, runtime-update queue, and STANDARD_PARAMETER_DESCRIPTORS with the other SoundTouchJS worklet packages. Override beforePipeProcess and extractSamples from the base if you need to customise the processing hooks.

License

MPL-2.0 — see LICENSE for details.