Special-case yuu.Instrument for single-sample sounds.
authorJoe Wreschnig <joe.wreschnig@gmail.com>
Fri, 24 Apr 2015 22:30:36 +0000 (00:30 +0200)
committerJoe Wreschnig <joe.wreschnig@gmail.com>
Fri, 24 Apr 2015 22:30:36 +0000 (00:30 +0200)
Simplify Envelope as it no longer needs to special-case optimize
constants itself; FastInstruments have no envelopes at all.

src/yuu/audio.js

index 7a66e70..d127fb8 100644 (file)
     }
 
     var Envelope = yuu.Envelope = yT({
     }
 
     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) {
 
         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);
         }
     });
 
         }
     });
 
 
     yuu.Modulator = yT({
         constructor: function (dfn) {
 
     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;
         },
             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) {
         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(
             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) {
         },
 
         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;
 
             var ret = ctx.createGain();
             var dst = ret;
 
                     ctx, t0, fundamental, duration);
             }, this.modulator);
 
                     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(
                 var src = ctx.createBufferSource(name);
                 src.loop = params.loop || false;
                 src.playbackRate.value = applyMod(
                 src.connect(dst);
             }, this.sample);
 
                 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);
                 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;
             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 },
     yuu.Instruments = yf.mapValues(yf.new_(yuu.Instrument), {
         SINE: {
             envelope: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },