Web Audio APIを使えばブラウザでも簡単に音楽プログラミングができる!と思いたいのですが、どうやらAPIがあるだけでは不十分で少しばかりは音楽プログラミングの知識が必要そうです。ということで、この記事では世界で最も有名な効果音「コインの音」を題材に楽譜のパラメータ化とWeb Audio APIでの実装をできるだけ丁寧に解説します。
楽譜
楽譜を手軽に探すには「coin sheet music」などで画像検索します。コインの音は商標出願されていますので、今回はそれを参考にします。(画像は商標プラットフォームのスクリーンショットです。)
この楽譜からは次の点が読み取れます。
- テンポが180
- 最初の音が十六分音符の「シ」で次の音は「ミ」
- 全体の長さは四分音符が3つぶん
楽譜がまったく読めないという場合は、音階(ドレミファソラシド)と音長(四分音符とか)が読める程度に覚えれば良いと思います。(初学者向け教本の冒頭20ページくらいを把握していれば十分です。)
楽譜をパラメータ化する
楽譜を元にプログラムを書くにあたって次のパラメータを計算します。
- 音高(pitch)
- 音長(duration)
音高 (pitch)
Web Audio APIのOscillatorNodeでは音の高さを周波数(frequency)で指定します。周波数というのは1秒間に振動が繰り返される回数で、周波数が大きければ音は高くなります。音高の周波数については次の点を知っておくと良いでしょう。
- 周波数が 2倍 になれば 1オクターブ 上がる
- 周波数が 1/2倍 になれば 1オクターブ 下がる
- 440Hzが「ラ」
しかし、音高を周波数で直接扱うのは面倒なので、多くの場合ではMIDIノート番号を経由して周波数を計算します。MIDIノート番号を簡単に説明するとピアノの鍵盤に番号を振ったものです。MIDIノート番号と音高の関係については次の点を知っておくと良いでしょう。
- MIDIノート番号が +12 で 1オクターブ 上がる(周波数が 2倍 になる)
- MIDIノート番号が -12 で 1オクターブ 下がる(周波数が 1/2倍 になる)
- 69 が 「ラ」の 440Hz
MIDIノート番号から周波数への変換は次の関数が使えます。
function mtof(midi) {
return 440 * Math.pow(2, (midi - 69) / 12);
}
var si = mtof(83); // → 987.7666025122483 「シ」の音
var mi = mtof(88); // → 1318.5102276514797 「ミ」の音
MIDIノート番号がパッと出てこない場合は、60が「ド」、69が「ラ」くらいを覚えておいてノートに鍵盤の絵を書いて確認したり、安めのキーボードに直接番号を書いておくと便利です。
音長 (duration)
Web Audio APIでは音の長さを含む時間やスケジューリングのタイミングは秒で指定します。音長と時間の関係については次の点を知っておくと良いでしょう。
- テンポ120 のとき 四分音符 は 0.5秒 である
- テンポが 2倍 になると 長さは 1/2倍 になる
- 八分音符 は 四分音符 の 1/2倍 の長さ 0.25秒 である
- 二分音符 は 四分音符 の 2倍 の長さ 1.0秒 である
音の長さは次の関数で計算できます。
function tdur(tempo, length) {
return (60 / tempo) * (4 / length);
}
var t1 = tdur(180, 16); // → 0.08333333333333333 「テンポ180 16分音符」
var t2 = tdur(180, 4) * 3; // → 1 「テンポ180 4分音符 x 3」
実装
上記のmtof
とtdur
で音高と音長を元の楽譜にプロットすると次のようになります。
これをWeb Audio APIで実装します。
function coin(destination, playbackTime) {
var t0 = playbackTime;
var t1 = t0 + tdur(180, 16);
var t2 = t0 + tdur(180, 4) * 3;
var si = mtof(83);
var mi = mtof(88);
var audioContext = destination.context;
var oscillator = audioContext.createOscillator();
var gain = audioContext.createGain();
oscillator.type = "square";
oscillator.frequency.setValueAtTime(si, t0);
oscillator.frequency.setValueAtTime(mi, t1);
oscillator.start(t0);
oscillator.stop(t2);
oscillator.connect(gain);
gain.gain.setValueAtTime(0.5, t0);
gain.gain.setValueAtTime(0.5, t1);
gain.gain.linearRampToValueAtTime(0, t2);
gain.connect(destination);
}
function mtof(midi) {
return 440 * Math.pow(2, (midi - 69) / 12);
}
function tdur(tempo, length) {
return (60 / tempo) * (4 / length);
}
以下はこのコードについて解説です。
オーディオコンテキスト
Web Audio APIはオーディオコンテキスト(AudioContext)というオブジェクトを中心にデザインされています。オーディオコンテキストの主な役割はオーディオノード(AudioNode)を生成するメソッドの提供と、オーディオノードを組み合わせたオーディオグラフにもとづいた音声処理を行うことにあります。ひとつのオーディオコンテキストでも複雑なオーディオグラフを扱えますので、通常はひとつのドキュメント(オーディオアプリケーション)につきひとつのオーディオコンテキストしか必要ありません。
オーディオコンテキストは次のように生成します。
var audioContext = new AudioContext();
注意点としてSafariでは今だにwebkitプリフィクスが必要なので、事前にAudioContext
を初期化しておくと良いでしょう。
window.AudioContext = window.AudioContext || window.webkitAudioContext;
オーディオグラフ
Web Audio APIには音を生成したり加工したりするための部品がいくつか用意されています。その部品のことをオーディノードと呼びます。そして、複数のオーディオノードを組み合わせたものをオーディオグラフと呼びます。
Web Audio APIのオーディオグラフは通常、OscillatorNodeやAudioBufferSourceNodeといった音源ノードで音を生成して、GainNodeやBiquadFilterNodeなどの変換ノードで音を加工、最終的にはAudioDestinationNodeという出力ノードで音声出力を行うように接続します。
コインの音では基本的な音を生成するOscillatorNodeと音量を調整するGainNode、2つのオーディオノードを使います。(デモページではさらにAnalyserNodeという分析ノードを使って波形を表示しています。)
スケジューリング
Web Audio APIではオーディオノードの動作や将来のパラメータの変化を正確なタイミングでスケジューリングできます。スケジューリング機能を使わずにJavaScriptのタイマーAPIなどを駆使してリアルタイムにパラメータを設定することもできますが、タイミングが不正確になったりプログラムが複雑になる場合が多いです。
コインの音では t0
t1
t2
、3つのタイミングでスケジューリングを行います。
OscillatorNode
OscillatorNodeは基本的な音を生成するためのオーディオノードです。type
で音の種類、frequency
で音の高さを指定できます。OscillatorNodeはAudioContextのcreateOscillator()
メソッドで生成します。
var oscillator = audioContext.createOscillator();
type
音の種類を基本波形から指定します。基本波形は "sine", "triangle", "sawtooth", "square" の4つが定義されています。それぞれに音色的な特徴があるのですが、とりあえず右にいくにつれて派手に(キラキラに)なると覚えておけば良いかも知れません。
コインの音では "square" を使います。
oscillator.type = "square";
frequency
音の高さを周波数で指定します。
コインの音ではmtof
関数で既に計算してあるsi
とmi
に、それぞれt0
とt1
で設定されるようにスケジューリングしています。
oscillator.frequency.setValueAtTime(si, t0);
oscillator.frequency.setValueAtTime(mi, t1);
setValueAtTime(value, startTime)
はstartTime
になったらvalue
の値をセットするメソッドです。
再生/停止
OscillatorNodeはstart(when)
とstop(when)
メソッドで再生と停止をスケジューリングできます。OscillatorNodeの再生は一度きりでstop()
で指定したタイミングで自動的にオーディオグラフから破棄されます。破棄されたOscillatorNodeはもう一度再生することはできません。もう一度音を出したい場合はその都度OscillatorNodeを生成する必要があります。音の長さが最初から分かっている場合は、同時に呼び出しておくと簡単で良いでしょう。
コインの音ではtdur
関数で既に計算してあるt0
で発音を開始し、t2
で停止するようにスケジューリングしています。
oscillator.start(t0);
oscillator.stop(t2);
接続
オーディオノードを組み合わせてオーディオグラフにするにはconnect(destination)
メソッドを使用します。
コインの音ではOscillatorNodeをGainNodeに接続したいので次のように記述します。
oscillator.connect(gain);
接続したオーディオノードを切断するにはdisconnect()
メソッドを使いますが、上述のように不要になったオーディオノードは自動的にオーディオグラフから削除されるので、明示的に呼び出す必要は特にありません。
connect()
した以上、どうしてもdisconnect()
したい!対称性!!!という場合は、次のようにOscillatorNodeの停止イベントで切断すれば良いです。
oscillator.onended = function() {
oscillator.disconnect();
};
GainNode
GainNode は音の強弱や音量の遷移(エンベロープと呼ばれます)を制御するためのオーディオノードです。gain
で音の大きさを指定できます。GainNodeはAudioContextのcreateGain()
メソッドで生成します。
var gain = audioContext.createGain();
gain
音の大きさを比率で指定します。
コインの音ではt1
からt2
にかけて減衰するような台形型エンベロープをスケジューリングしています。
gain.gain.setValueAtTime(0.5, t0);
gain.gain.setValueAtTime(0.5, t1);
gain.gain.linearRampToValueAtTime(0, t2);
linearRampToValueAtTime(value, endTime)
はendTime
にvalue
の値になるように線形変化させるメソッドです。このメソッドには変化の終端の値と時間を指定します。ですから必ず変化の始端になるメソッドとペアで使用する必要があります。
使う
coin
は関数化してあるので呼び出すだけでコインの音が鳴ります。引数destination
で任意のオーディオノードに接続でき、playbackTime
で好きなタイミングに発音できます。こういう仕組みにしておくと、この音をフィルタで加工したり、リズミカルに鳴らすことが容易になります。
document.getElementById("coin").onclick = function() {
coin(audioContext.destination, audioContext.currentTime);
};
おわり
というわけで思ったよりも長くなってしまいましたが、Web Audio APIでコインの音をつくってみました。単純な音ですが、Web Audio APIの基本が詰まった良い題材だと思います。この音から色々な音作りの練習をはじめてみると良いかも知れません。
それでは良いウェブオーディオライフを!
参考
おかしな部分や不明な点があれば気軽にコメントしてください。