AudioBufferSourceクラスは, 楽器音などのワンショットサンプルのオーディオデータの再生に利用することを想定されていると解説しました.
ワンショットサンプルを利用する場合において, おそらく必要となってくるのがサウンドスケジューリングです.
AudioContextインスタンスのcurrentTimeプロパティを利用して,
サウンドスケジューリングに応じた時刻をstart / stopメソッドの引数に指定することでスケジューリングを実装可能です.
currentTimeプロパティは, AudioContextインスタンスの生成時点を基準時として, そこからの経過時間を秒単位で格納しています.
ちなみに, startメソッドの第2引数には, オーディオデータの開始位置を指定することが可能です.
第3引数には, 第2引数で指定した開始位置から残りの再生時間を指定します.
startメソッドにこれらの引数を指定する場合は, フォールバックにnoteOnメソッドではなく,
noteGrainOnメソッドを利用します.
サンプルコード 12は, ワンショットサンプルのオーディオデータを扱う場合の骨組みとなるコードです.
サンプルコード 12
window.AudioContext = window.AudioContext || window.webkitAudioContext;
// Create the instance of AudioContext
var context = new AudioContext();
// for legacy browsers
context.createGain = context.createGain || context.createGainNode;
// Create the instance of GainNode
var gain = context.createGain();
var base = 'demos/audio-oneshot/';
var urls = [
(base + 'C.mp3'),
(base + 'D.mp3'),
(base + 'E.mp3'),
(base + 'F.mp3'),
(base + 'G.mp3'),
(base + 'A.mp3'),
(base + 'B.mp3'),
(base + 'C1.mp3')
];
// for the instances of AudioBuffer
var buffers = new Array(urls.length);
// for the instances of AudioBufferSourceNode
var sources = new Array(urls.length);
// Create original event
var event = document.createEvent('Event');
event.initEvent('complete', true, true);
// Get ArrayBuffer by Ajax
var load = function(url, index) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
if (xhr.status === 200) {
var arrayBuffer = xhr.response; // Get ArrayBuffer
if (arrayBuffer instanceof ArrayBuffer) {
// The 2nd argument for decodeAudioData
var successCallback = function(audioBuffer) {
// Get the instance of AudioBuffer
buffers[index] = audioBuffer;
// The loading instances of AudioBuffer has completed ?
for (var i = 0, len = buffers.length; i < len; i++) {
if (buffers[i] === undefined) {
return;
}
}
// dispatch 'complete' event
document.dispatchEvent(event);
};
// The 3rd argument for decodeAudioData
var errorCallback = function(error) {
if (error instanceof Error) {
window.alert(error.message);
} else {
window.alert('Error : "decodeAudioData" method.');
}
};
// Create the instance of AudioBuffer (Asynchronously)
context.decodeAudioData(arrayBuffer, successCallback, errorCallback);
}
}
};
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.send(null);
};
for (var i = 0, len = urls.length; i < len; i++) {
load(urls[i], i);
}
document.addEventListener('complete', function() {
this.addEventListener('keydown', function(event) {
// Space ?
if (event.keyCode !== 32) {
return;
}
event.preventDefault();
// Get base time
var t0 = context.currentTime;
for (var i = 0, len = buffers.length; i < len; i++) {
// Create the instance of AudioBufferSourceNode
sources[i] = context.createBufferSource();
// for legacy browsers
sources[i].start = sources[i].start || sources[i].noteGrainOn; // noteGrainOn
sources[i].stop = sources[i].stop || sources[i].noteOff;
// Set the instance of AudioBuffer
sources[i].buffer = buffers[i];
// AudioBufferSourceNode (Input) -> GainNode (Master Volume) -> AudioDestinationNode (Output)
sources[i].connect(gain);
gain.connect(context.destination);
sources[i].start((t0 + i), 0, sources[i].buffer.duration);
sources[i].stop(t0 + i + sources[i].buffer.duration);
}
}, true);
}, false);
少しややこしいので, アルゴリズムの概要をまとめておきます.
- 1. すべてのAudioBufferインスタンスを取得したときに発生する独自イベントの定義
-
// ....
// Create original event
var event = document.createEvent('Event');
event.initEvent('complete', true, true);
// ....
- 2. ワンショットサンプルのAudioBufferインスタンスを取得
-
// ....
// for the instances of AudioBuffer
var buffers = new Array(urls.length);
// ....
// Get ArrayBuffer by Ajax
var load = function(url, index) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
if (xhr.status === 200) {
var arrayBuffer = xhr.response; // Get ArrayBuffer
if (arrayBuffer instanceof ArrayBuffer) {
// The 2nd argument for decodeAudioData
var successCallback = function(audioBuffer) {
// Get the instance of AudioBuffer
buffers[index] = audioBuffer;
// ....
};
// The 3rd argument for decodeAudioData
var errorCallback = function() {
// ....
};
// Create the instance of AudioBuffer (Asynchronously)
context.decodeAudioData(arrayBuffer, successCallback, errorCallback);
}
}
};
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.send(null);
};
for (var i = 0, len = urls.length; i < len; i++) {
load(urls[i], i);
}
// ....
- 3. 2. が完了したら独自イベントを発行して, イベントリスナーを設定
-
var load = function(url, index) {
// ....
xhr.onload = function() {
// ....
// The 2nd argument for decodeAudioData
var successCallback = function(audioBuffer) {
// ....
// The loading instances of AudioBuffer has completed ?
for (var i = 0, len = buffers.length; i < len; i++) {
if (buffers[i] === undefined) {
return;
}
}
// dispatch 'complete' event
document.dispatchEvent(event);
};
// ....
};
// ....
};
// ....
document.addEventListener('complete', function() {
this.addEventListener('keydown', function(event) {
// Space ?
if (event.keyCode !== 32) {
return;
}
event.preventDefault();
// ....
- 4. スケジューリングの基準となる時刻を取得
-
// ....
document.addEventListener('complete', function() {
this.addEventListener('keydown', function(event) {
// ....
// Get base time
var t0 = context.currentTime;
// ....
- 5. AudioBufferSourceNodeインスタンスの生成・プロパティの設定・ノード接続
-
// ....
// for the instances of AudioBufferSourceNode
var sources = new Array(urls.length);
// ....
document.addEventListener('complete', function() {
this.addEventListener('keydown', function(event) {
// ....
for (var i = 0, len = buffers.length; i < len; i++) {
// Create the instance of AudioBufferSourceNode
sources[i] = context.createBufferSource();
// for legacy browsers
sources[i].start = sources[i].start || sources[i].noteGrainOn; // noteGrainOn
sources[i].stop = sources[i].stop || sources[i].noteOff;
// Set the instance of AudioBuffer
sources[i].buffer = buffers[i];
// AudioBufferSourceNode (Input) -> GainNode (Master Volume) -> AudioDestinationNode (Output)
sources[i].connect(gain);
gain.connect(context.destination);
// ....
}
}, true);
}, false);
- 6. 基準の時刻から, 1秒ごとにオーディオを0秒の位置から開始するようにスケジューリング
-
// ....
document.addEventListener('complete', function() {
this.addEventListener('keydown', function(event) {
// ....
for (var i = 0, len = buffers.length; i < len; i++) {
// ....
sources[i].start((t0 + i), 0, sources[i].buffer.duration);
// ....
}
}, true);
}, false);
- 7. 再生時間が経過すれば停止するようにスケジューリング
-
// ....
document.addEventListener('complete', function() {
this.addEventListener('keydown', function(event) {
// ....
for (var i = 0, len = buffers.length; i < len; i++) {
// ....
sources[i].stop(t0 + i + sources[i].buffer.duration);
}
}, true);
}, false);
サウンドスケジューリングの本質的な処理はごくわずかで, 4. 6. 7. のみです.
些細な点では, フォールバックにnoteGrainOnを利用していることぐらいでしょう.
また, AudioBufferSourceNodeは使い捨てのノードなので,
イベント発生のたびにインスタンスを生成していることにも着目してください.
デモ 08は, サンプルコード 12を改良して, スケジューリングの時刻を変更できるようにしていますが,
本質的な処理は変わらないので, 実際に試してみて, ワンショットサンプルのサウンドスケジューリング設定の基本を習得してみてください.
デモ 08
ちなみに, ワンショットサンプルのオーディオデータを利用する場合には,
すべてのピッチ (音の高さ) に対応するオーディオデータを作成するのは大変です.
データのロードもオーディオデータが多くなるほど時間を要します.
そこで, playbackRateプロパティを利用してピッチを変更することによって, 1つのピッチに対応するオーディオデータから様々なピッチに対応させることが可能です.
表1 - 2 - c. playbackRateプロパティとピッチ
playbackRate | Pitch |
0.125 | 3オクターブ低い |
0.250 | 2オクターブ低い |
0.500 | 1オクターブ低い |
1.000 | ピッチは変わらない |
2.000 | 1オクターブ高い |
4.000 | 2オクターブ高い |
8.000 | 3オクターブ高い |