В связи с потребностями проекта нам необходимо реализовать функцию записи на веб-стороне. Вначале было найдено два решения: одно через iframe, а другое через API getUserMedia из html5. Поскольку наша функция записи не обязательно должна быть совместима с браузером IE, мы без колебаний выбрали getUserMedia, предоставляемый html5, для ее реализации. Основная идея состоит в том, чтобы объединить ее с официальной документацией API и некоторыми решениями, найденными в Интернете, чтобы создать решение, соответствующее потребностям проекта. Но поскольку нам нужно было обеспечить возможность включения функции записи одновременно и на пэде, и на ПК, возникли некоторые подводные камни. Далее идет процесс восстановления.
Шаг 1Потому что новый API передает navigator.mediaDevices.getUserMedia и возвращает обещание.
Старый API — navigator.getUserMedia, поэтому совместимость была обеспечена. Код выглядит следующим образом:
// Старые браузеры могут вообще не реализовывать mediaDevices, поэтому мы можем сначала установить пустой объект if (navigator.mediaDevices === undefine) { navigator.mediaDevices = {};}// Некоторые браузеры частично поддерживают mediaDevices. Мы не можем напрямую установить getUserMedia// для объекта, поскольку это может перезаписать существующие свойства. Здесь мы добавим атрибут getUserMedia только в том случае, если он не существует. if (navigator.mediaDevices.getUserMedia === undefine) { let getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; navigator.mediaDevices.getUserMedia = function(constraints) { // Во-первых, если есть getUserMedia, получите его // Некоторые браузеры вообще его не реализуют — затем верните ошибку при отклонении обещания для поддержания единого интерфейса if (!getUserMedia) { return Promise.reject(new Error(' getUserMedia не реализована в этом браузере')); // В противном случае оберните Promise для старого метода navigator.getUserMedia return new Promise(function(resolve, отклонить) { getUserMedia.call (навигатор, ограничения, разрешить, отклонить });Шаг 2
Это метод, существующий в Интернете и инкапсулирующий HZRecorder. В основном ссылаясь на этот метод. Интерфейс записи можно вызвать, вызвав HZRecorder.get. Этот метод передает функцию обратного вызова. После создания нового HZRecorder выполняется функция обратного вызова и передается материализованный объект HZRecorder. Такие функции, как начало записи, приостановка, остановка и воспроизведение, могут быть реализованы с помощью методов этого объекта.
var HZRecorder = function (stream, config) { config = config || {}; config.sampleBits = config.sampleBits || 8; // биты выборки 8, 16 config.sampleRate || (44100/6) ; //Частота дискретизации (1/6 44100) //Создаем объект аудиосреды audioContext = window.AudioContext || window.webkitAudioContext; var context = new audioContext(); //Вводим звук в этот объект var audioInput = context.createMediaStreamSource(stream); //Устанавливаем узел громкости var Volume = context.createGain(); .connect(volume); //Создаем кеш для кеширования звука var bufferSize = 4096 //Создаем узел кеширования звука, метод createScriptProcessor // Второй и третий параметры относятся к тому, что вход и выход являются бинауральными. var Recorder = context.createScriptProcessor(bufferSize, 2, 2); var audioData = { size: 0 //Длина файла записи, буфер: [] //Кэш записи, inputSampleRate: context.sampleRate //Частота дискретизации ввода, inputSampleBits: 16 //Входные цифры выборки 8, 16, outputSampleRate: config.sampleRate //Выходная частота выборки, oututSampleBits: config.sampleBits //Выходные биты выборки 8, 16, input: function (data) { this.buffer.push(new Float32Array(data)); this.size += data.length }, compress: function () { / /Сжатие слияния//Объединить данные var = new Float32Array(this.size); var offset = 0; for (var i = 0; i < this.buffer.length; i++) { data.set(this.buffer[i], offset); offset += this.buffer[i].length } //Сжатие var Compression = parseInt(this.inputSampleRate / this.outputSampleRate); / сжатие var = новый Float32Array (длина); var index = 0, j = 0; while (index < length) { result[index] = data[j]; j += сжатие; index++; } вернуть результат; }, encodeWAV: function () { var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate); var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits = this); .compress(); var dataLength = bytes.length * (sampleBits / 8 буфер = новый); ArrayBuffer (44 + dataLength); var data = new DataView (buffer); var ChannelCount = 1; // Mono var offset = 0; var writeString = function (str) {for (var i = 0; i <str. length; i++) { data.setUint8(offset + i, str.charCodeAt(i) } } // Идентификатор файла обмена ресурсами writeString('RIFF'); offset += 4; //Общее количество байт от следующего адреса до конца файла, то есть размер файла -8 data.setUint32(offset, 36 + dataLength, true offset += 4; / Флаг файла WAV writeString(' WAVE'); // Флаг формата сигнала writeString('fmt '); // Фильтровать байты, обычно 0x10 = 16; data.setUint32(offset, 16, true); offset += 4; // Категория формата (данные выборки формата PCM) data.setUint16(offset, 1, true); // Количество каналов data.setUint16; ( offset,channelCount, true); offset += 2; // Частота дискретизации, количество выборок в секунду, представляет скорость воспроизведения каждого канала data.setUint32(offset, sampleRate, true); offset += 4; // Скорость передачи данных сигнала (в среднем в байтах в секунду) Mono × бит данных в секунду × бит данных на выборку/8 data.setUint32(offset,channelCount * sampleRate * (sampleBits / 8), true); ); offset += 4; // Количество байтов, занимаемых выборкой числа быстрой корректировки данных за один раз, равно моно × количество битов данных на выборку/8 data.setUint16(offset,channelCount *); (sampleBits / 8), true); offset += 2; // Количество битов данных на выборку data.setUint16(offset, sampleBits, true); // Идентификатор данных writeString('data'); += 4; //Общее количество выборочных данных, то есть общий размер данных -44 data.setUint32(offset, dataLength, true); //Запись выборочных данных if (sampleBits); === 8) { for (var i = 0; i < bytes.length; i++, offset++) { var s = Math.max(-1, Math.min(1, bytes[i])); s <0 ? s * 0x8000 : s * 0x7FFF; val = parseInt(255/(65535/(val + 32768))); data.setInt8(offset, val, true } } else { for (var i = 0; i < bytes.length; i++, offset += 2) { var s = Math.max(-1) , Math.min(1, bytes[i])); data.setInt16(смещение, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } return new Blob([data], { type: 'audio/wav' } }); //Начнем запись this.start = function () { audioInput.connect(recorder); Connect(context.destination); //Остановим это.stop = function () { Recorder.disconnect() }; //Завершим это.end = function() { context.close(); } //Продолжить this.again = function() { Recorder.connect(context.destination } //Получаем аудиофайл this.getBlob = function () { this.stop(); return audioData.encodeWAV(); ; } // Воспроизведение this.play = function (audio) { audio.src = window.URL.createObjectURL(this.getBlob()); // Загрузите this.upload = function (url, обратный вызов) { var fd = new FormData(); fd.append('audioData', this.getBlob()); var xhr = new XMLHttpRequest(); if (обратный вызов) { xhr.upload.addEventListener('progress', function (e) ) { callback('uploading', e }, false); xhr.addEventListener('load', function (e) { callback('ok', e); }, false); xhr.addEventListener('error', function (e) { callback('error', e); }, false); xhr.addEventListener('abort', function); (е) { обратный вызов ('отмена', е); ложь } xhr.open('POST', url); // Коллекция аудио Recorder.onaudioprocess = function (e) { audioData.input(e.inputBuffer.getChannelData(0)); //record(e.inputBuffer.getChannelData(0) }; .throwError = function (message) { throw new function () { this.toString = function () { return message };}; //Поддерживается ли запись HZRecorder.canRecording = (navigator.getUserMedia != null //Получаем рекордер HZRecorder.get = function (callback, config) { if (callback) { navigator.mediaDevices .getUserMedia({ audio: true); }) .then(function(stream) { let Rec = new HZRecorder(stream, config); обратный вызов(rec); }) .catch(function(error) { HZRecorder.throwError('Невозможно выполнить запись, проверьте состояние устройства'); }}; window.HZRecorder = HZRecorder;
Вышеупомянутое уже может удовлетворить большинство потребностей. Но нам нужно быть совместимым со стороной площадки. У нас есть несколько проблем с нашим планшетом, которые необходимо решить.
Ниже приведены решения обеих проблем.
Шаг 3Ниже приведено решение для реализации формата записи как mp3 и window.URL.createObjectURL для передачи данных blob и сообщения об ошибке на стороне панели.
1. Измените код объекта audioData в HZRecorder. И представьте js-файл lamejs.js от замечательного человека из Интернета.
const lame = new lamejs();let audioData = { sampleMono: null, maxSamples: 1152, mp3Encoder: new lame.Mp3Encoder(1, context.sampleRate || 44100, config.bitRate || 128), dataBuffer: [], size : 0, // Буфер записи длины файла: [], // Буфер записи inputSampleRate: context.sampleRate, // Частота входной выборки inputSampleBits: 16, // Входные цифры выборки 8, 16 outputSampleRate: config.sampleRate, // Выходная частота дискретизации inputSampleBits: config.sampleBits, // Выходные цифры выборки 8, 16 ConvertBuffer: функция (arrayBuffer) {let data = new Float32Array(arrayBuffer); let out = new Int16Array(arrayBuffer.length); this.floatTo16BitPCM(data, out); return out; }, floatTo16BitPCM: function(input, output) { for (let i = 0; i < input. length; i++) { let s = Math.max(-1, Math.min(1, input[i])); = s < 0 ? s * 0x8000 : s * 0x7fff; } }, addToBuffer: function(mp3Buf) { this.dataBuffer.push(new Int8Array(mp3Buf) }, encode: function(arrayBuffer) { this.samplesMono = this .convertBuffer(arrayBuffer); пусть осталось = this.samplesMono.length; (let i = 0; rest >= 0; i += this.maxSamples) { let left = this.samplesMono.subarray(i, i + this.maxSamples); let mp3buf = this.mp3Encoder.encodeBuffer(left); .appendToBuffer(mp3buf); остаток -= this.maxSamples } }, закончить: function() {; this.appendToBuffer(this.mp3Encoder.flush()); return new Blob(this.dataBuffer, { type: 'audio/mp3' }), input: function(data) { this.buffer.push(new Float32Array() data)); this.size += data.length }, compress: function() { // Сжатие слиянием // Слияние let data = new Float32Array(this.size); let offset = 0; for (let i = 0; i < this.buffer.length; i++) { data.set(this.buffer[i], offset += this.buffer); [i].length } // Сжатие let сжатие = parseInt(this.inputSampleRate / this.outputSampleRate, 10); пусть длина = data.length / сжатие; Float32Array(length); let index = 0; while (index < length) { result[index] = data[j]; j += index++; return result; { let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate); let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits); пусть байты = this.compress(); пусть dataLength = bytes.length * (sampleBits / 8); пусть буфер = новый ArrayBuffer (44 + dataLength); пусть данные = новый DataView (буфер); 1; //моно let offset = 0; let writeString = function(str) { for (let i = 0; i < str.length; i++) { data.setUint8(offset + i, str.charCodeAt(i)); // Идентификатор файла обмена ресурсами writeString('RIFF'); // Общее количество байтов от следующего адреса до конца. номер файла, то есть размер файла - 8 data.setUint32(offset, 36 + dataLength, true); // флаг файла WAV writeString('WAVE'); // Флаг формата волны writeString('fmt '); // Байты фильтра, обычно 0x10 = 16 data.setUint32(offset, 16, true); // Категория формата (выборка формы PCM); data) data.setUint16(offset, 1, true); offset += 2; // Номер канала data.setUint16(offset,channelCount, true); += 2; // Частота дискретизации, количество выборок в секунду, указывающее скорость воспроизведения каждого канала data.setUint32(offset, sampleRate, true offset += 4; // Скорость передачи данных сигнала (среднее количество байт в секунду); ) Моно × бит данных в секунду × бит данных на выборку / 8 data.setUint32(offset,channelCount * sampleRate * (sampleBits/8), true offset += 4; Число байтов быстрой корректировки данных, занимаемое одним моноканалом выборки. Number data.setUint16(offset, sampleBits, true offset += 2; // Идентификатор данных writeString('data'); // Общее количество; выборочные данные, то есть общий размер данных - 44 data.setUint32(offset, dataLength, true); offset += 4; // Запись выборочных данных if (sampleBits === 8) { for (let i = 0; i < bytes.length; i++, offset++) { const s = Math.max(-1, Math.min(1, bytes[i])); пусть val = s < 0 ? s * 0x8000 : s * 0x7fff; val = parseInt(255/(65535/(val + 32768)), 10); data.setInt8(offset, val, true } } else { for (let i = 0; i < bytes.length; i++); , смещение += 2) { const s = Math.max(-1, Math.min(1, bytes[i])); data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true } } return new Blob ([data], { type: 'audio/wav' }});
2. Измените метод вызова аудиоколлекции HZRecord.
// Коллекция аудио Recorder.onaudioprocess = function(e) { audioData.encode(e.inputBuffer.getChannelData(0));};
3. Метод getBlob HZRecord.
this.getBlob = function() { this.stop() return audioData.finish();};
4. Метод воспроизведения HZRecord. Преобразуйте большой двоичный объект в base64url.
this.play = function(func) { readBlobAsDataURL(this.getBlob(), func);};function readBlobAsDataURL(data, callback) { let fileReader = new FileReader(); fileReader.onload = function(e) {callback(e) .target.result); }; fileReader.readAsDataURL(данные);}
На данный момент две вышеуказанные проблемы решены.
Шаг 4Здесь мы в основном рассказываем, как создавать динамические эффекты во время записи. Одно из наших требований к анимации:
В соответствии с поступающим объемом дуга окружности динамически расширяется.
//Создаем узел анализатора и получаем данные о времени и частоте звука const анализатор = context.createAnalyser();audioInput.connect(analyser);const inputAnalyser = new Uint8Array(1);const WrapEle = $this.refs['wrap'] ; let ctx = WrapEle.getContext('2d');const ширина = WrapEle.width;const высота = WrapEle.height;const center = { x: ширина/2, y: высота/2}; функция drawArc(ctx, color, x, y, radius, BeginAngle, endAngle) { ctx.beginPath(); ctx.strokeStyle = цвет; ctx.arc(x, y, радиус, (Math.PI * BeginAngle) / 180, (Math.PI * endAngle) / 180); ctx.stroke();}(function drawSpectrum() { analyser.getByteFrequencyData(inputAnalyser); // Получаем данные частотной области ctx.clearRect(0, 0, width, height); // Рисуем линии for (let i = 0; i < 1; i++) { let value = inputAnalyser[i] / 3; // <===Получить данные let Colors = []; if (value <= 16) { Colors = ['#f5A631', '#f5A631', '#e4e4e4', '#e4e4e4', '#e4e4e4', '# e4e4e4']; } else if (значение <= 32) {colors = ['#f5A631', '#f5A631', '#f5A631', '#f5A631', '#e4e4e4', '#e4e4e4'] } else { Colors = ['#f5A631', '#f5A631', '#f5A631', '#f5A631; ', '#f5A631', '#f5A631']; } drawArc(ctx, Colors[0], center.x, center.y, 52 + 16, -30, 30); , 52 + 16, 150, 210; drawArc(ctx, Colors[2], center.x, center.y, 52 + 32, -22.5, 22.5); drawArc(ctx, Colors[3], center.x, center.y, 52 + 32, 157.5, 202.5); drawArc(ctx,colors[4], center.x, center.y, 52); + 48, -13, 13); drawArc(ctx, цвета[5], center.x, center.y, 52 + 48, 167, 193); } // Запрос следующего кадра requestAnimationFrame(drawSpectrum);})();Конец судьбы
На данный момент полное решение функции записи HTML5 завершено. Если есть что-то, что нужно добавить, пожалуйста, оставьте сообщение, если есть что-то необоснованное.
PS: lamejs может сослаться на этот github
Выше приведено все содержание этой статьи. Я надеюсь, что она будет полезна для изучения всеми. Я также надеюсь, что все поддержат сеть VeVb Wulin.