@soundtouchjs/formant-correction-worklet

May 15, 2026 · View on GitHub

An AudioWorklet integration that applies SoundTouch pitch-shifting with LPC-based formant preservation. Use FormantCorrectionNode instead of SoundTouchNode when you need to shift pitch without the "chipmunk" or "giant" effect on voices.

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/formant-correction-worklet

The problem this solves

When you pitch-shift a voice with standard SoundTouch, the formants (vocal resonances that make a voice sound like a specific person) shift along with the pitch. Shifting a voice up by a perfect fifth sounds like a chipmunk; shifting it down sounds like a giant.

This package uses LPC (Linear Predictive Coding) to separate the formant envelope from the pitch-bearing signal, pitch-shift the signal, and then re-apply the original formant envelope at the new pitch — preserving the natural timbre of the voice.

How it works

For each render block:

  1. SoundTouch processes the original input → pitch-shifted output.
  2. LPC coefficients are computed from a 512-sample sliding window of the input signal (order 16, Hamming-windowed autocorrelation + Levinson-Durbin recursion).
  3. The LPC analysis filter removes the shifted formants from the pitch-shifted output (spectrally whitens it).
  4. The LPC synthesis filter re-applies the original input formant envelope.
  5. The formantStrength AudioParam blends the corrected and uncorrected signals.

Setup

Resolving processorUrl

Vite

import processorUrl from '@soundtouchjs/formant-correction-worklet/processor?url';

webpack 5

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

Static / CDN

Copy .dist/formant-correction-processor.js to your public directory:

const processorUrl = '/formant-correction-processor.js';

Usage

import { FormantCorrectionNode } from '@soundtouchjs/formant-correction-worklet';

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

const node = new FormantCorrectionNode({ context: audioCtx });

node.pitchSemitones.value = 7;   // shift up a perfect fifth
node.formantStrength.value = 1;  // keep original timbre (default)

node.connect(audioCtx.destination);
sourceNode.connect(node);

Offline processing

Use processOffline() to render the formant-correction pipeline in an OfflineAudioContext:

import { processOffline } from '@soundtouchjs/formant-correction-worklet';

const processed = await processOffline({
  input: audioBuffer,
  processorUrl,
  pitchSemitones: 7,
  formantStrength: 1,
  playbackRate: 1.2,
});

A/B comparison

// A: raw pitch shift (chipmunk)
node.formantStrength.value = 0;

// B: formant-corrected
node.formantStrength.value = 1;

// Blend (crossfade between the two)
node.formantStrength.value = 0.5;

API

Top-level exports:

ExportDescription
FormantCorrectionNodeMain-thread AudioWorkletNode wrapper
PROCESSOR_NAMEProcessor registration id
processOffline(options)Offline rendering helper using FormantCorrectionNode
autocorrelate, levinsonDurbin, applyAnalysisFilter, applySynthesisFilterLPC primitives for custom processing

FormantCorrectionNode

Extends AudioWorkletNode. Provides the same API as SoundTouchNode plus a formantStrength AudioParam.

Static methods

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

Constructor options

OptionTypeDefaultDescription
contextBaseAudioContextrequiredAudioContext or OfflineAudioContext
outputChannelCount1 | 22Set to 1 for mono destinations
sampleBufferType'circular' | 'fifo''circular'Internal buffer strategy
interpolationStrategyRateTransposerInterpolationStrategy'lanczos'Rate-transposer kernel

AudioParams

ParamDefaultRangeDescription
pitch1.00.1–8.0Pitch multiplier (k-rate)
pitchSemitones0-24–24Semitone pitch shift (k-rate)
playbackRate1.00.1–8.0Match the source node's playbackRate for accurate pitch compensation (k-rate)
formantStrength1.00.0–1.00 = raw pitch shift; 1 = full formant correction (k-rate)

Methods

MethodDescription
setInterpolationStrategy(strategy)Switches interpolation strategy at runtime.
setInterpolationStrategyParams(params)Updates parameters for the active strategy.
setStretchParameters(params)Applies WSOLA timing parameters.

Processor observability

node.addEventListener('metrics', (e) => {
  const { framesBuffered, underrunCount } = (e as CustomEvent<ProcessorMetrics>).detail;
});
const m = node.metrics; // ProcessorMetrics | null

LPC primitives

The package also exports its internal LPC functions for custom use:

import {
  autocorrelate,
  levinsonDurbin,
  applyAnalysisFilter,
  applySynthesisFilter,
  LPC_ORDER,
  LPC_WINDOW,
} from '@soundtouchjs/formant-correction-worklet';
FunctionDescription
autocorrelate(frame, order)Hamming-windowed biased autocorrelation, returns r[0..order]
levinsonDurbin(r, order)Levinson-Durbin recursion → LPC predictor coefficients
applyAnalysisFilter(frame, a, zi)FIR whitening filter; removes formant coloring
applySynthesisFilter(frame, a, zi)IIR coloring filter; restores formant coloring

Trade-offs vs SoundTouchNode

SoundTouchNodeFormantCorrectionNode
Formant shift with pitchYes (chipmunk/giant effect)Corrected — original timbre preserved
ComputationLowerHigher (LPC per render block)
Best use caseInstruments, musicVocals, speech
formantStrength = 0 modeIdentical to SoundTouchNode

Architecture

FormantCorrectionProcessor extends SoundTouchProcessorBase from @soundtouchjs/worklet-base, sharing the DSP pipeline, runtime-update queue, and STANDARD_PARAMETER_DESCRIPTORS with the other SoundTouchJS worklet packages. The formant correction logic lives in beforePipeProcess (LPC analysis) and extractSamples (analysis/synthesis filter application + formantStrength blend) — both hooks defined by the base class contract.

License

MPL-2.0 — see LICENSE for details.