Есть два способа реализовать запись во внешнем интерфейсе, один из них — использоватьMediaRecorder, другой - использовать getUserMedia WebRTC в сочетании с AudioContext, MediaRecorder появился раньше, но такие браузеры, как Safari/Edge, не были реализованы, поэтому совместимость не очень хорошая, и WebRTC поддерживается всеми основными браузерами, такими как Поддерживается с Сафари 11. Поэтому мы используем WebRTC для записи.
Используя AudioContext для воспроизведения звука, у меня естьChrome 66 отключает звук после автозапуска", в этой статье мы продолжим использовать API AudioContext.
Чтобы реализовать функцию записи, давайте начнем с воспроизведения музыки из локального файла, потому что некоторые API-интерфейсы являются общими.
1. Реализация воспроизведения локального аудиофайла
Для воспроизведения звука можно использовать аудиотег или AudioContext. Для аудиотега требуется URL-адрес, который может быть URL-адресом удаленного протокола HTTP или URL-адресом локального протокола большого двоичного объекта. Как создать локальный URL-адрес?
Используйте следующий HTML-код в качестве иллюстрации:
<input type="file" onchange="playMusic.call(this)" class="select-file">
<audio class="audio-node" autoplay></audio>
Предоставляет элемент управления загрузкой ввода файла, чтобы пользователь мог выбрать локальный файл и звуковой тег, готовый к воспроизведению. Когда пользователь выбирает файл, запускается событие onchange, и содержимое файла может быть получено в обратном вызове onchange, как показано в следующем коде:
function playMusic () {
if (!this.value) {
return;
}
let fileReader = new FileReader();
let file = this.files[0];
fileReader.onload = function () {
let arrayBuffer = this.result;
console.log(arrayBuffer);
}
fileReader.readAsArrayBuffer(this.files[0]);
}
Здесь FileReader используется для чтения файла, чтения его как ArrayBuffer, то есть исходного двоичного содержимого, и вывода его следующим образом:
Вы можете использовать этот ArrayBuffer для создания экземпляра Uint8Array для чтения его содержимого.Каждый элемент в массиве Uint8Array представляет собой целое 8-битное число без знака, то есть 0 ~ 255, что эквивалентно содержимому 0101 на байт. целое число. Для более подробного обсуждения см. этоВнешняя локальная файловая операция и загрузка".
Этот массивBuffer можно преобразовать в большой двоичный объект, а затем использовать этот большой двоичный объект для создания URL-адреса, как показано в следующем коде:
fileReader.onload = function () {
let arrayBuffer = this.result;
// 转成一个blob
let blob = new Blob([new Int8Array(this.result)]);
// 生成一个本地的blob url
let blobUrl = URL.createObjectURL(blob);
console.log(blobUrl);
// 给audio标签的src属性
document.querySelector('.audio-node').src = blobUrl;
}
API URL.createObjectURL в основном используется для создания URL-адреса большого двоичного объекта. URL-адрес печатается следующим образом:
blob:null/c2df9f4d-a19d-4016-9fb6-b4899bac630d
Затем добавьте его в тег audio для воспроизведения, что эквивалентно удаленному URL-адресу http.
При использовании ArrayBuffer для создания объектов больших двоичных объектов вы можете указать тип файла или тип MIME, как показано в следующем коде:
let blob = new Blob([new Int8Array(this.result)], {
type: 'audio/mp3' // files[0].type
});
Этот mime можно получить через files[0].type входного файла, а files[0] является экземпляром File.File имеет тип mime, а Blob имеет его, потому что File наследуется от Blob, и эти два являются тот же корень. Таким образом, в приведенном выше коде реализации на самом деле нет необходимости читать его как ArrayBuffer, а затем инкапсулировать в большой двоичный объект, просто используйте File напрямую, как показано в следующем коде:
function playMusic () {
if (!this.value) {
return;
}
// 直接使用File对象生成blob url
let blobUrl = URL.createObjectURL(this.files[0]);
document.querySelector('.audio-node').src = blobUrl;
}
Чтобы использовать AudioContext, вам нужно получить содержимое файла, а затем вручную декодировать аудио для воспроизведения.
2. Модель аудиоконтекста
Как использовать AudioContext для воспроизведения звука, разберем его модель, как показано на следующем рисунке:
После того, как мы получим ArrayBuffer, используйте decodeAudioData AudioContext для декодирования, создания экземпляра AudioBuffer и использования его в качестве свойства буфера объекта AudioBufferSourceNode. Этот узел наследуется от AudioNode. Он также имеет два метода: подключение и запуск. Перед началом воспроизведения необходимо настроить соединение и подключить этот узел к audioContext.destination, устройству громкоговорителя. Код выглядит следующим образом:
function play (arrayBuffer) {
// Safari需要使用webkit前缀
let AudioContext = window.AudioContext || window.webkitAudioContext,
audioContext = new AudioContext();
// 创建一个AudioBufferSourceNode对象,使用AudioContext的工厂函数创建
let audioNode = audioContext.createBufferSource();
// 解码音频,可以使用Promise,但是较老的Safari需要使用回调
audioContext.decodeAudioData(arrayBuffer, function (audioBuffer) {
console.log(audioBuffer);
audioNode.buffer = audioBuffer;
audioNode.connect(audioContext.destination);
// 从0s开始播放
audioNode.start(0);
});
}
fileReader.onload = function () {
let arrayBuffer = this.result;
play(arrayBuffer);
}
Распечатайте декодированный аудиобуфер, как показано на следующем рисунке:
Он имеет несколько свойств, видимых разработчикам, включая продолжительность звука, количество каналов и частоту дискретизации. Из распечатанных результатов мы можем узнать, что воспроизводимый звук — это 2 канала, частота дискретизации — 44,1 кГц, а продолжительность — 196,8 с. Значение этих атрибутов звука можно найти в "Реализация потоковой передачи аудио/видео из исходного кода Chrome".
Как видно из приведенного выше кода, использование AudioContext для обработки звука имеет очень важный опорный элемент AudioNode, который использует AudioBufferSourceNode, и его данные поступают из декодированного полного буфера. Другими унаследованными от AudioNode являются GainNode: используется для установки громкости, BiquadFilterNode: используется для фильтрации, ScriptProcessorNode: обеспечивает обратный вызов аудиопроцесса для анализа и обработки аудиоданных, MediaStreamAudioSourceNode: используется для подключения микрофонных устройств и т. д. Эти узлы могут использовать режим декоратора для соединения слой за слоем.Например, bufferSourceNode, используемый в приведенном выше коде, может быть сначала подключен к GainNode, а затем GainNode подключен к динамику, и громкость может быть отрегулирована.
Как показано на рисунке ниже:
Эти узлы создаются с помощью фабричной функции audioContext, такой как вызов createGainNode для создания GainNode.
Сказав так много, это подготовка к записи, а запись должна использовать ScriptProcessorNode.
3. Осуществление записи
Источником музыки, воспроизводимой выше, является локальный аудиофайл, а источником записи — микрофон.Чтобы получить микрофон и получить данные, вам необходимо использовать getUserMedia WebRTC, как показано в следующем коде;
<button onclick="record()">开始录音</button>
<script>
function record () {
window.navigator.mediaDevices.getUserMedia({
audio: true
}).then(mediaStream => {
console.log(mediaStream);
beginRecord(mediaStream);
}).catch(err => {
// 如果用户电脑没有麦克风设备或者用户拒绝了,或者连接出问题了等
// 这里都会抛异常,并且通过err.name可以知道是哪种类型的错误
console.error(err);
}) ;
}
</script>
При вызове getUserMedia указываем, что нужно записывать аудио.Если нужно одновременно записывать видео, можно добавить видео: true.Также можно указать формат записи:
window.navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 44100, // 采样率
channelCount: 2, // 声道
volume: 1.0 // 音量
}
}).then(mediaStream => {
console.log(mediaStream);
});
При вызове в браузере появится окно с вопросом, разрешить ли использование микрофона:
Если пользователь нажмет «Отклонить», будет выброшено исключение, которое можно поймать в catch, и если все в порядке, будет возвращен объект MediaStream:
Это абстракция аудиопотока.Используйте этот поток для инициализации объекта MediaStreamAudioSourceNode, затем подключите этот узел к JavascriptProcessorNode, получите аудиоданные в его onaudioprocess, а затем сохраните его, чтобы получить записанные данные.
Если вы хотите воспроизвести записанный звук напрямую, просто подключите его к динамику, как показано в следующем коде:
function beginRecord (mediaStream) {
let audioContext = new (window.AudioContext || window.webkitAudioContext);
let mediaNode = audioContext.createMediaStreamSource(mediaStream);
// 这里connect之后就会自动播放了
mediaNode.connect(audioContext.destination);
}
Однако, если вы записываете и играете одновременно, если вы не используете наушники, легко получить эхо, поэтому не играйте здесь.
Чтобы получить данные записанного звука, мы подключаем его к javascriptProcessorNode и сначала создаем для него экземпляр:
function createJSNode (audioContext) {
const BUFFER_SIZE = 4096;
const INPUT_CHANNEL_COUNT = 2;
const OUTPUT_CHANNEL_COUNT = 2;
// createJavaScriptNode已被废弃
let creator = audioContext.createScriptProcessor || audioContext.createJavaScriptNode;
creator = creator.bind(audioContext);
return creator(BUFFER_SIZE,
INPUT_CHANNEL_COUNT, OUTPUT_CHANNEL_COUNT);
}
Вот объект, созданный с помощью createScriptProcessor, и необходимо передать три параметра: один — размер буфера, который обычно устанавливается равным 4 КБ, а два других — количество входных и выходных каналов, которые здесь установлены на два канала. В нем есть два буфера, один — входной inputBuffer, а другой — выходной outputBuffer, которые являются экземплярами AudioBuffer. Данные inputBuffer можно получить в обратном вызове onaudioprocess, обработать и затем поместить в outputBuffer, как показано на следующем рисунке:
Например, мы можем подключить bufferSourceNode, который использовался для воспроизведения звука на шаге 1, к jsNode, а затем подключить jsNode к динамику, чтобы звуковые данные можно было обрабатывать пакетами в обратном вызове процесса, например, для шумоподавления. Когда динамик использует выходной буфер размером 4 КБ, будет запущен обратный вызов процесса. Таким образом, обратный вызов процесса срабатывает постоянно.
В примере записи нам нужно подключить mediaNode к этому jsNode, а затем получить данные записи и непрерывно помещать данные в массив, пока запись не будет остановлена. Как показано в следующем коде:
function onAudioProcess (event) {
console.log(event.inputBuffer);
}
function beginRecord (mediaStream) {
let audioContext = new (window.AudioContext || window.webkitAudioContext);
let mediaNode = audioContext.createMediaStreamSource(mediaStream);
// 创建一个jsNode
let jsNode = createJSNode(audioContext);
// 需要连到扬声器消费掉outputBuffer,process回调才能触发
// 并且由于不给outputBuffer设置内容,所以扬声器不会播放出声音
jsNode.connect(audioContext.destination);
jsNode.onaudioprocess = onAudioProcess;
// 把mediaNode连接到jsNode
mediaNode.connect(jsNode);
}
Мы распечатываем inputBuffer и видим, что каждый сегмент составляет около 0,09 с:
То есть он будет срабатывать каждые 0,09 секунды. Следующая задача — постоянно сохранять записанные данные в обратном вызове процесса, как показано в следующем коде, для получения данных левого и правого каналов соответственно:
function onAudioProcess (event) {
let audioBuffer = event.inputBuffer;
let leftChannelData = audioBuffer.getChannelData(0),
rightChannelData = audioBuffer.getChannelData(1);
console.log(leftChannelData, rightChannelData);
}
Когда вы распечатаете его, вы увидите, что это Float32Array, то есть каждое число в массиве представляет собой 32-разрядное число с плавающей запятой одинарной точности, как показано на следующем рисунке:
Здесь возникает вопрос,что представляют собой записанные данные?Они дискретизируются для обозначения силы звука.Звуковые волны преобразуются микрофоном в токовые сигналы разной силы.Эти числа представляют собой силу сигнала. Его диапазон значений — [-1, 1], что указывает на относительную шкалу.
Затем продолжайте вводить в массив:
let leftDataList = [],
rightDataList = [];
function onAudioProcess (event) {
let audioBuffer = event.inputBuffer;
let leftChannelData = audioBuffer.getChannelData(0),
rightChannelData = audioBuffer.getChannelData(1);
// 需要克隆一下
leftDataList.push(leftChannelData.slice(0));
rightDataList.push(rightChannelData.slice(0));
}
Наконец добавьте кнопку, чтобы остановить запись и ответить на операцию:
function stopRecord () {
// 停止录音
mediaStream.getAudioTracks()[0].stop();
mediaNode.disconnect();
jsNode.disconnect();
console.log(leftDataList, rightDataList);
}
Сохраненные данные распечатываются следующим образом:
В обычном массиве много Float32Array, а потом они объединяются в один Float32Array:
function mergeArray (list) {
let length = list.length * list[0].length;
let data = new Float32Array(length),
offset = 0;
for (let i = 0; i < list.length; i++) {
data.set(list[i], offset);
offset += list[i].length;
}
return data;
}
function stopRecord () {
// 停止录音
let leftData = mergeArray(leftDataList),
rightData = mergeArray(rightDataList);
}
Тогда почему бы не сделать его одним прямо в начале, потому что такой массив нелегко расширять. Я не знаю общую длину массива в начале, потому что не уверен, как долго он будет записываться, поэтому удобнее его слить, когда запись закончится.
Затем объедините данные левого и правого каналов.Когда сохраняется формат wav, это не левый канал, а затем правый канал, а данные левого канала и данные правого канала чередуются, как показано в следующем коде. :
// 交叉合并左右声道的数据
function interleaveLeftAndRight (left, right) {
let totalLength = left.length + right.length;
let data = new Float32Array(totalLength);
for (let i = 0; i < left.length; i++) {
let k = i * 2;
data[k] = left[i];
data[k + 1] = right[i];
}
return data;
}
Наконец, создайте файл wav, сначала запишите информацию заголовка wav, включая настройку канала, частоту дискретизации, бит звука и т. д., как показано в следующем коде:
function createWavFile (audioData) {
const WAV_HEAD_SIZE = 44;
let buffer = new ArrayBuffer(audioData.length * 2 + WAV_HEAD_SIZE),
// 需要用一个view来操控buffer
view = new DataView(buffer);
// 写入wav头部信息
// RIFF chunk descriptor/identifier
writeUTFBytes(view, 0, 'RIFF');
// RIFF chunk length
view.setUint32(4, 44 + audioData.length * 2, true);
// RIFF type
writeUTFBytes(view, 8, 'WAVE');
// format chunk identifier
// FMT sub-chunk
writeUTFBytes(view, 12, 'fmt ');
// format chunk length
view.setUint32(16, 16, true);
// sample format (raw)
view.setUint16(20, 1, true);
// stereo (2 channels)
view.setUint16(22, 2, true);
// sample rate
view.setUint32(24, 44100, true);
// byte rate (sample rate * block align)
view.setUint32(28, 44100 * 2, true);
// block align (channel count * bytes per sample)
view.setUint16(32, 2 * 2, true);
// bits per sample
view.setUint16(34, 16, true);
// data sub-chunk
// data chunk identifier
writeUTFBytes(view, 36, 'data');
// data chunk length
view.setUint32(40, audioData.length * 2, true);
}
function writeUTFBytes (view, offset, string) {
var lng = string.length;
for (var i = 0; i < lng; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
Затем запишите данные записи. Мы собираемся записать 16-битную глубину, то есть использовать 16-битный двоичный код для представления силы звука. Диапазон 16-битного представления составляет [-32768, +32767], и максимальное значение – 32 767 или 0x7FFF. Диапазон значений – [-1, 1], что представляет собой относительное соотношение. Умножение этого отношения на максимальное значение представляет собой фактическое значение, которое необходимо сохранить. Как показано в следующем коде:
function createWavFile (audioData) {
// 写入wav头部,代码同上
// 写入PCM数据
let length = audioData.length;
let index = 44;
let volume = 1;
for (let i = 0; i < length; i++) {
view.setInt16(index, audioData[i] * (0x7FFF * volume), true);
index += 2;
}
return buffer;
}
Наконец, используйте URL-адрес большого двоичного объекта, упомянутый в пункте 1, для создания локально воспроизводимого URL-адреса большого двоичного объекта для воспроизведения только что записанного звука, как показано в следующем коде:
function playRecord (arrayBuffer) {
let blob = new Blob([new Uint8Array(arrayBuffer)]);
let blobUrl = URL.createObjectURL(blob);
document.querySelector('.audio-node').src = blobUrl;
}
function stopRecord () {
// 停止录音
let leftData = mergeArray(leftDataList),
rightData = mergeArray(rightDataList);
let allData = interleaveLeftAndRight(leftData, rightData);
let wavBuffer = createWavFile(allData);
playRecord(wavBuffer);
}
Или загрузите большой двоичный объект с помощью FormData.
Реализация всей записи в основном закончена, и код ссылается на библиотеку записиRecordRTC.
4. Резюме
Напомним, что общий процесс выглядит следующим образом:
Сначала вызовите getUserMediaStream из webRTC, чтобы получить аудиопоток, используйте этот поток для инициализации mediaNode, подключите его к jsNode и непрерывно получайте записанные данные в обратном вызове процесса jsNode.После остановки записи объедините эти данные в 16 бит. , Целочисленные данные и запись информации заголовка wav для создания буфера памяти аудиофайла wav, инкапсуляции буфера в файл Blob, создания URL-адреса, а затем воспроизведения его локально или загрузки с помощью FormData. Этот процесс не очень сложен для понимания.
В этой статье используются API-интерфейсы WebRTC и AudioContext, основное внимание уделяется общей модели AudioContext и известно, что аудиоданные на самом деле являются записью силы звука.При сохранении они преобразуются в 16-битную глубину путем умножения максимального значения 16 -битное целое представление. В то же время ссылку на локальные данные можно создать с помощью blob и URL.createObjectURL.
В конце библиотеки записи RecordRTC также используется веб-воркер для объединения данных левого и правого каналов и создания файла wav, что может еще больше повысить эффективность и предотвратить зависание файла записи при последующей обработке.