Everything is created by async functions. CreateAudioEngineAsync() wraps an AudioContext; sounds feed buses, buses may feed other buses (a cycle throws), and a MainAudioBus, the engine's defaultMainBus unless you create more, feeds the speakers. Browsers keep audio suspended until the user interacts, so the engine shows an unmute button and resumes on the first click, or you call unlockAsync() from your own gesture:
const audio = await BABYLON.CreateAudioEngineAsync({ disableDefaultUI: true }); // no button
const ambience = await BABYLON.CreateAudioBusAsync('ambience', { volume: 0.4 });
console.log(`engine ${audio.state}, ambience -> ${ambience.outBus.name}`);
view.addEventListener('pointerdown', () => audio.unlockAsync(), { once: true });Output
engine suspended, ambience -> default
The bus feeds the main bus, named default. Buses give a group of sounds one volume, pan or position; sounds, buses and the engine all have volume and a setVolume(value, { duration, shape }) ramp for fades.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 300px; display: block; touch-action: none; outline: none; }
#bar { max-width: 600px; display: flex; gap: 10px; align-items: center; padding: 6px 8px; } button { font: inherit; padding: 3px 10px; } #info { font-family: monospace; font-size: 12px; }
</style>
<canvas id="view"></canvas>
<div id="bar"><button id="unlock">Click to unlock audio</button><span id="info"></span></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.js"></script>
<script>
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.2, 9, new BABYLON.Vector3(0, 0.6, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.5), scene);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
// Boxes for the graph's nodes, tubes for its connections
const node = (name, x, y, hex) => {
const box = BABYLON.MeshBuilder.CreateBox(name, { width: 1.6, height: 0.6, depth: 0.5 }, scene);
box.position.set(x, y, 0);
box.material = new BABYLON.StandardMaterial(name, scene);
box.material.diffuseColor = BABYLON.Color3.FromHexString(hex);
const label = new BABYLON.GUI.TextBlock('', name);
Object.assign(label, { color: 'white', fontSize: 12, fontWeight: 'bold', resizeToFit: true, outlineWidth: 2, outlineColor: '#333' });
ui.addControl(label);
label.linkWithMesh(box);
return box;
};
const link = (a, b) => {
const path = [a.position.add(new BABYLON.Vector3(0.8, 0, 0)), b.position.add(new BABYLON.Vector3(-0.8, 0, 0))];
BABYLON.MeshBuilder.CreateTube('link', { path, radius: 0.04 }, scene);
return path;
};
const chimeNode = node('chime sound', -3.3, 1.2, '#1f5f8b');
const clockNode = node('clock sound', -3.3, 0, '#5b3f99');
const busNode = node('ambience bus', -0.9, 0.6, '#3f7d3a');
const mainNode = node('main bus "default"', 1.5, 0.6, '#e09a10');
const outNode = node('speakers', 3.6, 0.6, '#b5452f');
const paths = [link(chimeNode, busNode), link(clockNode, busNode), link(busNode, mainNode), link(mainNode, outNode)];
const pulses = paths.map(() => { // dots that travel while audio runs
const dot = BABYLON.MeshBuilder.CreateSphere('pulse', { diameter: 0.16 }, scene);
dot.material = new BABYLON.StandardMaterial('pulseMat', scene);
dot.material.emissiveColor = BABYLON.Color3.FromHexString('#ffe082');
dot.isVisible = false;
return dot;
});
const info = document.getElementById('info');
let audio = null;
(async () => {
audio = await BABYLON.CreateAudioEngineAsync({ disableDefaultUI: true }); // no unmute button
const ambience = await BABYLON.CreateAudioBusAsync('ambience', { volume: 0.4 }); // feeds the main bus
info.textContent = `engine ${audio.state}, ambience -> ${ambience.outBus.name}`;
// Browsers keep audio suspended until a gesture: unlock from a click
document.getElementById('unlock').addEventListener('click', async () => {
await audio.unlockAsync();
const rate = 48000; // a short computed chime through the bus
const samples = new Float32Array(rate).map((_, i) => Math.sin(2 * Math.PI * 880 * i / rate) * Math.exp(-4 * i / rate));
const buffer = new AudioBuffer({ length: rate, sampleRate: rate });
buffer.copyToChannel(samples, 0);
const chime = await BABYLON.CreateSoundAsync('chime', buffer, { outBus: ambience });
chime.play();
info.textContent = `engine ${audio.state}, ambience -> ${ambience.outBus.name}`;
});
})().catch((error) => { info.textContent = `audio unavailable: ${error.message}`; });
scene.onBeforeRenderObservable.add(() => {
const running = audio?.state === 'running';
const t = (performance.now() / 900) % 1;
pulses.forEach((dot, i) => {
dot.isVisible = running;
dot.position = BABYLON.Vector3.Lerp(paths[i][0], paths[i][1], t);
});
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>