Reliable Web Audio Playback on Mobile Browsers
Audio activation, context management and scheduling on iOS and Android
The Web Audio API supports synthesis, audio processing and precisely scheduled playback directly in JavaScript. It is well suited to metronomes, ear-training exercises, synthesisers and other interactive music applications. However, code that produces audio reliably on desktop browsers can be silent on mobile devices, even when the application appears to be running correctly.
The main implementation concerns are browser autoplay policies, the AudioContext lifecycle and the relationship between audio activation and user interaction.
1. Audio activation requires user interaction
Mobile browsers generally prevent a page from starting audible playback without a user gesture. An AudioContext created during page load may therefore begin in the suspended state.
const context = new AudioContext();
console.log(context.state); // May be "suspended"Audio activation should be initiated directly from a click or touch-driven control. The simplest implementation resumes the context before scheduling playback:
const context = new AudioContext();
playButton.addEventListener('click', async () => {
await context.resume();
startPlayback(context);
});This is the standard starting point. It is not, however, an end-to-end guarantee of audible output. A context can report running while an application still has a problem with its source nodes, signal routing, timing or platform-specific activation behaviour.
2. Start activation before unrelated asynchronous work
A common mistake is to perform asynchronous setup before requesting audio activation. Browser permissions may depend on a short-lived user-activation state, and an asynchronous operation can separate the request from the original gesture.
playButton.addEventListener('click', async () => {
await loadSamples();
await initialiseInterface();
await context.resume();
startPlayback(context);
});Instead, initiate the activation request immediately and complete other work afterwards. Ideally, load and decode samples before the user presses Play.
playButton.addEventListener('click', async () => {
const activation = context.resume();
await activation;
await loadSamples();
startPlayback(context);
});3. Explicitly initialise the audio output when necessary
Some applications benefit from starting a short audio source during the playback gesture, in addition to resuming the context. A silent oscillator is one possible compatibility technique:
function primeWebAudio(context) {
const oscillator = context.createOscillator();
const gain = context.createGain();
gain.gain.value = 0;
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start();
oscillator.stop(context.currentTime + 0.01);
}This technique is not required by the Web Audio specification and is not universally reliable. It should be treated as a tested workaround for particular browser behaviour, rather than as a substitute for correct audio-context management.
Another approach, useful when diagnosing mobile playback failures, is to compare a Web Audio oscillator with playback through an HTML <audio> element. In some configurations, starting a valid, effectively silent audio element during the same user gesture can help establish a working playback path. If used, that behaviour should be encapsulated and tested on target devices, rather than assumed to work everywhere.
4. Reuse the AudioContext
An audio context should normally be created once and reused for subsequent playback. Stopping a metronome or an exercise does not require closing the context.
let context;
function getAudioContext() {
if (!context || context.state === 'closed') {
const AudioContextClass =
window.AudioContext || window.webkitAudioContext;
context = new AudioContextClass();
}
return context;
}
function stopPlayback() {
cancelScheduledEvents();
updateTransport();
// Leave the context available for the next Play.
}Repeatedly closing and recreating contexts introduces more activation transitions and makes recovery from mobile audio interruptions more complicated.
5. Handle suspension and interruption
Mobile devices can interrupt audio when the browser is backgrounded, the screen is locked, an audio route changes or another application takes control of playback. The application should observe the context state and provide a clear recovery path.
context.addEventListener('statechange', () => {
console.log('Audio context:', context.state);
});
playButton.addEventListener('click', async () => {
const context = getAudioContext();
await context.resume();
startPlayback(context);
});Automatic resumption may be possible, but a further user gesture can be required. For timing-sensitive applications, it is often better to pause the musical transport during an interruption and resynchronise when playback resumes.
6. Keep musical timing separate from JavaScript timers
Once audio is active, precise scheduling should use AudioContext.currentTime, not the execution timing of setInterval() or setTimeout().
function scheduleTone(context, frequency, at) {
const oscillator = context.createOscillator();
const gain = context.createGain();
oscillator.frequency.value = frequency;
gain.gain.setValueAtTime(0.3, at);
gain.gain.exponentialRampToValueAtTime(0.001, at + 0.1);
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start(at);
oscillator.stop(at + 0.1);
}A lookahead scheduler can use a JavaScript timer to submit future events, while the audio engine determines when those events actually sound. The user interface should follow the musical clock rather than control it.
7. Web Audio does not require pre-generated WAV files
Generating WAV files can be appropriate for recorded sounds or complex sample-based instruments. It is not generally necessary simply to support mobile browsers.
Web Audio can generate oscillators, play decoded samples, combine simultaneous notes, schedule melodies and rhythms, and apply effects. For algorithmic music exercises, retaining notes and durations as structured musical data is usually more flexible than rendering a separate file for every possible combination.
An interval can be represented as two pitches; a chord as a set of pitches; and a rhythm as a sequence of timed events. The audio engine can render these structures directly.
8. Cross-platform testing
The same Web Audio architecture can support iOS Safari, Android Chrome and modern desktop browsers. However, mobile playback should be tested on real devices, particularly for:
- First playback after page load.
- Stopping and restarting playback.
- Switching tabs or backgrounding the browser.
- Screen locking and device interruptions.
- Bluetooth and wired audio output.
- Recovery after an interrupted audio session.
Bluetooth latency is distinct from scheduling accuracy: audio events can remain evenly spaced while arriving noticeably later than the visual interface.
9. Use a shared audio layer
For a site containing several interactive music applications, context creation, activation, interruption handling and scheduling are better implemented in a common module than repeated independently in every exercise.
This keeps the application-specific code focused on musical concepts while centralising browser compatibility and testing.
Conclusion
Reliable mobile audio requires explicit activation within a user gesture, careful management of the audio context lifecycle and separation of audio scheduling from interface rendering. Where playback fails, test the audio output path independently before replacing dynamic synthesis with generated files.
Audio activation, musical scheduling and visual presentation should be separate responsibilities. This provides a maintainable foundation for interactive music applications across desktop and mobile browsers.