Web Audio API 解説
07.パラメータとオートメーション
Web Audio APIの各ノードのパラメータには AudioParam というオブジェクトになっているものがあります。AudioParamは単に値を入れるだけではなく、他のノードの出力を受け付ける機能とオートメーション機能が備わっています。
ここではこのオートメーション機能について説明します。下の表はAudioParamが持っているオートメーション機能の一覧です。
setValueAtTime(value, startTime) | startTime になったら値を value にする |
linearRampToValueAtTime(value, endTime) | endTime に丁度 value になるように現在値から直線的に変化させる |
exponentialRampToValueAtTime(value, endTime) | endTime に丁度 value になるように現在値から指数的に変化させる |
setTargetAtTime(target, startTime, timeConstant) | startTime になったら現在値から target に向かって漸近的に変化させる |
setValueCurveAtTime(values, startTime, duration) | startTime から duration の期間、values のテーブルに従って変化させる |
cancelScheduledValues(cancelTime) | cancelTime 以降のスケジュールをキャンセルする |
cancelAndHoldAtTime(cancelTime) | cancelTime 以降のスケジュールをキャンセルするが、cancelTime 時のオートメーション値をそのまま保持する |
指定した時間までに動作するものと指定した時間になったら動作を開始するものがありますので注意が必要です。
初期化後やキャンセルの後、現在値が必要な関数をいきなり使用すると思った動作にならないので、まず setValueAtTime で現在時刻と想定する現在値を設定した後に呼び出す必要があります。
シンセサイザー的なアプリケーションで使用する場合はエンベロープのカーブ等を作るために漸近的に変化する setTargetAtTime() が多く使われるのではないかと思いますが、指定時刻に決まった値になるわけではなく、指定時刻から動作を開始して変化の速度は timeConstant で指定します。timeConstant の値はターゲット値に 63% まで近づくまでの時間です。現在値が 0 で target が 1 の場合なら値が 0.63 になるまでの秒数という意味になります。
このオートメーション機能を利用したエンベロープジェネレータの例を示します (本当はリトリガーした時の挙動などまだ考えないといけない部分は残っています)。setInterval() 以降はついでに Canvas でエンベロープのグラフを表示しているコードです。
<!DOCTYPE html>
<html>
<body>
<h1>AudioParam Automation</h1>
<br/>
<table>
<tr><th>Attack</th><td>
<input id="atk" type="range" min="0" max="5" step="0.01" value="0.3"/></td>
<td id="atkval">0.3</td></tr>
<tr><th>Decay</th><td>
<input id="dcy" type="range" min="0" max="5" step="0.01" value="1"/></td>
<td id="dcyval">1</td></tr>
<tr><th>Sustain</th><td>
<input id="sus" type="range" min="0" max="1" step="0.01" value="0.5"/></td>
<td id="susval">0.5</td></tr>
<tr><th>Release</th><td>
<input id="rel" type="range" min="0" max="5" step="0.01" value="1"/></td>
<td id="relval">1</td></tr>
</table>
<br/>
<button id="key"> Press and Hold this button </button>
<br/>
<canvas id="canvas" width="500" height="256"></canvas>
<script>
window.addEventListener("load", ()=>{
const audioctx = new AudioContext();
const osc = new OscillatorNode(audioctx);
const gain = new GainNode(audioctx, {gain:0});
const ana = new AnalyserNode(audioctx);
let x = 0;
const canvasctx = document.getElementById("canvas").getContext("2d");
const graphdata = new Uint8Array(128);
canvasctx.fillStyle = "#222222";
canvasctx.fillRect(0, 0, 500, 256);
osc.connect(gain).connect(ana).connect(audioctx.destination);
osc.start();
document.getElementById("key").addEventListener("mousedown",()=>{
if(audioctx.state=="suspended")
audioctx.resume();
x = 0;
canvasctx.fillStyle = "#222222";
canvasctx.fillRect(0, 0, 500, 256);
const t0 = audioctx.currentTime;
const t1 = t0 + parseFloat(document.getElementById("atk").value);
const d = parseFloat(document.getElementById("dcy").value);
const s = parseFloat(document.getElementById("sus").value);
gain.gain.setValueAtTime(0, t0);
gain.gain.linearRampToValueAtTime(1, t1);
gain.gain.setTargetAtTime(s, t1, d);
});
document.getElementById("key").addEventListener("mouseup",()=>{
const r = parseFloat(document.getElementById("rel").value);
const t0 = audioctx.currentTime;
if(gain.gain.cancelAndHoldAtTime)
gain.gain.cancelAndHoldAtTime(t0);
gain.gain.setTargetAtTime(0, t0, r);
});
document.getElementById("atk").addEventListener("input",(ev)=>{
document.getElementById("atkval").innerHTML=ev.target.value;
});
document.getElementById("dcy").addEventListener("input",(ev)=>{
document.getElementById("dcyval").innerHTML=ev.target.value;
});
document.getElementById("sus").addEventListener("input",(ev)=>{
document.getElementById("susval").innerHTML=ev.target.value;
});
document.getElementById("rel").addEventListener("input",(ev)=>{
document.getElementById("relval").innerHTML=ev.target.value;
});
setInterval(()=>{
if(x<500) {
ana.getByteTimeDomainData(graphdata);
let y=0;
for(let i=0;i<128;++i){
const d = Math.abs(graphdata[i]-128);
if(Math.abs(d>y))
y=d;
}
canvasctx.fillStyle = "#222222";
canvasctx.fillRect(x, 0, 2, 256);
canvasctx.fillStyle = "#00ff00";
canvasctx.fillRect(x, 256 - 2*y, 2, 2*y);
}
x += 2;
},50);
});
</script>
<br/>
</body>
</html>
テストページ:パラメータオートメーション
このパラメータオートメーションは Oscillator / AudioBufferSource の start() / stop() と同じくオーディオコンテキストの currentTime ベースでの時間管理になりますので、ちゃんと使えば JavaScript の処理の負荷による影響を受けずに正確なタイミングを作る事ができます。
ただ、このオートメーション機構は、解消しつつはありますがブラウザによる解釈の違いや、バギーな挙動が目立つ部分でもありました。仕様書通りの動作を想定してシンセサイザーのエンベロープ曲線をきちんと作ろうとすると、後から追加されたメソッドである cancelAndHoldAtTime() を使用する必要がありますが、これをサポートしているのは Chrome だけです。このサンプルは Firefox でも同じように動作しますが、仕様の解釈の違いによるものです。