Posted at

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

More than 3 years have passed since last update.

Web Audio API の仕様は日々 GitHub上で議論 されていて、ある程度同意が得られたものは ドラフトページ で公開されています。仕様のブラッシュアップのほかに追加される 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

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 に代入する。

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

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

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

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

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

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

複雑なため省略


その他



  • AudioBufferSourceConvolverNodebuffer の再代入ができなくなる


    • 今は chrome だと warning が出るだけ (Convolverでは出ないけど)




  • ScriptProcessorNode が廃止され Worker ベースの AudioWorkerNode が導入される


    • 普通の AudioNode と同じように扱えるカスタムノードが生成できる