Утечки памяти веб-аудио API на мобильных платформах
Я работаю над приложением, которое будет использовать Audio довольно сильно, и я нахожусь на этапах исследования, чтобы решить, использовать ли API Web Audio на устройствах, которые могут его поддерживать. Я собрал очень простой тестовый слой, который загружает файл спрайта MP3 (размером ~ 600 КБ), имеет кнопку воспроизведения и паузы, а также кнопку уничтожения, что теоретически позволяет GC восстановить память, используемую при реализации API Web Audio API, Однако после загрузки и уничтожения ~ 5 раз iOS падает из-за исключения из памяти.
Я профилировал MobileSafari в XCode Instruments, и действительно, MobileSafari постоянно ест память. Кроме того, при декодировании 600kb MP3 будет использовать память ~ 80-90 МБ.
Мой вопрос: при декодировании аудиоданных с использованием API веб-аудио, почему использование памяти настолько велико, а также почему память не восстанавливается? Насколько я понимаю, декодирование - это асинхронная операция для браузера и, по-видимому, происходит в отдельном потоке? Возможно ли, что отдельные ветки браузеров никогда не освобождают память, используемую при декодировании?
Мой код ниже, любая помощь/объяснение очень ценится:
<!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>
Ответы
Ответ 1
Память велика, потому что API веб-аудио расшифровывает ваш маленький MP3 в 32-разрядный LPCM - который даст вам что-то порядка 10 МБ в минуту на канал.
Итак, 4-минутный стереофонический MP3 в итоге будет чем-то вроде 80 МБ.
Эта память не может быть восстановлена до тех пор, пока ваше приложение держит декодированный AudioBuffer
. До тех пор, пока у вас есть ссылка на него (в вашем случае soundBuffer
), эта память не может быть выпущена. Если бы это было так, вы не смогли воспроизвести звук.
Ответ 2
Я сделал сообщение в SoundJS issue tracker об этом, но я повторю его здесь для тех, кто смотрит:
Похоже, что просто отключить и разыменовать объект AudioBufferSourceNode на iOS Safari недостаточно; вам нужно вручную очистить ссылку на свой буфер, или сам буфер протекает. (Это означает, что сам AudioBufferSourceNode obj протекает, но мы не рассматривали это как практический предел в нашем проекте.)
К сожалению, для этого нужно создать 1-образный длинный буфер с нуля, поскольку присвоение значения null приведет к исключению. Заявление также должно быть завершено с помощью try-catch, так как Chrome/FF будет выдавать, когда .buffer переназначается в любое время.
Решение:
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;
}
}
}
Надеюсь, это тоже поможет!