No sound on iOS 6 Web Audio API

25,981

Solution 1

Edit (November 2015): iOS 9 no longer allows audio to start in a touchstart event, which breaks the solution below. However it works in a touchend event. The original answer for iOS 6 is left intact below, but for iOS 9 support make sure you use touchend.

Well, sorry to answer my own bounty question, but after hours of debugging I finally found the answer. Safari on iOS 6 effectively starts with the Web Audio API muted. It will not unmute until you attempt to play a sound in a user input event (create a buffer source, connect it to destination, and call noteOn()). After this, it unmutes and audio plays unrestricted and as it ought to. This is an undocumented aspect of how the Web Audio API works on iOS 6 (Apple's doc is here, hopefully they update it with a mention of this soon!)

The user can be touching the screen a lot, engaged in the game. But it will remain muted. You have to play inside a user input event like touchstart [edit: touchend for iOS 9+], once, then all audio unmutes. After that you can play audio at any time (doesn't have to be in a user input event).

Note this is different to the restrictions on HTML5 audio: typically you can only start audio at all in a user input event, and only play one sound at a time; the Web Audio API fully unmutes after the first play-in-user-input, so that you can play sounds at any time, and then you can mix them polyphonically, process cool effects, etc.

This means many games already on the web using the Web Audio API will never play audio, because they do not happen to issue a noteOn in a touch event. You have to adjust it to wait for the first user input event.

There are a few ways to work around this: don't play your title music until the user touches the screen; have an initial 'touch to enable audio' screen and play a sound then begin the game when they touch; etc. Hopefully this will help anyone else having the same problem save some time trying to debug it!

Solution 2

You can try to debug it using the Web Inspector on Safari 6 on a mac.

  1. Enable "Webkit Inspector" in Mobile Safari settings/advanced.
  2. Connect device to a Mac running Safari 6 using a USB cable.
  3. Load your page/game
  4. Go to menu Develop->[devicename]->[pageurl]

It doesn't work out of the box for me, but with a few tries it can help narrow down the problem.

Apparently there is also the thing that audio can only be triggered by a user action. I'm not sure this is true 'cos some code that works on iOS6 on iPhone4 doesn't play any sound on an iPad (also iOS6).

Update: Some success with web audio on iPhone4+iOS6. Found that the "currentTime" remains stuck at 0 for a while as soon as you create a new audio context on iOS6. In order to get it moving, you first need to perform a dummy API call (like createGainNode() and discard the result). Sounds play only when currentTime starts to run, but scheduling sounds exactly at currentTime doesn't seem to work. They need to be a little bit into the future (ex: 10ms). You can use the following createAudioContext function to wait until the context is ready to make noise. User action doesn't seem to be required on iPhone, but no such success on iPad just yet.

function createAudioContext(callback, errback) {
    var ac = new webkitAudioContext();
    ac.createGainNode(); // .. and discard it. This gets 
                         // the clock running at some point.

    var count = 0;

    function wait() {
        if (ac.currentTime === 0) {
            // Not ready yet.
            ++count;
            if (count > 600) {
                errback('timeout');
            } else {
                setTimeout(wait, 100);
            }
        } else {
            // Ready. Pass on the valid audio context.
            callback(ac); 
        }
    }

    wait();
}

Subsequently, when playing a note, don't call .noteOn(ac.currentTime), but do .noteOn(ac.currentTime + 0.01) instead.

Please don't ask me why you have to do all that. That's just the way it is at the moment - i.e. crazy.

Solution 3

I managed to figure out a simple solution which I'm sure must have been documented elsewhere - but sometimes we have to spend hours figuring these things out for ourselves...

So it seems many tutorials (such as this one on html5rocks) instruct you to do the following steps :

  • Create an instance of window.AudioContext and if that doesn't exist (which it doesn't on iOS) then create window.webkitAudioContext.

  • Create an XMLHttpRequest to load your sound file

  • On the load event run context.decodeAudioData(....) and then createBufferSource(), filling it with the decoded data, and finally source.start(0) to play the sound.

As others have pointed out you must create the AudioContext (which incidentally you must store and use for the lifetime of the page) as a result of a user interaction (click or touchstart).

HOWEVER : For iOS to 'unlock' its audio capabilities you MUST have audio data available when you create the AudioContext. If you load the data asynchronously there's nothing for it to play. It is not sufficient to merely create the AudioContext inside a click event.

Here's two solutions for reliable iOS playback:

  • 1) You must load at least one sound file before you even initialize the AudioContext, and then run all the above steps for that sound file immediately within a single user interaction (eg click).

  • OR 2) Create a sound dynamically in memory and play it.

This is how I did that second option:

REMEMBER - MUST BE within click / touch event for iOS:

 window.AudioContext = window.AudioContext || window.webkitAudioContext;
 var context = new window.AudioContext();

 // you should null check window.AudioContext for old browsers to not blow up

 // create a dummy sound - and play it immediately in same 'thread'
 var oscillator = context.createOscillator();
 oscillator.frequency.value = 400;
 oscillator.connect(context.destination);
 oscillator.start(0);
 oscillator.stop(.5);    // you can set this to zero, but I left it here for testing.

 // audio context is now 'unlocked' and ready to load and play sounds asynchronously
 // YOU MUST STORE 'context' for future usage. DON'T recreate more AudioContexts

I imagine this is a common mistake - and I'm surprised after 3 years that nobody seems to have pointed this out or discovered it :-/

Solution 4

So, I think I've figured it out.

It's a issue of Apple requiring an user action before sound can be allowed to play. It turns out, at least for me, that you shouldn't create the audio context at all except when the user calls for it. It's not enough to create the context when the page loads and then use createGainNode or similar on an user action.

In your case I'd create the context when the user clicks the "Touch to begin" button.

Solution 5

Answering to the original question, I can confirm some troubles with file formats on iPhone 4S/iOS 6 and MacOSX. If an MP3 file is "not good" for Safari, the decoding goes bad and calling AudioContext.createBuffer(array, bool) gives you and error.

The strange thing is about the error: "SYNTAX_ERR, DOM Exception 12", as pointed out by others. This makes me think it is a bug....

Same behavior also on MacOS, with Safari 6.0 (7536.25).

Share:
25,981
AshleysBrain
Author by

AshleysBrain

Developer for Construct and Construct 2, the open source game creators at http://www.scirra.com. This has given me lots of experience in C++, HTML5 and javascript, and I like hanging around reading and answering questions to help me learn more! http://www.hackernewsers.com/users/AshleysBrain.html

Updated on September 23, 2020

Comments

  • AshleysBrain
    AshleysBrain over 3 years

    I was really excited to see iOS 6 supports the Web Audio API, since we make HTML5 games. However, I cannot get iOS 6 to play any sound at all using the Web Audio API with examples that work fine in desktop Chrome.

    Here is a HTML5 game with touch controls and playing audio via the Web Audio API (if present - if not it will fall back to HTML5 audio):

    http://www.scirra.com/labs/sbios6b/

    Edit: @Srikumar suggested some workarounds. I applied them at the version below. It still does not work!

    http://www.scirra.com/labs/sbios6f/

    Everything plays just fine on desktop Chrome, but iOS 6 emits no sound at all. I'm having trouble debugging it because I only do Windows development, and iOS 6 replaced the debug mode with remote web inspector, which apparently is not available on Safari for Windows. Using a few alerts I did find it correctly identifies the Web Audio API, uses it, detects no Vorbis support so falls back to AAC audio, decodes a buffer and then plays it, and no errors are thrown, but I hear nothing. And, of course, I tried turning the volume up to max :)

    There should not be a codec problem, because iOS 6 can play AAC just fine - you can browse to one of the .m4a's the game plays and it plays fine visited direct from Safari.

    Looking at the Web Audio API examples here on iOS 6: http://chromium.googlecode.com/svn/trunk/samples/audio/samples.html - some of them work, and others don't. For example, the Chrome Audio Visualizer works, but Javascript Drone doesn't.

    There must be some subtle incompatibility between Web Audio on iOS 6 and desktop Chrome. What am I missing?

  • AshleysBrain
    AshleysBrain over 11 years
    Thanks for the answer, very interesting. Unfortunately it doesn't seem to apply to my first example. Consider this demo made from scratch which works fine on an iPad 2 with iOS 6: scirra.com/labs/webaudio-ios6-working - it just uses noteOn(0) and doesn't use your "create gain node on startup" workaround. There must be something else at work here...
  • AshleysBrain
    AshleysBrain over 11 years
    I don't think it's that - this example creates the audio context on startup and it works OK: scirra.com/labs/webaudio-ios6-working I also tried playing the sounds themselves on a timer (as in, not in a user input event) and it worked.
  • Oskar Eriksson
    Oskar Eriksson over 11 years
    Yeah, looks like it works ok. The original game, though, looks like it has some errors at the moment. I'm getting this when entering on the iPad: "sbios6b:9 - Viewport argument key "target-densitydpi" not recognized and ignored." "c2runtime.js:5534 - SYNTAX_ERR: DOM Exception 12: An invalid or illegal string was specified."
  • Oskar Eriksson
    Oskar Eriksson over 11 years
    Didn't both the previous answers state that an user action is required to get audio?
  • AshleysBrain
    AshleysBrain over 11 years
    I read them as either implying you can only create the context there (not quite right) or that audio not triggered in a user action will never play (also not quite right)
  • Srikumar
    Srikumar over 11 years
    It will not unmute until you attempt to play a sound in a user input event. I have a counter example for this - srikumarks.github.com/demos/tala . (It is a kind of "metronome" for South Indian talas.) If you load that in Safari/iOS6/iPhone and just wait for a while, the metronome will start ticking away, although the graphics doesn't work the way it does in Chrome. No user input needed (except keying in the URL?).
  • Mr. Flibble
    Mr. Flibble over 11 years
    I have found that you need to play a sound on a user event on the iPad but not iPhone. On the iPhone it appears to play automatically without any user input.
  • TomW
    TomW about 11 years
    The createGainNode() trick seemed to work for me - just make sure it is called in a callback from a touch event, and the restriction on sounds needing to be touch-triggered seems to disappear.
  • TomW
    TomW about 11 years
    It is documented, just poorly: User Control of Downloads Over Cellular Networks
  • Clafou
    Clafou almost 10 years
    This is the answer I was looking for--great solution!
  • maracuja-juice
    maracuja-juice over 6 years
    Also works when put into a function that is called from an event handler. Awesome!