Web Audioの新しいAPIについてざっくり解説

Web Audio API の仕様は日々 [GitHub上で議論](https://github.com/WebAudio/web-audio-api/issues) されていて、ある程度同意が得られたものは [ドラフトページ](http://webaudio.github.io/web-audio-api/) で公開されています。仕様のブラッシュアップのほかに追加される API や変更のある API があり、それらはちらほらとブラウザに実装もされてきているので、簡単な説明とサポート状況、ポリフィルについてまとめました。

この記事に書いてあるポリフィルは下記のリポジトリにまとめてあります。

https://github.com/mohayonao/web-audio-api-shim/

また、各API対応状況は下記のページでも確認できます。

http://mohayonao.github.io/web-audio-api-shim/test/impl.html
http://compatibility.shwups-cms.ch/en/home

:feelsgood: Microsoft Edge / Safari 9 の対応状況が調べられていないので、どなたか情報ください！！  

# 新しく追加されるAPI

## AnalyserNode#getFloatTimeDomainData
信号データを Float32Array で取得する。これまで波形データは `getByteTimeDomainData` を使って Uint8Array で取得するしかできなかった。

### Interface
```js
AnalyserNode : AudioNode {
  getFloatTimeDomainData(
    array: Float32Array
  ): void;
}
```

- `array` 結果を格納する配列

### Support
| Chrome  | Opera   | Firefox | Safari | Edge |
|:-------:|:-------:|:-------:|:------:|:----:|
| :ok: 37 | :ok: 22 | :ok: 30 | :x: 8  | :ok: |

### Polyfill
`getByteTimeDomainData` で uint8 型のデータを取得して変換した値を array に代入する。

```js
var uint8 = new Uint8Array(2048);

function getFloatTimeDomainData(array) {
  this.getByteTimeDomainData(uint8);
    
  for (var i = 0, imax = array.length; i < imax; i++) {
    array[i] = (uint8[i] - 128) * 0.0078125;
  }
}

if (!AnalyserNode.prototype.hasOwnProperty("getFloatTimeDomainData")) {
  AnalyserNode.prototype.getFloatTimeDomainData = getFloatTimeDomainData;
}
```

## AudioBuffer#copyFromChannel / copyToChannel

AudioBuffer のデータを部分的に読み書きする。


以前の Firefox では `getChannelData` を使うとバッファの全データをオーディオスレッドからコピーするため、例えば 1分 のバッファの場合 105864000 (= 44100 * 60 * 4) バイトのデータをコピーする必要があったり効率が悪かったのでこういうAPIを作った (という記憶がある) のだけど、今確認したらそうでもなさそうで良く分からないです。

とはいえ、AudioBuffer のデータを部分的に読み書きしたいケースはあまりないと思う。

### Interface
```
AudioBuffer {
  copyFromChannel(
    destination: Float32Array,
    channelNumber: number,
    startInChannel: number = 0
  ): void;
}
```

- `destination` コピーした結果を格納する配列
- `channelNumber` コピー対象のチャネル
- `startInChannel` コピー元の開始インデックス

```
AudioBuffer {
  copyToChannel(
    source: Float32Array, 
    channelNumber: number, 
    startInChannel: number = 0
  ): void;
}
```

- `source` コピーする配列
- `channelNumber` コピー対象のチャネル
- `startInChannel` コピー先の開始インデックス

### Support
| Chrome  | Opera   | Firefox | Safari | Edge |
|:-------:|:-------:|:-------:|:------:|:----:|
| :ok: 43 | :ok: 30 | :ok: 27 | :x: 8  | :x:  |

### Polyfill
```js
function copyFromChannel(destination, channelNumber, startInChannel) {
  var source = this.getChannelData(channelNumber|0).subarray(startInChannel|0);
  var minLength = Math.min(source.length, destination.length);

  destination.set(source.subarray(0, minLength));
}

function copyToChannel(source, channelNumber, startInChannel) {
  var minLength= Math.min(source.length, this.length - (startInChannel|0));
  var clipped = source.subarray(0, minLength);

  this.getChannelData(channelNumber|0).set(source, startInChannel|0);
}

if (!AudioBuffer.prototype.hasOwnProperty("copyFromChannel")) {
  AudioBuffer.prototype.copyFromChannel = copyFromChannel;
}
if (!AudioBuffer.prototype.hasOwnProperty("copyToChannel")) {
  AudioBuffer.prototype.copyToChannel = copyToChannel;
}
```

## AudioContext#createStereoPanner
ステレオパンナー。`pan` 属性で左右の位置を設定したりスケジューリングしたり、OscillatorNode と組み合わせてオートパンなどが簡単にできる。
いままではなぜか 3D空間上の定位 (空間のどこに音源があって、聴く人はどこにいるのか、どっちを向いているのか) しかなかった。

### Interface
```
AudioContext {
  createStereoPanner(): StereoPannerNode;
}

StereoPannerNode : AudioNode {
  pan: AudioParam;
}
```

### Support
| Chrome  | Opera   | Firefox | Safari | Edge |
|:-------:|:-------:|:-------:|:------:|:----:|
| :ok: 41 | :ok: 28 | :ok: 37 | :x: 8  | :ok: |

### Polyfill
https://github.com/mohayonao/stereo-panner-node

```js
var StereoPannerNode = require("stereo-panner-node");

function createStereoPanner() {
  return new StereoPannerNode(this);
}

if (!AudioContext.prototype.hasOwnProperty("createStereoPanner")) {
  AudioContext.prototype.createStereoPanner = createStereoPanner;
}
```

## AudioContext#suspend / resume / close
オーディオコンテキストの 停止 / 再開 / 終了 をする。コンテキストの現在の状態は `state` 属性で取得でき、変更があった場合は `onstatechange` が呼ばれる。
各コンテキストの状態遷移は以下のとおり。

```
new AudioContext()
      |
      V
+------------+                 +------------+
| state:     | -- suspend() -> | state:     |
|  running   | <- resume() --- |  suspended |
+------------+                 +------------+
      |                              |
      | close()                      | close()
      +------------------------------+
      |
      V
+-----------+
| state:    |
|  closed   |
+-----------+
```

```
new OfflineAudioContext(...args);
      |
      V
+------------+
| state:     |
|  suspended |
+------------+
      |
      | startRendering()
      V
+------------+
| state:     |
|  running   |
+------------+
      |
      | < rendering completed >
      V
+------------+
| state:     |
|  closed    |
+------------+
```

### Interface
```
AudioContext {
  state: string;
  onstatechange: EventHandler;

  suspend(): Promise<void>;
  resume(): Promise<void>;
  close(): Promise<void>;
}
```

### Support

| API     | Chrome  | Opera   | Firefox | Safari | Edge |
|:--------|:-------:|:-------:|:-------:|:------:|:----:|
| suspend | :ok: 41 | :ok: 28 | :x: 38  | :x: 8  | :x:  |
| resume  | :ok: 41 | :ok: 28 | :x: 38  | :x: 8  | :x:  |
| close   | :ok: 42 | :ok: 29 | :x: 38  | :x: 8  | :x:  |

### Polyfill
複雑なため省略

# 変更のあったAPI

## AudioContext#decodeAudioData
Promise ベースになる。今までのコールバックベースとの互換性はある。
API の変更とは関係ないけど Firefox の場合、audioData は transferred されて空になるので注意が必要です。

### Interface
```
AudioContext {
  decodeAudioData(
    audioData: ArrayBuffer,
    successCallback: function = null,
    errorCallback: function = null
  ): Promise<AudioBuffer>;
}
```

- `audioData` オーディオデータ
- `successCallback` 成功したときのコールバック (後方互換用)
- `errorCallback` 失敗したときのコールバック (後方互換用)

### Support
| Chrome  | Opera   | Firefox | Safari | Edge            |
|:-------:|:-------:|:-------:|:------:|:---------------:|
| :x: 43  | :x: 30  | :ok: 36 | :x: 8  | :grey_question: |

### Polyfill
```js
var originalDecodeAudioData = AudioContext.prototype.decodeAudioData;
var isPromiseBased = (function() {
  var audioContext = new OfflineAudioContext(1, 2, 44100);

  return !!audioContext.decodeAudioData(new Uint8Array(0).buffer, function() {});
})();

function decodeAudioData(audioData, successCallback, errorCallback) {
  var _this = this;
  var promise = new Promise(function(resolve, reject) {
    originalDecodeAudioData.call(_this, audioData, resolve, reject);
  });

  promise.then(successCallback, errorCallback);

  return promise;
};

if (!isPromiseBased) {
  AudioContext.prototype.decodeAudioData = decodeAudioData;
}
```

## OfflineAudioContext#startRendering
Promise ベースになる。今までのコールバックベースとの互換性はある。

### Interface
```
OfflineAudioContext {
  startRendering(): Promise<AudioBuffer>;
}
```

### Support
| Chrome  | Opera   | Firefox | Safari | Edge            |
|:-------:|:-------:|:-------:|:------:|:---------------:|
| :ok: 42 | :ok: 29 | :ok: 37 | :x: 8  | :grey_question: |

## Polyfill
```js
var originalStartRendering = OfflineAudioContext.prototype.startRendering;
var isPromiseBased = (function() {
  var audioContext = new OfflineAudioContext(1, 2, 44100);
  
  return !!audioContext.startRendering();
})();

function startRendering () {
  var _this = this;
  
  return new Promise(function(resolve, reject) {
    var oncomplete = _this.oncomplete;

    _this.oncomplete = function(e) {
      resolve(e.renderedBuffer);
      if (typeof oncomplete === "function") {
        oncomplete.call(_this, e);
      }
    };

    originalStartRendering.call(_this);
  });
}

if (!isPromiseBased) {
  OfflineAudioContext.prototype.startRendering = startRendering;
}
```

## AudioNode#disconnect
これまでは指定した出力チャネルの全ノードを切断するしかできなかったけど、特定のノードだけ切断するなど、より細かい制御が可能になる。

### Interface
```js
AudioNode {
  // 全チャネルの全ノードを切断 (今までの disconenct() とは異なる)
  disconnect(): void; 
  
  // 指定した出力チャネルの全ノードを切断 (今までと同じ)
  disconnect(output: number): void;
  
  // 指定したノードを切断
  disconnect(destination: AudioNode|AudioParam): void;
  
  // 指定した出力チャネルの指定したノードを切断
  disconnect(destination: AudioNode|AudioParam, output: number): void;
  
  // 指定した出力チャネルの指定したノードの指定した入力チャネルを切断
  disconnect(destination: AudioNode, output: number, input: number): void;
}
```

### Support
| Chrome  | Opera   | Firefox | Safari | Edge            |
|:-------:|:-------:|:-------:|:------:|:---------------:|
| :ok: 43 | :ok: 30 | :x: 38  | :x: 8  | :grey_question: |

### Polyfill
複雑なため省略

# その他
- `AudioBufferSource` や `ConvolverNode` で `buffer` の再代入ができなくなる
  - 今は chrome だと warning が出るだけ (Convolverでは出ないけど)
- `ScriptProcessorNode` が廃止され Worker ベースの `AudioWorkerNode` が導入される
  - 普通の `AudioNode` と同じように扱えるカスタムノードが生成できる