WebAudioAPIの使い方
この記事の内容
簡単な動くものの解説を通してブラウザで音声を扱うためのAPIであるWebAudioAPIの解説をしていきます。
まずは雰囲気とどのような流れになるかを掴んでもらうために本記事を書きました。
記事中でいくつかのより詳しい記事のリンク、主にMDNををつけるので音声処理に興味を持ってもらえると幸いです。
WebAudioAPIとは
ブラウザを使って低レベルに近い音声加工を提供するAPIである。
APIはAudioContext
(もしくはwebkitプリフィックス付き)を通して提供される。
そのため初期化はconst audioCtx= new (window.AudioContext || window.webkitAudioContext)();
のような初期化がMDNでは推奨されている。
AudioContext
からはbaseLatency
やoutputLatency
などから遅延時間を得たり、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)
のように書くこともできる。
基本的にAudioContext
のcreateXXX
のファクトリーメソッドを用いて生成される。
AudioParam
音声の制御をするパラメーターはAudioParamのインターフェイスで提供される。これらにはdefaultValue
、minValue
、maxValue
等の設置値(読み取り専用)と値自体がvalue
として設定されている。
valueを変更することで様々なエフェクトをかける。パラメータ名自体に値は設定を設定を代入スうるとエラーになるので注意!
またメソッドを通じたスケジュールングも可能である。
サンプリングレートに完全に追従するa-rate
パラメーターとそれより追従製の悪い(128サンプルごとの)k-rate
パラメーターがある。(細かい設定はドキュメント等を確認のこと)
いくつかの具体的な音源
OscillatorNode
発振器特定の周波数の波形の音源、type
プロパティにより波形を設定できる。
- sine---正弦波(デフォルト)
- square---矩形波
- sawtooth---ノコギリ波
- triangle---三角波
- custom---カスタム波形、
setPeriodicWave(wave)
により設定
がある。周波数はfrequency
とdetune
(AudioParam)で設定できる。
AudioBufferSourceNode
任意の波形波形自体はAudioBufferを使う。
playbackRate
やdetune
で音程を変更することが可能。
またloop
(bool値)でループ設定loopStart
(秒単位)でループ時に戻る地点、loopEnd
(秒単位)でループに入る地点を設定可能
今回は以下のような感じのコードでホワイトノイズのために使った。
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でも取ってこれてこのようにできる
function decodeAudioData(url){
fetch(url).then(res=>res.arrayBuffer()).then(arrayBuffer=>{
return audioCtx.decodeAudioData(arrayBuffer)
});
}
MediaStreamAudioSourceNode
マイクの音声の取得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モジュールはディレイ信号を混ぜて出すのでこのような構成になる。
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を揺らしたりすることが可能でありそれによりいわゆるコーラスが実現できる。
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で取得可能
全体的な使い方
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モジュールのmix
はvalue
プロパティではないので別関数を用意した。
(これらは一部なのでただコピペすると動かないと思う)
あとがき
一応、基礎的な概念の解説を兼ねて動く簡単なものの作り方を解説した。
より細かいノードやその他の解説はこの記事でも度々取り上げたg200kgさんのHPを参照して下さい。
またWeb Sounderでも簡単なサンプル付きで解説をしてくれています。
より詳細な解説はW3Cの文章(日本語訳)をより正確な情報はW3Cの原文を参照して下さい。
もう少し他のノードの解説とかは追加予定
Discussion