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 type { Application } from '@codexo/exojs';
import { CompressorEffect, DelayEffect, ReverbEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
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 });
application.audio.master.addEffect(compressor);
application.audio.master.addEffect(reverb);
application.audio.sound.addEffect(delay);
Remove a filter to take it out of the chain without destroying it:
import type { Application } from '@codexo/exojs';
import { ReverbEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
declare const reverb: ReverbEffect;
application.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.
You own every effect you create
Attaching an effect never transfers ownership. removeEffect, bus.destroy(), unregisterBus and app.audio.destroy() all only detach it — none of them calls destroy() on your effect, because the same instance may still be attached to a voice or to another bus. Call filter.destroy() yourself once it is no longer used anywhere.
Effect order is the order you add them
addEffect always appends to the end of the chain, so the sequence you add effects in is the sequence audio flows through — a compressor added before a reverb sounds different from one added after. To slot an effect in earlier, removeEffect the later ones and add them back in the order you want.
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:
import type { Application } from '@codexo/exojs';
import { CompressorEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
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
});
application.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: Seconds) {
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:
import { ReverbEffect } from '@codexo/exojs-audio-fx';
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
});
Keep durationSeconds and decay stable during playback
Changing durationSeconds or decay on a live ReverbEffect rebuilds its impulse response — a heavier operation than a simple parameter tweak. Set them at construction and leave them alone while audio is playing; automate wet for real-time changes instead.
DelayEffect creates an echo with configurable feedback:
import { DelayEffect } from '@codexo/exojs-audio-fx';
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.
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 { type Application, AudioBus } from '@codexo/exojs';
import { DuckingEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const voiceBus = new AudioBus('voice-over', { parent: application.audio.master });
application.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
});
application.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 type { Application } from '@codexo/exojs';
import { DistortionEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const dist = new DistortionEffect({ drive: 0.6, tone: 0.7, wet: 0.8 });
application.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 type { Application } from '@codexo/exojs';
import { PhaserEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const phaser = new PhaserEffect({ stages: 6, rateHz: 0.3, depth: 0.8, feedback: 0.4, wet: 0.6 });
application.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 type { Application } from '@codexo/exojs';
import { FlangerEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const flanger = new FlangerEffect({ delayMs: 3, depthMs: 2, rateHz: 0.25, feedback: 0.5, wet: 0.5 });
application.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 type { Application } from '@codexo/exojs';
import { TremoloEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const tremolo = new TremoloEffect({ rateHz: 5, depth: 0.7, autoPan: true });
application.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 type { Application } from '@codexo/exojs';
import { RingModulatorEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const ringMod = new RingModulatorEffect({ frequency: 220, waveform: 'sine', wet: 0.8 });
application.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 type { Application } from '@codexo/exojs';
import { AutoWahEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const wah = new AutoWahEffect({ baseFrequency: 300, sensitivity: 2500, q: 5, wet: 0.8 });
application.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 type { Application } from '@codexo/exojs';
import { PingPongDelayEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const pingPong = new PingPongDelayEffect({ delayTime: 0.3, feedback: 0.5, wet: 0.5 });
application.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 type { Application } from '@codexo/exojs';
import { LimiterEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
const limiter = new LimiterEffect({ threshold: -3, release: 0.1 });
application.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.
An impulse response is just an audio file, so it loads like any other sound — there is no separate asset type for it. Load it with Asset.type('sound', 'ir/concert-hall.wav') (or as a catalog leaf) and hand the result straight to the effect:
import { type Application, Sound } from '@codexo/exojs';
import { ConvolutionEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
declare const hall: Sound;
const convReverb = new ConvolutionEffect({ impulse: hall, wet: 0.6 });
application.audio.master.addEffect(convReverb);
Keep impulse responses short
Convolution cost scales with IR length. A two-second hall is already expensive on a busy bus; trim captures to the tail you actually need, and prefer one shared ConvolutionEffect on a bus over per-voice instances.
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.
Await ready before you depend on a worklet effect
A WorkletEffect loads its processor asynchronously — the bus rewires itself once it’s ready, but until then the effect passes audio through untouched. await effect.ready before you rely on its output or read state from it.
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:
import { type Application, AudioBus, LowpassFilter, Sound } from '@codexo/exojs';
import { ReverbEffect } from '@codexo/exojs-audio-fx';
declare const application: Application;
declare const wind: Sound;
const ambientBus = new AudioBus('ambient', { parent: application.audio.master });
application.audio.registerBus(ambientBus);
ambientBus.addEffect(new ReverbEffect({ wet: 0.6, durationSeconds: 3 }));
ambientBus.addEffect(new LowpassFilter({ frequency: 800 }));
// Route this play onto the ambient bus — reverb + lowpass applied.
application.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.


