Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web Audio API Memory Leaks on Mobile Platforms

I am working on an application that will be using Audio quite heavily and I am in the research stages of deciding whether to use Web Audio API on devices that can support it. I have put together a very simple test bed that loads an MP3 sprite file (~600kB in size), has a play and pause button and also a destroy button, which should in theory allow GC reclaim the memory used by the Web Audio API implementation. However, after loading and destroying ~5 times iOS crashes due to an out of memory exception.

I have profiled MobileSafari in XCode Instruments and indeed MobileSafari continually eats up memory. Furthermore the 600kb MP3 turns out to use ~80-90MB of memory when decoded.

My question is - When decoding audio data using Web Audio API, why is the memory usage so big and also why is the memory never reclaimed? From my understanding the decoding is an async operation for the browser and so presumably happens on a separate thread? Is it possible the browsers separate thread is never releasing the memory used during decoding?

My code is below, any help/explanation is greatly appreciated:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>Web Audio Playground</title>
</head>
<body>
<button id="load">
    Load
</button>
<button id="play">
    Play
</button>
<button id="pause">
    Pause
</button>
<button id="destroy">
    Destroy
</button>
<script type="application/javascript">
    (function () {
        window.AudioContext = window.AudioContext || window.webkitAudioContext;

        var loadButton = document.getElementById('load'),
                playButton = document.getElementById('play'),
                pauseButton = document.getElementById('pause'),
                destroyButton = document.getElementById('destroy'),
                audioContext = new window.AudioContext(),
                soundBuffer = null,
                soundSource = null;

        loadButton.addEventListener('click', function () {
            var request = new XMLHttpRequest();
            request.open('GET', 'live-sprite.mp3', true);
            request.responseType = 'arraybuffer';

            // Decode asynchronously
            request.onload = function () {
                audioContext.decodeAudioData(request.response, function (buffer) {
                    soundBuffer = buffer;
                });
            };
            request.send();
        });

        playButton.addEventListener('click', function () {
            soundSource = audioContext.createBufferSource();
            soundSource.buffer = soundBuffer;
            soundSource.connect(audioContext.destination);
            soundSource.start(0);
        });

        pauseButton.addEventListener('click', function () {
            if (soundSource) {
                soundSource.stop(0);
            }
        });

        destroyButton.addEventListener('click', function () {
            if (soundSource) {
                soundSource.disconnect(0);
                soundSource = null;
                soundBuffer = null;
                alert('destroyed');
            }
        });
    })();

</script>
</body>
</html>
like image 463
Shepless Avatar asked Jun 09 '14 11:06

Shepless


2 Answers

I made post on SoundJS issue tracker about this, but I'll reiterate it here for anyone looking:

It seems that simply disconnecting and dereferencing the AudioBufferSourceNode object on iOS Safari isn't enough; you need to manually clear out the reference to its buffer, or the buffer itself leaks. (This implies the AudioBufferSourceNode obj itself leaks, but we didn't see this as a practical limit in our project.)

Unfortunately to do this, a 1-sample long scratch buffer needs to get created, as assigning to null will cause an exception. The statement must be try-catch wrapped, too, as Chrome/FF will throw when .buffer is reassigned at any time.

The solution that worked was:

var ctx = new AudioContext(),
    scratchBuffer = ctx.createBuffer(1, 1, 22050);

class WebAudioAdapter extends AudioAdapter {
    close() {
        if( this.__src ) {
            this.__src.onended = null;
            this.__src.disconnect(0);
            try { this.__src.buffer = scratchBuffer; } catch(e) {}
            this.__src = null;
        }
    }
}

Hope this helps y'all too!

like image 161
konistehrad Avatar answered Oct 21 '22 00:10

konistehrad


The memory is large because the Web Audio API decodes your small MP3 into 32-bit LPCM – which will give you something on the order of 10MB per minute per channel.

So a 4 minute stereo MP3 would end up being something like 80MB.

This memory can't be reclaimed for as long as your application is holding on to the decoded AudioBuffer. So as long as you have a reference to it (in your case, soundBuffer), that memory can't be released. If it was, you couldn't play back the audio.

like image 34
Kevin Ennis Avatar answered Oct 21 '22 02:10

Kevin Ennis