Example 08: Mixing and loudness

A typical game mix: music, the spatial world and UI sounds, each with their own volume, and nothing that clips.

What it shows

Routing

Music clip -> Channel "Music" [Compressor] --+
UI clips   -> Channel "UI" ------------------+--> device master [Limiter] --> speakers
                                                                               ^
World sources -> SpatialAudioRenderer -> renderer.master [Limiter] ------------+

By default every spatial renderer has its own master channel, created with audioDevice.createMasterChannel(). It connects to the speakers next to the device's default master channel. A renderer can also send to a bus channel instead, with the output option; see Example 12 for a mix with a bus per category.

Code

import { AudioClip, Compressor, Limiter, SpatialAudioRenderer2D } from "@fluex/fluexgl-dsp";

// Assumes `audioDevice` from Example 01, and loaded `musicData` and `clickData`.
const deviceMaster = audioDevice.getMasterChannel();

// --- Music: compressed, not spatial ---
const music = audioDevice.createChannel("Music");
music.send(deviceMaster);
music.addEffect(new Compressor({ threshold: -18, ratio: 3, makeupGain: 2 }));
music.volume(0.5);

const musicClip = new AudioClip(musicData);
musicClip.send(music);
musicClip.setLoop(true);
musicClip.play();

// --- UI: not spatial ---
const ui = audioDevice.createChannel("UI");
ui.send(deviceMaster);

const clickClip = new AudioClip(clickData);
clickClip.send(ui);

// --- Safety limiter on the device master (music + UI) ---
deviceMaster.attachEffect(new Limiter({ ceiling: -1 }));

// --- World: spatial, with its own master and limiter ---
const world = new SpatialAudioRenderer2D(audioDevice, {
    limiter: { ceiling: -1, release: 0.15 }
});

// World volume, for example from a settings menu (0..1).
function setWorldVolume(volume: number) {
    world.master.gainNode?.gain.setTargetAtTime(volume, audioDevice.context.currentTime, 0.05);
}

// Duck the world while a dialog is open.
function setDialogOpen(open: boolean) {
    setWorldVolume(open ? 0.3 : 1);
}

Metering

setInterval(() => {
    console.log("world limiter reduction:", world.limiter?.reduction.toFixed(1), "dB");
}, 250);

If the limiter is reducing by more than a few dB most of the time, the mix is too loud: lower source volumes or world.master instead of relying on the limiter.

Notes

  • A renderer's master is a normal Master. Its gainNode sits after the limiter, so lowering it never causes clipping.
  • Effects attached to world.master after construction come after the built-in limiter. If you need effects before the limiter, create the renderer with limiter: false, attach your effects, then attach your own Limiter last.
  • To mute a channel, use channel.volume(0).
  • Two renderers can run side by side, for example one for the world and one for a separate minigame. They can share one AudioDevice.