こうこく
作 ▸
改 ▸

Web Audio APIでボリューム・パン・フィルタいじるサンプル

ブラウザゲーに音つけたいからWeb Audio APIいじりはじめた。

今回はあらかじめ用意したwavファイルを音源にしました。オシレータは使ってません。(ピコピコしたまぎらわしい音ですみません)

※音が出ます

Master
Vol
01
Vol
01
Pan
LR
LP
020000
Vol
01
Pan
LR
LP
020000
HTML
<div id="app-wrapper">
  <div id="app-cover">
    <p style="color: red">※音が出ます</p>
    <button type="button" id="start-button">開始</button>
  </div>
  <div class="track">
    Master
    <dl class="slider">
      <dt>Vol</dt>
      <dd>0<input type="range" min="0" max="1" step="0.01" value="0.8" id="volume_master" />1</dd>
    </dl>
  </div>
  <div class="track">
    <select id="select-audio_t1"></select>
    <button type="button" class="play-button" id="play_t1">play</button>
    <dl class="slider">
      <dt>Vol</dt>
      <dd>0<input type="range" min="0" max="1" step="0.01" value="0.8" id="volume_t1" />1</dd>
    </dl>
    <dl class="slider">
      <dt>Pan</dt>
      <dd>L<input type="range" min="-1" max="1" step="0.01" value="0" id="pan_t1" />R</dd>
    </dl>
    <dl class="slider">
      <dt>LP</dt>
      <dd>0<input type="range" min="0" max="20000" step="10" value="20000" id="lp_t1" />20000</dd>
    </dl>
  </div>
  <div class="track">
    <select id="select-audio_t2"></select
    ><button type="button" class="play-button" id="play_t2">play</button>
    <dl class="slider">
      <dt>Vol</dt>
      <dd>0<input type="range" min="0" max="1" step="0.01" value="0.8" id="volume_t2" />1</dd>
    </dl>
    <dl class="slider">
      <dt>Pan</dt>
      <dd>L<input type="range" min="-1" max="1" step="0.01" value="0" id="pan_t2" />R</dd>
    </dl>
    <dl class="slider">
      <dt>LP</dt>
      <dd>0<input type="range" min="0" max="20000" step="10" value="20000" id="lp_t2" />20000</dd>
    </dl>
  </div>
</div>
CSS
button {
  padding: 3px 8px;
}
input[type='range'] {
  margin: 0 7px;
}
#app-wrapper {
  position: relative;
  border-radius: 4px;
}
#app-cover {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  width: 100%;
  height: 100%;
  background-color: rgba(255, 255, 255, 0.5);
}
.track {
  margin: 5px 0;
  padding: 5px;
  max-width: 500px;
  border: 1px solid #999;
  border-radius: 3px;
}
.slider {
  display: flex;
  align-items: center;
}
.slider dt {
  width: 3em;
}
JavaScript
{
  /** @type {AudioContext | null} */
  let _ctx = null;

  /**
   * トラックを作成
   * @param {AudioNode} destination 出力先ノード
   */
  const createTrack = (destination) => {
    // Gainを生成して出力先ノードにつなげる
    const gain = _ctx.createGain();
    gain.connect(destination);
    gain.gain.value = 0.7;

    // Pannerを生成してGainに繋げる
    const panner = _ctx.createStereoPanner();
    panner.connect(gain);

    // BiquadFilterを生成してPannerに繋げる
    const filter = _ctx.createBiquadFilter();
    filter.connect(panner);
    filter.type = 'lowpass';
    filter.frequency.value = 20000;

    return {
      gain,
      panner,
      filter,
      audioBuffer: null,
      bufferSource: null,
      /**
       * トラックの終端ノード (ここではBiquadFilter)
       */
      terminal: filter,
      /**
       * トラックにAudioBufferを割り当てる
       * @param {AudioBuffer} audioBuffer
       */
      assign(audioBuffer) {
        this.audioBuffer = audioBuffer;
        return this;
      },
      /**
       * トラック音量をセット
       * @param {number} value 音量 (0 ~ 1)
       */
      set volume(value) {
        this.gain.gain.value = value;
      },
      /**
       * トラックのパンをセット
       * @param {number} value パン (-1 ~ 1)
       */
      set pan(value) {
        this.panner.pan.value = value;
      },
      /**
       * トラックのLowPassフィルタのカットオフをセット
       * @param {number} value カットオフ周波数 (0 ~ 20000)
       */
      set lowPassFilterFrequency(value) {
        this.filter.frequency.value = value;
      },
      /**
       * トラックに割り当てられたAudioBufferを再生する
       */
      play() {
        if (!this.audioBuffer) {
          console.log('?');
          return;
        }
        if (this.bufferSource) {
          console.log('??');
          // 再生中なら止める
          this.bufferSource.stop();
        }
        // BufferSource生成して再生
        this.bufferSource = _ctx.createBufferSource();
        this.bufferSource.buffer = this.audioBuffer;
        this.bufferSource.connect(this.terminal); // 終端のノードにつなぐ
        this.bufferSource.start(0);
        console.log('!! play');
      },
    };
  };

  /**
   * 指定した音声ファイルを全て取得してデコード
   * @param {string[]} fileUrlList 音声ファイルのURLの配列
   * @returns {Promise<void>}
   */
  const loadSamples = async (fileUrlList) => {
    const audioBuffers = {};
    const load = async (url) => {
      const res = await fetch(url);
      const arrayBuffer = await res.arrayBuffer();
      const audioBuffer = await _ctx.decodeAudioData(arrayBuffer);
      audioBuffers[url] = audioBuffer;
    };
    await Promise.all(fileUrlList.map(load));
    return audioBuffers;
  };

  // 開始ボタンを押したら
  document.getElementById('start-button').addEventListener('click', (e) => {
    document.getElementById('app-cover').style.display = 'none';

    // 初期化 (ユーザー操作をトリガーに初期化しないと動かないからここでやってる)
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    if (!window.AudioContext) {
      throw new Error('AudioContextに対応していないブラウザです。');
    }
    _ctx = new AudioContext();

    // 音声をロード…
    const urls = ['/assets/entry/20180506-webaudio/a.wav', '/assets/entry/20180506-webaudio/b.wav', '/assets/entry/20180506-webaudio/c.wav', '/assets/entry/20180506-webaudio/d.wav'];
    loadSamples(urls).then((audioBuffers) => {
      // 全てロード完了したらセレクトボックスに表示
      let options = '';
      for (const url of urls) {
        options += `<option value="${url}">${url}</option>`;
      }
      document.getElementById('select-audio_t1').innerHTML = options;
      document.getElementById('select-audio_t2').innerHTML = options;

      // トラック作成
      const master = createTrack(_ctx.destination);
      const tracks = {
        master,
        // 1と2はMasterの末端ノードにつなぐ
        t1: createTrack(master.terminal).assign(audioBuffers[urls[0]]),
        t2: createTrack(master.terminal).assign(audioBuffers[urls[0]]),
      };

      // トラックごとの操作ボタンなどの準備
      ['t1', 't2'].forEach((trackName) => {
        document.getElementById(`play_${trackName}`).addEventListener('click', (e) => {
          tracks[trackName].play();
        });
        document.getElementById(`select-audio_${trackName}`).addEventListener('change', (e) => {
          tracks[trackName].assign(audioBuffers[e.target.value]);
        });
        document.getElementById(`volume_${trackName}`).addEventListener('change', (e) => {
          tracks[trackName].volume = e.target.value;
        });
        document.getElementById(`pan_${trackName}`).addEventListener('change', (e) => {
          tracks[trackName].pan = e.target.value;
        });
        document.getElementById(`lp_${trackName}`).addEventListener('change', (e) => {
          tracks[trackName].lowPassFilterFrequency = e.target.value;
        });
      });
    });
  });
}

基本は AudioContext でノードを作って、ノード同士を繋げていく感じだった。繋げていく感じを掴むために、ここで作ったサンプルでは、トラック音量とマスター音量を用意してみた。各トラック内で LowPassフィルター → パンナー → Gain というふうに繋げて、それらをさらにマスタートラックに繋げた。

ざっくりした使い方は次のサイト様を参考にしました。

Getting Started with Web Audio API - HTML5 Rocks

フィルターとかパンナーとか細かいところは、g200kgさんのサイトを参考にしました。

Web Audio API 解説 - 01.前説 | g200kg Music & Software

それと、自分が使ってるブラウザがWeb Audio APIにどれだけ対応してるかを、g200kgさんの下記ツールでチェックできます。便利です。

WAAPISim - 2.API Checker | g200kg Music & Software

この記事に何かあればこちらまで (非公開)