Web Audio APIでコインの音を作る

Web Audio APIを使えばブラウザでも簡単に音楽プログラミングができる！と思いたいのですが、どうやらAPIがあるだけでは不十分で少しばかりは音楽プログラミングの知識が必要そうです。ということで、この記事では世界で最も有名な効果音「コインの音」を題材に楽譜のパラメータ化とWeb Audio APIでの実装をできるだけ丁寧に解説します。

## 楽譜

![sheet music](https://qiita-image-store.s3.amazonaws.com/0/2568/7a648e0b-b072-af97-4f08-87b544ca5ecf.png)

楽譜を手軽に探すには「coin sheet music」などで画像検索します。コインの音は商標出願されていますので、今回はそれを参考にします。（画像は[商標プラットフォーム](https://www.j-platpat.inpit.go.jp/web/all/top/BTmTopPage)のスクリーンショットです。）

この楽譜からは次の点が読み取れます。

- テンポが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ノート番号から周波数への変換は次の関数が使えます。

```js
function mtof(midi) {
  return 440 * Math.pow(2, (midi - 69) / 12);
}

var si = mtof(83); // →  987.7666025122483 「シ」の音
var mi = mtof(88); // → 1318.5102276514797 「ミ」の音
```

![mtof](https://qiita-image-store.s3.amazonaws.com/0/2568/16d5ce47-1f52-7be7-81f7-7987d67bae9e.png)

MIDIノート番号がパッと出てこない場合は、60が「ド」、69が「ラ」くらいを覚えておいてノートに鍵盤の絵を書いて確認したり、安めのキーボードに直接番号を書いておくと便利です。

![midi keyboard](https://qiita-image-store.s3.amazonaws.com/0/2568/02d11bd3-f408-8258-2f50-2560e0ede913.png)

### 音長 (duration)
Web Audio APIでは音の長さを含む時間やスケジューリングのタイミングは**秒**で指定します。音長と時間の関係については次の点を知っておくと良いでしょう。

- テンポ120 のとき 四分音符 は 0.5秒 である
- テンポが 2倍 になると 長さは 1/2倍 になる
- **八分音符** は 四分音符 の **1/2倍** の長さ 0.25秒 である
- **二分音符** は 四分音符 の **2倍** の長さ 1.0秒 である

音の長さは次の関数で計算できます。

```js
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」
```

![tdur](https://qiita-image-store.s3.amazonaws.com/0/2568/cd93f243-990a-e737-567f-290db969f33f.png)

### 実装
上記の`mtof`と`tdur`で音高と音長を元の楽譜にプロットすると次のようになります。

![annotated sheet music](https://qiita-image-store.s3.amazonaws.com/0/2568/b33656ea-08ec-b78a-3e3e-07fbf6f32a54.png)

これをWeb Audio APIで実装します。

https://jsfiddle.net/mohayonao/c9Ly3nLo/

```js
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](http://g200kg.github.io/web-audio-api-ja/#BaseAudioContext)）というオブジェクトを中心にデザインされています。オーディオコンテキストの主な役割はオーディオノード（[AudioNode](http://g200kg.github.io/web-audio-api-ja/#the-audionode-interface)）を生成するメソッドの提供と、オーディオノードを組み合わせたオーディオグラフにもとづいた音声処理を行うことにあります。ひとつのオーディオコンテキストでも複雑なオーディオグラフを扱えますので、通常はひとつのドキュメント（オーディオアプリケーション）につきひとつのオーディオコンテキストしか必要ありません。

オーディオコンテキストは次のように生成します。

```js
var audioContext = new AudioContext();
```

注意点としてSafariでは今だにwebkitプリフィクスが必要なので、事前に`AudioContext`を初期化しておくと良いでしょう。

```js
window.AudioContext = window.AudioContext || window.webkitAudioContext;
```

### オーディオグラフ
Web Audio APIには音を生成したり加工したりするための部品がいくつか用意されています。その部品のことをオーディノードと呼びます。そして、複数のオーディオノードを組み合わせたものをオーディオグラフと呼びます。

Web Audio APIのオーディオグラフは通常、OscillatorNodeやAudioBufferSourceNodeといった**音源**ノードで音を生成して、GainNodeやBiquadFilterNodeなどの**変換**ノードで音を加工、最終的にはAudioDestinationNodeという**出力**ノードで音声出力を行うように接続します。

コインの音では基本的な音を生成するOscillatorNodeと音量を調整するGainNode、2つのオーディオノードを使います。（デモページではさらにAnalyserNodeという**分析**ノードを使って波形を表示しています。）

<img width="414" alt="make-coin-sound-graph.png" src="https://qiita-image-store.s3.amazonaws.com/0/2568/1b61fd74-0469-f0ac-e9f1-b7e67b402e1e.png">

### スケジューリング
Web Audio APIではオーディオノードの動作や将来のパラメータの変化を正確なタイミングでスケジューリングできます。スケジューリング機能を使わずにJavaScriptのタイマーAPIなどを駆使してリアルタイムにパラメータを設定することもできますが、タイミングが不正確になったりプログラムが複雑になる場合が多いです。

コインの音では `t0` `t1` `t2`、3つのタイミングでスケジューリングを行います。

## OscillatorNode
OscillatorNodeは基本的な音を生成するためのオーディオノードです。`type`で音の種類、`frequency`で音の高さを指定できます。OscillatorNodeはAudioContextの`createOscillator()`メソッドで生成します。

```js
var oscillator = audioContext.createOscillator();
```

### type
音の種類を基本波形から指定します。基本波形は "sine", "triangle", "sawtooth", "square" の4つが定義されています。それぞれに音色的な特徴があるのですが、とりあえず右にいくにつれて派手に(キラキラに)なると覚えておけば良いかも知れません。

![oscillator.type](https://qiita-image-store.s3.amazonaws.com/0/2568/7097d606-66c3-fac7-e55b-18580b327c2f.png)

コインの音では "square" を使います。

```js
oscillator.type = "square";
```

### frequency
音の高さを周波数で指定します。

コインの音では`mtof`関数で既に計算してある`si`と`mi`に、それぞれ`t0`と`t1`で設定されるようにスケジューリングしています。

```js
oscillator.frequency.setValueAtTime(si, t0);
oscillator.frequency.setValueAtTime(mi, t1);
```

`setValueAtTime(value, startTime)`は`startTime`になったら`value`の値をセットするメソッドです。

![oscillator.frequency](https://qiita-image-store.s3.amazonaws.com/0/2568/6ab835b2-2f31-88d0-2fe2-b4bee4b44259.png)

### 再生/停止
OscillatorNodeは`start(when)`と`stop(when)`メソッドで再生と停止をスケジューリングできます。OscillatorNodeの再生は一度きりで`stop()`で指定したタイミングで自動的にオーディオグラフから破棄されます。破棄されたOscillatorNodeはもう一度再生することはできません。もう一度音を出したい場合はその都度OscillatorNodeを生成する必要があります。音の長さが最初から分かっている場合は、同時に呼び出しておくと簡単で良いでしょう。

コインの音では`tdur`関数で既に計算してある`t0`で発音を開始し、`t2`で停止するようにスケジューリングしています。

```js
oscillator.start(t0);
oscillator.stop(t2);
```

### 接続
オーディオノードを組み合わせてオーディオグラフにするには`connect(destination)`メソッドを使用します。

コインの音ではOscillatorNodeをGainNodeに接続したいので次のように記述します。

```js
oscillator.connect(gain);
```

接続したオーディオノードを切断するには`disconnect()`メソッドを使いますが、上述のように不要になったオーディオノードは自動的にオーディオグラフから削除されるので、明示的に呼び出す必要は特にありません。

`connect()`した以上、どうしても`disconnect()`したい！対称性！！！という場合は、次のようにOscillatorNodeの停止イベントで切断すれば良いです。

```js
oscillator.onended = function() {
  oscillator.disconnect();
};
```

## GainNode
GainNode は音の強弱や音量の遷移(エンベロープと呼ばれます)を制御するためのオーディオノードです。`gain`で音の大きさを指定できます。GainNodeはAudioContextの`createGain()`メソッドで生成します。

```js
var gain = audioContext.createGain();
```

### gain
音の大きさを比率で指定します。

コインの音では`t1`から`t2`にかけて減衰するような台形型エンベロープをスケジューリングしています。

```js
gain.gain.setValueAtTime(0.5, t0);
gain.gain.setValueAtTime(0.5, t1);
gain.gain.linearRampToValueAtTime(0, t2);
```

`linearRampToValueAtTime(value, endTime)`は`endTime`に`value`の値になるように線形変化させるメソッドです。このメソッドには変化の**終端**の値と時間を指定します。ですから必ず変化の**始端**になるメソッドとペアで使用する必要があります。

![gain.gain](https://qiita-image-store.s3.amazonaws.com/0/2568/d6c7c91e-1d88-c1d4-8b21-a84b3d08b633.png)

## 使う
`coin`は関数化してあるので呼び出すだけでコインの音が鳴ります。引数`destination`で任意のオーディオノードに接続でき、`playbackTime`で好きなタイミングに発音できます。こういう仕組みにしておくと、この音をフィルタで加工したり、リズミカルに鳴らすことが容易になります。

```js
document.getElementById("coin").onclick = function() {
  coin(audioContext.destination, audioContext.currentTime);
};
```

## おわり
というわけで思ったよりも長くなってしまいましたが、Web Audio APIでコインの音をつくってみました。単純な音ですが、Web Audio APIの基本が詰まった良い題材だと思います。この音から色々な音作りの練習をはじめてみると良いかも知れません。

それでは良いウェブオーディオライフを！:santa:

## 参考
- [Web Audio API (日本語訳)](http://g200kg.github.io/web-audio-api-ja/)
- [O'Reilly Japan - Web Audio API](https://www.oreilly.co.jp/books/9784873116419/)

おかしな部分や不明な点があれば気軽にコメントしてください。