<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>我的簡單網頁</title> <!-- 這裡可以新增CSS樣式連結,例如:<link rel="stylesheet" href="styles.css"> --> </head> <body> <header> <h1>歡迎來到我的網頁!</h1> </header> <main> <p>這是一個簡單的語音喚醒頁面示例。</p> <p>在頁面授權後 可以在任意事件點對著麥克風說話 系統會自動收集你說話聲音觸發錄音 將說話聲音生成為audio出現在下方可以看到測試結果</p> </main> <footer> <p>版權所有 © 2023 我的網站</p> </footer> <!-- 這裡可以新增JavaScript指令碼連結,例如:<script src="script.js"></script> --> </body> <script> class EventManager { constructor() { // 使用一個物件來儲存事件和對應的回撥函式 this.events = {}; } // 新增事件監聽器 on(eventName, callback) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(callback); } // 移除事件監聽器 off(eventName, callback) { if (this.events[eventName]) { const index = this.events[eventName].indexOf(callback); if (index > -1) { this.events[eventName].splice(index, 1); } } } // 觸發事件 emit(eventName, ...args) { if (this.events[eventName]) { this.events[eventName].forEach(callback => { callback(...args); }); } } // 檢查是否有某個事件 hasEvent(eventName) { return !!this.events[eventName]; } // 移除所有事件監聽器 removeAllListeners(eventName) { if (eventName) { delete this.events[eventName]; } else { this.events = {}; } } } class AudioTimer { // MediaRecorder例項 mediaRecorder = null; // 關於音訊處理器狀態 // 0 未啟動 通常是因為沒有獲取到許可權導致的 // 1 麥克風啟動中 // 2 在進行語音音訊音量處理 對環境噪音進行過濾 // 3 在請求伺服器進行文字識別 stateStyle = 0; /** * 建立一個用於儲存錄製資料的陣列 **/ recordedChunks = []; audioContext = new (window.AudioContext || window.webkitAudioContext)(); /** * 事件管理器 * 1. onRms // 聲聞超出標準值 觸發 */ $eventManager = new EventManager(); /** * 零界點 進入錄音儲存模式 */ isRmsUp = false; /** * 關於RMS值的臨界值 */ RMSVALUE = 0.05; /** * 建構函式 */ constructor() { // 獲取許可權 this.getPermissions().then((stream) => { // 獲取許可權後才能正常執行 // 建立一個MediaRecorder例項來錄製音訊 this.createMediaRecorder(stream); // 開啟麥克風定時啟動監聽音訊流 this.audioTimer(); }); } on(name, call) { // 繫結事件 return this.$eventManager.on(name, call); } /** * 關於麥克風的迴圈監聽流處理 * 考慮到效能併發問題 採用流式佇列處理器 */ audioTimer() { this.audioPeriodicity().then(() => { this.audioTimer(); }); } stop() { console.log("關閉音訊監控") this.mediaRecorder.stop(2000); } start() { console.log("開啟音訊監控") this.mediaRecorder.start(2000); } /** * 啟動一段音訊處理週期 */ audioPeriodicity() { return new Promise((resovle, err) => { // 開啟一個監聽 // 開始錄製(可以指定選項,例如{ mimeType: 'audio/webm; codecs=opus' }) // 監聽dataavailable事件以收集錄制的資料 this.mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0 && event.data.size > 10000) { // console.log("ondataavailableEvent", event); console.log("ondataavailable", this.recordedChunks.length); // 將階段音訊存入快取中 this.addRecordedChunks(event.data); // 處理一下快取資料的RMS值 this.handleAudioRms(event.data).then((Rms) => { console.log(Rms) // 判斷RMS值是否達到一定的零界點 如果達到了 就判斷為是為近距離說話 if (Rms > this.RMSVALUE) { // 如果是出發了零界點 則開啟錄製模式 this.isRmsUp = true; } else if (Rms < this.RMSVALUE) { if (this.isRmsUp) { // 如果沒有達到臨界值 但是 又是錄音狀態則掛不必錄音狀態 this.isRmsUp = false; // 然後將儲存後的錄音快取開啟並進行返回 this.$eventManager.emit("onRms", this.recordedChunks.concat()); } // 清除掉監聽事件 this.mediaRecorder.ondataavailable = function () { }; // 然後清空快取 this.clearRecordedChunks(); // 停止掉此次的監聽 this.stop(); resovle(); } }).catch(() => { console.log("音訊RMS計算出現了未知錯誤"); // 清除掉監聽事件 this.mediaRecorder.ondataavailable = null; // 停止掉此次的監聽 this.stop(); // 然後清空快取 this.clearRecordedChunks(); resovle(); }) } else { console.log("因為音訊size小於10000位元組判斷為關閉stop殘留資料 執行清除"); // 清除掉監聽事件 // this.mediaRecorder.ondataavailable = null; // 停止掉此次的監聽 // this.stop(); // 然後清空快取 // this.clearRecordedChunks(); // resovle(); } }; this.start(2000); // setTimeout(() => { // // 停止錄製(例如,在一段時間後或使用者點選按鈕時) // this.mediaRecorder.stop(); // // 處理一下資料的RMS值 // this.handleAudioRms().then((Rms) => { // console.log(Rms) // if (Rms > 0.01) { // this.$eventManager.emit("onRms", Rms); // } // }).finally(() => { // resovle(); // }); // }, 3000); // 10秒後停止錄製 }) } /** * 對音訊流進行RMS判斷 * RMS是一種對一段音訊進行峰值判斷的計算方法 * 當RMS達到一定值 我則認為是有效語音 不然則認為是環境音不做識別 */ async handleAudioRms(chunks) { console.log("處理音訊RMS", this.recordedChunks.map((e) => e.size)); // 建立一個Blob物件,它包含所有錄製的資料塊 this.recordedChunks const blob = new Blob(this.recordedChunks, { 'type': 'audio/ogg; codecs=opus' }); // 解碼音訊 Blob const audioBuffer = await this.audioContext.decodeAudioData(await blob.arrayBuffer()); let rmsSum = 0; let numSamples = 0; // 遍歷所有通道和樣本 for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) { console.log("處理音訊RMS最終資料Size", audioBuffer.numberOfChannels); let data = audioBuffer.getChannelData(audioBuffer.numberOfChannels - 1); data = data.slice(data.length - data.length / this.recordedChunks.length, data.length); for (let i = 0; i < data.length; i++) { const sample = data[i]; rmsSum += sample * sample; // 計算平方和 numSamples++; } } // 計算 RMS const rms = Math.sqrt(rmsSum / numSamples); return rms; } /** * 新增音訊buffer資料快取 * 快取會保留10秒內的資料 */ addRecordedChunks(arrayBuffer) { const index = this.recordedChunks.push(arrayBuffer); // if (index > 1) { // this.recordedChunks.splice(0, 1); // } } // 清除音訊快取 clearRecordedChunks() { this.recordedChunks = []; } /** * 建立一個MediaRecorder例項來錄製音訊 */ createMediaRecorder(stream) { // 建立一個MediaRecorder例項來錄製音訊 const mediaRecorder = new MediaRecorder(stream); this.mediaRecorder = mediaRecorder; // 監聽dataavailable事件以收集錄制的資料 // mediaRecorder.ondataavailable = (event) => { // if (event.data.size > 0) { // console.log("ondataavailable", this.recordedChunks.length); // this.addRecordedChunks(event.data); // } // }; // 監聽stop事件以處理錄製完成 // mediaRecorder.onstop = function () { // // 建立一個Blob物件,它包含所有錄製的資料塊 // const blob = new Blob(recordedChunks, { 'type': 'audio/ogg; codecs=opus' }); // // 建立一個指向Blob的URL,該URL可用於在瀏覽器中播放音訊 // // const audioURL = URL.createObjectURL(blob); // // 你可以在這裡使用audioURL,例如將其設定為audio元素的src屬性 // // const audioElement = document.createElement('audio'); // // audioElement.controls = true; // // audioElement.src = audioURL; // // document.body.appendChild(audioElement); // // 新增到音訊處理器 // // window.testAudio(recordedChunks); // // var reader = new FileReader() // // reader.onload = function() { // // console.log(this.result) // window.testAudio(blob); // // } // // reader.readAsArrayBuffer(blob) // // 清理資源(可選) // // URL.revokeObjectURL(audioURL); // }; } /** * 初始化麥克風許可權 判斷是否存在許可權 */ getPermissions() { // 請求訪問麥克風 return navigator.mediaDevices.getUserMedia({ audio: true }) .catch(function (err) { console.log('無法訪問麥克風: ' + err); }); } } const audios = new AudioTimer(); audios.on("onRms", (chunks) => { debugger; console.log("觸發語音喚醒輸入") // 建立一個Blob物件,它包含所有錄製的資料塊 const blob = new Blob(chunks, { 'type': 'audio/ogg; codecs=opus' }); // 建立一個指向Blob的URL,該URL可用於在瀏覽器中播放音訊 const audioURL = URL.createObjectURL(blob); const audio = document.getElementsByTagName("audio")[0]; audio && audio.remove(); // 你可以在這裡使用audioURL,例如將其設定為audio元素的src屬性 const audioElement = document.createElement('audio'); audioElement.controls = true; audioElement.src = audioURL; document.body.appendChild(audioElement); }) </script> </html>
1. 思路大概就是 監聽麥克風 然後根據麥克風捕捉到的音訊RMS值來判斷是否是高音 如果是 則判斷為近距離說話 從而進行捕捉擷取
後續 我是採用的第三方語音識別來轉化為文字 在使用 pinyin庫 轉化為拼音來匹配識別的。。