Web Audio API 解説
11.オシレーターのカスタム波形
カスタム波形の基本
Oscillator ノードで使用できる波形選択に "custom" というものがあり、これを使うとどんな波形でも自由に定義できます。ただし単純な波形のテーブルで定義するのではなく波形のスペクトラムで定義するというちょっとわかりにくい仕様になっています。簡単に言えば、倍音(ハーモニクス)の強さを指定して波形を作るという方法で、オルガンのドローバーのようなものと考えればよいかと思います。
まずハーモニクスのテーブルから new periodicWave() または createPeriodicWave() で periodicWave オブジェクトを作り、それを Oscillator に setPeriodicWave() で渡します。
const real = new Float32Array(10);
const imag = new Float32Array(10);
for(let i = 0; i < 10; ++i)
real[i] = imag[i] = 0;
imag[1] = 1;
imag[2] = 0.5;
const wavtable = audioctx.createPeriodicWave(real, imag);
osc.setPeriodicWave(wavtable);
Oscillator の通常の波形選択で使用する .type プロパティは setPeriodicWave() を呼び出す事で自動的に "custom" になりますが、直接ここに "custom" の値を入れる事はできません。
そして波形指定の内容となる real と imag は4096以下で同じ長さの Float32Array です。ここには、任意の波形 1 周期分を FFT にかけた結果と同じものを設定しますが、簡単にドローバー的に使う場合には、imag の 2番目 (imag[1]) から順に値を設定します。それぞれ基準となる音、2 倍音、3 倍音、4 倍音・・・の強さとなります。波形の大きさは最終的にピークが 1 の振幅になるように自動的に正規化されますので、テーブルの各値の比率だけ気にしていれば良いです。
また real[0] は DC オフセットをあらわしますが、現在のブラウザの実装ではこれは無視されるようです。imag[0] は使用禁止で必ず 0 にします。
FFT の定義的な意味で言えば real 側のテーブルは cos() による合成、 imag 側のテーブルは sin() による合成になり、位相がずれるだけで同じ大きさの値が入っていればどちらのテーブルを使っても聴感上はほとんど同じように聴こえるのですが、波形としてはかなり違うものになります。
フーリエ解析の説明などでよく出てくる「サイン波を合成して色々な波形を作る」という例と同じように波形を作るには imag 側を使用します。
カスタム波形を使用したサンプル
ではカスタム波形を使用したサンプルです。図のような構成になっています。
<!DOCTYPE html>
<html>
<body>
<h1>Oscillator Custom waveform</h1>
<hr/>
<div>
<button id="play">Play</button> <button id="stop">Stop</button><br/>
<table>
<tr><th>Freq</th><td><input type="range" id="freq" min="50" max="1000" value="440"/></td><td id="freqval"></td></tr>
<tr><th>Gain</th><td><input type="range" id="gain" min="0" max="1" step="0.01" value="0.5"/></td><td id="gainval"></td></tr>
<tr><td><br/></td></tr>
<tr><th>Harmonics</th><th>real</th><th></th><th>imag</th></tr>
<tr><th>0</th><td><input type="range" min="0" max="1" step="0.01" id="real0" value="0"/></td><td id="real0val"></td>
<td><input type="range" min="0" max="1" step="0.01" id="imag0" value="0"/></td><td id="imag0val"></td></tr>
<tr><th>1</th><td><input type="range" min="0" max="1" step="0.01" id="real1" value="0"/></td><td id="real1val"></td>
<td><input type="range" min="0" max="1" step="0.01" id="imag1" value="1"/></td><td id="imag1val"></td></tr>
<tr><th>2</th><td><input type="range" min="0" max="1" step="0.01" id="real2" value="0"/></td><td id="real2val"></td>
<td><input type="range" min="0" max="1" step="0.01" id="imag2" value="0.5"/></td><td id="imag2val"></td></tr>
<tr><th>3</th><td><input type="range" min="0" max="1" step="0.01" id="real3" value="0"/></td><td id="real3val"></td>
<td><input type="range" min="0" max="1" step="0.01" id="imag3" value="0"/></td><td id="imag3val"></td></tr>
<tr><th>4</th><td><input type="range" min="0" max="1" step="0.01" id="real4" value="0"/></td><td id="real4val"></td>
<td><input type="range" min="0" max="1" step="0.01" id="imag4" value="0"/></td><td id="imag4val"></td></tr>
<tr><th>5</th><td><input type="range" min="0" max="1" step="0.01" id="real5" value="0"/></td><td id="real5val"></td>
<td><input type="range" min="0" max="1" step="0.01" id="imag5" value="0"/></td><td id="imag5val"></td></tr>
<tr><th>6</th><td><input type="range" min="0" max="1" step="0.01" id="real6" value="0"/></td><td id="real6val"></td>
<th><input type="range" min="0" max="1" step="0.01" id="imag6" value="0"/></td><td id="imag6val"></td></tr>
<tr><th>7</th><td><input type="range" min="0" max="1" step="0.01" id="real7" value="0"/></td><td id="real7val"></td>
<td><input type="range" min="0" max="1" step="0.01" id="imag7" value="0"/></td><td id="imag7val"></td></tr>
</table>
<div><canvas id="waveform" width ="512" height="256"> </canvas></div>
</div>
<script>
window.addEventListener("load", ()=>{
const audioctx = new AudioContext();
const osc = new OscillatorNode(audioctx);
const gain = new GainNode(audioctx);
const ana = new AnalyserNode(audioctx);
osc.connect(gain).connect(ana).connect(audioctx.destination);
audioctx.suspend();
osc.start();
const tablen = 8;
const real = new Float32Array(tablen);
const imag = new Float32Array(tablen);
const capturebuf = new Float32Array(512);
const canvas = document.getElementById("waveform");
const canvasctx = canvas.getContext('2d');
document.getElementById("stop").addEventListener("click", ()=>{
audioctx.suspend();
});
document.getElementById("play").addEventListener("click", ()=>{
SetupWave();
audioctx.resume();
});
document.getElementById("freq").addEventListener("input", Setup);
document.getElementById("gain").addEventListener("input", Setup);
for(let i=0;i<8;++i){
document.getElementById("real"+i).addEventListener("input", SetupWave);
document.getElementById("imag"+i).addEventListener("input", SetupWave);
}
Setup();
SetupWave();
function Setup(){
osc.frequency.value = document.getElementById("freqval").innerHTML
= document.getElementById("freq").value;
gain.gain.value = document.getElementById("gainval").innerHTML
= document.getElementById("gain").value;
}
function SetupWave(){
for(i = 0; i < tablen; i++) { // make Harmonics
real[i] = parseFloat(document.getElementById("real"+i+"val").innerHTML
= document.getElementById("real"+i).value);
imag[i] = parseFloat(document.getElementById("imag"+i+"val").innerHTML
= document.getElementById("imag"+i).value);
}
const waveTable = audioctx.createPeriodicWave(real, imag); //create periodicWave
osc.setPeriodicWave(waveTable); //set to Oscillator
}
///////////// for Display
function DrawGraph() {
ana.getFloatTimeDomainData(capturebuf);
canvasctx.fillStyle = "#222222";
canvasctx.fillRect(0, 0, 512, 512);
canvasctx.fillStyle = "#00ff44";
canvasctx.fillRect(0, 128, 512, 1);
for(let i = 0; i < 512; ++i) {
const v = 128 - capturebuf[i] * 128;
canvasctx.fillRect(i, v, 1, 128 - v);
}
}
setInterval(DrawGraph, 1000);
});
</script>
</body>
</html>
ちょっと長ったらしくなっていますが、実際にオシレーターから出てくる波形を確認したかったので、波形データをキャプチャーして表示する部分などが含まれています。
テストページ:オシレータのカスタム波形