Allow to reject sound plays if maxInstances number is reached

Let’s say we have

const sound = await BABYLON.CreateSoundAsync("sound", "https://assets.babylonjs.com/sound/alarm-1.mp3", { maxInstances: 1 });
sound.play();

Now, if we play the second instance while the first one is still playing, the first one will stop and the second one will start.

I would like to have the setting that allows to just reject new instances if the maximum capacity is reached.

Why it’s important?
Imagine the cluster of 500 revenant’s homing missiles is chasing you and you decide to get rid of them by steering them into the wall. 500 explosions is playing simultaneously causing ears bleeding. With existing implementation explosion sounds will switch very fast, but still interfere with each other in very unpleasant way. Playthrough rejection seems better solution.

Supplemental
I actually did the rejection logic for the first version of audio engine. I would love to get rid of it, since now the BabylonJS native instance tracker is introduced.
But just for the reference, this is how I did it:

export default class AudioManager {
    constructor() {
        this.queue = [];
        this.collector = {};
        this.updateFunction = this.update.bind(this);
        this.stepId = 0;
        this.threshold = 15;
    }

    push(sound) {
        NodeMetadata.write(sound, METADATA_FIELDS.STEP_ID, this.stepId);
        this.queue.push(sound);
    }

    update() {
        while (this.queue.length > 0) {
            const sound = this.queue.shift();
            const incomingStepId = NodeMetadata.read(sound, METADATA_FIELDS.STEP_ID);
            if (!this.collector[sound.id] || incomingStepId - this.collector[sound.id] > this.threshold) {
                this.collector[sound.id] = incomingStepId;
                sound.play();
            }
        }
        this.stepId++;
    }
}

So, instead of mySound.play() you need to call myAudioManagerInstance.push(mySound).
Here are two important structures:

  1. this.queue contains sounds that were requested to play.
  2. this.collector contains sounds that were allowed to play.

When push method is called the sound is added to the queue. The specific stepId identificator will be added to the sound metadata, stepId is just a frame number.

update function is called on every frame. It gets the sound and checks if such sound already exists in the collector. If no, then just add and play; if yes, check the stepId difference between collector and queue instances of the sound. If it’s less or equal than threshold, reject the queue instance; if it’s bigger than threshold, then update info in the collector and play the sound.

This approach assumes that you always clone the sound, before calling the push, otherwise all entries will share the same metadata.

I think it’s pretty cannonical design pattern, but I don’t know if it has the cannonical name. “Rate-Limited Event Queue”, something like that.

1 Like

cc @docEdub

This is a good suggestion, thanks! There are many ways to prioritize sounds, though, so I want to think more about the best way to handle this so it fits well with future plans. For now I can expose the current number of playing instances as a property so you can know whether to play a sound or not if that’s what you want to do.

PR 17368 adds an activeInstancesCount property to expose the current number of playing instances. You can use this new property to skip playing a sound if a lot of instances are already playing.

2 Likes