如何用前端實現麥克風語音喚醒

blurs發表於2024-06-06
<!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>版權所有 &copy; 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庫 轉化為拼音來匹配識別的。。

相關文章