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.
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.
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 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.
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
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
The doc link @labris posted below is also a good solution
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.
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.
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.
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 = []
}
}
For what it is worth I have been struggling with Audio stream (MediaStream) coming from a webrtc communication. With all your remarks and picking info a little bit around other posts, I endeded up with making it work with additionnal actions:
storing BabylonJs ‘Sound’ in a static attribute of my class
unlocking AudioEngine if Needed
using the callback “readyToPlayCallBack” and trapping an exception around the “play”
(which I have not been able to understand)
only using the “stream” option
This code works on Chrome 125.0.6422.142 and on latest chromimum on Quest 2
here is the code to play the MediaStream in my class “ThreeDHandMenuController”:
//this is necessary for sound to work
if (!Engine.audioEngine.unlocked) {
Engine.audioEngine.unlock();
}
ThreeDHandMenuController.m_soundPlayer = new Sound(
'peer_audio',
this.m_peerStream,
this.m_scene,
function () {
try { //we have to trap error otherwise sound does not play
// Sound has been downloaded & decoded
ThreeDHandMenuController.m_soundPlayer.play();
console.log('[Comm] peer audio playing');
} catch (e) {
console.error('[Comm] error when playing remote peer sound: ' + e); //this error is raised but sound plays
}
},
{ streaming: true},
);