9
votes

On iOS 7.1, I keep getting a buzzing / noisy / distorted sound when playing back audio using the Web Audio API. It sounds distorted like this, in place of normal like this.

The same files are fine when using HTML5 audio. It all works fine on desktop (Firefox, Chrome, Safari.)

EDIT:

  • The audio is distorted in the iOS Simulator versions iOS 7.1, 8.1, 8.2. The buzzing sound often starts before I even playback anything.
  • The audio is distorted on a physical iPhone running iOS 7.1, in both Chrome and Safari.
  • The audio is fine on a physical iPhone running iOS 8.1, in both Chrome and Safari.

i.e.: the buzzing audio is on iOS 7.1. only.


Howler.js is not the issue. The problem is still there using pure JS like so:

var context;
var sound;
var extension = '.' + ( new Audio().canPlayType( 'audio/ogg' ) !== '' ? 'ogg' : 'mp3');


/** Test for WebAudio API support **/
try {
    // still needed for Safari
    window.AudioContext = window.AudioContext || window.webkitAudioContext;

    // create an AudioContext
    context = new AudioContext();
} catch(e) {
    // API not supported
    throw new Error( 'Web Audio API not supported.' );
}

function loadSound( url ) {
    var request = new XMLHttpRequest();
    request.open( 'GET', url, true );
    request.responseType = 'arraybuffer';

    request.onload = function() {
        // request.response is encoded... so decode it now
        context.decodeAudioData( request.response, function( buffer ) {
        sound = buffer;
        }, function( err ) {
            throw new Error( err );
        });
    }

    request.send();
}

function playSound(buffer) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.start(0);
}

loadSound( '/tests/Assets/Audio/En-us-hello' + extension );


$(document).ready(function(){ 

    $( '#clickme' ).click( function( event ) {
        playSound(sound);
    });


}); /* END .ready() */

A live version of this code is available here: Web Audio API - Hello world


Google did not bring up any result about such a distorted sound issue on iOS 7.1.

Has anyone else run into it? Should I file a bug report to Apple?

2
Have you had the same issue with other audio files or just that one? I tested your live demo on my iPhone with iOS 8.3 and didn't hear any distortion. - James Simpson
Yes, it occurs with different files. Here is another higher-level test: bilingueanglais.com/tmp/howler/v2-howler-hello.html and Chrome is fine, only Safari has the issue. - Fabien Snauwaert
That looks like a low-level Safari problem. Looking at the waveform you recorded, it looks like around 90 samples out of every 512 samples are zero - like, something about their block processing or decoding is having problems. - cwilso
It's still an issue with iOS 9.2. - Nikolay Tsenkov
Same problem here (tested on a iPhone 5 (IOS 9.2) and two different iPhone 6 (IOS 9.2 and IOS 9.2.1). Unable to reproduce the problem on a iPhone 5S (IOS 9.2.1). Pattern seems to be that it works first time, and always after reloading. But if I close / reopen Safari, the sound is always distorted the first time, and works after reload. - Patrick Fabrizius

2 Answers

5
votes

I believe the issue is caused due to resetting the audioContext.sampleRate prop, which seem to happen after the browser/OS plays something recorded in a different sampling rate.

I've devised the following workaround, which basically silently plays a short wav file recorded in the sampling rate that the device currently does playback on:

"use strict";

var getData = function( context, filePath, callback ) {
    var source = context.createBufferSource(),
        request = new XMLHttpRequest();

    request.open( "GET", filePath, true );

    request.responseType = "arraybuffer";

    request.onload = function() {
        var audioData = request.response;

        context.decodeAudioData(
            audioData,
            function( buffer ) {
                source.buffer = buffer;

                callback( source );
            },
            function( e ) {
                console.log( "Error with decoding audio data" + e.err );
            }
        );
    };

    request.send();
};

module.exports = function() {
    var AudioContext = window.AudioContext || window.webkitAudioContext,
        context = new AudioContext();

    getData(
        context,
        "path/to/short/file.wav",
        function( bufferSource ) {
            var gain = context.createGain();
            gain.gain.value = 0;
            bufferSource.connect( gain );
            gain.connect( context.destination );
            bufferSource.start( 0 );
        }
    );
};

Obviously, if some of the devices have different sampling rates, you would need to detect and use a specific file for every rate.

5
votes

it looks like iOS6+ Safari defaults to a sample rate of 48000. If you type this into the developer console when you first open mobile safari, you'll get 48000:

var ctx = new window.webkitAudioContext();
console.log(ctx.sampleRate);

Further Reference: https://forums.developer.apple.com/thread/20677

Then if you close the initial context on load: ctx.close(), the next created context will use the sample rate most other browsers use (44100) and sound will play without distortion.

Credit to this for pointing me in the right direction (and in case the above no longer works in the future): https://github.com/Jam3/ios-safe-audio-context/blob/master/index.js

function as of post date:

function createAudioContext (desiredSampleRate) {
  var AudioCtor = window.AudioContext || window.webkitAudioContext

  desiredSampleRate = typeof desiredSampleRate === 'number'
    ? desiredSampleRate
    : 44100
  var context = new AudioCtor()

  // Check if hack is necessary. Only occurs in iOS6+ devices
  // and only when you first boot the iPhone, or play a audio/video
  // with a different sample rate
  if (/(iPhone|iPad)/i.test(navigator.userAgent) &&
      context.sampleRate !== desiredSampleRate) {
    var buffer = context.createBuffer(1, 1, desiredSampleRate)
    var dummy = context.createBufferSource()
    dummy.buffer = buffer
    dummy.connect(context.destination)
    dummy.start(0)
    dummy.disconnect()

    context.close() // dispose old context
    context = new AudioCtor()
  }

  return context
}