<h1>Intro</h1>
<p>With HTML5 it's pretty straight-forward to play a sound from a javascript function. However, iOS does not always allow that. Safari on iOS only plays sounds from functions that are directly called from user interactions, like a button click. The solution to that is using the web audio API. This is still not straight-forward though. In order to play a sound programmatically on iOS Safari, the so-called audio context needs to be unlocked first. That unlocking has to be done, again, by playing a sound as a reaction to user interaction. Luckily, the audio context can be unlocked once and then forever be used to play sounds. So any touch event on the screen can be used for that. Or, as in this example, by pressing the unmute button. If you don't see an unmute button: you're lucky. You're using a browser that doesn't require unlocking!</p>
<h1>Demo</h1>
<p>If you are on iOS, then you should not hear any sounds playing yet and initially there should be an unmute button here:</p>
<button id="unmute">Unmute</button>
<p>If it is hidden, the audio context is unlocked and a sound is playing.</p>
<p>Manually starting a sound to play should always work in all browsers with web audio:</p>
<button id="play">Play</button>
body{
    font-family: sans-serif;
    line-height: 1.6;
}

button{
    padding: 10px 20px;
    border-radius: 20px;
}

  (function () {

    // Check if the browser supports web audio. Safari wants a prefix.
    if ('AudioContext' in window || 'webkitAudioContext' in window) {

      //////////////////////////////////////////////////
      // Here's the part for just playing an audio file.
      //////////////////////////////////////////////////
      var play = function play(audioBuffer) {
        var source = context.createBufferSource();
        source.buffer = audioBuffer;
        source.connect(context.destination);
        source.start();
      };

      var URL = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/Yodel_Sound_Effect.mp3';
      var AudioContext = window.AudioContext || window.webkitAudioContext;
      var context = new AudioContext(); // Make it crossbrowser
      var gainNode = context.createGain();
      gainNode.gain.value = 1; // set volume to 100%
      var playButton = document.querySelector('#play');
      var yodelBuffer = void 0;

      // The Promise-based syntax for BaseAudioContext.decodeAudioData() is not supported in Safari(Webkit).
      window.fetch(URL)
        .then(response => response.arrayBuffer())
        .then(arrayBuffer => context.decodeAudioData(arrayBuffer,
           audioBuffer => {
              yodelBuffer = audioBuffer;
            },
            error =>
              console.error(error)
          ))

      playButton.onclick = function () {
        return play(yodelBuffer);
      };

      // Play the file every 2 seconds. You won't hear it in iOS until the audio context is unlocked.
      window.setInterval(function(){
        play(yodelBuffer);
      }, 5000);


      //////////////////////////////////////////////////
      // Here's the part for unlocking the audio context, probably for iOS only
      //////////////////////////////////////////////////

      // From https://paulbakaus.com/tutorials/html5/web-audio-on-ios/
      // "The only way to unmute the Web Audio context is to call noteOn() right after a user interaction. This can be a click or any of the touch events (AFAIK – I only tested click and touchstart)."
      
      var unmute = document.getElementById('unmute');
      unmute.addEventListener('click', unlock);

      function unlock() {
        console.log("unlocking")
        // create empty buffer and play it
        var buffer = context.createBuffer(1, 1, 22050);
        var source = context.createBufferSource();
        source.buffer = buffer;
        source.connect(context.destination);

        // play the file. noteOn is the older version of start()
        source.start ? source.start(0) : source.noteOn(0);

        // by checking the play state after some time, we know if we're really unlocked
        setTimeout(function() {
          if((source.playbackState === source.PLAYING_STATE || source.playbackState === source.FINISHED_STATE)) {
            // Hide the unmute button if the context is unlocked.
            unmute.style.display = "none";
          }
        }, 0);
      }

      // Try to unlock, so the unmute is hidden when not necessary (in most browsers).
      unlock();
    }
  }
)();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.