Issue with Unmuting and Playing Sound in Browser

Hello,

I am encountering an issue with the unmute icon in my application. As shown in the attached image, when I attempt to play a sound, this icon appears. Despite clicking on it, the sound does not play.

no sound

My application relies on detecting the end of the sound playback to progress to the next phase. Unfortunately, since the sound never starts, the event signaling the end of the playback does not trigger. Currently, I do not have a fallback mechanism other than potentially building a timer-based solution myself.

Could you provide any insights or suggestions on how to resolve this issue? I suspect it may be related to browser restrictions on autoplaying sound.

Thank you for your assistance.

Best regards,

Cc @docEdub

Yes, when the browser restricts audio playback, attempting to play a Babylon Sound will make the mute button appear. This is expected. The sound should start when the unmute button is pressed, though. Can you share some code, maybe in a playground, that reproduces the issue?

One question, why would the browser restrict audio playback?

Really? :slight_smile:
You didn’t experience the era of loud ads/videos starting automatically after the web page was loaded? :hear_no_evil:

1 Like

One solution is to create an intro page which, you know that Press any button to start kind. Audio can be started upon user interaction so you can safely start the audio when the user clicks somewhere or presses a button on the keyboard.

1 Like

BTW, I was able to reproduce the issue using the usual sample from the documentation, which can be found here: https://playground.babylonjs.com/#PCY1J#1.

Also, I have attached a video that shows the behavior. When it is loading, the icon appears, but when clicked, the sound does not play (you can see this because there is no tiny icon on the panel header). However, when I run the code again (not reload), then the sound plays (you may see the small icon onto the header).

What I expected was for the sound to play but be muted. I believe this may be due to a misconfiguration on my machine, but it behaves the same in both Edge and Chrome.

Audio unlocking is a bit scuffed sometimes from my experience.
And it can be even more confusing in the playground!
Since audio has to be unlocked once per page session, it randomly isn’t working and then you click Run code button again and it works :joy:

So i think what is happening is music.play() is called, the audio has not been unlocked, so it fails to play, you unlock the audio, and now you have to re-run the code for the audio to work!

Here we tell the audio to autoplay and it should play once the audio is unlocked!

Alternatively listen for the audio to be unlocked

I would suggest checking and handling the unlocking yourself before you try to play any sounds though.
BABYLON.Engine.audioEngine has all the properties you need :slight_smile:

The doc link @labris posted below is also a good solution

2 Likes

Example from the docs should work - Babylon.js docs

1 Like

Thank’s, Already tried, but not solving. Monitoring the unlocking is the key in my case.

I see. Yes, this is the intended behavior for sounds that do not loop and are not set to autoplay. The unmute button will not auto-start one-shot sounds. They must have their play function called after the audio engine is unlocked.

2 Likes

It would be nice if there was some way to have audio sounds that were intended to be played before the audio engine unlocks, to be caught up and played where they should be once it finally unlocks. As it stands any background music/ambience that starts before unlocking gets ignored even after the unlock.

Hey,

This was a need of mine, so I developed a quick and dirty solution to address it. Basically, if the engine is locked, the sound is registered as a batch and will replay at the correct time index once the engine is unlocked. It’s not rocket science, but it’s useful while we wait for the new audio engine. I did not find such a utility with a quick search in the framework, so maybe I reinvented the wheel, but it’s worth 15 minutes of coding.

Please note that this code is for a proof of concept only and has not been tested yet. :blush:
Edit - Add Observable call to propagate event at the end of the batch.

   export class PlayCommand {
        timestamp: number
        target: BABYLON.Sound
        time: number
        offset: number
        length: number

        public constructor(target: BABYLON.Sound, time?: number, offset?: number, length?: number) {
            this.timestamp = Date.now()
            this.target = target
            this.time = time ?? 0
            const duration = (target as any)._audioBuffer.duration as number
            this.offset = Math.min(offset ?? 0, duration)
            const remaining = duration - this.offset
            this.length = Math.min(length ?? remaining, remaining)
        }

        public get duration(): number {
            return this.time + this.offset + this.length
        }
   }

   export class BatchSoundManager {
        _batches: Array<PlayCommand> = []

        public constructor() {
            let engine = BABYLON.Engine.audioEngine
            if (!engine.unlocked) {
                engine.onAudioUnlockedObservable.addOnce(this._onAudioEngineUnlocked.bind(this))
            }
        }

        public play(target: BABYLON.Sound, time?: number, offset?: number, length?: number): void {
            if (BABYLON.Engine.audioEngine.unlocked) {
                target.play(time, offset, length)
                return
            }
            let command = new PlayCommand(target, time, offset, length)
            this._batches.push(command)
            const durationSeconds = command.duration
            const durationMillis = durationSeconds * 1000
            setTimeout(() => {
                target.onEndedObservable.notifyObservers(target)
                // remove the command from the list
                this._batches.splice(this._batches.indexOf(command), 1)
            }, durationMillis)
        }

        protected _onAudioEngineUnlocked(): void {
            const now = Date.now()
            this._batches.forEach((command) => {
                // play the sound at the right time
                const elapsed = now - command.timestamp

                // we are on delay
                if (elapsed > command.time) {
                    command.target.play(command.time - elapsed, command.offset, command.length)
                    return
                }

                // not reach the offset yet
                let start = command.time + command.offset
                if (elapsed < start) {
                    command.target.play(0, start - elapsed, command.length)
                    return
                }

                // we are in the middle of the playable sound
                command.target.play(0, elapsed - start, command.duration - elapsed)
            })
            this._batches = []
        }
   }
2 Likes

I’m thinking the same thing.

I think this case should be covered by the autoplay property?