CreateSoundAsync(name, source, options) makes a StaticSound, decoded whole into memory: it starts instantly, loops exactly, changes playbackRate and pitch, and overlaps instances (maxInstances caps them). source is a URL, an array of URLs (the first codec the browser supports wins), a shared sound buffer, or an AudioBuffer, like this computed door chime:
const rate = 48000; // samples per second
const samples = new Float32Array(rate).map((_, i) => // one second of a fading 880 Hz
Math.sin(2 * Math.PI * 880 * i / rate) * Math.exp(-4 * i / rate));
const chimeBuffer = new AudioBuffer({ length: rate, sampleRate: rate });
chimeBuffer.copyToChannel(samples, 0);
const chime = await BABYLON.CreateSoundAsync('chime', chimeBuffer, { volume: 0.5 });
const narration = await BABYLON.CreateStreamingSoundAsync('narration',
'https://assets.babylonjs.com/sound/testing/60-count.mp3');
chime.play(); // locked: dropped
console.log(`chime ${BABYLON.SoundState[chime.state]}, narration ` +
`${BABYLON.SoundState[narration.state]}`);chime Stopped, narration Stopped
A StreamingSound plays through an HTMLMediaElement, holding only a chunk in memory: right for music and narration minutes long, but with fewer options (no loopStart or loopEnd, no slice shorter than the file) and a delay while its buffer fills, which preloadCount hides. While the engine is locked, play() on a one-shot sound is dropped, as the Stopped state shows; a looping or autoplay sound waits and starts on unlock.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 260px; display: block; touch-action: none; outline: none; }
#bar { max-width: 600px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 6px 8px; } button { font: inherit; padding: 3px 10px; } #info { max-width: 600px; padding: 0 8px; font-family: monospace; font-size: 12px; }
</style>
<canvas id="view"></canvas>
<div id="bar"><button id="chime">Play chime (static)</button><button id="melody">Play melody (streaming)</button></div><div id="info"></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
/*
The book streams an mp3 from Babylon's asset server. This sandbox cannot fetch files, so the
streamed sound is a WAV built in memory and handed over as a blob: URL, which a media element
may play.
*/
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, Math.PI / 2, 6, BABYLON.Vector3.Zero(), scene);
const rate = 48000;
const chimeSamples = new Float32Array(rate).map((_, i) => // one second of a fading 880 Hz
Math.sin(2 * Math.PI * 880 * i / rate) * Math.exp(-4 * i / rate));
const NOTES = [523, 659, 784, 1047, 784, 659]; // four seconds of a little tune
const melodySamples = new Float32Array(rate * 4).map((_, i) => {
const t = i / rate, note = NOTES[Math.floor(t / (4 / NOTES.length))], local = t % (4 / NOTES.length);
return 0.5 * Math.sin(2 * Math.PI * note * t) * Math.exp(-3 * local);
});
// Draw both waveforms as line systems: the picture of what each sound holds
function waveform(samples, y, color, seconds) {
const points = [];
const step = Math.floor(samples.length / 600);
for (let i = 0; i < samples.length; i += step) points.push(new BABYLON.Vector3(-3 + 6 * i / samples.length, y + samples[i] * 0.7, 0));
const line = BABYLON.MeshBuilder.CreateLines('wave', { points }, scene);
line.color = BABYLON.Color3.FromHexString(color);
return line;
}
waveform(chimeSamples, 0.9, '#1f5f8b');
waveform(melodySamples, -0.9, '#b5452f');
// A WAV file in memory for the streaming sound (16-bit mono PCM)
function wavUrl(samples) {
const view = new DataView(new ArrayBuffer(44 + samples.length * 2));
const text = (offset, s) => [...s].forEach((c, i) => view.setUint8(offset + i, c.charCodeAt(0)));
text(0, 'RIFF'); view.setUint32(4, 36 + samples.length * 2, true); text(8, 'WAVEfmt ');
view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true);
view.setUint32(24, rate, true); view.setUint32(28, rate * 2, true); view.setUint16(32, 2, true);
view.setUint16(34, 16, true); text(36, 'data'); view.setUint32(40, samples.length * 2, true);
samples.forEach((s, i) => view.setInt16(44 + i * 2, Math.max(-1, Math.min(1, s)) * 32767, true));
return URL.createObjectURL(new Blob([view], { type: 'audio/wav' }));
}
const info = document.getElementById('info');
(async () => {
const audio = await BABYLON.CreateAudioEngineAsync({ disableDefaultUI: true });
const chimeBuffer = new AudioBuffer({ length: rate, sampleRate: rate });
chimeBuffer.copyToChannel(chimeSamples, 0);
const chime = await BABYLON.CreateSoundAsync('chime', chimeBuffer, { volume: 0.5 }); // decoded whole
const melody = await BABYLON.CreateStreamingSoundAsync('melody', wavUrl(melodySamples)); // a media element
const report = () => {
info.textContent = `engine ${audio.state}; chime ${BABYLON.SoundState[chime.state]}, ` +
`melody ${BABYLON.SoundState[melody.state]}`;
};
chime.play(); // locked: dropped, as the state shows
report();
for (const [id, sound] of [['chime', chime], ['melody', melody]]) {
document.getElementById(id).addEventListener('click', async () => {
await audio.unlockAsync(); // the click is the gesture
sound.play();
report();
setTimeout(report, 1200);
});
}
})().catch((error) => { info.textContent = `audio unavailable: ${error.message}`; });
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>