[JavaScript]Webアプリの音声遅延を短縮するWeb Audio API(たしざんフレンズ開発記)

はじめに

足し算練習アプリを開発する過程で、音声再生の遅延という問題に直面しました。ユーザーがボタンを押してから音が鳴るまでの時間が長く、操作感が悪くなっていたのです。

この記事では、音声遅延の問題を段階的に解決していく過程と、最終的にWeb Audio APIを使って大幅な改善を実現した方法を共有します。

音声遅延の原因を特定する

最初に実装していた音声再生の仕組みは、非常にシンプルなものでした。

function playSound(soundName) {
    const audio = new Audio(`./audio/${soundName}.mp3`);
    audio.volume = 0.5;
    audio.play().catch(error => {
        console.log('音声再生エラー:', error);
    });
}
Code language: JavaScript (javascript)

このコードには大きな問題がありました。ボタンを押すたびに新しいAudioオブジェクトを作成し、その都度音声ファイルを読み込んでいたのです。これは、本を読むたびに図書館に借りに行くようなもので、非効率的でした。

音声ファイルの読み込みには時間がかかります。特に初回再生時には、ファイルのダウンロードと解析処理が必要となり、明らかな遅延が発生していました。

第一段階:事前読み込み方式による改善

問題の原因が分かったところで、最初の改善策として音声ファイルの事前読み込みを実装しました。

// 音声ファイルを事前読み込み
const sounds = {
    input: new Audio('./audio/input.mp3'),
    correct: new Audio('./audio/correct.mp3'),
    wrong: new Audio('./audio/wrong.mp3'),
    cancel: new Audio('./audio/cancel.mp3'),
    clear: new Audio('./audio/clear.mp3')
};

// 全ての音声の音量を設定
Object.values(sounds).forEach(audio => {
    audio.volume = 0.5;
});

// 音声再生関数
function playSound(soundName) {
    if (sounds[soundName]) {
        sounds[soundName].currentTime = 0; // 再生位置をリセット
        sounds[soundName].play().catch(error => {
            console.log('音声再生エラー:', error);
        });
    }
}
Code language: JavaScript (javascript)

この方式では、アプリケーション開始時に5つの音声ファイルすべてを読み込み、再生時には事前に作成済みのAudioオブジェクトを使用します。

currentTime = 0を設定することで、同じ音声が連続で再生される場合でも、前の再生を停止して最初から再生できます。これにより、キーを連続で押したときのレスポンスが向上しました。

この改善により、音声遅延は大幅に短縮されました。しかし、さらなる改善の余地があることが分かってきました。

イベントタイミングの最適化

音声の遅延改善と並行して、ボタン押下から音声再生までのタイミングも見直しました。

従来はonclickイベントを使用していましたが、これをより早いタイミングで発火するonmousedownに変更しました。

<!-- 変更前 -->
<button class="key-btn num-btn" onclick="inputNumber(7)">7</button>

<!-- 変更後 -->
<button class="key-btn num-btn" onmousedown="inputNumber(7)">7</button>
Code language: HTML, XML (xml)

onclickは、ユーザーがボタンを押して離したときに発火します。一方、onmousedownはボタンを押した瞬間に発火するため、より瞬時性のある操作感を実現できます。

現代のブラウザでは、スマートフォンのタップもmousedownイベントとして処理されるため、この変更だけでPCとスマートフォンの両方に対応できました。

第二段階:Web Audio APIによる根本的改善

事前読み込み方式で改善は見られましたが、さらなる高速化のためにWeb Audio APIの導入を検討しました。

Web Audio APIは、従来のAudio要素よりもはるかに高速で精密な音声制御を可能にするブラウザAPIです。Audio要素では20-50ms程度の遅延が一般的ですが、Web Audio APIでは5-10ms程度まで短縮できます。

Web Audio APIの初期化

Web Audio APIでは、音声ファイルをArrayBuffer(バイナリデータの配列)として読み込み、decodeAudioDataでデコードします。この処理により、音声データがブラウザの音声エンジンで直接処理できる形式に変換されます。

let audioContext;
let soundBuffers = {};
let gainNode;

async function initWebAudio() {
    try {
        audioContext = new (window.AudioContext || window.webkitAudioContext)();
        
        // 音量制御用のゲインノード
        gainNode = audioContext.createGain();
        gainNode.gain.value = 0.5; // 音量50%
        gainNode.connect(audioContext.destination);
        
        const soundFiles = ['input', 'correct', 'wrong', 'cancel', 'clear'];
        
        for (let soundName of soundFiles) {
            const response = await fetch(`./audio/${soundName}.mp3`);
            const arrayBuffer = await response.arrayBuffer();
            soundBuffers[soundName] = await audioContext.decodeAudioData(arrayBuffer);
        }
        
        console.log('Web Audio API初期化完了');
    } catch (error) {
        console.log('Web Audio API初期化エラー:', error);
        initFallbackAudio();
    }
}
Code language: JavaScript (javascript)

ゲインノード(GainNode)は音量制御を担当するコンポーネントです。従来のAudio要素のvolumeプロパティと同じ役割を果たしますが、より細かい制御が可能です。

高速音声再生の実装

Web Audio APIでは、音声を再生するたびに新しいBufferSourceNodeを作成します。これは使い捨ての音源として機能し、一度再生が終了すると自動的に破棄されます。

function playSound(soundName) {
    if (audioContext && soundBuffers[soundName]) {
        // Web Audio APIで再生
        const source = audioContext.createBufferSource();
        source.buffer = soundBuffers[soundName];
        source.connect(gainNode);
        source.start();
    } else if (window.fallbackSounds && window.fallbackSounds[soundName]) {
        // フォールバック: 従来のAudio要素で再生
        window.fallbackSounds[soundName].currentTime = 0;
        window.fallbackSounds[soundName].play().catch(error => {
            console.log('音声再生エラー:', error);
        });
    }
}
Code language: JavaScript (javascript)

この仕組みにより、同じ音声の複数同時再生や、音声の重ね合わせなども自然に処理できます。

フォールバック機能の重要性

Web Audio APIは強力ですが、すべての環境で利用できるわけではありません。古いブラウザや特定の設定では動作しない場合があります。

そのため、Web Audio APIが利用できない場合に備えて、従来のAudio要素を使用するフォールバック機能を実装しました。

function initFallbackAudio() {
    window.fallbackSounds = {
        input: new Audio('./audio/input.mp3'),
        correct: new Audio('./audio/correct.mp3'),
        wrong: new Audio('./audio/wrong.mp3'),
        cancel: new Audio('./audio/cancel.mp3'),
        clear: new Audio('./audio/clear.mp3')
    };
    
    Object.values(window.fallbackSounds).forEach(audio => {
        audio.volume = 0.5;
    });
}
Code language: JavaScript (javascript)

この設計により、どのような環境でもアプリケーションが正常に動作することを保証できます。

まとめ:改善結果の検証

一連の改善により、音声再生の遅延は劇的に短縮されました。

最初の実装では、ボタンを押してから音が鳴るまでに明らかな待ち時間がありました。事前読み込み方式により、この遅延は大幅に改善されました。さらにWeb Audio APIの導入により、ほぼ瞬時に音声が再生されるようになりました。

特に連続でボタンを押したときの応答性が向上し、ユーザーにとってストレスのない操作感を実現できました。

音声遅延の問題は、毎回新しいAudioオブジェクトを作成していることが根本的な原因でした。事前読み込み方式により大幅な改善を実現し、さらにWeb Audio APIの導入により5-10ms程度の遅延まで短縮できました。段階的な改善とフォールバック機能の実装により、すべての環境で安定した動作を確保することができました。