Copy & Paste Engineer

JavaScriptで音声録音時に無音を検知して自動停止する方法

audioweb-audio-api

JavaScriptで音声録音時に無音を検知して自動停止する方法

音声入力機能を持つWebアプリケーションを開発する際、「ユーザーが話し終わったら自動的に録音を停止したい」という要件はよくあります。今回は、JavaScriptのWeb Audio APIを使って、マイク入力の音量を監視し、一定時間(今回は5秒)無音が続いたら録音を終了する実装方法を紹介します。

実装のポイント

  1. MediaRecorder: 音声の録音に使用します。
  2. AudioContextAnalyserNode: 音声データの解析(音量取得)に使用します。
  3. requestAnimationFrame: リアルタイムに音量をチェックするために使用します。

サンプルコード

以下は、録音開始ボタンを押すと録音が始まり、無音が5秒続くと自動的に停止するサンプルのHTMLとJavaScriptです。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>無音検知録音サンプル</title>
</head>
<body>
    <h1>無音検知録音サンプル</h1>
    <button id="startBtn">録音開始</button>
    <button id="stopBtn" disabled>録音停止</button>
    <p id="status">待機中</p>
    <p>無音継続時間: <span id="silenceTime">0.00</span> 秒</p>

    <script>
        const startBtn = document.getElementById('startBtn');
        const stopBtn = document.getElementById('stopBtn');
        const statusEl = document.getElementById('status');
        const silenceTimeEl = document.getElementById('silenceTime');

        let mediaRecorder;
        let audioContext;
        let analyser;
        let microphone;
        let silenceStart = null;
        let isRecording = false;
        let animationId;

        // 無音とみなす閾値(0.0〜1.0)。環境に合わせて調整してください。
        const SILENCE_THRESHOLD = 0.02;
        // 録音を停止するまでの無音時間(ミリ秒)
        const SILENCE_DURATION = 5000;

        startBtn.addEventListener('click', startRecording);
        stopBtn.addEventListener('click', stopRecording);

        async function startRecording() {
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

                // AudioContextのセットアップ
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
                analyser = audioContext.createAnalyser();
                microphone = audioContext.createMediaStreamSource(stream);
                microphone.connect(analyser);

                // FFTサイズの設定(データ取得の解像度)
                analyser.fftSize = 2048;
                const bufferLength = analyser.frequencyBinCount;
                const dataArray = new Uint8Array(bufferLength);

                // MediaRecorderのセットアップ
                mediaRecorder = new MediaRecorder(stream);

                mediaRecorder.onstart = () => {
                    statusEl.textContent = '録音中...';
                    startBtn.disabled = true;
                    stopBtn.disabled = false;
                    isRecording = true;
                    silenceStart = performance.now();
                    detectSilence(dataArray, bufferLength);
                };

                mediaRecorder.onstop = () => {
                    statusEl.textContent = '録音終了';
                    startBtn.disabled = false;
                    stopBtn.disabled = true;
                    isRecording = false;
                    cancelAnimationFrame(animationId);

                    // ストリームのトラックを停止
                    stream.getTracks().forEach(track => track.stop());
                    if (audioContext && audioContext.state !== 'closed') {
                        audioContext.close();
                    }
                };

                mediaRecorder.ondataavailable = (e) => {
                    // ここで録音データを処理(保存や送信など)
                    console.log('データ取得:', e.data);
                };

                mediaRecorder.start();

            } catch (err) {
                console.error('マイクへのアクセスに失敗しました:', err);
                statusEl.textContent = 'エラー: マイクにアクセスできません';
            }
        }

        function stopRecording() {
            if (mediaRecorder && mediaRecorder.state !== 'inactive') {
                mediaRecorder.stop();
            }
        }

        function detectSilence(dataArray, bufferLength) {
            if (!isRecording) return;

            analyser.getByteTimeDomainData(dataArray);

            let sum = 0;
            for (let i = 0; i < bufferLength; i++) {
                // 128が中心(無音)なので、振幅を計算
                const x = (dataArray[i] - 128) / 128.0;
                sum += x * x;
            }
            // RMS(二乗平均平方根)を計算して音量とする
            const rms = Math.sqrt(sum / bufferLength);

            const now = performance.now();

            if (rms < SILENCE_THRESHOLD) {
                // 無音の場合
                if (!silenceStart) {
                    silenceStart = now;
                }

                const silenceDuration = now - silenceStart;
                silenceTimeEl.textContent = (silenceDuration / 1000).toFixed(2);

                if (silenceDuration > SILENCE_DURATION) {
                    console.log('無音が続いたため自動停止しました');
                    stopRecording();
                    return;
                }
            } else {
                // 音がある場合、タイマーリセット
                silenceStart = now;
                silenceTimeEl.textContent = "0.00";
            }

            animationId = requestAnimationFrame(() => detectSilence(dataArray, bufferLength));
        }
    </script>
</body>
</html>

解説

音量の取得方法

AnalyserNode.getByteTimeDomainData() を使用して、波形データを取得しています。このデータは 0〜255 の範囲で、128 が無音(振幅0)の状態です。 各データ点から 128 を引いて正規化し、二乗平均平方根(RMS)を計算することで、現在の音量レベルを算出しています。

無音判定ロジック

算出された RMS 値が SILENCE_THRESHOLD(閾値)を下回っている場合を「無音」と判定します。 requestAnimationFrame ループ内で毎回チェックを行い、無音が続いている時間を計測します。閾値を超えた(音が鳴った)場合は、計測開始時間 silenceStart を現在時刻にリセットします。

注意点

デモ

待機中

無音継続時間: 0.00

まとめ

Web Audio API を組み合わせることで、単純な録音だけでなく、入力レベルに応じた制御が可能になります。音声入力フォームやボイスメモアプリなどで活用してみてください。