demo · v139

Audio Mix Normalizer

Each RTCEncodedAudioFrame carries an audioLevel field on the 0–127 dBov scale (0 = maximum loudness, 127 = silence). A transform pipeline reads this per-frame without decoding and computes the gain correction needed to hit a target loudness.

Checking RTCEncodedAudioFrame support…
−20 dBov
dBov scale: 0 = maximum possible amplitude (full scale), 127 = effectively silence. Lower numbers are louder. The gain factor for a participant is computed as gain_dB = target_dBov − participant_dBov. A positive result means boost; negative means attenuate.

RTCEncodedTransform pattern

const sender = pc.getSenders().find(s => s.track.kind === 'audio');
const { readable, writable } = sender.createEncodedStreams();

const targetLevel = -20; // dBov

readable.pipeThrough(new TransformStream({
  transform(frame, controller) {
    const meta = frame.getMetadata();
    // meta.audioLevel: 0..127 dBov (Chrome 139+)
    const gainDb = targetLevel - meta.audioLevel;
    // gainDb > 0 → boost participant (they are quieter than target)
    // gainDb < 0 → cut  participant (they are louder than target)

    // Apply gain to the raw PCM in frame.data (Int16Array)
    const pcm = new Int16Array(frame.data);
    const factor = Math.pow(10, gainDb / 20);
    for (let i = 0; i < pcm.length; i++) {
      pcm[i] = Math.max(-32768, Math.min(32767, Math.round(pcm[i] * factor)));
    }
    controller.enqueue(frame);
  }
})).pipeTo(writable);

see also