En raison des besoins du projet, nous devons implémenter la fonction d'enregistrement côté Web. Au début, deux solutions ont été trouvées, l'une via iframe et l'autre via l'API getUserMedia de html5. Notre fonction d'enregistrement n'ayant pas besoin d'être compatible avec le navigateur IE, nous n'avons pas hésité à choisir getUserMedia fourni par html5 pour l'implémenter. L'idée de base est de le combiner avec la documentation officielle de l'API et certaines solutions trouvées en ligne pour créer une solution adaptée aux besoins du projet. Mais comme nous devons nous assurer que la fonction d'enregistrement peut être activée à la fois sur le pad et sur le PC, il existe également quelques pièges. Ce qui suit est une restauration de processus.
Étape 1Parce que la nouvelle API transmet navigator.mediaDevices.getUserMedia et renvoie une promesse.
L'ancienne API est navigator.getUserMedia, la compatibilité a donc été établie. Le code est le suivant :
// Les anciens navigateurs peuvent ne pas implémenter du tout mediaDevices, nous pouvons donc d'abord définir un objet vide if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {};}// Certains navigateurs prennent partiellement en charge mediaDevices. Nous ne pouvons pas définir directement getUserMedia// sur l'objet car cela pourrait écraser les propriétés existantes. Ici, nous ajouterons l'attribut getUserMedia uniquement s'il n'existe pas. if (navigator.mediaDevices.getUserMedia === non défini) { let getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || Tout d'abord, s'il existe getUserMedia, récupérez-le // Certains navigateurs ne l'implémentent pas du tout - puis renvoient une erreur de rejet de la promesse pour maintenir une interface unifiée if (!getUserMedia) { return Promise.reject(new Error(' getUserMedia is non implémenté dans ce navigateur')); } // Sinon, enveloppez une promesse pour l'ancienne méthode navigator.getUserMedia return new Promise(function(resolve, rejet) { getUserMedia.call(navigateur, contraintes, résolution, rejet });Étape 2
Il s'agit d'une méthode qui existe sur Internet, encapsulant un HZRecorder. Faisant essentiellement référence à cette méthode. L'interface d'enregistrement peut être appelée en appelant HZRecorder.get. Cette méthode transmet une fonction de rappel. Après un nouveau HZRecorder, la fonction de rappel est exécutée et un objet HZRecorder matérialisé est transmis. Des fonctions telles que le démarrage de l'enregistrement, la pause, l'arrêt et la lecture peuvent être implémentées via les méthodes de cet objet.
var HZRecorder = function (stream, config) { config = config || {}; config.sampleBits = config.sampleBits || Bits d'échantillonnage 8, 16 config.sampleRate = config.sampleRate || ; //Taux d'échantillonnage (1/6 44100) //Créer un objet d'environnement audio audioContext = window.AudioContext || window.webkitAudioContext; var context = new audioContext(); //Entrer le son dans cet objet var audioInput = context.createMediaStreamSource(stream); .connect(volume); //Créer un cache pour mettre en cache le son var bufferSize = 4096 //Créer un nœud de cache sonore, méthode createScriptProcessor // Les deuxième et troisième paramètres font référence au fait que l'entrée et la sortie sont binaurales. var recorder = context.createScriptProcessor(bufferSize, 2, 2); var audioData = { size: 0 //Longueur du fichier d'enregistrement, buffer: [] //Cache d'enregistrement, inputSampleRate: context.sampleRate //Taux d'échantillonnage d'entrée, inputSampleBits: 16 //Chiffres d'échantillonnage d'entrée 8, 16, outputSampleRate : config.sampleRate //Taux d'échantillonnage de sortie, oututSampleBits : config.sampleBits //Échantillons de sortie 8, 16, entrée : function (data) { this.buffer.push(new Float32Array(data)); this.size += data.length }, compress: function () { / /Fusionner la compression//Fusionner les données 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; } //Compression var compression = parseInt(this.inputSampleRate / this.outputSampleRate); / compression; var result = new Float32Array(length); var index = 0, j = 0; while (index < longueur) { result[index] = data[j]; index++; } renvoie le résultat } , encodeWAV : function () { var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate); .compress(); var dataLength = octets.length * (sampleBits / var buffer = nouveau); 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)); // Identifiant du fichier d'échange de ressources writeString('RIFF'); offset += 4; //Le nombre total d'octets depuis l'adresse suivante jusqu'à la fin du fichier, c'est-à-dire la taille du fichier -8 data.setUint32(offset, 36 + dataLength, true offset += 4; /WAV indicateur de fichier writeString(' WAVE'); offset += 4; // Indicateur de format de forme d'onde writeString('fmt '); // Filtrer les octets, généralement 0x10 = 16 data.setUint32(offset, 16, true); offset += 4; // Catégorie de format (données d'échantillonnage au format PCM) data.setUint16(offset, 1, true offset += 2); ( offset, channelCount, true); offset += 2; // Le taux d'échantillonnage, le nombre d'échantillons par seconde, représente la vitesse de lecture de chaque canal data.setUint32(offset, sampleRate, true); offset += 4; //Taux de transfert de données de forme d'onde (octets moyens par seconde) Mono × bits de données par seconde × bits de données par échantillon/8 data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true ); offset += 4; // Le nombre d'octets occupés par l'échantillonnage du numéro d'ajustement rapide des données à un moment donné est mono × le nombre de bits de données par échantillon/8 data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2; // Nombre de bits de données par échantillon data.setUint16(offset, sampleBits, true offset += 2; // Identificateur de données writeString('data'); += 4; //Nombre total de données échantillonnées, c'est-à-dire taille totale des données -44 data.setUint32(offset, dataLength, true offset += 4; === 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, octets[i])); data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } return new Blob([data], { type: 'audio/wav' }); //Commencer à enregistrer this.start = function () { audioInput.connect(recorder); connect(context.destination); }; //Arrêtez ceci.stop = function () { recorder.disconnect( }; //Fin this.end = function() { context.close(); }; //Continuer this.again = function() { recorder.connect(context.destination }; //Récupérer le fichier audio this.getBlob = function () { this.stop(); ; }; //Lecture de this.play = function (audio) { audio.src = window.URL.createObjectURL(this.getBlob() }; { var fd = new FormData(); fd.append('audioData', this.getBlob()); var xhr = new XMLHttpRequest(); if (callback) { xhr.upload.addEventListener('progress', function (e ) { callback('téléchargement', e); }, false); xhr.addEventListener('load', function (e) { callback('ok', e); }, false); xhr.addEventListener('erreur', fonction (e) { callback('erreur', e); }, false); (e) { rappel('annuler', e); }, false } xhr.open('POST', url); //Collection audio 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 };}; //Si l'enregistrement est pris en charge HZRecorder.canRecording = (navigator.getUserMedia != null); //Obtenir l'enregistreur HZRecorder.get = function (callback, config) { if (callback) { navigator.mediaDevices .getUserMedia({ audio: true }) .then(function(stream) { let rec = new HZRecorder(stream, config); callback(rec); }) .catch(function(error) { HZRecorder.throwError('Impossible d'enregistrer, veuillez vérifier l'état de l'appareil'); } }}; window.HZRecorder = HZRecorder;
Ce qui précède peut déjà répondre à la plupart des besoins. Mais il faut être compatible côté pad. Nous avons plusieurs problèmes avec notre pad qui doivent être résolus.
Vous trouverez ci-dessous des solutions aux deux problèmes.
Étape 3Ce qui suit est une solution pour moi pour implémenter le format d'enregistrement au format mp3 et window.URL.createObjectURL pour transmettre des données blob et signaler une erreur du côté du pad.
1. Modifiez le code objet audioData dans HZRecorder. Et présentez un fichier js lamejs.js d'une personne formidable sur Internet
const lame = new lamejs();let audioData = { samplesMono : null, maxSamples : 1152, mp3Encoder : new lame.Mp3Encoder(1, context.sampleRate || 44100, config.bitRate || 128), dataBuffer : [], taille : 0, // Tampon de longueur du fichier d'enregistrement : [], // Tampon d'enregistrement inputSampleRate : context.sampleRate, // Taux d'échantillonnage d'entrée inputSampleBits : 16, // Exemples de chiffres d'entrée 8, 16 outputSampleRate : config.sampleRate, // Taux d'échantillonnage de sortie outputSampleBits : config.sampleBits, // Exemples de chiffres de sortie 8, 16 convertBuffer : function (arrayBuffer) { let data = new Float32Array(arrayBuffer); let out = new Int16Array(arrayBuffer.length); this.floatTo16BitPCM(data, out); floatTo16BitPCM: function(input, output) { for (let i = 0; i < input. longueur ; i++) { let s = Math.max(-1, Math.min(1, entrée[i])); = s < 0 ? s * 0x8000 : s * 0x7fff; } }, appendToBuffer : function(mp3Buf) { this.dataBuffer.push(new Int8Array(mp3Buf)); .convertBuffer(arrayBuffer); laissez restant = this.samplesMono.length; (soit i = 0 ; restant >= 0 ; i += this.maxSamples) { let left = this.samplesMono.subarray(i, i + this.maxSamples); let mp3buf = this.mp3Encoder.encodeBuffer(left); .appendToBuffer(mp3buf); restant -= this.maxSamples; } }, terminez : 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() { // Fusionner la compression // Fusionner les données = new Float32Array(this.size); let offset = 0; for (let i = 0; i < this.buffer.length; i++) { data.set(this.buffer[i], offset offset += this.buffer); [i].length; } // Compression let compression = parseInt (this.inputSampleRate / this.outputSampleRate, 10); Float32Array(length); let index = 0; let j = 0; while (index < length) { result[index] = data[j]; j += index++; { let sampleRate = Math.min (this.inputSampleRate, this.outputSampleRate); let sampleBits = Math.min (this.inputSampleBits, this.oututSampleBits); let bytes = this.compress(); let dataLength = bytes.length * (sampleBits / 8); let buffer = new ArrayBuffer (44 + dataLength); 1; //mono let offset = 0; let writeString = function(str) { for (let i = 0; i < str.length; i++) { data.setUint8(offset + i, str.charCodeAt(i)); } }; // Identifiant du fichier d'échange de ressources writeString('RIFF'); // Total d'octets depuis l'adresse suivante jusqu'à la fin du numéro de fichier, c'est-à-dire taille du fichier - 8 data.setUint32(offset, 36 + dataLength, true); // indicateur de fichier WAV writeString('WAVE'); // Indicateur de format d'onde writeString('fmt '); offset += 4; // Filtrer les octets, généralement 0x10 = 16 data.setUint32(offset, 16, true); // Catégorie de format (échantillonnage de formulaire PCM); data) data.setUint16(offset, 1, true); offset += 2; // Numéro de canal data.setUint16(offset, channelCount, true offset); += 2; // Taux d'échantillonnage, le nombre d'échantillons par seconde, indiquant la vitesse de lecture de chaque canal data.setUint32(offset, sampleRate, true offset += 4; par seconde ) Mono × bits de données par seconde × bits de données par échantillon / 8 data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true offset += 4; Nombre d'octets d'ajustement rapide des données occupés par un canal mono d'échantillon Number data.setUint16(offset, sampleBits, true += 2; // Identificateur de données writeString('data'); // Nombre total de données échantillonnées, c'est-à-dire taille totale des données - 44 data.setUint32(offset, dataLength, true); offset += 4; // Écrire des exemples de données if (sampleBits === 8) { for (let i = 0; i < bytes.length; i++, offset++) { const s = Math.max(-1, Math.min(1, octets[i])); soit val = s < 0 ? s * 0x8000 : s * 0x7fff; val = parseInt(255 / (65535 / (val + 32768)), 10); data.setInt8(offset, val, true); , offset += 2) { const s = Math.max(-1, Math.min(1, octets[i])); data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); } } return new Blob([data], { type: 'audio/wav' } }});
2. Modifiez la méthode d'appel de la collection audio de HZRecord.
// Collection audio recorder.onaudioprocess = function(e) { audioData.encode(e.inputBuffer.getChannelData(0));};
3. Méthode getBlob de HZRecord.
this.getBlob = function() { this.stop(); return audioData.finish();};
4. Méthode de lecture de HZRecord. Convertissez le blob en 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(données);}
Jusqu’à présent, les deux problèmes ci-dessus ont été résolus.
Étape 4Ici, nous présentons principalement comment créer des effets dynamiques pendant l'enregistrement. L’une de nos exigences en matière d’animation est :
En fonction du volume entrant, un arc de cercle s’étend dynamiquement.
//Créez un nœud d'analyseur et obtenez des données audio de temps et de fréquence. const analyser = context.createAnalyser();audioInput.connect(analyser);const inputAnalyser = new Uint8Array(1);const wrapEle = $this.refs['wrap'] ; soit ctx = wrapEle.getContext('2d');const width = wrapEle.width;const height = wrapEle.height;const center = { x : largeur / 2, y : hauteur / 2}; function drawArc (ctx, couleur, x, y, rayon, débutAngle, finAngle) { ctx.beginPath(); ; ctx.StrokeStyle = couleur; ctx.arc(x, y, rayon, (Math.PI * startAngle) / 180, (Math.PI * endAngle) / 180); ctx.stroke();}(function drawSpectrum() { analyser.getByteFrequencyData(inputAnalyser); // Obtenir les données du domaine fréquentiel ctx.clearRect(0, 0, width, height); // Tracez des lignes pour (let i = 0; i < 1; i++) { let value = inputAnalyser[i] / 3; <===Obtenir les données let colours = []; if (valeur <= 16) { colours = ['#f5A631', '#f5A631', '#e4e4e4', '#e4e4e4', '#e4e4e4', '# e4e4e4']; } else if (valeur <= 32) { couleurs = ['#f5A631', '#f5A631', '#f5A631', '#f5A631', '#e4e4e4', '#e4e4e4']; autre { couleurs = ['#f5A631', '#f5A631', '#f5A631', '#f5A631 ', '#f5A631', '#f5A631']; } drawArc(ctx, couleurs[0], center.x, center.y, 52 + 16, -30, 30); drawArc(ctx, couleurs[1], center.x, center.y); , 52 + 16, 150, 210); drawArc(ctx, couleurs[2], centre.x, centre.y, 52 + 32, -22,5, 22,5); drawArc(ctx, couleurs[3], centre.x, centre.y, 52 + 32, 157,5, 202,5); + 48, -13, 13); drawArc(ctx, couleurs[5], center.x, center.y, 52 + 48, 167, 193); } // Demander la prochaine image requestAnimationFrame(drawSpectrum);})();Fin du destin
À ce stade, une solution complète de fonction d'enregistrement HTML5 a été complétée. S'il y a quelque chose à ajouter, veuillez laisser un message s'il y a quelque chose de déraisonnable.
ps : lamejs peut faire référence à ce github
Ce qui précède représente l’intégralité du contenu de cet article. J’espère qu’il sera utile à l’étude de chacun. J’espère également que tout le monde soutiendra le réseau VeVb Wulin.