adding decorators which validate the input value range

This commit is contained in:
Yotam Mann 2019-12-16 17:13:03 -05:00
parent 2b0c2a64f1
commit 48284d78fb
5 changed files with 146 additions and 122 deletions

View file

@ -5,7 +5,8 @@ import { optionsFromArguments } from "../../core/util/Defaults";
import { isArray, isObject, isString } from "../../core/util/TypeCheck";
import { connectSignal, Signal } from "../../signal/Signal";
import { OfflineContext } from "../../core/context/OfflineContext";
import { assertRange } from "../../core/util/Debug";
import { assert } from "../../core/util/Debug";
import { range, timeRange } from "../../core/util/Decorator";
type BasicEnvelopeCurve = "linear" | "exponential";
type InternalEnvelopeCurve = BasicEnvelopeCurve | number[];
@ -55,24 +56,84 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
readonly name: string = "Envelope";
/**
* Private container for the attack value
* When triggerAttack is called, the attack time is the amount of
* time it takes for the envelope to reach it's maximum value.
* ```
* /\
* /X \
* /XX \
* /XXX \
* /XXXX \___________
* /XXXXX \
* /XXXXXX \
* /XXXXXXX \
* /XXXXXXXX \
* ```
* @min 0
* @max 2
*/
private _attack!: Time;
@timeRange(0)
attack: Time;
/**
* Private holder of the decay time
* After the attack portion of the envelope, the value will fall
* over the duration of the decay time to it's sustain value.
* ```
* /\
* / X\
* / XX\
* / XXX\
* / XXXX\___________
* / XXXXX \
* / XXXXX \
* / XXXXX \
* / XXXXX \
* ```
* @min 0
* @max 2
*/
private _decay!: Time;
@timeRange(0)
decay: Time;
/**
* private holder for the sustain value
* The sustain value is the value
* which the envelope rests at after triggerAttack is
* called, but before triggerRelease is invoked.
* ```
* /\
* / \
* / \
* / \
* / \___________
* / XXXXXXXXXXX\
* / XXXXXXXXXXX \
* / XXXXXXXXXXX \
* / XXXXXXXXXXX \
* ```
*/
private _sustain!: NormalRange;
@range(0, 1)
sustain: NormalRange;
/**
* private holder for the release value
* After triggerRelease is called, the envelope's
* value will fall to it's miminum value over the
* duration of the release time.
* ```
* /\
* / \
* / \
* / \
* / \___________
* / X\
* / XX\
* / XXX\
* / XXXX\
* ```
* @min 0
* @max 5
*/
private _release!: Time;
@timeRange(0)
release: Time;
/**
* The automation curve type for the attack
@ -153,106 +214,6 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
return this.getValueAtTime(this.now());
}
/**
* When triggerAttack is called, the attack time is the amount of
* time it takes for the envelope to reach it's maximum value.
* ```
* /\
* /X \
* /XX \
* /XXX \
* /XXXX \___________
* /XXXXX \
* /XXXXXX \
* /XXXXXXX \
* /XXXXXXXX \
* ```
* @min 0
* @max 2
*/
get attack(): Time {
return this._attack;
}
set attack(time) {
assertRange(this.toSeconds(time), 0);
this._attack = time;
}
/**
* After the attack portion of the envelope, the value will fall
* over the duration of the decay time to it's sustain value.
* ```
* /\
* / X\
* / XX\
* / XXX\
* / XXXX\___________
* / XXXXX \
* / XXXXX \
* / XXXXX \
* / XXXXX \
* ```
* @min 0
* @max 2
*/
get decay(): Time {
return this._decay;
}
set decay(time) {
assertRange(this.toSeconds(time), 0);
this._decay = time;
}
/**
* The sustain value is the value
* which the envelope rests at after triggerAttack is
* called, but before triggerRelease is invoked.
* ```
* /\
* / \
* / \
* / \
* / \___________
* / XXXXXXXXXXX\
* / XXXXXXXXXXX \
* / XXXXXXXXXXX \
* / XXXXXXXXXXX \
* ```
*/
get sustain(): NormalRange {
return this._sustain;
}
set sustain(val) {
assertRange(this.toSeconds(val), 0, 1);
this._sustain = val;
}
/**
* After triggerRelease is called, the envelope's
* value will fall to it's miminum value over the
* duration of the release time.
* ```
* /\
* / \
* / \
* / \
* / \___________
* / X\
* / XX\
* / XXX\
* / XXXX\
* ```
* @min 0
* @max 5
*/
get release(): Time {
return this._release;
}
set release(time) {
assertRange(this.toSeconds(time), 0);
this._release = time;
}
/**
* Get the curve
* @param curve
@ -359,7 +320,7 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
return this._decayCurve;
}
set decayCurve(curve) {
this.assert(["linear", "exponential"].some(c => c === curve), `Invalid envelope curve: ${curve}`);
assert(["linear", "exponential"].some(c => c === curve), `Invalid envelope curve: ${curve}`);
this._decayCurve = curve;
}
@ -421,8 +382,6 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
if (this._decayCurve === "linear") {
this._sig.linearRampToValueAtTime(decayValue, decay + decayStart);
} else {
this.assert(this._decayCurve === "exponential",
`decayCurve can only be "linear" or "exponential", got ${this._decayCurve}`);
this._sig.exponentialApproachValueAtTime(decayValue, decayStart, decay);
}
}
@ -453,7 +412,7 @@ export class Envelope extends ToneAudioNode<EnvelopeOptions> {
} else if (this._releaseCurve === "exponential") {
this._sig.targetRampTo(0, release, time);
} else {
this.assert(isArray(this._releaseCurve), "releaseCurve must be either 'linear', 'exponential' or an array");
assert(isArray(this._releaseCurve), "releaseCurve must be either 'linear', 'exponential' or an array");
this._sig.cancelAndHoldAtTime(time);
this._sig.setValueCurveAtTime(this._releaseCurve, time, release, currentValue);
}

View file

@ -0,0 +1,51 @@
import { assertRange } from "./Debug";
import { Time } from "../type/Units";
/**
* Assert that the time is in the given range
*/
export function range(min: number, max = Infinity) {
const valueMap: WeakMap<any, number> = new WeakMap();
return function(target: any, propertyKey: string | symbol) {
Reflect.defineProperty(
target,
propertyKey,
{
configurable: true,
enumerable: true,
get: function() {
return valueMap.get(this);
},
set: function(newValue: number) {
assertRange(newValue, min, max);
valueMap.set(this, newValue);
}
},
);
};
}
/**
* Convert the time to seconds and assert that the time is in between the two
* values when being set.
*/
export function timeRange(min: number, max = Infinity) {
const valueMap: WeakMap<any, Time> = new WeakMap();
return function(target: any, propertyKey: string) {
Reflect.defineProperty(
target,
propertyKey,
{
configurable: true,
enumerable: true,
get: function() {
return valueMap.get(this);
},
set: function(newValue: Time) {
assertRange(this.toSeconds(newValue), min, max);
valueMap.set(this, newValue);
}
},
);
};
}

View file

@ -8,6 +8,8 @@ import { noOp } from "../core/util/Interface";
import { isArray, isNote, isNumber } from "../core/util/TypeCheck";
import { Instrument, InstrumentOptions } from "../instrument/Instrument";
import { ToneBufferSource, ToneBufferSourceCurve } from "../source/buffer/ToneBufferSource";
import { timeRange } from "../core/util/Decorator";
import { assert } from "../core/util/Debug";
interface SamplesMap {
[note: string]: ToneAudioBuffer | AudioBuffer | string;
@ -64,6 +66,7 @@ export class Sampler extends Instrument<SamplerOptions> {
* @min 0
* @max 1
*/
@timeRange(0)
attack: Time;
/**
@ -71,6 +74,7 @@ export class Sampler extends Instrument<SamplerOptions> {
* @min 0
* @max 1
*/
@timeRange(0)
release: Time;
/**
@ -101,7 +105,7 @@ export class Sampler extends Instrument<SamplerOptions> {
const urlMap = {};
Object.keys(options.urls).forEach((note) => {
const noteNumber = parseInt(note, 10);
this.assert(isNote(note)
assert(isNote(note)
|| (isNumber(noteNumber) && isFinite(noteNumber)), `url key is neither a note or midi pitch: ${note}`);
if (isNote(note)) {
// convert the note name to MIDI
@ -263,7 +267,7 @@ export class Sampler extends Instrument<SamplerOptions> {
const computedTime = this.toSeconds(time);
this.triggerAttack(notes, computedTime, velocity);
if (isArray(duration)) {
this.assert(isArray(notes), "notes must be an array when duration is array");
assert(isArray(notes), "notes must be an array when duration is array");
(notes as Frequency[]).forEach((note, index) => {
const d = duration[Math.min(index, duration.length - 1)];
this.triggerRelease(note, computedTime + this.toSeconds(d));
@ -281,7 +285,7 @@ export class Sampler extends Instrument<SamplerOptions> {
* @param callback The callback to invoke when the url is loaded.
*/
add(note: Note | MidiNote, url: string | ToneAudioBuffer | AudioBuffer, callback?: () => void): this {
this.assert(isNote(note) || isFinite(note), `note must be a pitch or midi: ${note}`);
assert(isNote(note) || isFinite(note), `note must be a pitch or midi: ${note}`);
if (isNote(note)) {
// convert the note name to MIDI
const mid = new FrequencyClass(this.context, note).toMidi();

View file

@ -434,20 +434,29 @@ describe("Signal", () => {
});
it("converts NormalRange units", () => {
const signal = new Signal(2, "normalRange");
expect(() => {
new Signal(2, "normalRange");
}).to.throw(RangeError);
const signal = new Signal(1, "normalRange");
expect(signal.value).to.be.closeTo(1, 0.01);
signal.dispose();
});
it("converts AudioRange units", () => {
const signal = new Signal(-2, "audioRange");
expect(() => {
new Signal(-2, "audioRange");
}).to.throw(RangeError);
const signal = new Signal(-1, "audioRange");
expect(signal.value).to.be.closeTo(-1, 0.01);
signal.dispose();
});
it("converts Positive units", () => {
const signal = new Signal(-2, "positive");
expect(signal.value).to.be.closeTo(0, 0.01);
expect(() => {
new Signal(-2, "positive");
}).to.throw(RangeError);
const signal = new Signal(100, "positive");
expect(signal.value).to.be.closeTo(100, 0.01);
signal.dispose();
});

View file

@ -13,6 +13,7 @@
"moduleResolution": "node",
"strictPropertyInitialization" : true,
"downlevelIteration" : true,
"experimentalDecorators": true,
"lib": ["es6", "dom", "es2015"],
"baseUrl": "./"
},