tmlib.jsで音リソースをまとめて読み込む

こんばぬわ、
 [お遊びゲーム開発その６](http://qiita.com/teradonburi/items/7e41a4665861e43f18cc)でも書きましたが
tmlib.js音再生周りを
やっぱりローディング処理でまとめて読み込めなきゃ不便ということでチャレンジしてみます
今回の全ソースコードはこちら
tm.asset.Loaderで音リソースを読み込むのでtm.sound.WebAudioクラスで読み込みます
（tm.sound.WebAudioクラスはXHRで読み込む関係上、ローカルで動かすとCross Originエラーになるので
nodejs,express上の公開フォルダに放り込みます）


```js:test.html
<script src="tmlib.js"></script>
<script>

/**
 * スクリーンサイズ
 */
var SCREEN_WIDTH  = 960;
var SCREEN_HEIGHT = 640;
var SCREEN_CENTER_X  = SCREEN_WIDTH/2;
var SCREEN_CENTER_Y = SCREEN_HEIGHT/2;

tm.main(function() {
    // アプリケーション作成
    var app = tm.app.CanvasApp("#world");
    app.resize(SCREEN_WIDTH, SCREEN_HEIGHT); // 画面サイズに合わせる
    app.fitWindow(); // リサイズ対応
    app.background = "rgb(0, 0, 0)"; // 背景色をセット

    
    // ローディング表示
    var label = tm.display.Label("Now Loading");
    label.setPosition(SCREEN_CENTER_X, SCREEN_CENTER_Y);
    label.setAlign("center").setBaseline("middle");         
    app.currentScene.addChild(label);
    
    
    // リソースの読み込み
    var loader = tm.asset.Loader();
    loader.load({"bgm":"bgm.mp3"});
    // 読み込み中（リソースが１つ読み込まれるたびコールバック）
    loader.onprogress = function() {
        label.text += ".";
        console.log(label.text);
    };
    // 全リソース読み込み完了
    loader.onload = function() {
        // シーンの切り替え
        app.replaceScene(GameScene());
    };

    // tmlibの実行
    app.run();
});
    

tm.define("GameScene", {
    superClass : "tm.app.Scene",

    // シーン初期化処理
    init : function() {
        this.superInit();

        // BGM再生        
        this.bgm = tm.asset.Manager.get("bgm");
        this.bgm.volume = 1.0;
        this.bgm.loop = true;        
            
        this.bgm.play();
        this.counter = 0;
    },

    // シーン更新処理
    update: function (app) {

        this.bgm.volume -= 0.001;
        if(this.counter == 1000)
            this.bgm.stop();
        
        this.counter++;
    },
});
           
</script>
<canvas id="world"></canvas>
```

ただし、ブラウザによってはこのままでは動きません。
tm.asset.Manager.getで取得する
tm.sound.WebAudioクラスのplayメソッドとstopメソッドでエラーになります。
（WebAudio APIの仕様がかわっているのが原因）
少なくともMacのChromeとFirefoxでは動きませんでした。
のでtmlib.jsを直接修正しちゃいます。

具体的には
noteOn、noteOffが使えていないことが原因のようです

ここによると、start,stopになっている場合があるようです。
http://curtaincall.weblike.jp/portfolio-web-sounder/webaudioapi-basic/audio

また、複数回playメソッドで再生させようとしたり、
複数回stopメソッドで停止させようとすると
エラーになるため、制御してやる必要があります。

volume設定周りでもエラーを吐いていたので修正します。

tmlib.jsの17309行目あたりから定義してある
tm.sound.WebAudioクラスを修正しました。
修正部分含めてごっそり乗せます

```
(function() {

    var isAvailable = tm.global.webkitAudioContext ? true : false;
    var context = isAvailable ? new webkitAudioContext() : null;

    /**
     * @class tm.sound.WebAudio
     * WebAudioクラス
     * @extends tm.event.EventDispatcher
     */
    tm.sound.WebAudio = tm.createClass({
        superClass: tm.event.EventDispatcher,

        /** loaded */
        loaded: false,
        /** context */
        context: null,
        /** panner */
        panner: null,
        /** volume */
        volume: 0.8,
        /** isPlay */
        isPlay: false, ////////// teradonburi追加

        /**
         * @constructor
         */
        init: function(src_or_buffer) {
            this.superInit();

            this.context = context;
            var type = typeof(src_or_buffer);

            if (type==="string") {
                this.loaded = false;
                this._load(src_or_buffer);
            }
            else if (type==="object") {
                this._setup();
                this.buffer = src_or_buffer;
                this.loaded = true;
                this.dispatchEvent( tm.event.Event("load") );
            }
            else {
                this._setup();
                this.loaded = false;
            }
        },

        /**
         * 再生
         * - noteGrainOn ... http://www.html5rocks.com/en/tutorials/casestudies/munkadoo_bouncymouse/
         */
        play: function(time) {
            ////////////teradonburi修正
            if(this.isPlay)
                return;
            this.isPlay = true;

            if (time === undefined) time = 0;
            this.source.start = this.source.start || this.source.noteOn;
            this.source.start(this.context.currentTime + time);
            ////////////teradonburi修正終了

            /*
            this.source.noteGrainOn(this.context.currentTime + time, 0, this.buffer.duration);
            console.dir(this.buffer.duration);
            console.dir(this.context.currentTime)
            */

            return this;
        },

        /**
         * 停止
         */
        stop: function(time) {
            ////////////teradonburi修正開始
            if(!this.isPlay)
                return;
            this.isPlay = false;

            if (time === undefined) time = 0;
            this.source.stop = this.source.stop || this.source.noteOff;
            this.source.stop(this.context.currentTime + time);
            ////////////teradonburi修正終了
            
            var buffer = this.buffer;
            var volume = this.volume;
            var loop   = this.loop;
            
            this.source = this.context.createBufferSource();
            this.source.connect(this.panner);
            this.buffer = buffer;
            this.volume = volume;
            this.loop = loop;

            return this;
        },

        /**
         * @TODO ?
         */
        pause: function() {
            this.source.disconnect();

            return this;
        },

        /**
         * @TODO ?
         */
        resume: function() {
            this.source.connect(this.panner);

            return this;
        },

        /**
         * 複製
         */
        clone: function() {
            var webAudio = tm.sound.WebAudio(this.buffer);
            webAudio.volume = this.volume;
            return webAudio;
        },
        /**
         * dummy
         */
        setPosition: function(x, y, z) {
            this.panner.setPosition(x, y||0, z||0);

            return this;
        },
        /**
         * dummy
         */
        setVelocity: function(x, y, z) {
            this.panner.setVelocity(x, y||0, z||0);

            return this;
        },
        /**
         * dummy
         */
        setOrientation: function(x, y, z) {
            this.panner.setOrientation(x, y||0, z||0);

            return this;
        },

        /**
         * dummy
         * チェーンメソッド用
         */
        setBuffer: function(v) {
            this.buffer = v;
            return this;
        },


        /**
         * dummy
         * チェーンメソッド用
         */
        setLoop: function(v) {
            this.loop = v;
            return this;
        },


        /**
         * dummy
         * チェーンメソッド用
         */
        setVolume: function(v) {
            this.volume = v;
            return this;
        },


        /**
         * チェーンメソッド用
         */
        setPlaybackRate: function(v) {
            this.playbackRate = v;
            return this;
        },

        /**
         * @TODO ?
         * @private
         */
        _load: function(src) {
            if (!this.context) return ;

            var xhr = new XMLHttpRequest();
            var self = this;
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200 || xhr.status === 0) {
                        self.context.decodeAudioData(xhr.response, function(buffer) {
                            self._setup();
                            self.buffer = buffer;
                            self.loaded = true;
                            self.dispatchEvent( tm.event.Event("load") );
                        });
                    } else {
                        console.error(xhr);
                    }
                }
            };
            xhr.open("GET", src, true);
            xhr.responseType = "arraybuffer";
            xhr.send();
        },

        /**
         * @TODO ?
         * @private
         */
        _setup: function() {
            this.source     = this.context.createBufferSource();
            ////////////teradonburi修正開始
            if (!this.context.createGain){
                this.context.createGain = this.context.createGainNode;
            }
            this.gainNode   = this.context.createGain();
            this.panner     = this.context.createPanner();
            this.analyser   = this.context.createAnalyser();

            this.source.connect(this.gainNode);  
            this.gainNode.connect(this.panner);
            ////////////teradonburi修正終了            
            this.panner.connect(this.analyser);
            this.analyser.connect(this.context.destination);
        },

        /**
         * @TODO ?
         */
        tone: function(hertz, seconds) {
            // handle parameter
            hertz   = hertz !== undefined ? hertz : 200;
            seconds = seconds !== undefined ? seconds : 1;
            // set default value    
            var nChannels   = 1;
            var sampleRate  = 44100;
            var amplitude   = 2;
            // create the buffer
            var buffer  = this.context.createBuffer(nChannels, seconds*sampleRate, sampleRate);
            var fArray  = buffer.getChannelData(0);
            // fill the buffer
            for(var i = 0; i < fArray.length; i++){
                var time    = i / buffer.sampleRate;
                var angle   = hertz * time * Math.PI;
                fArray[i]   = Math.sin(angle)*amplitude;
            }
            // set the buffer
            this.buffer = buffer;
            return this;    // for chained API
        },
    });

    /**
     * @property    buffer
     * @TODO ?
     */
    tm.sound.WebAudio.prototype.accessor("buffer", {
        get: function()  { return this.source.buffer; },
        set: function(v) { this.source.buffer = v; }
    });

    /**
     * @property    loop
     * @TODO ?
     */
    tm.sound.WebAudio.prototype.accessor("loop", {
        get: function()  { return this.source.loop; },
        set: function(v) { this.source.loop = v; }
    });

    /**
     * @property    volume
     * @TODO ?
     */    
    tm.sound.WebAudio.prototype.accessor("volume", {
            ////////////teradonburi修正開始
        get: function()  { return this.gainNode.gain.value; },
        set: function(v) { this.gainNode.gain.value = v; }
            ////////////teradonburi修正終了
    });
    

    /**
     * @property    playbackRate
     * @TODO ?
     */
    tm.sound.WebAudio.prototype.accessor("playbackRate", {
        get: function()  { return this.source.playbackRate.value; },
        set: function(v) { this.source.playbackRate.value = v; }
    });

    /** @static @property */
    tm.sound.WebAudio.isAvailable = tm.global.webkitAudioContext ? true : false;

})();
```