Taking Web Audio Offline in iOS 6 Safari

by .

Playing cached audio for offline use on iOS Safari has long been a challenge that has proved to be mission impossible. But with the advent of the (WebKit-only) Web Audio API, it is now finally achievable — although you still need to jump through a few hoops.

The bad news is that you still can’t cache an MP3 file using Application Cache and then simply load it using an XmlHttpRequest. Safari on iOS 6 will cache the MP3 but then refuse to play it and fail silently (how useful!).

But all is not lost…

Base64 to the Rescue

Because the Web Audio API offers developers direct control over the AudioBuffer, you can now convert data formats on-the-fly and feed them directly to the Web Audio API for playback. For example, if you encode an MP3 file as a Base64 string, you can then decode it to an ArrayBuffer and convert the raw audio data.

Encoding an Audio File

You can easily convert an MP3 file to a Base64 string using OpenSSL. If you are on Mac OS X, it comes pre-installed, so just open up Terminal.app and type the following command:

openssl base64 -in [infile] -out [outfile]

Make sure to replace [infile] with the path to your MP3 and [outfile] with your chosen destination for the encoded data.

This will output a Base64-encoded string representation of your sound file. You can then cache the string using any form of web storage you choose (e.g., Application Cache, localStorage, or webSQL).

Base64 to ArrayBuffer

In order to decode the Base64 string back into an ArrayBuffer, you’ll need to use a custom method. Check out Daniele Guerrero’s base64-binary.js as a good script that can be used exactly for this purpose. It decodes a Base64 string into a Uint8Array typed array and stores it in an ArrayBuffer.

Once this is done, you can simply decode the audio data using the Web Audio API’s decodeAudioData() method:

var buff = Base64Binary.decodeArrayBuffer(sound);

myAudioContext.decodeAudioData(buff, function(audioData) {
  myBuffer = audioData;
});

Once you have the audio data decoded, pass it to your audio buffer source and play the sound:

mySource = myAudioContext.createBufferSource();
mySource.buffer = myBuffer;
mySource.connect(myAudioContext.destination);
mySource.noteOn(0);

Full Demo and Source Code

Check out the online demo and source code for a complete example of the techniques discussed in this article.

Browser Support

Currently the demo works in Safari 6, Chrome Desktop, and iOS 6 Safari. The technique has potential to work in any browser that supports Web Audio API, so hopefully Chrome Mobile can soon add support as well.

The W3C is currently pursuing the Web Audio API as a standard.

12 Responses on the article “Taking Web Audio Offline in iOS 6 Safari”

  • Peaches says:

    Very impressed! However, I cannot get the sound to play via javascript, only via your button click. It works fine on Safari desktop, but not on iOS6…

    For example, I added:

    <script type="text/javascript">
    var t = setTimeout(play(),1000);
    </script>

    to your example code after the <body> tag, but it still only plays the sound when the button gets clicked. And not programatically. Why?

  • Alex Gibson says:

    Yes, this is a restriction on Web Audio using iOS Safari. Sound can only be first initiated via a direct user action (e.g. a button click). This is not related to audio being offline.

  • Peaches says:

    Great, you’ve just solved my problem. Thanks! I had spent a few hours banging my head against the wall on this one…

    I have a button that sends an ajax request, and the confirmation sound needed to play on a successful response. I just put a false sound on the button click:

    onclick="mySource.noteOn(1);mySource.noteOff();"

    and now can play the sound via javascript in my response function.

    Thanks for your help!

  • Adrian says:

    I was working on an all purpose in browser audio player recently, and I did something similar with the fileReader api. Using a file input box , I passed the data url into an audio element as source and it played just fine.
    OpenSSL to do the encoding never occurred to me, great idea.

    Here’s a demo.

    http://fumatica.com/fumatic.html

    By the way, I love Newcastle.
    I spent a bunch of time over there
    with a gorgeous Geordie girl.

    Adrian

  • Alex Gibson says:

    Thanks for sharing, Adrian

    Glad you like my home town :)

  • Johnny says:

    Hey, it does work perfectly under Safari, but I have trouble to encode my own mp3 file and make it work. openssl base64 encoding outputs is a several lines of fixed length, which is recognised by browser as EOF. How exactly did you encode mp3 file to base64?
    ps. copy pasting that base64 value and decoding this (back into binary file) also is not working for me. Am I missing something?

    cheers,
    Johnny

  • Alex Gibson says:

    Hi Jonny,

    I can’t see your example, but it sounds like you might have answered your own question? Just try using the base64 string on a single line in your CSS, as shown in the demo: https://github.com/alexgibson/offlinewebaudio/blob/master/sounds/ufo.js

  • Alex Gibson says:

    Excuse the typo, I meant to say in your JavaScript, not CSS obviously :)

  • Johnny says:

    Hi,
    thanks for reply.
    In a meanwhile I figured out what the problem was. After base64 encoding, you shall append ‘\n’ after every 64 chars.

  • kirilloid says:

    Decoding base64 is much simpler (and possibly faster) with built-in `atob` function + `charCodeAt`:
    function decodeBase64ToArrayBuffer(base64str) {
    var l = (base64str.length/4) * 3,
    s = atob(base64str),
    a = new Uint8Array(new ArrayBuffer(l)),
    i;
    for (i = 0; i < l; i++) a[i] = s.charCodeAt(i);
    return a;
    }

  • asquare says:

    Thank you! This article was super useful on a project about a year ago when I got the nasty surprise that audio wouldn’t play offline.

    Revisiting the project again this year (and this article), I wanted to use kirilloid’s suggestion above but was having issues in webkit with the returned value not being a true ArrayBuffer. For whatever reason declaring the Uint8Array and ArrayBuffer separately appears to work in Chrome and Mobile Safari.

    Something like the following is still nice and compact:


    function decodeBase64ToArrayBuffer(base64str) {
    var l = (base64str.length/4) * 3,
    s = atob(base64str),
    a = new ArrayBuffer(l),
    b = new Uint8Array(a),
    i;

    for (i = 0; i &lt l; i++) b[i] = s.charCodeAt(i);
    return a;
    }

  • Corey says:

    Great post! I think it’s worth mentioning that openssl needs the `-A` open to create a base64 encoding without line breaks. This way readers wont get confused when they try to wrap their encoding in quotes and run into EOF errors.

  • Join the discussion.

    Some HTML is ok

    You can use these tags:
    <a href="" title="">
    <abbr title="">
    <b>
    <blockquote cite="">
    <cite>
    <del datetime="">
    <em>
    <i>
    <q cite="">
    <strong>

    You can also use <code>, and remember to use &lt; and &gt; for brackets.