Audio
Playing audio files in your nx.js application
nx.js provides two complementary ways to play audio:
- The
Audioelement — a simple way to play back an audio file, implementing a subset of the webHTMLAudioElementAPI. Covered in the first half of this page. - The Web Audio API — a node-graph engine for low-latency playback, synthesis, mixing, panning, and real-time processing. Covered in the second half.
Both are backed by the same FFmpeg decoder, so they support the same set of file formats.
Supported Formats
Audio decoding is backed by FFmpeg, so essentially every common audio format is supported, including:
| Format | MIME Type |
|---|---|
| MP3 | audio/mpeg |
| WAV | audio/wav |
| OGG Vorbis | audio/ogg |
| Opus | audio/opus |
| FLAC | audio/flac |
| AAC / M4A | audio/aac |
Note
The same FFmpeg pipeline backs the Web Audio API's
decodeAudioData() and the Video element.
Basic Playback
Create an Audio instance and wait for the audio data to load before playing:
const audio = new Audio('romfs:/music.mp3');
audio.addEventListener('canplaythrough', () => {
audio.play();
});Important
Audio data is loaded and decoded asynchronously. Always wait for the
canplaythrough event (or loadedmetadata) before calling play().
Loading Audio
The src property accepts any URL supported by fetch(),
including romfs:/ paths for bundled assets and http:// / https:// URLs:
// From the ROM filesystem
const sfx = new Audio('romfs:/sounds/jump.wav');
// From the SD card
const music = new Audio('sdmc:/switch/my-app/soundtrack.ogg');
// From a remote server
const stream = new Audio('https://example.com/audio.mp3');Setting src automatically calls load(), which fetches and decodes the audio data.
Playback Controls
Play and Pause
await audio.play();
audio.pause();Volume
Volume ranges from 0 (silent) to 1 (full volume):
audio.volume = 0.5; // 50% volumeLooping
audio.loop = true;
audio.play();Seeking
Use currentTime to seek to a specific position (in seconds):
audio.currentTime = 30; // Jump to 30 seconds
console.log(audio.duration); // Total duration in secondsPlayback Rate
Adjust the playback speed:
audio.playbackRate = 1.5; // 1.5x speed
audio.playbackRate = 0.5; // Half speedEvents
The Audio class dispatches standard media events:
| Event | Description |
|---|---|
loadedmetadata | Audio metadata has been loaded and decoded |
canplaythrough | Enough data is available to begin playback |
play | Playback has started |
pause | Playback has been paused |
ended | Playback has reached the end (when not looping) |
timeupdate | The current playback position has changed |
error | An error occurred during loading or decoding |
const audio = new Audio('romfs:/music.mp3');
audio.onended = () => {
console.log('Track finished!');
};
audio.onerror = (e) => {
console.error('Audio error:', e.error);
};
audio.addEventListener('canplaythrough', () => {
audio.play();
});Ready States
You can check readyState to determine the loading status:
| Constant | Value | Meaning |
|---|---|---|
Audio.HAVE_NOTHING | 0 | No data loaded |
Audio.HAVE_METADATA | 1 | Metadata decoded |
Audio.HAVE_ENOUGH_DATA | 4 | Ready to play |
Example: Background Music with Sound Effects
import { Button } from '@nx.js/constants';
// Background music (looping)
const bgm = new Audio('romfs:/music/background.mp3');
bgm.loop = true;
bgm.volume = 0.3;
// Sound effect
const sfx = new Audio('romfs:/sounds/select.wav');
bgm.addEventListener('canplaythrough', () => {
bgm.play();
});
function update() {
requestAnimationFrame(update);
const [pad] = navigator.getGamepads();
if (pad?.buttons[Button.A].pressed) {
// Play sound effect (reload to restart if already playing)
sfx.load();
sfx.addEventListener('canplaythrough', () => sfx.play(), { once: true });
}
}
update();Tip
For sound effects that need to be played rapidly or overlapping, create
multiple Audio instances from the same source.
Web Audio API
For low-latency playback, synthesis, mixing, and real-time processing, nx.js implements the Web Audio API, which builds an audio processing graph out of connected nodes.
Audio is processed by a native graph engine running on a dedicated real-time
render thread (in 128-frame quanta, like browsers), and streamed to the Switch's
audren audio service.
Implemented Interfaces
| Interface | Description |
|---|---|
AudioContext | The main real-time audio graph |
OfflineAudioContext | Renders a graph to an AudioBuffer faster than real-time |
AudioBuffer | In-memory PCM audio data |
AudioBufferSourceNode | Plays back an AudioBuffer |
GainNode | Volume control |
StereoPannerNode | Left/right stereo panning |
AudioParam | Automatable parameter (with scheduling) |
Playing an AudioBuffer
Decode an audio file with decodeAudioData(),
then play it through an AudioBufferSourceNode:
const ctx = new AudioContext();
const data = await fetch('romfs:/sound.mp3').then((r) => r.arrayBuffer());
const buffer = await ctx.decodeAudioData(data);
const source = ctx.createBufferSource();
source.buffer = buffer;
source.connect(ctx.destination);
source.start();Synthesizing audio
You don't need any asset files — you can fill an AudioBuffer with samples you
compute yourself. Here's a simple sine-wave beep:
const ctx = new AudioContext();
// 0.5 seconds of a 440 Hz sine wave
const length = Math.floor(0.5 * ctx.sampleRate);
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < length; i++) {
const t = i / ctx.sampleRate;
data[i] = Math.sin(2 * Math.PI * 440 * t) * 0.3;
}
const source = ctx.createBufferSource();
source.buffer = buffer;
source.connect(ctx.destination);
source.start();Building a graph
Nodes can be chained with connect() to build a processing graph. Here a source
is routed through a gain (volume) node and a stereo panner before reaching the
destination:
const ctx = new AudioContext();
const gain = ctx.createGain();
gain.gain.value = 0.5; // 50% volume
const panner = ctx.createStereoPanner();
panner.pan.value = -1; // hard left
source.connect(gain).connect(panner).connect(ctx.destination);
source.start();Parameter automation
AudioParam values can
be scheduled to change over time — for example, a fade-out:
const gain = ctx.createGain();
gain.connect(ctx.destination);
// Fade from full volume to silence over 1 second
gain.gain.setValueAtTime(1, ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, ctx.currentTime + 1);See the apps/web-audio
example for a complete demo that synthesizes sound effects and music
procedurally.