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

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

ティラノスクリプト で WEB_AUDIO_API (Javascript) を利用するライブラリ

今までの記事の内容を踏まえて ティラノスクリプト で WEB_AUDIO_API (Javascript) を利用するライブラリを作りました。

使い方

audio-context.js の使い方を説明します。

準備

前提として、「data/others/」に以下のファイルをインストールしてください。

  • audio-metadata.min.js (前回の記事を参照)
  • audio-context.js (記事の後半で掲載)


まず、インストールしたライブラリを読み込みます。
ゲームに組み込む際は最初に一回だけ読み込めば良いです。

[loadjs storage="audio-metadata.min.js"]
[loadjs storage="audio-context.js"]


その際、必ず「audio-metadata.min.js」の方から先に読み込んでください。


これでaudio-context.js を使う準備ができました。

(1)サウンドリストを作る

AudioBufferSourceNode は一度 play関数を呼び出すと使えなくなってしまいます。
なので、音声ファイルからロードしたデータを保持するサウンドリストを実装することで再度play関数を呼び出す際の手間を省いています。


サウンドリストは種類毎に生成することが可能なので、BGMとSEでサウンドリストを分けることが可能です。

[iscript]
//BGM と SE
// コンストラクタの引数はサウンドデータの入ったフォルダーのURI。下記を参考に
tf.bgm = new soundlist("data/bgm/");
tf.se = new soundlist("data/sound/");
[endscript]


サウンドリストはゲームに組み込む際は最初に一回だけ作れば良いと思うので、ライブラリをロードした直後にでも作っておくと良いかもしれません。
その際は必ず「tf」などのシステム変数に定義するようにしてください。
ロードした音声ファイルデータは一時的なデータなので、オススメは「tf」です。
(ゲーム終了時に破棄される)

(2)音声ファイルをロードする

AudioBufferSourceNode はロードしながら再生することができないので、再生するタイミングよりも先にロードしておく必要があります。
ロード時間はパソコンのスペックや通信状況によって左右されるのでなんとも言えません。
実際にテストプレイで試してみるのが最も良いでしょう。


ちなみにティラノスタジオ上で動かした際は 1~3MB 程度の音声ファイルをロードするのに 短くて1秒未満、長くて2秒程度かかっていました。

[iscript]
// .add は単体ロード用
tf.bgm.add("sample1.ogg");
// .load は複数ロード用
tf.bgm.load(["sample2.ogg","sample3.ogg","sample4.ogg","sample5.ogg"]);
[endscript]


なお、すでにロードしているファイルだった場合、処理はスキップされます。

(3)音声ファイルを再生する

再生する際は、再生時のパラメータをまとめた playparam を利用してください。
playparam に設定されているパラメータは四つです。

  • volume : 音量(0~100)。なお100以上でも可だが音割れが発生します。
  • loop : ループの是非。true or false。
  • endTime : 開始時のフェードイン(0 → volumeへ)が終わるまでの時間。単位ミリ秒。例、3秒後=3000。
  • start : 再生開始位置。主にレジューム再生で利用しています。いきなり曲のサビから入るのも可能です。


なお、param に null が渡された場合、以下のデフォルト値で再生が行われます。

  • volume : 100 = 音量100
  • loop : false = ループ再生しない
  • endTime : 0 = フェードインなし
  • start : 0 = 音声ファイルの先頭から再生する


いよいよ再生です。再生は play関数 を使ってください。
play関数の引数は三つです。

play(filename, param, buf)
  • filename : ファイル名。まだロードしていないファイルだった場合、この時点からロードが始まります。事前ロード強力推奨。
  • param : playparam。先述したとおり再生用パラメータオブジェクト。
  • buf : 再生リスト番号。デフォルトは 0。再生中に音量操作などで利用します。


buf を使うことで同じファイルを時間差で複数鳴らすことも可能です。
再生開始後の音量調整や停止、フェードなどの操作は buf で指定した番号を使って実現しています。

[iscript]
var param = new playparam();
param.setVolume(50);// 50 = 音量50
param.setLoop(true);// true = ループ再生する
param.setEndTime(3000);// 3000 = フェードインする(0 → 50に3秒間かけて)
param.setStart(10.5);// 10.5 = 再生開始位置は先頭から10.5秒後

//sample1.ogg を プレイリスト0番で再生する
tf.bgm.play("sample1.ogg",param,0);
[endscript]

(4)再生を停止する(一時停止)

再生中のファイルを停止させるには stop関数 を使ってください。
stop関数の引数は二つです。

stop(endTime, buf)
  • endTime : 停止時のフェードアウト(現在の音量 → 0へ)が終わるまでの時間。単位ミリ秒。例、3秒後=3000。
  • buf : 再生リスト番号。デフォルトは 0。停止させる再生リスト番号を指定します。


停止の際はファイル名は使いません。
現在再生リスト番号の何番で何が鳴っているのかは使う側で把握していてください。


また、停止する際にはどのファイルがどこまで再生したかを保持する仕様です。
続きから再生したい場合は resume関数(後述) を利用してください。

[iscript]
//プレイリスト0番 をフェードアウトして停止する(現在音量 → 0に3秒間かけて)
tf.bgm.stop(3000,0);
[endscript]

(5)音量を操作する

再生中のファイルの音量を操作するには volume関数 を使ってください。
volume関数の引数は三つです。

volume(size, endTime, buf)
  • size : 変更後の音量(0~100)。なお100以上でも~(略)
  • endTime : 変更時のフェード(現在の音量 → sizeへ)が終わるまでの時間。単位ミリ秒。例、3秒後=3000。
  • buf : 再生リスト番号。デフォルトは 0。音量操作する再生リスト番号を指定します。


繰り返しになりますが、現在再生リスト番号の何番で何が鳴っているのかは使う側で把握していてください。

[iscript]
//プレイリスト0番 をフェードアウトして音量変更する(現在音量 → 25に3秒間かけて)
tf.bgm.volume(25,3000,0);
[endscript]

(6)再生を再開する

停止中のファイルの再生を再開するには resume関数 を使ってください。
resume関数の引数は三つです。

resume(filename, param, buf)
  • filename : 再生再開するファイル名。
  • param : playparam。再生用パラメータオブジェクト。詳細は playparam を参照。
  • buf : 再生リスト番号。デフォルトは 0。再生の再開に使用する再生リスト番号を指定します。


なお、ファイル毎に記録する停止位置は一つだけです。

[iscript]
var param = new playparam();
param.setVolume(50);// 50 = 音量50
param.setLoop(true);// true = ループ再生する
param.setEndTime(3000);// 3000 = フェードインする(0 → 50に3秒間かけて)
// setStart をすると前回停止位置より優先するので書かない

//sample1.ogg をプレイリスト0番で前回停止位置から再開する(0 → 50に3秒間かけて)
tf.bgm.resume("sample1.ogg",3000,0);
[endscript]

audio-context.js

//シームレスなループを実現するための音楽プレイヤー
//AudioBuffersourceNodeを使う都合上、
//先にファイルを読み込んでおかないとうまく再生できない可能性が高い

var context = new AudioContext();

function playparam () {
  var endTime,
      volume,
      loop,
      start;
  this.setEndTime = function(time){ endTime = time;}
  this.setVolume = function(vol){ volume = vol;}
  this.setLoop = function(singi){ loop = singi;}
  this.setStart = function(time){ start = time;}
  this.getEndTime = function(){return endTime;}
  this.getVolume = function(){return volume;}
  this.getLoop = function(){return loop;}
  this.getStart = function(){return start;}
  this.log = function(){
    console.log("[loopbgm] volume:"+volume+", start:"+start+", loop:"+loop+", endTime:"+endTime);
  }
}

function soundlist (folderstring) {
  var folder = folderstring,
      list = {},
      playlist = [],
      that = this;

  this.load = function(filearray){
    filearray.forEach(function(filename){that.add(filename);});
  }
  this.add = function (filename) {
    if (typeof list[filename] !== 'undefined') return;

    list[filename] = new soundloader(filename,folder);
    console.log("[loopbgm] "+filename+" をlistに追加:ロード開始");
  }
  this.get = function (filename) {
    return list[filename];
  }

  this.play = function (filename,param,buf) {
    that.add(filename);
    buf = buf || 0;

    playlist[buf] = list[filename].createsoundplayer();
    playlist[buf].play(param);
    console.log("[loopbgm] "+filename+" をplaylist["+buf+"]に追加:再生開始");
  }
  this.stop = function (endTime,buf){
    buf = buf || 0;
    if (typeof playlist[buf] === 'undefined') return;

    var time = playlist[buf].stop(endTime),
        filename = playlist[buf].getName();
    list[filename].setResumeTime(time);// レジュームデータの保存
    delete playlist[buf];//重要!
    console.log("[loopbgm] "+filename+" をplaylist["+buf+"]から削除:再生終了:"+endTime+"ms かけて 0vol へ");
  }
  this.resume = function (filename,param,buf){
    buf = buf || 0;

    var resumetime = list[filename].getResumeTime(),
        start = param.getStart();
    if(typeof start === 'undefined'){
      param.setStart(resumetime);
    }
    playlist[buf] = list[filename].createsoundplayer();
    playlist[buf].play(param);
    console.log("[loopbgm] "+filename+" をplaylist["+buf+"]に追加:復元再生開始");
  }
  this.volume = function (size,endTime,buf){
    buf = buf || 0;
    if (typeof playlist[buf] === 'undefined') return;

    playlist[buf].volume(size,endTime);
    console.log("[loopbgm] playlist["+buf+"]の音量を操作:"+endTime+"ms かけて "+size+"vol へ");
  }
}

function soundloader (filename,folder) {
  var buffer,
      meta,
      decodebuf,
      resumetime,
      name = filename,
      that = this;

  this.load = function(name){
    return new Promise(function(resolve, reject){
      var request = new XMLHttpRequest();
      request.open('GET', folder + name, true);
      request.responseType = 'arraybuffer';
      request.onload = function () {
        buffer = request.response;
        meta = AudioMetadata.ogg(buffer);
        console.log("[loopbgm] "+name+" のロードが完了");
        resolve();
      };
      request.send();
    });
  }
  this.decodeBuffer = function(){
    return new Promise(function(resolve, reject){
      context.decodeAudioData(buffer, function (decodedata) {
        decodebuf = decodedata;
        resolve();
      });
    });
  }
  this.createsoundplayer = function(){
    var sound = new soundplayer(decodebuf);
    sound.setName(name);
    if (typeof meta.loopstart !== 'undefined') sound.setLoopStart(meta.loopstart);
    if (typeof meta.loopend !== 'undefined') sound.setLoopEnd(meta.loopend);
    sound.setEnable(true);
    console.log("[loopbgm] soundplayer 再生準備完了:" + sound.getEnable());
    return sound;
  }
  this.setResumeTime = function(time){
    resumetime = time;
  }
  this.getResumeTime = function(){
    return resumetime;
  }

  this.load(filename).then(this.decodeBuffer);
}

function soundplayer (decodebuf) {
  var SAMPLE_RATE = 44100,
      position = function(samplenum, samplerate){
        return Math.round((samplenum / samplerate) * 1000) / 1000;
      },
      source,
      gain,
      name,
      enable = false;

  this.createBuffer = function(decodebuf){
    return new Promise(function(resolve, reject){
      source = context.createBufferSource();
      gain = context.createGain();
      source.buffer = decodebuf;
      source.connect(gain);
      gain.connect(context.destination);
      resolve();
    });
  }

  this.setName = function(filename){
    name = filename;
  }
  this.getName = function(){
    return name;
  }
  this.setLoopStart = function(loopstart){
    source.loopStart = position(loopstart,SAMPLE_RATE);
  }
  this.setLoopEnd = function(loopend){
    source.loopEnd = position(loopend,SAMPLE_RATE);
  }
  this.setEnable = function(bool){
    enable = bool;
  }
  this.getEnable = function(){
    return enable;
  }

  this.play = function(param){
    if (!enable) {
      console.log("[loopbgm] soundplayer はロード未完了かすでに再生済みです");
      return;
    }
    param = param || new playparam();

    var volume = param.getVolume(),
        start = param.getStart(),
        loop = param.getLoop(),
        endTime = param.getEndTime();
    volume  = volume  || 100;
    loop    = loop    || false;
    endTime = endTime || 0;
    if(typeof start === 'undefined') begin = 0;
    if(typeof start !== 'undefined') begin = start;

    console.log(
      "[loopbgm] volume:"+volume+
      ", start:"+start+
      ", loop:"+loop+
      ", endTime:"+endTime+
      ", begin:"+begin);

    //endTime が 0 のとき、フェードインなし
    if(endTime === 0){
      // volume 必須 音量(0-100)
      gain.gain.value = volume / 100;
    } else {
      // endTime オプション フェードイン終了位置(ミリ秒)
      gain.gain.linearRampToValueAtTime(0, context.currentTime);
      gain.gain.linearRampToValueAtTime(volume / 100, context.currentTime + (endTime / 1000));
    }
    // loop オプション ループ設定(true/false)
    source.loop = loop;
    // 再生
    source.start(context.currentTime,begin);
    enable = false;
  }
  this.stop = function(endTime){
    if(typeof endTime === 'undefined') endTime = 0;
    //endTime が 0 のとき、フェードアウトなし
    if(endTime === 0){
      // 以前のスケジュールをキャンセルする
      gain.gain.cancelScheduledValues(0);
      source.stop();
      time = context.currentTime;
    }else{
      // 以前のスケジュールをキャンセルする
      gain.gain.cancelScheduledValues(0);
      // endTime オプション フェードアウト終了位置(ミリ秒)
      gain.gain.linearRampToValueAtTime(0, context.currentTime + (endTime / 1000));
      time = context.currentTime + (endTime / 1000);
    }
    return time;
  }
  this.volume = function(size,endTime){
    if(typeof endTime === 'undefined') endTime = 0;
    //endTime が 0 のとき、フェードアウトなし
    if(endTime === 0){
      // 以前のスケジュールをキャンセルする
      gain.gain.cancelScheduledValues(0);
      // size 必須 音量(0-100)
      gain.gain.setValueAtTime(size / 100, 0);
    }else{
      // 以前のスケジュールをキャンセルする
      gain.gain.cancelScheduledValues(0);
      // endTime オプション フェードアウト終了位置(ミリ秒)
      gain.gain.linearRampToValueAtTime(size / 100, context.currentTime + (endTime / 1000));
    }
  }

  this.createBuffer(decodebuf);
}

ライセンス

audio-context.js は WTFPL の下でライセンスされています。
(WTFPL – Do What the Fuck You Want to Public License
筆者意訳:汝の為したいように為すが良い公式ライセンス


www.wtfpl.net


改変、二次配布、商業利用など、すべてご自由にどうぞ。
私ことシャチは一切の責任を負いませんのであしからず。


終わりに

これにて「ティラノスクリプトで WEB_AUDIO_API を利用する」シリーズは終了です。
長い間お待たせして大変申し訳ありませんでした。


また不具合などありましたらご報告いただけたらありがたいです。
暇なときに対応いたします。


それでは。