👻

WebAudioAPIの使い方

2020/12/08に公開

この記事の内容

簡単な動くものの解説を通してブラウザで音声を扱うためのAPIであるWebAudioAPIの解説をしていきます。
まずは雰囲気とどのような流れになるかを掴んでもらうために本記事を書きました。
記事中でいくつかのより詳しい記事のリンク、主にMDNををつけるので音声処理に興味を持ってもらえると幸いです。

WebAudioAPIとは

ブラウザを使って低レベルに近い音声加工を提供するAPIである。
APIはAudioContext(もしくはwebkitプリフィックス付き)を通して提供される。
そのため初期化はconst audioCtx= new (window.AudioContext || window.webkitAudioContext)();のような初期化がMDNでは推奨されている
AudioContextからはbaseLatencyoutputLatencyなどから遅延時間を得たり、currentTimeで(コンテキストが作られてからの)現在時間を得たり、またstateで状態を得たりすることができる。

ユーザーアクションによる発火とAudioContext.resume()

2020/12/17 追記
Web Audio APIの闇にあるようにGoogle Chromeではユーザーアクション、例えばボタンクリックなど(Confirmはユーザーアクションとはみなされない模様)、によってAudioContext.resume()を呼ばないときちんと動かないようである。これの返り値はPromise<>なのでプログラムの構造自体を書き換える必要があるので注意が必要である

AudioNode

WebAudioAPIではAudioNodeを継承したものを接続していってAudioContext.destination(出力先)に接続することで音声を発生させる。
この接続はinput.connect(output)のように接続するがこのメソッドは接続先自身を返すのでinput.connect(output).connect(next)のように書くこともできる。
基本的にAudioContextcreateXXXのファクトリーメソッドを用いて生成される。

AudioParam

音声の制御をするパラメーターはAudioParamのインターフェイスで提供される。これらにはdefaultValueminValuemaxValue等の設置値(読み取り専用)と値自体がvalueとして設定されている。
valueを変更することで様々なエフェクトをかける。パラメータ名自体に値は設定を設定を代入スうるとエラーになるので注意!
またメソッドを通じたスケジュールングも可能である。
サンプリングレートに完全に追従するa-rateパラメーターとそれより追従製の悪い(128サンプルごとの)k-rateパラメーターがある。(細かい設定はドキュメント等を確認のこと)

いくつかの具体的な音源

発振器 OscillatorNode

特定の周波数の波形の音源、typeプロパティにより波形を設定できる。

  • sine---正弦波(デフォルト)
  • square---矩形波
  • sawtooth---ノコギリ波
  • triangle---三角波
  • custom---カスタム波形、setPeriodicWave(wave)により設定

がある。周波数はfrequencydetune(AudioParam)で設定できる。

任意の波形 AudioBufferSourceNode

波形自体はAudioBufferを使う。
playbackRatedetuneで音程を変更することが可能。
またloop(bool値)でループ設定loopStart(秒単位)でループ時に戻る地点、loopEnd(秒単位)でループに入る地点を設定可能
今回は以下のような感じのコードでホワイトノイズのために使った。

whiteNoise.js
function makeWhiteNoise(){
    const buffer=audioCtx.createBuffer(2, 5*audioCtx.sampleRate, audioCtx.sampleRate);
    for( let channel=0; channel<buffer.numberOfChannels; channel++ ){
        const data=buffer.getChannelData(channel);
        for( let i=0; i<buffer.length; i++ ){
            data[i]=Math.random()*2.0-1.0; // [ 0: 1.0 ]-> [ -1.0 : 1.0 ]の乱数へ
        }
    }
    
    const bufferSrc=audioCtx.createBufferSource();
    bufferSrc.buffer=buffer;
    bufferSrc.loop=true;
    return bufferSrc;
}

音源ファイルのデコード

音源ファイルを使いたいときはAudicoContext.decodeAudioData(arrayBuffer)を用いる。
これも昔ながらのコールバック形式とPromise形式があるようだが今だとPromise形式をだいたいサポートしていると思う。(そもそもPromiseが使えないブラウザがWebAudioAPI自体をサポートしてるとは思えない)
arrayBuffer自体はXMLHTTPRequestかFileReaderで取ってくる(とMDNには書いてある)。
もちろんFetchAPIでも取ってこれてこのようにできる

decodeAudioData.js
function decodeAudioData(url){
  fetch(url).then(res=>res.arrayBuffer()).then(arrayBuffer=>{
        return audioCtx.decodeAudioData(arrayBuffer)
    });
}

マイクの音声の取得 MediaStreamAudioSourceNode

getMediaDevices.js
async function(){
    const stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
    const audioStream=audioCtx.createMediaStreamSource(stream);
    return audioStream;
}

navigatorからこのようなコードでマイクのストリームを取ってきてWebAudioAPIで加工することができる。

エフェクター系ノードの紹介

GainNode

一つのAudioParam、gainを持ったシンプルなノードで主に音量に用いられる。
その他、いくつかのチャンネルをまとめるためにも用いられたりする。

DelayNode

一つのAudioParam、delayTimeを持ったノードで信号を遅延させる。
注意点としては完全な遅延なので単体では使えずに何かでループを作るなどしないと使えない、ここではg200kgさんの Web Audio API 解説 > 13.ディレイの使い方を参考に以下の2つの例を作ってみた。

Delayモジュール

いわゆるエフェクターとしてのDelayモジュールはディレイ信号を混ぜて出すのでこのような構成になる。

SimpleDelay.js
class SimpleDelay{
    constructor(audioCtx){
        this.inputNode=audioCtx.createGain();
        this.delayNode=audioCtx.createDelay();
        this.feedbackNode=audioCtx.createGain();
        this.dryNode=audioCtx.createGain();
        this.wetNode=audioCtx.createGain();

        this.delayNode.delayTime.value=1.0;
        this.feedbackNode.gain.value=0.5;
        this.dryNode.gain.value=0.8;
        this.wetNode.gain.value=0.2;

        this.inputNode.connect(this.delayNode).connect(this.feedbackNode).connect(this.delayNode);
        this.delayNode.connect(this.wetNode);
        this.inputNode.connect(this.dryNode);
    }

    connect(next){
        this.dryNode.connect(next);
        this.wetNode.connect(next);
        return next;
    }

    get input(){ return this.inputNode; }

    get delayTime(){ return this.delayNode.delayTime; }
    get feedback(){ return this.feedbackNode.gain; }

    get mix(){ return this.wetNode.gain.value; }
    set mix(value){
        if( value<0 && 1<value ) new Error(`Invalid mix value ${val}`);
        this.wetNode.gain.value=value;
        this.dryNode.gain.value=1.0-value;
    }
}

g200kgさんにもカスタムノードの作り方があったのですが既存のAudioNodeの継承で独自にnewを叩く必要性があるのと少し特殊なので継承せずに各ノードをクラスメンバーとする方法を取った。その結果としてインプット側でチェーンを用いたconnectが使えなくなっている。
get xxx()の形にすることによりあたかも各AudioNodeのプロパティAudioNodeを自分のもののように見せかけている。
また出力側のconnectは中で2つの出口に繋いで繋いだノードを返すのでこちらはチェーンが使える。

AudioParamへの接続、揺れ系エフェクターChorus

AudioNodeはAudioParamにも接続可能でそれを使うことにより例えばDelayTimeを揺らしたりすることが可能でありそれによりいわゆるコーラスが実現できる。

Chorus.js
class Chorus{
    constructor(audioCtx){
        this.inputNode=audioCtx.createGain();
        this.delayNode=audioCtx.createDelay();
        this.mixNode=audioCtx.createGain();

        this.lfo=audioCtx.createOscillator();
        this.depthNode=audioCtx.createGain();

        this.lfo.frequency.value=5;
        this.depthNode.gain.value=0.005;
        this.mixNode.gain.value=0.2;

        this.lfo.connect(this.depthNode).connect(this.delayNode.delayTime); // この部分
        this.inputNode.connect(this.delayNode).connect(this.mixNode);
    }

    connect(next){
        this.mixNode.connect(next);
        this.input.connect(next);
        return next;
    }

    get input(){ return this.inputNode; }

    get speed(){ return this.lfo.frequency; }
    get depth(){ return this.depthNode.gain; }
    get mix(){ return this.mixNode.gain; }
}

lfoというのはLow Frequency Oscillatorの意味でリアルのエフェクターなどではよく出てくる(ようである)。

BiquadFilterNode(双二次フィルター)

いわゆる普通のフィルター
周波数設定(frequencyとdetune)、Q値とgainのAudioParamと種類を表すtype(プロパティ値)がある

BiquadFilterNodeの種類

  • lowpass
  • highpass
  • bandpass
  • lowshelf
  • highshelf
  • peaking
  • notch
  • allpass

周波数特性はBiquadFilterNode.getFrequencyResponseで取得可能

全体的な使い方

index.js
onst audioCtx = new (window.AudioContext || window.webkitAudioContext)();

window.addEventListener('DOMContentLoaded', async ()=>{
    const masterGain=audioCtx.createGain();
    bindValue(document.getElementById('master-vol'), masterGain.gain);
    const sourceInput=audioCtx.createGain();

    const simpleDelay=new SimpleDelay(audioCtx);
    bindValue(document.getElementById("delay-time"), simpleDelay.delayTime);
    bindValue(document.getElementById("feedback-gain"), simpleDelay.feedback);
    bindProp(document.getElementById('delay-mix'), simpleDelay, 'mix');
    
    const oscillator=audioCtx.createOscillator();
    oscillator.start();
    bindValue(document.getElementById('oscillator-freq'), oscillator.frequency);
    bindSelect(document.getElementById('oscillator-type'), oscillator, 'type');
    
    oscillator.connect(sourceInput).connect(simpleDelay.input); // can not connect as chain
    simpleDelay.connect(masterGain).connect(audioCtx.destination);
    
    document.getElementById('start-btn', ()=>{ oscillator.start() });
});

function bindValue(element, param){ element.addEventListener('change', ()=>{ param.value=element.value; }); }
function bindProp(element, param, prop){ element.addEventListener('change', ()=>{ param[prop]=element.value; }); }
function bindSelect(select, node, prop){ select.addEventListener('change', ()=>{ node[prop]=[ ...select.children ].find(a=> a.selected).value; }); }

のようにして使う。
bindValueはAudioParamのvalueとHTMLElement(input[type="range"])の値を直接束縛している
bindSelectは同様に発振器のタイプをHTMLElementのselectタグと結びつけている。
Delayモジュールのmixvalueプロパティではないので別関数を用意した。
(これらは一部なのでただコピペすると動かないと思う)

あとがき

一応、基礎的な概念の解説を兼ねて動く簡単なものの作り方を解説した。
より細かいノードやその他の解説はこの記事でも度々取り上げたg200kgさんのHPを参照して下さい。
またWeb Sounderでも簡単なサンプル付きで解説をしてくれています。
より詳細な解説はW3Cの文章(日本語訳)をより正確な情報はW3Cの原文を参照して下さい。
もう少し他のノードの解説とかは追加予定

Discussion