Audio Engine

CreateAudioEngineAsync and the New Audio Graph

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:

An audio engine, a bus for ambience, and a gesture to unlock themJavaScript
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.

The Audio Engine v2 graph drawn in 3D: two sounds into an ambience bus, into the main bus, into the speakersHTMLLive
<!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>