Audio effects
Shape sound with filters and bus-level processing.
Audio effects
Every bus in ExoJS carries a filter chain — an ordered list of AudioEffect instances that process audio in series. Filters are applied at the bus level, which means adding a filter to app.audio.music affects every voice routing through the music bus. You can also create custom buses for isolated effect chains.
The filter chain
A bus’s filter chain is inputNode → [filter₁ → filter₂ → ...] → panNode → outputNode. Adding a filter appends it to the end of the chain. Order matters — a compressor before reverb sounds different from a compressor after reverb.
import { CompressorEffect, DelayEffect, ReverbEffect } from '@codexo/exojs-audio-fx';
const compressor = new CompressorEffect({ threshold: -20, ratio: 8 });
const reverb = new ReverbEffect({ wet: 0.3 });
const delay = new DelayEffect({ delaySeconds: 0.25, wet: 0.2, feedback: 0.4 });
app.audio.master.addEffect(compressor);
app.audio.master.addEffect(reverb);
app.audio.sound.addEffect(delay);
Remove a filter to take it out of the chain without destroying it:
app.audio.master.removeEffect(reverb);
Filters are reusable — move one from bus to bus by removing it from the old bus and adding it to the new one. Destroy a filter with filter.destroy() when it’s no longer needed anywhere.
CompressorEffect
A dynamics compressor that clamps the dynamic range of audio passing through it. Useful for evening out volume, adding punch to SFX, and preventing clipping when many sounds play simultaneously:
const compressor = new CompressorEffect({
threshold: -24, // dBFS — signals above this get compressed
ratio: 12, // 1:1 = no compression, 20:1 = near-limiting
attack: 0.003, // seconds — how fast compression kicks in
release: 0.25, // seconds — how fast it lets go
knee: 30, // dB — softens the threshold transition
});
app.audio.master.addEffect(compressor);
All properties are live get/set. The compressor.reduction getter returns the current gain reduction in dB (always ≤ 0) — useful for live level metering:
update(delta) {
const gr = this.compressor.reduction;
this.meterBar.height = Math.abs(gr) * 10; // scale to pixels
}
ReverbEffect and DelayEffect
ReverbEffect simulates acoustic space by convolving audio through a procedurally-generated impulse response:
const reverb = new ReverbEffect({
durationSeconds: 2, // IR length — longer = bigger space
decay: 2, // exponential decay factor
wet: 0.4, // 0 = dry only, 1 = wet only
});
DelayEffect creates an echo with configurable feedback:
const delay = new DelayEffect({
delaySeconds: 0.3, // echo interval
feedback: 0.45, // 0 = one echo, 0.95 = long tail
wet: 0.5, // dry/wet mix
});
Both expose live wet, delaySeconds, feedback, decay, and durationSeconds properties. Changing durationSeconds or decay on a reverb rebuilds the impulse response — prefer to keep these stable during playback.
DuckingEffect
A sidechain compressor — when the sidechain bus exceeds a threshold, the main signal is attenuated. Common use case: automatically lower music volume when voice-over plays:
import { AudioBus } from '@codexo/exojs';
import { DuckingEffect } from '@codexo/exojs-audio-fx';
const voiceBus = new AudioBus('voice-over', { parent: app.audio.master });
app.audio.registerBus(voiceBus);
const ducker = new DuckingEffect({
sidechain: voiceBus,
threshold: -30, // dBFS — sidechain level that triggers ducking
ratio: 6, // gain reduction ratio
attackMs: 25, // how fast ducking engages
releaseMs: 260, // how fast audio recovers after sidechain drops
});
app.audio.music.addEffect(ducker);
threshold, ratio, attackMs, and releaseMs are live get/set. The filter.ready promise resolves when the worklet processor is loaded and the chain is fully wired.
Shaping and modulation
Several filters cover tone shaping and modulation effects:
import { HighpassFilter, LowpassFilter } from '@codexo/exojs';
import { ChorusEffect, EqualizerEffect } from '@codexo/exojs-audio-fx';
// Three-band equalizer — low shelf, peaking mid, high shelf
const eq = new EqualizerEffect({ low: 3, mid: -2, high: 4 });
// Frequency cuts — animate the frequency property for sweeps
const lp = new LowpassFilter({ frequency: 2000, resonance: 1 });
const hp = new HighpassFilter({ frequency: 200, resonance: 1 });
// Modulated delay for thickness and doubling (native nodes, instant startup)
const chorus = new ChorusEffect({ rateHz: 1.5, depthMs: 5, wet: 0.5 });
All parameters on these effects are live get/set. The EqualizerEffect API reference documents individual band frequencies; the ChorusEffect and LowpassFilter references cover constructor options in detail.
Distortion and modulation effects
Like ChorusEffect, the six filters below are built from native Web Audio nodes — they’re ready the instant you construct them, with no worklet loading or ready promise involved. All expose a live wet dry/wet mix; see each API reference for the full option list.
DistortionEffect
Soft-clip saturation through a tanh-based WaveShaperNode curve, followed by a tone-control lowpass on the wet path. drive (0..1) controls clip intensity and tone (0..1) sweeps the wet-path cutoff logarithmically from 100 Hz to 20 kHz:
import { DistortionEffect } from '@codexo/exojs-audio-fx';
const dist = new DistortionEffect({ drive: 0.6, tone: 0.7, wet: 0.8 });
app.audio.sound.addEffect(dist);
PhaserEffect
Sweeps a cascade of allpass filters with a sine LFO, producing moving notches in the spectrum. stages (an even number, 2..12) sets the notch count and feedback (0..0.9) sharpens the resonance:
import { PhaserEffect } from '@codexo/exojs-audio-fx';
const phaser = new PhaserEffect({ stages: 6, rateHz: 0.3, depth: 0.8, feedback: 0.4, wet: 0.6 });
app.audio.music.addEffect(phaser);
FlangerEffect
A short (0.5–20 ms) LFO-modulated delay with a feedback loop, producing the classic sweeping “jet-plane” comb-filter sound. Unlike ChorusEffect — a longer, feedback-free delay used for thickening — the feedback here reinforces phase cancellation:
import { FlangerEffect } from '@codexo/exojs-audio-fx';
const flanger = new FlangerEffect({ delayMs: 3, depthMs: 2, rateHz: 0.25, feedback: 0.5, wet: 0.5 });
app.audio.sound.addEffect(flanger);
TremoloEffect
Amplitude-modulates the signal with a sine LFO for classic volume pulsing. Set autoPan: true to have the same LFO drive stereo panning in sync with the pulse:
import { TremoloEffect } from '@codexo/exojs-audio-fx';
const tremolo = new TremoloEffect({ rateHz: 5, depth: 0.7, autoPan: true });
app.audio.music.addEffect(tremolo);
RingModulatorEffect
Multiplies the input by a carrier oscillator, producing sum/difference sidebands while suppressing the fundamental. Mid-range carrier frequencies (100–1000 Hz) give the classic robotic ring-mod timbre; sub-audio rates (< 20 Hz) sound like tremolo:
import { RingModulatorEffect } from '@codexo/exojs-audio-fx';
const ringMod = new RingModulatorEffect({ frequency: 220, waveform: 'sine', wet: 0.8 });
app.audio.sound.addEffect(ringMod);
AutoWahEffect
An envelope-driven wah: a rectifier and smoothing filter track input loudness and sweep a resonant bandpass filter upward from baseFrequency. sensitivity sets the maximum sweep in Hz and responseMs controls how snappy or legato the response feels:
import { AutoWahEffect } from '@codexo/exojs-audio-fx';
const wah = new AutoWahEffect({ baseFrequency: 300, sensitivity: 2500, q: 5, wet: 0.8 });
app.audio.sound.addEffect(wah);
Ping-pong delay, limiting, and convolution
PingPongDelayEffect
A stereo delay whose taps cross-feed between channels, bouncing echoes hard-left/hard-right. delayTime sets the per-tap interval in seconds and feedback (0..0.9) controls how long the bounce sustains:
import { PingPongDelayEffect } from '@codexo/exojs-audio-fx';
const pingPong = new PingPongDelayEffect({ delayTime: 0.3, feedback: 0.5, wet: 0.5 });
app.audio.music.addEffect(pingPong);
LimiterEffect
A brick-wall limiter — a DynamicsCompressorNode fixed at a high ratio and hard knee — meant as a safety net at the end of a chain to catch peaks before they clip:
import { LimiterEffect } from '@codexo/exojs-audio-fx';
const limiter = new LimiterEffect({ threshold: -3, release: 0.1 });
app.audio.master.addEffect(limiter);
ConvolutionEffect
Convolves the signal with a real impulse response instead of ReverbEffect’s procedurally generated one — useful for captured spaces, cabinet/speaker simulation, or telephone-style filtering. Pass a decoded AudioBuffer or a loaded Sound; call setImpulse() to swap the IR later:
import { ConvolutionEffect } from '@codexo/exojs-audio-fx';
import { Sound } from '@codexo/exojs';
await loader.load(Sound, { hall: 'hall.wav' });
const convReverb = new ConvolutionEffect({ impulse: loader.get(Sound, 'hall'), wet: 0.6 });
app.audio.master.addEffect(convReverb);
Worklet-based effects
Four filters use AudioWorkletProcessor and load asynchronously. They extend WorkletEffect, which exposes a ready promise — the engine registers the worklet, you construct the filter and add it to a bus, and the chain rewires automatically once the processor is loaded.
import { BitCrusherEffect, GranularEffect, PitchShiftEffect, VocoderEffect } from '@codexo/exojs-audio-fx';
// Granular pitch shifting (0.25x to 4x)
const shifter = new PitchShiftEffect({ pitch: 1.2, wet: 1.0 });
// Cross-synthesis between a carrier signal and a modulator bus
const vocoder = new VocoderEffect({ modulator: modulatorBus, numBands: 14 });
// Slices input into short grains with random pitch and time offset
const granular = new GranularEffect({ grainSize: 0.05, density: 50, wet: 1.0 });
// Lo-fi bit-depth and sample-rate reduction
const crusher = new BitCrusherEffect({ bits: 4, frequencyReduction: 0.3, wet: 0.8 });
All four have live wet controls. PitchShiftEffect exposes pitch; GranularEffect exposes grainSize, density, spread, pitchMin, and pitchMax for real-time grain cloud manipulation; BitCrusherEffect exposes bits (1..16, quantization depth) and frequencyReduction (0..1, sample-and-hold rate). The API reference documents the full constructor options for each.
Custom buses
Bus-level filters affect everything on that bus. Create isolated chains for specific sound categories:
const ambientBus = new AudioBus('ambient', { parent: app.audio.master });
app.audio.registerBus(ambientBus);
ambientBus.addEffect(new ReverbEffect({ wet: 0.6, durationSeconds: 3 }));
ambientBus.addEffect(new LowpassFilter({ frequency: 800 }));
const wind = loader.get(Sound, 'wind');
// Route this play onto the ambient bus — reverb + lowpass applied.
this.app.audio.play(wind, { bus: ambientBus, loop: true });
registerBus stores the bus by name, retrievable later via app.audio.getBus('ambient'). unregisterBus removes and destroys a custom bus. The three built-in buses (master, music, sound) cannot be unregistered.
Examples
Interactive compressor with live gain-reduction meter — drag sliders to adjust threshold, ratio, attack, and release.
Reverb and delay filters on the SFX bus, with live wet/dry and delay-time sliders.
Where to go next
The next chapter, Beat detection, covers tempo tracking and beat analysis — how to sync game logic to the rhythm of your music.