Эксперименты с WEB Audio API


Браузеры взрослеют не по дням, а по часам.
Добавляются все новые и новые API. То, с чем раньше можно было только поэкспериментировать, сейчас уже работает во всех современных браузерах.

Расскажу про такую интересную вещь, как WEB Audio API.

Audio API позволяет полноценно работать со звуковыми файлами из браузера. Причем не только проигрывать какие-либо файлы, но и генерировать звук "на лету", накладывать звуковые эффекты, управлять балансом, громкостью и т.д.

Расскажу только про базовые вещи - а эксперименты и чтение RFC - самостоятельно :)

По мне так нет ничего лучше, чем учить что-то на работающих примерах. Надеюсь все это поможет Вам в освоении этого очень интересного функционала.

На самом деле API жутко и ни к месту наворочено, и иногда кажется, что писал его вообще не человек, но въехать вполне можно. Самое простое - это разбираться по исходникам.

Некоторое может вас удивить, некоторое - понравится, ну а от некоторого можно просто о#еть и проклясть разработчиков. Но помните, что стандарт еще не устоялся, возможно, таких мест потом будет очень мало.

Кратко там все пляшет от блоков, соединяемых между собой.


Как то так:
  1. загрузил файл со звуком
  2. распарсерил его
  3. создал аудио буфер и указал ему на распарсенный файл
  4. соединил его с тем, куда играть

Теперь его можно проиграть, но только один раз, чтобы еще раз запустить это дело - надо пересоздать его снова по этой-же схеме.


Хочется поуправлять балансом? Нет проблем:
  1. загрузил файл со звуком
  2. распарсерил его
  3. создал аудио буфер и указал ему на распарсенный файл
  4. создал объект управления балансом
  5. соединил аудио буфер с объектом управления балансом
  6. соединил объект управления баланса с тем, куда играть

Если надо управлять чем-то еще, кроме баланса - насоздавал нужных объектов, сколько надо и соединил их друг с другом, затем первый из них соединил с аудио буфером, последний - с тем, куда играть.

Достаточно логично кроме того, что после проигрывания оно все самоликвидируется, но этому есть тоже в RFC какое-то тухлое объяснение по поводу особенности работы с железом и т.д. - я не вникал :)

В принципе, это все терпимо, но дает кое-какие неудобства вроде отсутствия простой возможности реализовать обычную паузу - приходится тупо запоминать время на котором поставили на паузу и после пересоздания буфера запускать проигрывание с этого времени.

Базовый пример загрузки и проигрывания звука

window.AudioContext = window.AudioContext || window.webkitAudioContext;
 
function play( snd ) {
	var audioCtx = new AudioContext();
 
	var request = new XMLHttpRequest();
	request.open( "GET", snd, true );
	request.responseType = "arraybuffer";
	request.onload = function () {
		var audioData = request.response;
 
		audioCtx.decodeAudioData(
			audioData,
			function ( buffer ) {
				var smp = audioCtx.createBufferSource();
				smp.buffer = buffer;
				smp.connect( audioCtx.destination );
				smp.start( 0 );
			},
			function ( e ) {
				alert( "Error with decoding audio data" + e.err );
			}
		);
	};
	request.send();
}
 
//URL до аудио файла (mp3, ogg, wav)
play( "sample.ogg" );
играть


Управление громкостью

Чтобы управлять громкостью требуется создать еще один объект GainNode.
Изменятся громкость следующим образом GainNode.gain.value = значение, где значение должно быть в диапазоне от 0 (тишина) до 1 (полная громкость).
На самом деле, можно попробовать установить значение большее 1 что должно давать переусиление, но работает это не везде.

Менять его можно как до запуска звука на проигрывание, так и во время проигрывания.
window.AudioContext = window.AudioContext || window.webkitAudioContext;
 
function play( snd, vol ) {
	var audioCtx = new AudioContext();
 
	var request = new XMLHttpRequest();
	request.open( "GET", snd, true );
	request.responseType = "arraybuffer";
	request.onload = function () {
		var audioData = request.response;
 
		audioCtx.decodeAudioData(
			audioData,
			function ( buffer ) {
				var smp = audioCtx.createBufferSource();
				smp.buffer = buffer;
				//создание объекта GainNode и его привязка
				var gainNode = audioCtx.createGain ? audioCtx.createGain() : audioCtx.createGainNode();
				smp.connect( gainNode );
				gainNode.connect( audioCtx.destination );
				gainNode.gain.value = vol;
				smp.start( 0 );
			},
			function ( e ) {
				alert( "Error with decoding audio data" + e.err );
			}
		);
	};
	request.send();
}
 
//URL до аудио файла (mp3, ogg, wav)
play( "sample.ogg", 0.3 );


играть


Управление балансом

Тут нам тоже потребуется создать новый объект Panner.
Управление им чуть хитрее (подробности гляньте в RFC), для большинства случаев за глаза хватит режима equalpower, и управлять им так: Panner.setPosition( значение, 0, 1 - Math.abs( значение ) ), где значение должно быть в диапазоне от -1 (лево) до 1 (право).

Менять его можно тоже как до запуска звука на проигрывание, так и во время проигрывания.
window.AudioContext = window.AudioContext || window.webkitAudioContext;
 
function play( snd, balance ) {
	var audioCtx = new AudioContext();
	audioCtx.listener.setPosition( 0, 0, 0 );
 
	var request = new XMLHttpRequest();
	request.open( "GET", snd, true );
	request.responseType = "arraybuffer";
	request.onload = function () {
		var audioData = request.response;
 
		audioCtx.decodeAudioData(
			audioData,
			function ( buffer ) {
				var smp = audioCtx.createBufferSource();
				smp.buffer = buffer;
				//создание объекта Panner и его привязка
				var panner = audioCtx.createPanner();
				panner.setPosition( 0, 0, 1 );
				panner.panningModel = "equalpower";
				panner.setPosition( balance, 0, 1 - Math.abs( balance ) );
				smp.connect( panner );
				panner.connect( audioCtx.destination );
				smp.start( 0 );
			},
			function ( e ) {
				alert( "Error with decoding audio data" + e.err );
			}
		);
	};
	request.send();
}
 
//URL до аудио файла (mp3, ogg, wav)
play( "sample.ogg", -1 );
играть


Визуализация / изменение звука на лету

Будем работать с новым объект ScriptProcessor.
Он позволяет перехватить и обработать буфер со звуком до того, как он будет проигран.
Это дает потрясные возможности - возможно наложить на звук какой-либо эффект, смикшировать его или визуализировать.

Данные звука идут в диапазоне от -1 до 1.

В этом примере будет показываться зависимость амплитуды звука от времени.
window.AudioContext = window.AudioContext || window.webkitAudioContext;
 
function play( snd ) {
	var audioCtx = new AudioContext();
	audioCtx.listener.setPosition( 0, 0, 0 );
 
	var canvas = document.createElement( "canvas" );
	canvas.style.display = "block";
	var canvasW = 512;
	var canvasH = 64;
	var canvasHh = Math.floor( canvasH / 2 );
	canvas.width  = canvasW;
	canvas.height = canvasH;
	canvas.ctx    = canvas.getContext( "2d" );
	canvas.ctx.strokeStyle = "#000000";
	canvas.ctx.lineWidth = 2;
	canvas.ctx.fillStyle = "#FFFFFF";
	canvas.ctx.fillRect( 0, 0, canvasW, canvasH );
	document.body.appendChild( canvas );
 
	function vizualize( sample ) {
		canvas.ctx.fillRect( 0, 0, canvasW, canvasH );
		canvas.ctx.beginPath();
 
		canvas.ctx.moveTo( -1, canvasHh );
		for ( var x = 0; x < canvasW; x++ ) {
			var y = canvasHh - Math.floor( sample[x] * canvasHh );
			canvas.ctx.lineTo( x, y );
		}
		canvas.ctx.stroke();
	}
 
	var request = new XMLHttpRequest();
	request.open( "GET", snd, true );
	request.responseType = "arraybuffer";
	request.onload = function () {
		var audioData = request.response;
 
		audioCtx.decodeAudioData(
			audioData,
			function ( buffer ) {
				var smp = audioCtx.createBufferSource();
				smp.buffer = buffer;
				//создание объекта ScriptProcessor
				//аргументы: длина буфера, количество входящих каналов, количество исходящих каналов
				//чем больше буфер - тем меньшее число раз будет вызыван код обработки,
				//должен быть кратен степени двойки
				var sp = audioCtx.createScriptProcessor ? audioCtx.createScriptProcessor( 512, 2, 2 ) : audioCtx.createJavaScriptNode( 512, 2, 2 );
 
				sp.onaudioprocess = function ( ape ) {
					var inputBuffer = ape.inputBuffer;
					var outputBuffer = ape.outputBuffer;
 
					var channel;
					var channelsLen = outputBuffer.numberOfChannels;
					var sample;
					var sampleLen = inputBuffer.length;
 
					//для визулизации создаем монобуфер
					var mono = new Array( sampleLen );
					for ( sample = 0; sample < sampleLen; sample++ ) mono[sample] = 0;
 
					for ( channel = 0; channel < channelsLen; channel++ ) {						
						var inputData = inputBuffer.getChannelData( channel );
						var outputData = outputBuffer.getChannelData( channel );
						//устанавливаем выходные данные = входным
						//здесь можно изменить в них что-то или наложить эффект
						outputData.set( inputData );
 
						//микшируем в монобуфер все каналы
						for ( sample = 0; sample < sampleLen; sample++ ) mono[sample] = ( mono[sample] + inputData[sample] ) / 2;
					}
 
					vizualize( mono );
				};
 
				smp.connect( sp );
				sp.connect( audioCtx.destination );
				smp.start( 0 );
			},
			function ( e ) {
				alert( "Error with decoding audio data" + e.err );
			}
		);
	};
	request.send();
}
 
//URL до аудио файла (mp3, ogg, wav)
play( "sample.ogg" );
играть


Дополнительно

Практически все объекты данного API имеют какие-то свои события на которые можно подписываться. С помощью них очень удобно контролировать интерфейс проигрывателя и много чего еще.

Например BufferSource (smp в исходниках) имеет событие onended, вызываемое, когда буфер доиграл.

И еще небольшая добавка - при работе с ScriptProcessor лучше после остановки проигрывания или когда буфер доиграл, его отсоединить от всего.

Например так (после создания smp, и не забудьте вынести sp из локальной видимости DecodeAudioData, чтобы событие имело доступ до него):
	smp.onended = function() {
		if ( smp ) {
			smp.disconnect( sp );
			sp.disconnect( audioCtx.destination );
		}
	};

Ну вот, собственно, и все.
Звук - это волшебство, и оно теперь доступно для Вас прямо из браузера.
Приятных Вам экспериментов!

Скачать примеры работы, версия от 04.01.15

скачать

02.01.2015, Protocoder
Protocoder21.01.2015 16:48:57#ответить
Подправил работу в WebKit-ах (Chrome, Safari и т.д.)
Александр13.03.2016 14:49:18#ответить
Доброго времени! А как работать с audio web api, используя данные не из буфера (который загружается аяксом),а используя объект HTML5 <audio> ?
Protocoder14.03.2016 15:52:16#ответить
Если Вас интересует именно тэг <аudio> и работа с ним, начать можно отсюда:

https://habrahabr.ru/post/148202/
https://habrahabr.ru/post/148368/
https://habrahabr.ru/post/149518/

А потом, после представлении о том, что это - просто используйте доку от W3C.
Борис18.07.2017 05:11:49#ответить
Благодарю Вас за Примеры! Отправил Вам на почту (надеюсь e-mail , указанный в "ОБО МНЕ" за эти годы не изменился) просьбу о помощи. Прочтите, пожалуйста.
Igor Khomenko19.07.2017 14:16:22#ответить
Спасибо за примеры.

Маленькая корректировочка: не обязательно при повторной игре звука его перегружать, вызывая load-функцию.

Обратитесь к ней один раз при загрузке документа, переменные smp, buf, gainNode сделайте глобальными. buf и gainNode заполните в load-функции, а для воспроизведения звука достаточно простой функции из трех строк:
smp = context.createBufferSource();
source.connect( gainNode );
source.buffer = buf;

С уважением к Вам.
Protocoder20.07.2017 00:47:00#ответить
Вам спасибо за дельный комментарий!
Igor Khomenko22.07.2017 18:09:49#ответить
Жаль, что gain.value ограничено 1. Не получается усилить звук в n-раз.
Придется, наверное, с buffer работать: перемножать каждый sample, потом перезаписывать.
Геморройно...
Написать комментарий