Skip to content
Browse files

gh-8: Tested implementation of the simple ASR envelope.

Minor linting of the ugens file.
  • Loading branch information...
1 parent 30864bf commit 31345e930b4c1c83aa515929f103d303138bed84 @colinbdclark committed Apr 11, 2012
Showing with 143 additions and 45 deletions.
  1. +61 −29 flocking/flocking-ugens.js
  2. +82 −16 tests/flocking/js/ugen-test.js
View
90 flocking/flocking-ugens.js
@@ -599,12 +599,11 @@ var flock = flock || {};
source = that.buffer,
bufIdx = that.model.idx,
bufLen = source.length,
- endIdx = bufIdx + numSamps,
loop = that.inputs.loop.output[0],
i;
// If the channel has changed, update the buffer we're reading from.
- if (that.model.channel != chan) {
+ if (that.model.channel !== chan) {
that.model.channel = chan;
that.buffer = source = flock.enviro.shared.buffers[that.model.name][chan];
}
@@ -632,12 +631,11 @@ var flock = flock || {};
source = that.buffer,
bufIdx = that.model.idx,
bufLen = source.length,
- endIdx = bufIdx + numSamps,
loop = that.inputs.loop.output[0],
i;
// If the channel has changed, update the buffer we're reading from.
- if (that.model.channel != chan) {
+ if (that.model.channel !== chan) {
that.model.channel = chan;
that.buffer = source = flock.enviro.shared.buffers[that.model.name][chan];
}
@@ -885,26 +883,59 @@ var flock = flock || {};
flock.ugen.env.simpleASR = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
- // TODO: This assumes "gate" is running at the control rate. Implement an audio version, too.
- // TODO: What happens if this envelope is triggered again? And when it's midway through a transition?
+ // TODO: This implementation currently outputs at audio rate, which is perhaps unnecessary.
+ // "gate" is also assumed to be control rate.
that.gen = function (numSamps) {
var out = that.output,
+ prevGate = that.model.previousGate,
gate = that.inputs.gate.output[0],
level = that.model.level,
- stage = gate > 0.0 ? that.model.attack : that.model.release,
- stepIdx = stage.stepIdx,
+ stage = that.model.stage,
+ currentStep = stage.currentStep,
+ stepInc = stage.stepInc,
numSteps = stage.numSteps,
- inc = stage.inc,
+ targetLevel = that.model.targetLevel,
+ stepsNeedRecalc = false,
+ stageTime,
i;
+
+ // Recalculate the step state if necessary.
+ if (prevGate <= 0 && gate > 0) {
+ // Starting a new attack stage.
+ targetLevel = that.inputs.sustain.output[0];
+ stageTime = that.inputs.attack.output[0];
+ stepsNeedRecalc = true;
+ } else if (gate <= 0 && currentStep >= numSteps) {
+ // Starting a new release stage.
+ targetLevel = that.inputs.start.output[0];
+ stageTime = that.inputs.release.output[0];
+ stepsNeedRecalc = true;
+ }
+
+ // TODO: Can we get rid of this extra branch without introducing code duplication?
+ if (stepsNeedRecalc) {
+ numSteps = Math.round(stageTime * that.sampleRate);
+ stepInc = (targetLevel - level) / numSteps;
+ currentStep = 0;
+ }
+ // Output the the envelope's sample data.
for (i = 0; i < numSamps; i++) {
out[i] = level;
- level += stepIdx >= numSteps ? 0 : inc; // Hold the last value if the stage is complete, otherwise increment.
- stepIdx++;
+ currentStep++;
+ // Hold the last value if the stage is complete, otherwise increment.
+ level = currentStep < numSteps ?
+ level + stepInc : currentStep === numSteps ?
+ targetLevel : level;
}
+ // Store instance state.
that.model.level = level;
- stage.stepIdx = stepIdx;
+ that.model.targetLevel = targetLevel;
+ that.model.previousGate = gate;
+ stage.currentStep = currentStep;
+ stage.stepInc = stepInc;
+ stage.numSteps = numSteps;
};
that.onInputChanged = function () {
@@ -923,26 +954,27 @@ var flock = flock || {};
if (!that.inputs.release) {
that.inputs.release = flock.ugen.value({value: 1.0}, new Float32Array(1));
}
-
- var stageCalculator = function (startLevel, endLevel, duration) {
- var stage = {
- stepIdx: 0,
- numSteps: Math.round(duration * that.sampleRate)
- };
- stage.inc = (endLevel - startLevel) / stage.numSteps;
-
- return stage;
- };
-
- var startLevel = that.inputs.start.output[0],
- sustainLevel = that.inputs.sustain.output[0];
- that.model.attack = stageCalculator(startLevel, sustainLevel, that.inputs.attack.output[0]);
- that.model.release = stageCalculator(sustainLevel, startLevel, that.inputs.release.output[0]);
+ if (!that.inputs.gate) {
+ that.inputs.gate = flock.ugen.value({value: 0.0}, new Float32Array(1));
+ }
};
- that.onInputChanged();
- that.model.level = that.inputs.start.output[0];
+ that.init = function () {
+ that.onInputChanged();
+
+ // Set default model state.
+ that.model.stage = {
+ currentStep: 0,
+ stepInc: 0,
+ numSteps: 0
+ };
+ that.model.previousGate = 0.0;
+ that.model.level = that.inputs.start.output[0];
+ that.model.targetLevel = that.inputs.sustain.output[0];
+ };
+
+ that.init();
return that;
};
View
98 tests/flocking/js/ugen-test.js
@@ -539,39 +539,105 @@ flock.test = flock.test || {};
start: 0.0,
attack: 1 / (44100 / 63), // 64 Samples, in seconds
sustain: 1.0,
- release: 1 / (44100 / 63), // 128 Samples
- gate: 1.0
+ release: 1 / (44100 / 63) // 128 Samples
}
};
+ var testEnvelopeStage = function (buffer, numSamps, expectedStart, expectedEnd, stageName) {
+ equal(buffer[0], expectedStart,
+ "During the " + stageName + " stage, the starting level should be " + expectedStart + ".");
+ equal(buffer[numSamps - 1], expectedEnd,
+ "At the end of the " + stageName + " stage, the expected end level should have been reached.");
+ flock.test.assertUnbroken(buffer, "The output should not contain any dropouts.");
+ flock.test.assertWithinRange(buffer, 0.0, 1.0,
+ "The output should always remain within the range between " + expectedStart + " and " + expectedEnd + ".");
+ flock.test.assertContinuous(buffer, 0.02, "The buffer should move continuously within its range.");
+
+ var isClimbing = expectedStart < expectedEnd;
+ var directionText = isClimbing ? "climb" : "fall";
+ flock.test.assertRamping(buffer, isClimbing,
+ "The buffer should " + directionText + " steadily from " + expectedStart + " to " + expectedEnd + ".");
+ };
+
test("simpleASR constant values for all inputs", function () {
var asr = flock.parse.ugenForDef(simpleASRDef);
+ // Until the gate is closed, the ugen should just output silence.
+ asr.gen(64);
+ flock.test.assertArrayEquals(asr.output, flock.test.constantBuffer(64, 0.0),
+ "When the gate is open at the beginning, the envelope's output should be 0.0.");
+
+ // Trigger the attack stage.
+ asr.input("gate", 1.0);
asr.gen(64);
- equal(asr.output[0], 0.0, "During the attack stage, the starting level should be 0.0.");
- equal(asr.output[63], 1.0, "At the end of the attack stage, the sustain level should have been reached.");
- flock.test.assertUnbroken(asr.output, "The output should not contain any dropouts.");
- flock.test.assertWithinRange(asr.output, 0.0, 1.0, "The output should always remain within the range between 0.0 and 1.0.");
- flock.test.assertContinuous(asr.output, 0.02, "The buffer should move continuously within its range.");
- flock.test.assertRamping(asr.output, true, "The buffer should climb steadily from 0.0 to 1.0.");
+ testEnvelopeStage(asr.output, 64, 0.0, 1.0, "attack");
+ // Output a full control period of the sustain value.
asr.gen(64);
flock.test.assertArrayEquals(asr.output, flock.test.constantBuffer(64, 1.0),
"While the gate is open, the envelope should hold at the sustain level.");
+ // Release the gate and test the release stage.
asr.input("gate", 0.0);
asr.gen(64);
- equal(asr.output[0], 1.0, "During the release stage, the starting level should be 1.0.");
- equal(asr.output[63], 0.0, "By the end of the release stage, the starting level should have been reached.");
- flock.test.assertUnbroken(asr.output, "The output should not contain any dropouts.");
- flock.test.assertWithinRange(asr.output, 0.0, 1.0, "The output should always remain within the range between 1.0 and 0.0.");
- flock.test.assertContinuous(asr.output, 0.02, "The buffer should move continuously within its range.");
- flock.test.assertRamping(asr.output, false, "The buffer should climb steadily from 0.0 to 1.0.");
-
+ testEnvelopeStage(asr.output, 64, 1.0, 0.0, "release");
+ // Test a full control period of the end value.
asr.gen(64);
flock.test.assertArrayEquals(asr.output, flock.test.constantBuffer(64, 0.0),
- "When the gate is closed and the release stage has completed, the envelope's output should be 0.0.");
+ "When the gate is closed and the release stage has completed, the envelope's output should be 0.0.");
+
+ // Trigger the attack stage again.
+ asr.input("gate", 1.0);
+ asr.gen(64);
+ testEnvelopeStage(asr.output, 64, 0.0, 1.0, "second attack");
+
+ // And the release stage again.
+ asr.input("gate", 0.0);
+ asr.gen(64);
+ testEnvelopeStage(asr.output, 64, 1.0, 0.0, "second release");
+ });
+
+ test("simpleASR release midway through attack", function () {
+ var asr = flock.parse.ugenForDef(simpleASRDef);
+ asr.input("gate", 1.0);
+ asr.gen(32);
+ testEnvelopeStage(asr.output.subarray(0, 32), 32, 0.0, 0.4920634925365448, "halfway through the attack");
+
+ // If the gate closes during the attack stage, the remaining portion of the attack stage should be output before the release stage starts.
+ asr.input("gate", 0.0);
+ asr.gen(32);
+ testEnvelopeStage(asr.output.subarray(0, 32), 32, 0.5079365372657776, 1.0, "rest of the attack");
+
+ // After the attack stage has hit 1.0, it should immediately start the release phase.
+ asr.gen(64);
+ testEnvelopeStage(asr.output, 64, 1.0, 0.0, "release");
+ });
+
+ test("simpleASR attack midway through release", function () {
+ var asr = flock.parse.ugenForDef(simpleASRDef);
+
+ // Trigger the attack stage, then the release stage immediately.
+ asr.input("gate", 1.0);
+ asr.gen(64);
+ testEnvelopeStage(asr.output, 64, 0.0, 1.0, "attack");
+ asr.input("gate", 0.0);
+ asr.gen(32);
+ testEnvelopeStage(asr.output.subarray(0, 32), 32, 1.0, 0.5079365372657776, "halfway release");
+
+ // Then trigger a new attack halfway through the release stage.
+ // The envelope should immediately pick up the attack phase from the current level
+ // TODO: Note that there will be a one-increment lag before turning direction to the attack phase in this case. Is this a noteworthy bug?
+ asr.input("gate", 1.0);
+ asr.gen(32);
+ testEnvelopeStage(asr.output.subarray(0, 32), 32, 0.4920634925365448, 0.7420005202293396, "attack after halfway release");
+
+ // Generate another control period of samples, which should be at the sustain level.
+ asr.gen(64);
+ testEnvelopeStage(asr.output.subarray(0, 32), 32, 0.7500630021095276, 1.0, "second half of the attack after halfway release second half.");
+ flock.test.assertArrayEquals(asr.output.subarray(32), flock.test.constantBuffer(32, 1.0),
+ "While the gate remains open after a mid-release attack, the envelope should hold at the sustain level.");
+
});

0 comments on commit 31345e9

Please sign in to comment.
Something went wrong with that request. Please try again.