Говоря об обработке звука H5 (подробнее о направлении записи)

Node.js Байду внешний интерфейс JavaScript

В последнее время мне нужно сделать запись в реальном времени, а затем вернуть функции вызова интерфейсов анализа тишины (VAD) и распознавания речи (ASR) в реальном времени в соответствии с аудиопотоком. Поэтому я начал изучать поддержку H5 в этом отношении.

API веб-аудио для H5

Сначала нам нужно кое-что прояснить,Web Audio APIи H5<audio>Дело вовсе не в размере.<audio>Очень удобно, что можно закинуть в него аудиофайл и привнести разные навороченные функции. Но если вы напрямую используетеWeb Auido APIПоиграйте с ним, и вы даже сможете создавать звуки из ничего. Вот приблизительный список того, что он может сделать (насколько я могу найти):

  1. смешивать простые или сложные звуки;
  2. Точный контроль плотности звука и ритма;
  3. Встроенное затухание, затухание, шум частиц, регулировка тембра и другие эффекты;
  4. Гибкая обработка каналов в аудиопотоках, их разделение и объединение;
  5. обработано из<audio>аудио или<video>Медиа-элементы и аудиоисточники для видео.
  6. использоватьMediaStream的getUserMedia()Метод обрабатывает входной звук в режиме реального времени, например, изменение голоса;
  7. Стереозвук, поддерживающий различные 3D-игры и иммерсивные среды.
  8. Используйте механизм свертки для создания различных линейных эффектов, таких как маленькие/большие комнаты, соборы, концертные залы, пещеры, туннели, коридоры, леса и многое другое. Особенно полезно для создания высококачественных комнатных эффектов. (Это своего рода спецэффект музыкального проигрывателя~)
  9. Эффективный временной и частотный анализ в реальном времени с помощью CSS3, Canvas или WebGL может способствовать визуализации музыки.
  10. Алгоритм управления звуковой волной, то есть, пока вы изучаете достаточно тщательно, вы можете перенести AU в сеть, чтобы реализовать это.

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

простая генерация звука

Веб-страницы, как правило, молчат, но когда вы пытаетесь генерировать звук для своих кликов, это может освежить клиентов в особых ситуациях. Этот пример в основном относится кЧжан Синьсюй — использование HTML5 Web Audio API для добавления звука при взаимодействии с веб-страницейприведенный пример. Давайте пройдемся по коду строка за строкой, чтобы увидеть, как достигается этот эффект.

1.  window.AudioContext = window.AudioContext || window.webkitAudioContext;
// 生成一个AudioContext对象
2.  var audioCtx = new AudioContext();
// 创建一个OscillatorNode, 它表示一个周期性波形(振荡),基本上来说创造了一个音调
3.  var oscillator = audioCtx.createOscillator();
 // 创建一个GainNode,它可以控制音频的总音量
4.  var gainNode = audioCtx.createGain();
// 把音量,音调和终节点进行关联
5.  oscillator.connect(gainNode);
// audioCtx.destination返回AudioDestinationNode对象,表示当前audio context中所有节点的最终节点,一般表示音频渲染设备
6.  gainNode.connect(audioCtx.destination);
// 指定音调的类型,其他还有square|triangle|sawtooth
7.  oscillator.type = 'sine';
// 设置当前播放声音的频率,也就是最终播放声音的调调
8.  oscillator.frequency.value = 196.00;
// 当前时间设置音量为0
9.  gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
 // 0.01秒后音量为1
10. gainNode.gain.linearRampToValueAtTime(1, audioCtx.currentTime + 0.01);
// 音调从当前时间开始播放
11. oscillator.start(audioCtx.currentTime);
// 1秒内声音慢慢降低,是个不错的停止声音的方法
12. gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1);
// 1秒后完全停止声音
13. oscillator.stop(audioCtx.currentTime + 1);

oscillator

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

Затем процесс создания тона выглядит следующим образом: установить форму волны --> установить частоту

форма волны

В основном есть 4 встроенных волновых формы, соответствующих разным звукам. Естьsine(синусоидальная волна),square(прямоугольная волна),triangle(треугольная волна) иsawtoothПилообразная волна. Конечно, вы также можете использовать его, если это необходимоsetPeriodicWaveПользовательские формы волны.

частота

С частотой все понятно. Это «до, ре, ми, фа, соль, ла, си», с которыми мы соприкасаемся в своей жизни Чем меньше значение, тем оно ниже, чем больше значение, тем оно четче.

connect

На самом деле, это очень важная концепция h5audio. В частности, я просто понимаю это как концепцию промежуточного программного обеспечения. Например, в этом примере промежуточное ПО, которое обрабатывает громкость после генерации тонов, а затем выводит эти звуковые ноды на динамики. (В других статьях есть очень подробные введения, и я не хочу здесь распространяться. Можно найти внешнюю ссылку внизу)

Все, что осталось, это настройки для постепенного появления и исчезновения и того, что воспроизводить звук. Эти части подробно описаны в блоге Чжан Да.Содержимое этого блока также упоминается в его сообщении в блоге для вторичного перевода, чтобы записать некоторые мои понимания (очень поверхностные), а затем обобщить.

Воспроизвести запись

Получить исходные данные PCM

Создание собственного голоса — это слишком высоко, а просто играть голосом немного скучно. Тогда давайте просто проиграем запись. Включить запись очень просто, нужна только такая простая строчка кода (конечно без учета совместимости)

navigator.mediaDevices.getUserMedia({audio: true, video: true})

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

navigator.mediaDevices.getUserMedia(videoObj, (stream) => {}, errBack)

Вы должны сразу спросить, куда делся записанный звук. После того, как мы вызываем этот метод из this, мы видим, что на самом деле посередине есть функция обратного вызова, которая позволяет нам получить поток данных, генерируемый микрофоном и камерой. В этот момент мы можем позвонитьAudioContextИнтерфейс заставляет аудиоданные PCM проходить через различные узлы обработки (усиление, сжатие и т. д.), прежде чем достичь места назначения, поэтому нам нужно начать здесь.

navigator.mediaDevices.getUserMedia({audio: true}, initRecorder)

function initRecorder(stream) {
    const AudioContext = window.AudioContext
    const audioContext = new AudioContext()
    // 创建MediaStreamAudioSourceNode对象,给定媒体流(例如,来自navigator.getUserMedia实例),然后可以播放和操作音频。
    const audioInput = audioContext.createMediaStreamSource(stream)
    // 缓冲区大小为4096,控制着多长时间需触发一次audioprocess事件
    const bufferSize = 4096
    // 创建一个javascriptNode,用于使用js直接操作音频数据
    // 第一个参数表示每一帧缓存的数据大小,可以是256, 512, 1024, 2048, 4096, 8192, 16384,值越小一帧的数据就越小,声音就越短,onaudioprocess 触发就越频繁。4096的数据大小大概是0.085s,就是说每过0.085s就触发一次onaudioprocess,第二,三个参数表示输入帧,和输出帧的通道数。这里表示2通道的输入和输出,当然我也可以采集1,4,5等通道
    const recorder = audioContext.createScriptProcessor(bufferSize, 1, 1)
    // 每个满足一个分片的buffer大小就会触发这个回调函数
    recorder.onaudioprocess = recorderProcess
    // const monitorGainNode = audioContext.createGain()
    // 延迟0.01秒输出到扬声器
    // monitorGainNode.gain.setTargetAtTime(音量, audioContext.currentTime, 0.01)
    // monitorGainNode.connect(audioContext.destination)
    // audioInput.connect(monitorGainNode)
    // const recordingGainNode = audioContext.createGain()
    // recordingGainNode.gain.setTargetAtTime(音量, audioContext.currentTime, 0.01)
    // recordingGainNode.connect(audioContext.scriptProcessorNode)
    // 将音频的数据流输出到这个jsNode对象中
    audioInput.connect(recorder)
    // 最后先音频流输出到扬声器。(将录音流原本的输出位置再定回原来的目标地)
    recorder.connect(audioContext.destination)
}

На этом шаге мы уже можем получить данные записи, и мы еще получаем данные буфера, которые были разделены согласно нашим ожиданиям. Затем мы, наконец, можем начать радостно обрабатывать наши потоки записи.

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

function recorderProcess(e) {
  // 左声道
  const left = e.inputBuffer.getChannelData(0);
}

обрати внимание наrecorderProcessкоторый вызываетgetChannelDataметод, вы можете передать целое число, получить данные соответствующего канала и обработать их отдельно. Поскольку мы записываем в моно, нам нужно получить поток данных только левого канала.

Если вам не нужно сейчас управлять аудиовыходом, вы можете напрямую использовать эти двоичные данные.websoketперевод на задний план.

Обработка и преобразование данных

Если уж совсем прискорбно, то данные, которые нужны закулисному начальству, совсем не то, что ваши вещи.У начальства есть требования к качеству звука: принимается только один wav-файл с частотой дискретизации 8 кГц и разрядностью 16. Очень хорошо, тогда, чтобы извлечь ключевые слова, нам нужно сначала подтвердить, что наши данные PCM имеют частоту 8 кГц и глубину цвета 16. Наконец, эти двоичные комбинации преобразуются в формат wav. Это выглядит сложно, ничего страшного, давайте пошагово.

Частота дискретизации (sampleRate) и разрядность (bitDepth)

Во-первых, какая частота дискретизации (sampleRate) у Baidu.

Частота дискретизации звука относится к тому, сколько раз записывающее устройство производит выборку звукового сигнала в одну секунду.Чем выше частота дискретизации, тем более реалистичным и естественным будет звук. На современных популярных картах захвата частота дискретизации обычно делится на три уровня: 22,05 кГц, 44,1 кГц и 48 кГц.22,05 кГц может обеспечить качество звука только FM-радио, 44,1 кГц является теоретическим пределом качества звука компакт-диска, а 48 кГц еще точнее Будьте точны.

Затем следующий код позволит вам получить частоту дискретизации вашего микрофона.

const AudioContext = window.AudioContext
const audioContext = new AudioContext()
// 可读属性
console.log(audioContext.sampleRate)
// 44100

Очень хорошо, этот выход означает, что частота дискретизации вашего записывающего оборудования достигает 44 100 Гц, поэтому вам необходимо уменьшить частоту дискретизации звука в соответствии с вашими потребностями. К сожалению, браузеры не позволяют изменять частоту дискретизации записи, и разные компьютерные устройства ведут себя по-разному. Это означает, что вам нужно снова обработать бинарник, полученный промежуточным узлом. Так как же уменьшить частоту дискретизации? Согласно приведенной выше информации Baidu, вы можете легко обнаружить, что частота дискретизации на самом деле просто зависит от того, сколько точек данных есть в аудио в одну секунду.Нам нужно уменьшить исходный поток данных из 44 100 точек в одну секунду в. поток данных 8000 точек. Это очень просто, не так ли, но есть небольшой момент, о котором нужно знать.

Downsample PCM audio from 44100 to 8000Смотрите ответ здесь: Давайте возьмем простой случай понижения дискретизации в 2 раза (например, 44100->22050). Наивным подходом было бы просто отбрасывать все остальные сэмплы. Но представьте на секунду, что в исходном файле 44,1 кГц был один синусоидальная волна присутствует на частоте 20 кГц. Она находится в пределах Найквиста (fs / 2 = 22050) для этой частоты дискретизации. После того, как вы отбросите все остальные сэмплы, она все еще будет на частоте 10 кГц, но теперь она будет выше Найквиста (fs / 2). =11025), и он будет накладываться на ваш выходной сигнал. Конечным результатом является то, что у вас будет большая толстая синусоида с частотой 8975 Гц! Чтобы избежать этого наложения во время субдискретизации, вам нужно сначала разработать фильтр нижних частот с отсечкой, выбранной в соответствии с вашим коэффициентом прореживания.В приведенном выше примере вы сначала обрезаете все выше 11025, а затем прореживаете.

Общий смысл здесь в том, что просто рисовать точки недостаточно. Конечно, внутренняя логика также включает в себя частоту и длину волны, поэтому я, естественно, не очень в этом разбираюсь. Заинтересованные друзья могут узнать больше о причинах. К счастью, здесь также предусмотрена реализация кода.

   function interleave(e){
      var t = e.length;
      sampleRate += 0.0;
      outputSampleRate += 0.0;
      var s = 0,
      o = sampleRate / outputSampleRate,
      u = Math.ceil(t * outputSampleRate / sampleRate),
      a = new Float32Array(u);
      for (i = 0; i < u; i++) {
        a[i] = e[Math.floor(s)];
        s += o;
      }
      return a;
    }

Тогда проблема частоты дискретизации решается. Остается два вопроса. Преобразование аудиопотока в 16-битную глубину здесь проще. Просто убедитесь, что вы генерируете достаточно битов. Например, для 8-битного звука требуется только генерироватьUint8Array, глубина 16 бит будет генерировать 2 длины.new Unit8Array(bitDepth / 8)

данные pcm в wav

Наконец добрался до последнего шага. как поставитьpcmданные дляwavданные. Я думал, что это будет чрезвычайно трудная проблема, но, к счастью.pcm->wavМетод очень простой, т.wavПосле открытия файла в двоичном режиме информация о заголовочном файле, которая удаляет первые 44 байта, представляет собой разделpcmданные. Затем нам просто нужно собрать весь контент в процессе записи, а затем вставить информацию о заголовочном файле. Этот шаг намного проще предыдущего.

Но нам еще предстоит решить проблему. «Как JavaScript манипулирует двоичными данными». Итак, мы представим эти вещи дальше.

Болб, ArrayBuffer, Unit8Array, DataView

После долгих поисков, просмотра исходного кода и просмотра API, я наконец собрал все объекты, связанные с этой операцией, и я могу вызвать дракона!

Эти несколько объектов действительно являются теми, которые Лао На изучала так долго и никогда не вступала в контакт с ними, а некоторые даже никогда о них не слышали. Итак, один за другим, и я могу предоставить только некоторые из моих первоначальных идей. (Добро пожаловать к большому парню, чтобы исправить меня, если вы хотите узнать больше, лучше искать в одиночку)

ArrayBufferЧто это.ArrayBufferТакже известен как типизированный массив. Типизированные массивы, я помню, видел их в статье, в которой подробно объяснялись массивы, но я забыл о них позже, потому что они были не очень полезны. Общий смысл в том, что объект-массив js не похож на реализацию других объектно-ориентированных языков, память не непрерывна, а тип неуправляем, что серьезно влияет на производительность. (Но все основные браузерные движки оптимизированы, поэтому реальную кодировку учитывать не нужно). иArrayBufferЭто двоичные данные, состоящие из 0 и 1, поэтому, когда вы захватите данные PCM, распечатайте их на консоли, и вы увидите, что этоArrayBufferМассив типов заключается в том, что этот раздел - это двоичные данные.

иArrayBufferОбъекты не предоставляют никакого способа чтения и записи памяти, но позволяют создавать «представления» поверх них для вставки и чтения данных в памяти. Так что же такое вид? ?

тип просмотра тип данных Занятые биты Занятые байты со знаком или без
Int8Array целое число 8 1 имеют
Uint8Array целое число 8 1 никто
Uint8ClampedArray целое число 8 1 никто
Int16Array целое число 16 2 имеют
Uint16Array целое число 16 2 никто
Int32Array целое число 32 4 имеют
Uint32Array целое число 32 4 никто
Float32Array целое число 32 4 \
Float64Array число с плавающей запятой 64 8 \

Это кошмар для старшего брата с очень плохой компьютерной базой. Так много дел, что мне делать. Как выбрать, ах, я вот-вот рухну. Но для работы с бинарником проще всего запросить библиотеку исходного кода. Эта вещь находится в исходном коде.

DataViewПосмотреть. Чтобы решить проблему путаницы при декодировании, вызванную разными настройками порядка байтов по умолчанию для различных аппаратных устройств, передачи данных и т. д.,javascriptпри условииDataViewВведите представление, чтобы позволить разработчикам вручную устанавливать тип порядка следования байтов при чтении и записи памяти. тогда.wavЗаголовок файла должен быть написан так. Если вы хотите знать, как это написать, вам все равно придется выйти из Baidu..wavКак выделить информационные байты заголовка файла

    var view = new DataView(wav.buffer)

    view.setUint32(0, 1380533830, false) // RIFF identifier 'RIFF'
    view.setUint32(4, 36 + dataLength, true) // file length minus RIFF identifier length and file description length
    view.setUint32(8, 1463899717, false) // RIFF type 'WAVE'
    view.setUint32(12, 1718449184, false) // format chunk identifier 'fmt '
    view.setUint32(16, 16, true) // format chunk length
    view.setUint16(20, 1, true) // sample format (raw)
    view.setUint16(22, this.numberOfChannels, true) // channel count
    view.setUint32(24, this.sampleRate, true) // sample rate
    view.setUint32(28, this.sampleRate * this.bytesPerSample * this.numberOfChannels, true) // byte rate (sample rate * block align)
    view.setUint16(32, this.bytesPerSample * this.numberOfChannels, true) // block align (channel count * bytes per sample)
    view.setUint16(34, this.bitDepth, true) // bits per sample
    view.setUint32(36, 1684108385, false) // data chunk identifier 'data'
    view.setUint32(40, dataLength, true) // data chunk length

Вышеупомянутое содержимое является методом записи информации заголовка. Наконец, данныеunit8ArrayФормат для записи двоичных данных wav. Все, что нам нужно, это преобразовать его в файловый объект.

BlobВозможно, вы не слышали об этом, ноFileВы, должно быть, слышали об этом, потому что часто приходится передавать файлы в форме. Тогда вы понимаетеBlobэтоJavaScriptтип объекта.HTML5объект файловой операции,fileобъектBlobветвь или подмножество .

поэтомуFileОбъект может выполнять разбиение и загрузку файлов, то есть использоватьBlobМетоды работы с бинарными данными.

new Bolb(wavData, { type: 'audio/wav' })

Проверьте эффект

Приведенный выше код представляет собой бинарную операцию, не слишком информативную. Тогда проверьте это! положить сгенерированныйblobобъект, сделайте это:const url = URL.createObjectURL(blob)тогда брось его<a>тег для загрузки файла. Инструменты командной строки:

$ file test.wav
test.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 8000 Hz

Полностью соответствует эффекту. Хорошо, оставшаяся проблема - вернуться к проблеме обработки данных PCM в реальном времени.

передача массива буфера

Мы будем передавать файл, но как передать массив бинарных буферов? только если вы используетеblobСгенерированный объект можно использовать как файловый объект. Но как мы можем передать его как файл, если мы все еще являемся сегментом данных PCM.

Вот лишь небольшой список моих собственных попыток, не обязательно правильный способ его использования. используется здесьwebsocket

// client
const formData = new FormData()
formData.append(blob, new Blob(arrayBuffer))

// server
console.log(ctx.args[0]) // ArrayBuffer<xx,xxx>

Сделайте небольшую оптимизацию

Web WorkerУзнайте, как это связано с перекодированием файлов. Так что это очень требовательно к производительности, и пришло время убратьweb WorkerГлубоко оптимизировать это дело.

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

Здесь записано лишь несколько питов, и я не буду их подробно представлять:

  1. Веб-воркер должен обслуживаться сервером. Рабочий процесс не может быть открыт при использовании простого html-файла. Рабочий должен пройти.jsПуть к файлу и политика одинакового происхождения гарантируются.
  2. Не передавайте функции воркерам, иначе возникнут ошибки. И браузер еще не выдает ошибку. После долгого расследования
  3. Рабочий должен пройти путь js. В Vue вы можете поместить его в статический файл только для ссылки на мяу.

Здесь все еще есть небольшие проблемы

  1. Как отобразить громкость записи в реальном времени. Прежде всего, размер буфера, который может получить том, используется в качестве эталона в децибелах. Однако конкретный метод реализации не совсем понятен или не существует простого метода реализации.
  2. Как сделать воспроизведение в реальном времени, когда внешний интерфейс получает буфер?Способ сделать это сейчас — встроить его обратно в конец. Я провел небольшой эксперимент, но он не сработал. (Напрямую преобразовать преобразованное содержимое записи обратно в ArrayBuffer, но не удалось). Такое ощущение, что это должно быть возможно, если есть бэкэнд для передачи данных.
audioContext.decodeAudioData(play_queue[index_play], function(buffer) {//解码成pcm流
            var audioBufferSouceNode = audioContext.createBufferSource();
            audioBufferSouceNode.buffer = buffer;
            audioBufferSouceNode.connect(audioContext.destination);
            audioBufferSouceNode.start(0);
        }, function(e) {
            console.log("failed to decode the file");
        });
  1. Как рассчитать текущую реализацию звукозаписи в реальном времени. Поскольку поток данных не может быть преобразован в звуковой объект, эти методы не могут быть получены. Теперь грубый метод состоит в том, чтобы вычислить длину байта, частоту дискретизации и количество битов, но я чувствую, что объем вычислений относительно велик и не является хорошей реализацией?
  2. Не удалось вызвать API для изменения высоты тона и положения звука.
const filterNode = audioContext.createBiquadFilter();
// ...
source.connect(filterNode);
filterNode.connect(source.destination);

const updateFrequency = frequency => filterNode.frequency.value = frequency;

const rangeX = document.querySelector('input[name="rangeX"]');
const source = audioContext.createBufferSource();
const pannerNode = audioContext.createPanner();

source.connect(pannerNode);
pannerNode.connect(source.destination);

rangeX.addEventListener('input', () => pannerNode.setPosition(rangeX.value, 0, 0));

Вырезать аудиофайлы на стороне узла

Вот немного дополнительных знаний. Нет особенно хорошей библиотеки для обработки звука на стороне узла. После долгих запросов я нашел среду вызывающей машиныffmpegБиблиотека для помощи в обработке. По крайней мере, это не проблема использовать, и это очень всеобъемлющий. (В конце концов, люди, имеющие дело со звуком, должны знать этот материал.) Вот приблизительная отметка на API, который я использовал раньше.

      var command = ffmpeg(filePath)
        // .seekInput(60.0) // 开始切割的时间
        .seekInput(7.875) // 开始切割的时间,延迟7秒?为啥
        .duration(4.125) // 需要切割的音频时长
        .save(path.join(__dirname, '../../../app/public/test-1.wav'))
        .on('end', () => {
          console.log('finish')
        })
        .run()

Наконец, я хотел бы поблагодарить г-на Фу и г-на Ту за терпеливое разъяснение мне различных вопросов, связанных со звуком. Спасибо фронтенд-команде за терпеливое обсуждение со мной проблем, несмотря на то, что у меня не было слишком много контактов с контентом, который я не понимал.


Справочные статьи: все статьи, перечисленные здесь, — это хорошие статьи, которые я наткнулся на поиск информации. Это не обязательно тесно связано с вышеперечисленным контентом. Рекомендуется, чтобы друзья, которые интересуются аудио, могли увидеть его сами.

Пример записи с использованием HTML5

Getting Started with Web Audio API

Using Recorder.js to capture WAV audio in HTML5 and upload it to your server or download locally

Управление записью HTML5

chris-rudmin/opus-recorder

Tutorial: HTML Audio Capture streaming to Node.js (no browser extensions)

[Внешнее руководство] HTML5 делает забавный детектор громкости микрофона (API веб-аудио)

Downsample PCM audio from 44100 to 8000 DataView Typed Arrays: Binary Data in the Browser Технический совет: частота дискретизации и разрядность — введение в сэмплирование How to convert ArrayBuffer to and from String Используйте html5-audio-api для разработки 3D-звуковых эффектов и микширования звука для игр. Понимание типов данных DOMString, Document, FormData, Blob, File, ArrayBuffer. Углубленный веб-аудио API