Concept

Concurrency

Voice limits with a steal strategy. Keeps polyphony bounded, and decides which voice dies when the limit is hit.

Hold a button that fires a 50 ms click every frame. Thirty seconds later you have eighteen hundred live voices, a stalled audio thread, and a fan at full tilt. Nothing in the Web Audio API stops you.

Concurrency is a per-bus cap on how many voices may sound at once, together with a rule for what happens when a new voice arrives at a full bus. That rule is the steal strategy. It picks an existing voice to end so the new one can play, and it runs synchronously, before play() returns.

How many voices can play at once?

As many as you allow. The cap is declared per bus, so a UI bus can hold four while an SFX bus holds thirty-two. A bus with no concurrency block is unbounded, which is the right default for music and the wrong one for anything a player can spam.

SLOTS — max: 3, steal: oldest slot 1 voice A slot 2 voice B slot 3 voice C new! voice D A is the oldest → stolen, slot freed AFTER: [ B C D ] — A.ended fires synchronously OTHER STRATEGIES lowest-priority quietest none — reject new
Slots are bounded. When full, the steal strategy picks a victim, synchronously, before play() returns.

Strategies

  • oldest kills the voice that started first. This is the default, and the right call for SFX rain.
  • lowest-priority kills the voice with the smallest priority. Use it when you have a hierarchy: a hero attack outranks a footstep, which outranks an ambience tick.
  • quietest kills the least audible voice. It ranks candidates by an existing meter tap where one exists and by the voice's own gain otherwise, so it costs nothing per candidate.
  • none rejects the new voice. The returned Voice is already ended.

API

At construction ts
createEngine({
  buses: {
    sfx: { concurrency: { max: 32, steal: 'oldest' } },
    voice: { concurrency: { max: 1, steal: 'lowest-priority' } },
  },
});
Live tuning ts
engine.bus('sfx').setConcurrency({ max: 8, steal: 'lowest-priority' });

Live demo

Change max and steal, then mash "Fire voice". Slots fill, and once you pass the cap you can watch which voice the strategy picks.

Recipes

Protect important voices

ts ts
// Hero hit — protected from stealing.
engine.sound('player-hit').play({ priority: 10 });

// Footstep — cheap, expendable.
engine.sound('footstep').play({ priority: 0 });

With steal: 'lowest-priority', the player-hit voice survives until it ends naturally or another priority-10 voice arrives.

Pitfalls

Don't use steal: 'none' for SFX.
Rejecting voices makes a player notice missing sounds in exactly the dense moments where they are paying attention. Stealing one is far less obvious. Reserve 'none' for a voice or alarm bus, where you want the announcement that is already playing to finish.
Don't set max too low for sprites.
A cascading match-3 can fire twelve or more one-shots in a single frame. At max: 4 that sounds chopped. Profile real gameplay before clamping.

Related

  • Voice carries the priority you set on play.
  • Bus is where the concurrency block lives.