Include URL in audio load error message.
[pwl6.git] / src / yuu / audio.js
index 881924e..cc00cf7 100644 (file)
@@ -1,7 +1,7 @@
 /* Copyright 2014 Yukkuri Games
    Licensed under the terms of the GNU GPL v2 or later
-   @license http://www.gnu.org/licenses/gpl-2.0.html
-   @source: http://yukkurigames.com/yuu/
+   @license https://www.gnu.org/licenses/gpl-2.0.html
+   @source: https://yukkurigames.com/yuu/
 */
 
 (function (yuu) {
@@ -29,7 +29,6 @@
             this._masterVolume.gain.value = 0.5;
             this._musicVolume.gain.value = 0.5;
 
-            this._bufferCache = {};
             this._mute = false;
             this._storage = null;
             this._volume = this._masterVolume.gain.value;
@@ -90,7 +89,7 @@
 
         currentTime: { alias: "_ctx.currentTime" },
 
-        decodeAudioData: function (data) {
+        decodeAudioData: function (data, hint) {
             var ctx = this._ctx;
             try {
                 return ctx.decodeAudioData(data);
@@ -99,7 +98,8 @@
                     ctx.decodeAudioData(data, function (buffer) {
                         resolve(buffer);
                     }, function () {
-                        reject(new Error("Error decoding audio buffer"));
+                        reject(new Error("Error decoding audio buffer"
+                                         + (hint ? ": " + hint : "")));
                     });
                 });
             }
 
         createBufferSource: function (path) {
             var source = this._ctx.createBufferSource();
-            var sample = new yuu.AudioSample(path, this);
+            var sample = yf.isString(path)
+                ? new yuu.AudioSample(path, this)
+                : path;
             if ((source.buffer = sample.buffer) === null) {
                 sample.ready.then(function () {
                     source.buffer = sample.buffer;
     }
 
     var Envelope = yuu.Envelope = yT({
-        constructor: yf.argcd(
-            function (pairs) {
-                Envelope.call(this, pairs, 1);
-            },
-            function (pairs, scale) {
-                pairs = pairs || { "0": 1, "100%": 1 };
-                this.ts = Object.keys(pairs);
-                this.vs = yf.map.call(pairs, yf.getter, this.ts);
-                this.scale = scale;
-                var a = 0, b = 0;
-                var unlimited = false;
-                yf.each(function (t) {
-                    if (+t) {
-                        a = Math.max(+t, a);
-                        b = Math.min(+t, b);
-                    }
-                    unlimited = unlimited || (t[t.length - 1] === "%");
-                }, this.ts);
-                this.minDuration = a - b;
-                this.maxDuration = (unlimited || a === b)
-                    ? Infinity
-                    : this.minDuration;
-                var vMin = Math.min.apply(Math, this.vs);
-                var vMax = Math.max.apply(Math, this.vs);
-                this.constant = vMin === vMax && this.vs[0] * this.scale;
-            }
-        ),
+        constructor: function (timeline) {
+            timeline = timeline || { 0: 1 };
+            this.ts = Object.keys(timeline);
+            this.vs = yf.map.call(timeline, yf.getter, this.ts);
+            var ts = this.ts.filter(isFinite);
+            this.duration = (Math.max.apply(Math, ts)
+                             - Math.min.apply(Math, ts));
+            this.unlimited = !this.duration || ts.length !== this.ts.length;
+        },
+
+        clampDuration: function (duration) {
+            return this.unlimited
+                ? Math.max(duration, this.duration)
+                : this.duration;
+        },
 
         schedule: function (param, t0, scale, duration) {
-            if (this.constant !== false) {
-                param.setValueAtTime(scale * this.constant, t0);
-            } else {
-                yf.each.call(this, function (s, v) {
-                    v = v * scale * this.scale;
-                    var t = t0 + applyMod(s, duration);
-                    if (t === t0)
-                        param.setValueAtTime(v, t);
-                    else
-                        param.linearRampToValueAtTime(v, t);
-                }, this.ts, this.vs);
-            }
+            yf.each(function (s, v) {
+                v = v * scale;
+                var t = t0 + applyMod(s, duration);
+                if (t === t0)
+                    param.setValueAtTime(v, t);
+                else
+                    param.linearRampToValueAtTime(v, t);
+            }, this.ts, this.vs);
         }
     });
 
             var url = yuu.resourcePath(path, "sound", "wav");
             this.data = null;
             this.ready = yuu.GET(url, { responseType: "arraybuffer" })
-                .then(ctx.decodeAudioData.bind(ctx))
+                .then(function (data) {
+                    return ctx.decodeAudioData(data, url);
+                })
                 .then(yf.setter.bind(this, "buffer"))
                 .then(yf.K(this));
         }
 
     yuu.Modulator = yT({
         constructor: function (dfn) {
-            this.envelope = new yuu.Envelope(dfn.envelope);
+            this.envelope = new Envelope(dfn.envelope);
             this.frequency = dfn.frequency;
             this.index = dfn.index || 1.0;
         },
         }
     });
 
-    yuu.Instrument = yT({
+    var BaseInstrument = yT({
+        play: yf.argcd(
+            function () {
+                return this.play(null, 0, 0, 1, 1);
+            },
+            function (ctx, t, freq, amp, duration) {
+                ctx = ctx || yuu.audio;
+                t = t || ctx.currentTime;
+                var g = this.createSound(ctx, t, freq, amp, duration);
+                g.connect(ctx.destination);
+                return g;
+            }
+        )
+    });
+
+    var FastInstrument = yT(BaseInstrument, {
         constructor: function (dfn) {
-            if (yf.isString(dfn)) {
-                var sampleName = dfn;
-                dfn = { sample: {} };
-                dfn.sample[sampleName] = {};
+            this.sample = new yuu.AudioSample(dfn);
+            this.ready = this.sample.ready.then(yf.K(this));
+        },
+
+        createSound: function (ctx, t0, fundamental, amplitude, duration) {
+            var src = ctx.createBufferSource(this.sample);
+            var ret = src;
+            src.start(t0, 0);
+            src.stop(t0 + duration);
+            if (amplitude !== 1.0) {
+                ret = ctx.createGain();
+                src.connect(ret);
+                ret.gain.value = amplitude;
             }
+            return ret;
+        }
+    });
+
+    var Instrument = yT(BaseInstrument, {
+        constructor: function (dfn) {
             this.envelope = new yuu.Envelope(dfn.envelope);
             this.frequency = dfn.frequency || (dfn.sample ? {} : { "x1": 1.0 });
             this.modulator = yf.map(
         },
 
         createSound: function (ctx, t0, fundamental, amplitude, duration) {
-            // TODO: In the case of exactly one sample with a constant
-            // envelope, optimize out the extra gain node.
-            duration = yf.clamp(duration || 0,
-                                this.envelope.minDuration,
-                                this.envelope.maxDuration);
+            duration = this.envelope.clampDuration(duration || 0);
             var ret = ctx.createGain();
             var dst = ret;
 
                     ctx, t0, fundamental, duration);
             }, this.modulator);
 
-            yf.ipairs.call(this, function (name, params) {
+            yf.ipairs(function (name, params) {
                 var src = ctx.createBufferSource(name);
                 src.loop = params.loop || false;
                 src.playbackRate.value = applyMod(
                 src.connect(dst);
             }, this.sample);
 
-            yf.ipairs.call(this, function (mfreq, mamp) {
+            yf.ipairs(function (mfreq, mamp) {
                 var osc = ctx.createOscillator();
                 osc.frequency.value = applyMod(mfreq, fundamental);
                 osc.start(t0);
             ret.gain.value = 0;
             this.envelope.schedule(ret.gain, t0, amplitude, duration);
             return ret;
-        },
-
-        play: yf.argcd(
-            function () {
-                return this.play(null, 0, 0, 1, 1);
-            },
-            function (ctx, t, freq, amp, duration) {
-                ctx = ctx || yuu.audio;
-                t = t || ctx.currentTime;
-                var g = this.createSound(ctx, t, freq, amp, duration);
-                g.connect(ctx.destination);
-                return g;
-            }
-        )
+        }
     });
 
+    yuu.Instrument = function (dfn) {
+        return yf.isString(dfn)
+            ? new FastInstrument(dfn)
+            : new Instrument(dfn);
+    };
+
     yuu.Instruments = yf.mapValues(yf.new_(yuu.Instrument), {
         SINE: {
             envelope: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },