I've been struggling with an elusive audio distortion bug using webkitAudioContext in HTML5 under iOS 6. It can happen in other circumstances, but the only way I can get 100% repro is on the first visit to my page after power cycling the device. It seems like if you visit any audio-capable page prior to visiting this one, the problem will not occur.
The distortion only happens to audio generated by webkitAudioContext.decodeAudioData() and then played through webkitAudioContext.createBufferSource(). Audio playback of webkitAudioContext.createMediaElementSource() will not distort.
Am I missing some initialisation step? Here's the code and HTML in its entirety that I submitted to Apple as a bug report (but have received no reply):
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
var buffer = null;
var context = null;
var voice = null;
function load_music(file) {
context = new webkitAudioContext();
voice = context.createBufferSource();
var request = new XMLHttpRequest();
request.onload = function() {
context.decodeAudioData(request.response, function(result) {
buffer = result;
document.getElementById("start").value = "Start";
});
};
var base = window.location.pathname;
base = base.substring(0, base.lastIndexOf("/") + 1);
request.open("GET", base + file, true);
request.responseType = "arraybuffer";
request.send(null);
}
function start_music() {
if (!buffer) {
alert("Not ready yet");
return;
}
voice.buffer = buffer;
voice.connect(context.destination);
voice.noteOn(0);
document.getElementById("compare").style.display = "block";
}
</script>
</head>
<body onload="load_music('music.mp3')">
<p>This is a simple demo page to reproduce a <strong>webkitAudio</strong>
problem occurring in Safari on iOS 6.1.4. This is a stripped down demo
of a phenomenon discovered in our HTML5 game under development,
using different assets.</p>
<p><u>Steps to reproduce:</u></p>
<ol>
<li>Power cycle <strong>iPhone 5 with iOS 6.1.4</strong>.</li>
<li>Launch Safari immediately, and visit this page.</li>
<li>Wait for "Loading..." below to change to
"Start".</li>
<li>Tap "Start".</li>
</ol>
<p><u>Issue:</u></p>
<p>Audio will be excessively distorted and play at wrong pitch. If
another audio-enabled web site is visited before this one, or this
site is reloaded, the audio will fix. The distortion only happens on
the first visit after cold boot. <strong>To reproduce the bug, it is
critical to power cycle before testing.</strong></p>
<p>This bug has not been observed on any other iOS version (e.g. does
not occur on iPad Mini or iPod 5 using iOS 6.1.3).</p>
<input id="start" type="button" value="Loading..." onmousedown="start_music()" />
<span id="compare" style="display:none;"><p><a href="music.mp3">Direct link</a> to audio file, for
comparison.</p></span>
</body>
</html>
Note: The body text suggests this only occurs on iOS 6.1.4, but I mean to say that the problem only occurs upon power cycling in this situation. I've experienced the problem on the iPad Mini under 6.1.3, too, but not upon power cycling.
Edit: a few things I've tried... Deferring the creation of the buffer source makes no difference. Using different transcoders to generate the .mp3 file it plays makes no difference. Playing throwaway silence as the first sound makes no difference as the distortion continues for every decodeAudioData sound until the page reloads. If createMediaElementSource and createBufferSource sources are mixed in the same page, only the createBufferSource audio (using decodeAudioData) will distort. When I check the request.response.byteLength in the failure case and the non-failure case, they are the same, suggesting the XMLHttpRequest is not returning incorrect data, though I would think corruption of the data would damage the MP3 header and render the file unplayable anyway.
There is one observable difference between the failure condition and the non-failure condition. The read-only value context.sampleRate will be 48000 in the failure state and 44100 in the non-failure state. (Yet the failure state sounds lower pitch than the non-failure state.) The only thing that occurs to me is a hack wherein I refresh the page via JavaScript if 48000 is detected on a browser that should be reporting 44100, but that's serious userAgent screening and not very future proof, which makes me nervous.
I have been having similar problems, even on iOS 9.2.
Even without a <video>
tag, playback is distorted when first playing audio on the page after cold boot. After a reload, it works fine.
The initial AudioContext
seems to default to 48 kHz, which is where distortion is happening (even with our audio at 48 kHz sample rate). When playback is working properly, the AudioContext
has a sample rate of 44.1 kHz.
I did find a workaround: it is possible to re-create the AudioContext
after playing an initial sound. The newly-created AudioContext
seems to have the correct sample rate. To do this:
// inside the click/touch handler
var playInitSound = function playInitSound() {
var source = context.createBufferSource();
source.buffer = context.createBuffer(1, 1, 48000);
source.connect(context.destination);
if (source.start) {
source.start(0);
} else {
source.noteOn(0);
}
};
playInit();
if (context.sampleRate === 48000) {
context = new AudioContext();
playInit();
}
I found a related bug with HTML5 video and think I discovered the root of the problem.
I noticed that if you play a video using a <video>
tag, it sets the context.sampleRate value to whatever the video's audio was encoded at. It seems as if iOS Safari has one global sampleRate that it uses for everything. To see this, try the following:
// Play a video with audio encoded at 44100 Hz
video.play();
// This will console log 44100
var ctx = new webkitAudioContext();
console.log(ctx.sampleRate);
// Play a video with audio encoded at 48000 Hz
video2.play();
// This will console log 48000
var ctx = new webkitAudioContext();
console.log(ctx.sampleRate);
This global sample rate appears to persist across page loads and is shared between tabs and browser instances. So, playing a youtube video in another tab could break all your decoded audio.
The audio becomes distorted when it is decoded at one sample rate and played at another one.
I don't know why it's happening after a cold start. If I had to guess, it's that Safari doesn't initialize this global sample rate until you try to use it.
The problem is still there on iOS 7, so I don't think a fix is coming anytime soon. We're stuck with hacks in the mean time like checking for a changed sample rate.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With