latest log

酩酊状態で書いたエンジニアポエムです。酩酊状態で読んでください。

UnitePlayer.js 作ったよ

UnitePlayer はモバイルとゲームに特化した HTML5 な音楽再生プレイヤー

UnitePlayer は、モバイルブラウザ上で動作するゲームに音をもたらします。
扱いが難しい Mobile SafariAndroid ブラウザの音周りをフォーマット化し、とっても扱いやすくします。

フィーチャーフォン用のソーシャルゲームが大流行な昨今ですが、
フィーチャーフォン用のゲームって音が無いですよね?
そのゲームをそのままスマートフォン向けにコンバートしても、音がならずに寂しい感じですよね?
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

UnitePlayer ソースコード

ソースコード (minified)

音源の用意から再生までの流れ

ざざっと音源を1つのファイルにまとめます。

  • 音源の用意
  • 音源の配置(マッピング)
  • 音源を mp3 と ogg で書き出し
  • 音源マップをjsで記述
音源の用意

まず、適当な音源を用意します。
UnitePlayer が想定している音源は3種類。BGM, ジングル/アイキャッチ(ちょっと長めの効果音), SE(効果音) です。

  • BGM は自動的にループ再生します。ループしている音源を用意すると良いでしょう
  • SE には、余韻を長引かせた音ではなく、スタッカートの効いた歯切れの良い音が良いでしょう
    • Mobile Safari(iPhone)では BGM を止めて SE を鳴らすため、余韻が長いとその分 BGM の再生が遅れてしまいます
  • ステレオとモノラルの混在は避けましょう。どちらかに統一します
  • BGM を控えめにする
    • Android では BGM と SE を同時に再生できますが、BGM と SE の音量だけを個別に制御できません
    • あらかじめ BGM のレベルを絞り、SE と同時に再生しても SE が判別できる状態にしておくとよいでしょう

これだと BGM のレベルが大きすぎて SE が隠れるため
f:id:uupaa:20111212215317p:image

BGM のレベルを SE よりも控えめに
f:id:uupaa:20111213143145p:image

音源の配置(マッピング)

Audacity などの編集ソフトで、あつめた音源を1つにします。

ちょっとした決まりごとがあります。

  • 先頭15秒と、末尾5秒を無音にします
  • BGM, ジングル, SE の順に配置します。長く,ループする音源を先に配置します
  • 各音源の間には、5秒以上の無音時間を入れます

f:id:uupaa:20111213143145p:image


配置が終わったら mp3 と ogg ファイルに書き出します。

音源を mp3 と ogg での書き出し

mp3 と ogg で書き出します(oggFirefox用です)。
ファイルサイズやダウンロード時間を考慮し、大きくても1.5MBぐらいに収めるとよいでしょう。
128kbps もあれば十分高音質なのですが、 32~64kbps ぐらいでもなんとかなります。

音源マップを js で記述

次に音源マップを js で記述します。

以下は、BGM x 2, ジングル x 3, SE x6 の配置例です。

  • 配置時間を "00:00:00.000" 形式の文字列か、12345.678 の数値形式で指定していきます
  • BGM なら配列の三番目に true を指定します
  • volume は指定可能ですが、iPhoneAndroid では効きません(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() を行う必要があります
    • タッチ操作を起点とせずにデータのロードや再生を行う事はできません。
    • これはパケット従量制のキャリアでiPhoneを利用するユーザがパケ死しないための規制です
Android Browserに関する制限

Android標準ブラウザの制限と仕様です。

  • 同時発音数は2以上(デバイス依存かも… 詳細不明)。BGMを鳴らしながらSEを再生できます
  • プログラムから volume は制御できません。プログラムから mute もできません
    • ハードキーからボリューム操作は可能です
  • mp3 は再生できますが ogg は再生できません
  • Mobile Safari と異なり、ユーザのタッチイベントを起点にしなくても音源のダウンロードと再生が可能です
Mobile Firefox に関する制限

Android上で動作するMobile Firefox(Fennec)の制限と仕様です。

  • 最大同時発音数は2以上。BGMを鳴らしながらSEを再生できます
  • プログラムから volume を制御できます。 プログラムから mute もできます
  • ogg は再生できますが mp3 は再生できません
    • ogg の再生でノイズが乗り正常に再生不能です
      • vorbis公式サイトのファイルでも音は鳴るが、2~3秒でしゃっくりしてしまいます
  • <audio controls> が挙動不審です。再生開始は可能ですが、その後コントロールできない状態になります
Opera Mobile に関する制限

Android上で動作するOpera Mobileの制限と仕様です。

  • audio.addEventListener でイベントハンドラがコールバックされません
    • コールバックされないため、まともにロジックが組めません。再生するぐらいしかできません
仕様/制限一覧
モバイル
ブラウザ
OS 同時
発音数
BGM+SE
同時再生
mp3 ogg Mute Volume イイね
Safari 4.0+ 1 △ (擬似) × × × ☆☆☆
Android 2.3+ 2+ × × × ☆☆☆
Fennec - 2+ ×
Opera - 2+ × × -

ゴニョゴニョ

  • 先頭の15秒の無音時間には2つの理由があります
    • iOSの制限を回避しつつコードをシンプルにするため、audio.load() ではなく audio.play() でデータのロードを開始しています。その際に先走って音がならないように無音時間が必要になります
    • 先頭の0~10秒を使って、BGM を OFF にできます(実際には無音のBGMが再生されつづけていますが)
  • 再生が終わってしまうと、次回の再生開始や頭出しに時間がかかることがあるため、常に再生し続けるようにしています
  • 再生開始タイミングと再生終了タイミングは iPhone で実際に聞いて耳で合わせてください
    • Android は再生時間が短い(0.1~0.5秒)と再生されたりされなかったりと不安定になるため、内部で再生時間を延長する等の補正を行なっています