How to auto-play audio in Safari with JavaScript

Curtis Robinson
4 min readJul 22, 2018

Playing audio files in JavaScript is easy until it doesn’t work. Safari will refuse to play any audio until the user has tapped something that triggers audio. Chrome is now doing this also. I will try to keep this article up to date as browsers change. (Updated July 2019)

Mobile Safari doesn’t even starting loading any of the audio files until you want to play one from a user tap. This causes a lag on the first time the user decides to play a sound. It only loads the metadata (readyState is stuck at 1).

The (hack) solution used to be to play a short blank audio file on the first user tap. Then you could play whatever you want whenever you want. Now Safari has gotten stricter — each sound file is now locked individually until it has been specifically played during a user tap.

The solution: Play every single audio file at the same time on the first user tap! This will cause an explosion of sound into the user’s unsuspecting earholes. Actually, you can just stop each audio immediately when you first play it. The user won’t hear anything.

You basically need to do this on the first touchstart:

document.body.addEventListener('touchstart', function() { if(audiosWeWantToUnlock) {  for(let audio of audiosWeWantToUnlock) {
audio.play()
audio.pause()
audio.currentTime = 0
} audiosWeWantToUnlock = null}}, false)//where earlier you did:
var audiosWeWantToUnlock = []
audiosWeWantToUnlock.push(new Audio('mySoundEffect.wav'))
audiosWeWantToUnlock.push(new Audio('myOtherSoundEffect.wav'))

Then you’ve “unlocked” each audio object, so you can play them whenever you want! Even if the first tap is actually the user scrolling the page, it works.

Try it out: https://curtastic.com/testaudio.html

Want your own blank wav file to play to check if audio is unlocked, or to unlock the AudioContext? You can download this one https://touchbasic.app/nothing.wav

How to check? Try playing an audio on pageload.

var nothing = new Audio('https://touchbasic.app/nothing.wav')window.onload = function() {nothing.play().then(function() {
alert('Audio started unlocked!')
}).catch(function(){
alert('Audio started locked')
})
}

If you’re making a real-time JavaScript game you shouldn’t be using Audio() objects at all, because it causes game lag on each audio.play() Instead use AudioContext commands. Rather than Audio objects, you’ll create AudioBuffer objects, which are actually easier to unlock also. Just play any one of them during the first tap then all the rest are unlocked.

It’s more complicated to make and play AudioBuffer objects instead of Audio objects. Here’s some example code: (or you can google it)

// Small game audio library I made
var audioContext = new (window.AudioContext || window.webkitAudioContext)()
function loadSound(filename) {
var sound = {volume: 1, audioBuffer: null}

var ajax = new XMLHttpRequest()
ajax.open("GET", filename, true)
ajax.responseType = "arraybuffer"
ajax.onload = function()
{
audioContext.decodeAudioData
(
ajax.response,
function(buffer) {
sound.audioBuffer = buffer
},
function(error) {
debugger
}
)
}

ajax.onerror = function() {
debugger
}

ajax.send()

return sound
}
function playSound(sound) {
if(!sound.audioBuffer)
return false

var source = audioContext.createBufferSource()
if(!source)
return false

source.buffer = sound.audioBuffer
if(!source.start)
source.start = source.noteOn

if(!source.start)
return false

var gainNode = audioContext.createGain()
gainNode.gain.value = sound.volume
source.connect(gainNode)
gainNode.connect(audioContext.destination)

source.start(0)

sound.gainNode = gainNode
return true
}
function stopSound(sound) {
if(sound.gainNode)
sound.gainNode.gain.value = 0
}
function setSoundVolume(sound, volume) {
sound.volume = volume

if(sound.gainNode)
sound.gainNode.gain.value = volume
}
// How to use:
var mySound = loadSound("myFile.wav")
// Then later after audio is unlocked and the sound is loaded:
playSound(mySound)
// How to unlock all sounds:
var emptySound = loadSound("https://touchbasic.app/nothing.wav")
document.body.addEventListener('touchstart', function(){playSound(emptySound)}, false)

Test page full source code: (for non-game audio)

<html>
<body style=’font-size:40px’>
<div id=lockeddiv style=’font-style:italic’></div>
<div id=messagediv style=’font-style:italic’>Tap for music.</div>
<script>
var music = new Audio(“https://curtastic.com/nightmare.mp3")
var chime = new Audio(“https://curtastic.com/gold.wav")
var nothing = new Audio(“https://touchbasic.app/nothing.wav")
var allAudio = []
allAudio.push(music)
allAudio.push(chime)

var tapped = function() {
messagediv.innerHTML = “tapped”

// Play all audio files on the first tap and stop them immediately.
if(allAudio) {
for(var audio of allAudio) {
audio.play()
audio.pause()
audio.currentTime = 0
}
allAudio = null
}

// We should be able to play music delayed now (even when it’s not during the tap event).
messagediv.innerHTML = “Music starts in 2 seconds…”
setTimeout(function() {
messagediv.innerHTML = “Music playing. <button onclick=’stop()’>Stop</button>”
music.play()
}, 2000)

}
document.body.addEventListener(‘touchstart’, tapped, false)
document.body.addEventListener(‘click’, tapped, false)

var stop = function() {
music.pause()
loop = null
document.body.removeEventListener(‘touchstart’, tapped, false)
document.body.removeEventListener(‘click’, tapped, false)
}

// Check if audio starts already unlocked by playing a blank wav.
nothing.play().then(function() {
lockeddiv.innerHTML = “Audio started unlocked!”
}).catch(function(){
lockeddiv.innerHTML = “Audio started locked :(“
})

var loop = function() {
// Try to play chimes whenever we want (not during user action).
if(Math.random() < .01) {
chime.play().then(function(){
lockeddiv.innerHTML = “Audio is now unlocked!”
})
}
setTimeout(loop, 16)
}
loop()
</script>
<div style=’height:70vh;background:#EFE’>
The rest of the webpage takes up all of this space.
On Safari audio will start on the first tap anywhere.
On Chrome it will try to determine if the user has interacted much with this page or this domain, before playing.
</div>
</body>
</html>

--

--

Curtis Robinson

Game developer of Tower Swap. “The best hit in recent TDs”