UnitePlayer.js 作ったよ
UnitePlayer はモバイルとゲームに特化した HTML5 な音楽再生プレイヤー
UnitePlayer は、モバイルブラウザ上で動作するゲームに音をもたらします。
扱いが難しい Mobile Safari や Android ブラウザの音周りをフォーマット化し、とっても扱いやすくします。
フィーチャーフォン用のソーシャルゲームが大流行な昨今ですが、
フィーチャーフォン用のゲームって音が無いですよね?
そのゲームをそのままスマートフォン向けにコンバートしても、音がならずに寂しい感じですよね?
2012年は UnitePlayer で BGM も SE も鳴らしちゃいましょう。
そして没入感や色々なものを高めちゃいましょう!
PCブラウザでもそのまま動くから、横展開もお手軽に!
UnitePlayer なら iPhone でも BGM と SE を擬似的に同時再生できますよ。
UnitePlayer API リファレンス (version 2.0)
http://code.google.com/p/uupaa-js-spinoff/wiki/UnitePlayer_API
音源の用意から再生までの流れ
ざざっと音源を1つのファイルにまとめます。
- 音源の用意
- 音源の配置(マッピング)
- 音源を mp3 と ogg で書き出し
- 音源マップをjsで記述
音源の用意
まず、適当な音源を用意します。
UnitePlayer が想定している音源は3種類。BGM, ジングル/アイキャッチ(ちょっと長めの効果音), SE(効果音) です。
- BGM は自動的にループ再生します。ループしている音源を用意すると良いでしょう
- SE には、余韻を長引かせた音ではなく、スタッカートの効いた歯切れの良い音が良いでしょう
- ステレオとモノラルの混在は避けましょう。どちらかに統一します
- BGM を控えめにする
- Android では BGM と SE を同時に再生できますが、BGM と SE の音量だけを個別に制御できません
- あらかじめ BGM のレベルを絞り、SE と同時に再生しても SE が判別できる状態にしておくとよいでしょう
音源の配置(マッピング)
Audacity などの編集ソフトで、あつめた音源を1つにします。
ちょっとした決まりごとがあります。
- 先頭15秒と、末尾5秒を無音にします
- BGM, ジングル, SE の順に配置します。長く,ループする音源を先に配置します
- 各音源の間には、5秒以上の無音時間を入れます
配置が終わったら mp3 と ogg ファイルに書き出します。
音源を mp3 と ogg での書き出し
mp3 と ogg で書き出します(ogg は Firefox用です)。
ファイルサイズやダウンロード時間を考慮し、大きくても1.5MBぐらいに収めるとよいでしょう。
128kbps もあれば十分高音質なのですが、 32~64kbps ぐらいでもなんとかなります。
音源マップを js で記述
次に音源マップを js で記述します。
以下は、BGM x 2, ジングル x 3, SE x6 の配置例です。
- 配置時間を "00:00:00.000" 形式の文字列か、12345.678 の数値形式で指定していきます
- BGM なら配列の三番目に true を指定します
- volume は指定可能ですが、iPhone や Android では効きません(PCブラウザ用の設定です)
- offset に 0 以外の値を指定すると preset: {...} で指定した時間を一括でずらす事ができます
- 開始時刻 >= 終了時刻となるようなデータは指定できません
var param = { mp3: "uni.mp3", // mp3ファイル ogg: "uni.ogg", // oggファイル(省略可能) volume: 0.5, // 初期ボリューム offset: 0, // マッピングのオフセット値(省略可能) preset: { // ここに座標データを置いていきます // プリセット名 [開始時刻, 終了時刻, BGMならtrue] BGM0: ["0:00", "0:10", true], // 先頭の無音時間を無音BGMに応用 BGM1: ["0:15", "1:28", true], BGM2: ["1:35", "2:38", true], Z_before_boss: ["3:01", "3:04.5"], Z_after_boss: ["3:09", "3:13.6"], Z_gacha: ["4:45", "4:53.0"], SE_001: ["3:17", "3:17.5"], SE_002: [ 201 , 203.0 ], // 数値でも指定可能 SE_003: ["3:25", "3:28.2"], SE_004: ["3:31", "3:31.6"], SE_005: ["3:35", "3:35.3"], SE_006: ["3:39", "3:40.8"], } };
鳴らしてみましょう
<!DOCTYPE html><html><head><meta charset="UTF-8" /> <title>UnitePlayer DEMO</title> <style>.pconly{border:1px solid orange}</style> <script src="uni.js"></script> <script> document.write(navigator.userAgent); window.onerror = function(msg, file, line) { alert("ERROR: msg=" + msg + ", file=" + file + ", line=" + line); }; window.track0 = null; window.track1 = null; window.track2 = null; function log(args) { var id = "logger", d = document, n, parentNode = d.getElementById(id) || d.body.appendChild(n = d.createElement("div"), n.id = "logger", n); node = d.createElement("div"); if (parentNode.childNodes.length > 100) { parentNode.innerHTML = ""; } node.textContent = Array.prototype.slice.apply(arguments).join(" "); parentNode.appendChild(node); } function init(iOSSimulate) { if (!window.HTMLAudioElement || !window.HTMLAudioElement.UnitePlayer) { alert("Need iOS 4.0 and later, Android 2.3 and later"); return; } if (window.track0) { return; } var param = { mp3: "uni.mp3", ogg: "uni.ogg", volume: 0.5, offset: 0, preset: { BGM0: ["0:00", "0:10", true], BGM1: ["0:15", "1:28", true], BGM2: ["1:35", "2:38", true], Z_before_boss: ["3:01", "3:04.5"], Z_after_boss: ["3:09", "3:13.6"], Z_gacha: ["4:45", "4:53.0"], SE_001: ["3:17", "3:17.5"], SE_002: ["3:21", "3:23.0"], SE_003: ["3:25", "3:28.2"], SE_004: ["3:31", "3:31.6"], SE_005: ["3:35", "3:35.3"], SE_006: ["3:39", "3:40.8"] } }; var UnitePlayer = HTMLAudioElement.UnitePlayer; window.track0 = new UnitePlayer(param, 0, function(evt, that, track, time) { log("track" + track, evt.type, time.toFixed(3)); }); if (!iOSSimulate) { if (window.track0.info().unite) { // single audio ; } else { // multi audio window.track1 = new UnitePlayer(param, 1, function(evt, that, track, time) { log("track" + track, evt.type, time.toFixed(3)); }); window.track2 = new UnitePlayer(param, 2, function(evt, that, track, time) { log("track" + track, evt.type, time.toFixed(3)); }); } } } function preset(name, track) { switch (track) { case 2: if (window.track2) { window.track2.preset(name); break; } case 1: if (window.track1) { window.track1.preset(name); break; } case 0: window.track0.preset(name); } } </script> </head> <body> <hr /> <input type="button" value="init" onclick="init(0)"></input> <input type="button" value="init( iOS Simulate )" onclick="init(1)"></input> | <hr />Track0: <input type="button" value="BGM0 (MUTE)" onclick="preset('BGM0', 0)"></input> <input type="button" value="BGM1" onclick="preset('BGM1', 0)"></input> <input type="button" value="BGM2" onclick="preset('BGM2', 0)"></input> | <input type="button" value="pause" onclick="track0.pause()"></input> <input type="button" class="pconly" value="mute" onclick="track0.mute()"></input> <!-- iOS not work --> <input type="button" class="pconly" value="vol+0.1" onclick="track0.volume(0.1, '+')"></input> <!-- iOS not work --> <input type="button" class="pconly" value="vol-0.1" onclick="track0.volume(0.1, '-')"></input> <!-- iOS not work --> <input type="button" value="seek+10" onclick="track0.seek(10, '+')"></input> <input type="button" value="seek-10" onclick="track0.seek(10, '-')"></input> <hr />Track1: <input type="button" value="SE_001(レベルアップ)" onclick="preset('SE_001', 1)"></input> <input type="button" value="SE_002(技発動)" onclick="preset('SE_002', 1)"></input> <input type="button" value="SE_003(打撃音)" onclick="preset('SE_003', 1)"></input> <input type="button" value="SE_004(勝利)" onclick="preset('SE_004', 1)"></input> <input type="button" value="SE_005(敗北)" onclick="preset('SE_005', 1)"></input> <input type="button" value="SE_006(決定ボタン)" onclick="preset('SE_006', 1)"></input> <hr />Track2: <input type="button" value="Z_before_boss(ボス戦前)" onclick="preset('Z_before_boss', 2)"></input> <input type="button" value="Z_after_boss(ボス戦後)" onclick="preset('Z_after_boss', 2)"></input> <input type="button" value="Z_gacha(ガチャ)" onclick="preset('Z_gacha', 2)"></input> <div id="logger"></div> </body> </html>
資料
Mobile Safariに関する制限
Mobile Safari(iPhoneのブラウザ)の制限と仕様です。
- 最大同時発音数が1。本来であれば BGM を鳴らしながら SE を再生できません
- 複数のオーディオファイルをロードできません。最後にロードされたファイルしか再生できません
- プログラムから volume を制御できません。プログラムから mute もできません
- ハードキーやリモコンから本体のボリューム操作は可能です
- mp3 は再生できますが ogg は再生できません
- <audio controls> が利用可能です
- <audio autoplay> や preload は利用不能です
- ユーザのタッチ操作を起点として(イベントハンドラ内で)、audio.play() や audio.load() を行う必要があります
Mobile Firefox に関する制限
Android上で動作するMobile Firefox(Fennec)の制限と仕様です。
- 最大同時発音数は2以上。BGMを鳴らしながらSEを再生できます
- プログラムから volume を制御できます。 プログラムから mute もできます
- ogg は再生できますが mp3 は再生できません
- ogg の再生でノイズが乗り正常に再生不能です
- vorbis公式サイトのファイルでも音は鳴るが、2~3秒でしゃっくりしてしまいます
- ogg の再生でノイズが乗り正常に再生不能です
- <audio controls> が挙動不審です。再生開始は可能ですが、その後コントロールできない状態になります
ゴニョゴニョ
- 先頭の15秒の無音時間には2つの理由があります
- iOSの制限を回避しつつコードをシンプルにするため、audio.load() ではなく audio.play() でデータのロードを開始しています。その際に先走って音がならないように無音時間が必要になります
- 先頭の0~10秒を使って、BGM を OFF にできます(実際には無音のBGMが再生されつづけていますが)
- 再生が終わってしまうと、次回の再生開始や頭出しに時間がかかることがあるため、常に再生し続けるようにしています
- 再生開始タイミングと再生終了タイミングは iPhone で実際に聞いて耳で合わせてください
- Android は再生時間が短い(0.1~0.5秒)と再生されたりされなかったりと不安定になるため、内部で再生時間を延長する等の補正を行なっています