Как реализовать функцию фронтальной записи

внешний интерфейс WebRTC

Есть два способа реализовать запись во внешнем интерфейсе, один из них — использовать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, что может еще больше повысить эффективность и предотвратить зависание файла записи при последующей обработке.