Написать пост

Web Audio и объёмный звуковой ландшафт: реализация

Аватар Иван Бирюков

Обложка поста Web Audio и объёмный звуковой ландшафт: реализация

Рассказывает Si Robertson

В этом руководстве мы создадим простой Web Audio API для воспроизведения звуков в объёмном пространстве, который можно использовать в интерактивных приложениях с эффектом погружения.

Это вторая и последняя часть этого руководства. Если вы ещё не прочитали первую часть руководства, вам стоит это сделать: в ней разбираются принципы работы основных элементов Web Audio.

Демонстрация

Прежде чем мы начнём, вот небольшая демка, использующая упрощённый API, о котором пойдёт речь. Звуки (представленные квадратиками) позиционируются случайным образом и воспроизводятся при помощи функции HRTF, предоставляемой Web Audio.

Обзор

Поскольку упрощённый API (AudioPlayer) уже был создан и доступен для загрузки, мы внимательно ознакомимся с AudioPlayer API и его кодом.

AudioPlayer

Класс AudioPlayer содержит наш API и внедрен в объект window , а также в стандартные классы Web Audio, но только если Web Audio поддерживается браузером. Это значит, что мы должны удостовериться в существовании класса, прежде чем пытаться работать с ним.

			if (window.AudioPlayer !== undefined) {
    audioPlayer = new AudioPlayer()
}
		

Мы могли бы попробовать создать новый объект AudioPlayer внутри try...catch, но простая проверка тоже прекрасно работает.

За кадром: audioPlayer создаёт новые объекты AudioContext и AudioGainNode и подключает объект GainNode к ноде destination, внедрённой в объект AudioContext.

			var m_context = new AudioContext()
var m_gain = m_context.createGain()
...
m_gain.connect(m_context.destination)
		

При создании и воспроизведении звуки будут подключены к ноде m_gain, что позволит нам контролировать их громкость.

audioPlayer также конфигурирует listener, внедрённый в m_context, таким образом, чтобы его система координат соответствовала в WebGL. Ось z направлена из экрана, ось y — вверх, ось x — вправо.

			m_context.listener.setOrientation(0, 0, -1, 0, 1, 0)
		

Позиция listener — всегда ноль; он расположен в центре координат.

Загрузка звуков

Прежде чем мы сможем создавать или проигрывать звуки, мы должны загрузить звуковые файлы; к счастью, audioPlayer берет на себя всю сложную работу. В него внедрена функция load(...), которая используется для загрузки звуков, и три обработчика событий, позволяющих отслеживать процесс загрузки.

			audioPlayer.onloadstart = function() { ... }
audioPlayer.onloaderror = function() { ... }
audioPlayer.onloadcomplete = function() { ... }
 
audioPlayer.load("sound-01.ogg")
audioPlayer.load("sound-02.ogg")
audioPlayer.load("sound-03.ogg")
		

Набор поддерживаемых звуковых форматов зависит от браузера. Например, Chrome и Firefox поддерживают OGG Vorbis, а IE — нет. Все три браузера поддерживают MP3, но проблемой MP3 является отсутствие возможности бесшовно зацикливать аудио, которая есть в OGG Vorbis.

Вызывая функцию load(...) несколько раз, audioPlayer создает очередь запросов и загружает их по очереди. Когда все звуки загружены и декодированы, вызывается хэндлер onloadcomplete.

За кадром: audioPlayer использует один объект XMLHttpRequest для загрузки звуков. responseType запроса установлен в "arraybuffer", и, когда файл загружен, буфер отправляется в m_context для декодирования.

			// simplified example
 
m_loader = new XMLHttpRequest()
m_queue = []
 
function load() {
    m_loader.open("GET", m_queue[0])
    m_loader.responseType = "arraybuffer"
    m_loader.onload = onLoad
    m_loader.send()
}
 
function onLoad(event) {
    var data = m_loader.response
    var status = m_loader.status
 
    m_loader.abort() // resets the loader
 
    if (status < 400) {
        m_context.decodeAudioData(data, onDecode)
    }
}
		

Если файл был успешно загружен и декодирован, audioPlayer либо загрузит следующий файл в очереди, либо даст знать, что все файлы загружены.

Создание звуков

Теперь, когда мы загрузили звуковые файлы, мы можем создавать и воспроизводить звуки. Сначала мы должны дать команду audioPlayer создать звуки, что можно сделать, используя функцию create(...), внедрённую в audioPlayer.

			var sound1 = audioPlayer.create("sound-01.ogg")
var sound2 = audioPlayer.create("sound-02.ogg")
var sound3 = audioPlayer.create("sound-03.ogg")
		

Мы можем создавать сколько угодно звуков, даже если у нас есть всего один звуковой файл.

			var a = audioPlayer.create("beep.ogg")
var b = audioPlayer.create("beep.ogg")
var c = audioPlayer.create("beep.ogg")
		

Путь, передаваемый функции create(...), говорит audioPlayer, какой файл должен использовать созданный звук. Если при вызове create(...) соответствующий файл не был загружен, будет выброшена ошибка выполнения.

Воспроизведение звуков

После создания одного или нескольких звуков мы можем воспроизводить их в любое время. Для этого мы используем функцию play(...), внедрённую в audioPlayer.

			audioPlayer.play(sound1)
		

Для того, чтобы понять, нужно ли зациклить звук, мы можем также передать функции play(...) булевскую величину. Если она равна true, звук будет повторяться до принудительного выключения.

			audioPlayer.play(sound1, true)
		

Для остановки воспроизведения используется функция stop(...).

			audioPlayer.stop(sound1)
		

Функция isPlaying(...) показывает, воспроизводится ли сейчас тот или иной звук.

			if (audioPlayer.isPlaying(sound1)) { ... }
		

За кадром: audioPlayer приходится делать немало работы для воспроизведения звука из-за модульной структуры Web Audio. При получения запроса на воспроизведение звука audioPlayer создаёт новые объекты AudioSourceBufferNode и PannerNode, настраивает и подключает их, а затем подключает звук к ноде m_gain. К счастью, Web Audio хорошо оптимизирован, поэтому создание и настройка новых аудиоузлов несильно нагружает систему.

			sound.source = m_context.createBufferSource()
sound.panner = m_context.createPanner()
 
sound.source.buffer = sound.buffer
sound.source.loop = loop
sound.source.onended = onSoundEnded
 
// This is a bit of a hack but we need to reference the sound
// object in the onSoundEnded event handler, and doing things
// this way is more optimal than binding the handler.
sound.source.sound = sound
             
sound.panner.panningModel = "HRTF"
sound.panner.distanceModel = "linear"
sound.panner.setPosition(sound.x, sound.y, sound.z)
 
sound.source.connect(sound.panner)
sound.panner.connect(m_gain)
 
sound.source.start()
		

Воспроизведение звуков — штука, безусловно, полезная, но назначением audioPlayer является воспроизведение звуков в объёмной системе координат, поэтому сперва нам нужно задать позиции звуков. audioPlayer содержит для этого несколько функций.

Позиционирование звуков

  • Функции setX(...) и getX(...), внедрённые в audioPlayer, используются для установки и получения координаты звука по оси x.
  • Функции setY(...) и getY(...) используются для установки и получения координаты звука по оси y.
  • Функции setZ(...) и getZ(...) используются для установки и получения координаты звука по оси z.
  • И наконец, функция setPosition(...) используется для установки координат звука в пространстве.
			audioPlayer.setX(sound1, 100)
audioPlayer.setZ(sound1, 200)
 
console.log(audioPlayer.getX(sound1)) // 100
console.log(audioPlayer.getZ(sound1)) // 200
 
audioPlayer.setPosition(sound1, 300, 0, 400)
 
console.log(audioPlayer.getX(sound1)) // 300
console.log(audioPlayer.getZ(sound1)) // 400
		

Чем дальше звук от центра, тем он тише. На расстоянии 10000 (стандарт Web Audio) громкость будет равна нулю.

Громкость

Мы можем управлять громкостью звуков, используя функции setVolume(...) и getVolume(...) , внедренные в audioPlayer.

			audioPlayer.setVolume(0.5) // 50%
 
console.log(audioPlayer.getVolume()) // 0.5
		

Функция setVolume(...) имеет второй параметр, который использует для постепенного затихания звука. Например, чтобы звук затих за две секунды, мы можем сделать следующее:

			audioPlayer.setVolume(0.0, 2.0)
		

Это используется в демке для плавного затухания звуков.

За кадром: audioPlayer просто отдает ноде m_gain команду линейно изменять значение gain.

			var currentTime = m_context.currentTime
var currentVolume = m_gain.gain.value
 
m_gain.gain.cancelScheduledValues(0.0)
m_gain.gain.setValueAtTime(currentVolume, currentTime)
m_gain.gain.linearRampToValueAtTime(volume, currentTime + time)
		

audioPlayer использует 0.01 секунду в качестве минимального значения времени затухания, чтобы не возникало резких звуковых артефактов.

Заключение

В этом руководстве мы взглянули на способ создания простого Web Audio API, который используется для воспроизведения звуков в трёхмерной системе координат.

Файлы AudioPlayer и демки доступны на GitHub. Исходники снабжены хорошими комментариями, поэтому в них стоит заглянуть.

Следите за новыми постами
Следите за новыми постами по любимым темам
5К открытий5К показов