サークル燃えないゴミ仮拠点

サークル代表師路射地の連絡所代わりのブログ

WEB_AUDIO_API (Javascript) で フェードイン・フェードアウトする方法

今回はWEB_AUDIO_APIでフェードイン・フェードアウトをする方法についてお話しします。


WEB_AUDIO_API でフェードイン・フェードアウトを行うコツ

WEB_AUDIO_APIのGainNodeを使ったフェード操作にはコツがあります。


それは必ず以下の手順を守ることです。

  1. 再生スケジュールのキャンセル
  2. 再生スケジュールにフェード終了位置と目標音量を指定


WEB_AUDIO_APIは再生スケジュールというタイムラインを使って演奏を制御しています。
よってフェードインにしろアウトにしろ再生スケジュールに予定を書き込むことで実現するしかありません。


その際、前に入れた予定をキャンセルしておかないとフェードがうまく機能しません。
とても重要なのでそういうものとしてよく覚えておいてください。


サンプル

以上を踏まえてWEB_AUDIO_APIでフェードするサンプルを載せておきます。

用意するもの

  • ティラノスクリプトの新しいゲームプロジェクト
  • 少し長めの音声ファイル(名前を「sample.ogg」にしておきます)

準備

「sample.ogg」をゲームプロジェクトの「data/bgm/」フォルダーに配置したら準備完了です。
それではサンプルコードを「data/scenario/」フォルダーの「first.ks」に書いていきます。

data/scenario/first.ks

[iscript]
// https://syachi.hatenablog.jp/entry/2019/07/07/174015 に載っけたサンプルを流用しています。
tf.context = new AudioContext();
var arrayBuffer;
var audioBuffer;
var source;
tf.gain;

function getArrayBuffer(url){
  return new Promise(function(resolve){
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';
    request.onload = function () {
      arrayBuffer = request.response;
      resolve();
    };
    request.send();
  });
}
function createAudioBuffer(){
  return new Promise(function(resolve){
    tf.context.decodeAudioData(arrayBuffer, function (buf) {
      audioBuffer = buf;
      resolve();
    });
  });
}
function createBuffer(){
  return new Promise(function(resolve){
    source = tf.context.createBufferSource();
    tf.gain = tf.context.createGain();
    source.buffer = audioBuffer;
    source.connect(tf.gain);
    tf.gain.connect(tf.context.destination);
    resolve();
  });
}
function playOGG(){
  // 冒頭のフェードイン(10秒)
  // 開始時の音量は 0, 開始位置はタイムラインの現在位置(未再生なら0)を示す context.currentTime
  tf.gain.gain.linearRampToValueAtTime(0, tf.context.currentTime);
  // context.currentTime から10秒後に音量 1 となるようにフェードイン
  tf.gain.gain.linearRampToValueAtTime(1, tf.context.currentTime + 10);
  // ファイルの先頭から再生開始
  source.start(0);
}

// ファイルのパス
var SOUND_URL = 'data/bgm/sample.ogg';
getArrayBuffer(SOUND_URL).then(createAudioBuffer).then(createBuffer).then(playOGG);
[endscript]
ファイル先頭から10秒かけてフェードイン。[r]
[wait time="10000"]
10秒経過。音量[emb exp="tf.gain.gain.value"][r]
[iscript]
// 予定のキャンセル
tf.gain.gain.cancelScheduledValues(0);
// context.currentTime から20秒後に音量 0 となるようにフェードアウト
tf.gain.gain.linearRampToValueAtTime(0, tf.context.currentTime + 20);
[endscript]
次は20秒かけてフェードアウト。音量[emb exp="tf.gain.gain.value"][r]
[wait time="14000"]
14秒経過、やっぱりフェードアウトはやめる。音量[emb exp="tf.gain.gain.value"][r]
[iscript]
// 予定のキャンセル
tf.gain.gain.cancelScheduledValues(0);
// context.currentTime から5秒後に音量 1 となるようにフェードイン
tf.gain.gain.linearRampToValueAtTime(1, tf.context.currentTime + 5);
[endscript]
(1) 5秒かけてフェードイン。音量[emb exp="tf.gain.gain.value"][r]
[wait time="5000"]
5秒経過。音量[emb exp="tf.gain.gain.value"][r]
[iscript]
// 予定のキャンセル
tf.gain.gain.cancelScheduledValues(0);
// context.currentTime から20秒後に音量 0 となるようにフェードアウト
tf.gain.gain.linearRampToValueAtTime(0, tf.context.currentTime + 20);
[endscript]
もう一度20秒かけてフェードアウト。音量[emb exp="tf.gain.gain.value"][r]
今度は予定のキャンセルを入れなかったバージョン。[r]
[wait time="14000"]
14秒経過、やっぱりフェードアウトはやめる。音量[emb exp="tf.gain.gain.value"][r]
[iscript]
// 予定のキャンセルは入れない
//tf.gain.gain.cancelScheduledValues(0);
// context.currentTime から5秒後に音量 1 となるようにフェードイン
tf.gain.gain.linearRampToValueAtTime(1, tf.context.currentTime + 5);
[endscript]
(2) 5秒かけてフェードイン。音量[emb exp="tf.gain.gain.value"][r]
[wait time="5000"]
5秒経過。音量[emb exp="tf.gain.gain.value"]


実行時は (1) と (2) の音量に注目してください。
予定をキャンセルしなかった場合だと、フェードインの始まりが遅くなっているのです。


まとめ

最後に今回の要点をまとめましょう。


WEB_AUDIO_APIでフェードを行う際には以下の手順を守ること。

  1. 再生スケジュールのキャンセル
  2. 再生スケジュールにフェード終了位置と目標音量を指定


なお、ファイル冒頭のフェードインだけは再生スケジュールのキャンセルが必要ありません。
その代わり、音量を0から始める必要があるのでその設定を先に記述してください。


以上を理解すれば、今日からあなたも音量操作の魔術師です。
本能の赴くままフェードイン・フェードアウトを使いまくってください。

2020/12/09追記

現在の ティラノビルダーver1.86 Chroniumではサンプルがうまく機能しない場合があります。
その際は以下のようにサンプルを修正してみて下さい。

[iscript]
// 予定のキャンセル
tf.gain.gain.cancelScheduledValues(0);
// フェード操作の直前に、現在の音量と再生位置を与える
tf.gain.gain.linearRampToValueAtTime(tf.gain.gain.value, tf.context.currentTime);
// context.currentTime から20秒後に音量 0 となるようにフェードアウト
tf.gain.gain.linearRampToValueAtTime(0, tf.context.currentTime + 20);
[endscript]

これでうまくフェードするはずです。