UNPKG

tone

Version:

A Web Audio framework for making interactive music in the browser.

491 lines 17.7 kB
import * as tslib_1 from "tslib"; import { ToneAudioNode } from "../../core/context/ToneAudioNode"; 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 { assert } from "../../core/util/Debug"; import { range, timeRange } from "../../core/util/Decorator"; /** * Envelope is an [ADSR](https://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope) * envelope generator. Envelope outputs a signal which * can be connected to an AudioParam or Tone.Signal. * ``` * /\ * / \ * / \ * / \ * / \___________ * / \ * / \ * / \ * / \ * ``` * * @example * import { Envelope, Gain } from "tone"; * // an amplitude envelope * const gainNode = new Gain(); * const env = new Envelope({ * attack: 0.1, * decay: 0.2, * sustain: 1, * release: 0.8, * }); * env.connect(gainNode.gain); * @category Component */ var Envelope = /** @class */ (function (_super) { tslib_1.__extends(Envelope, _super); function Envelope() { var _this = _super.call(this, optionsFromArguments(Envelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"])) || this; _this.name = "Envelope"; /** * the signal which is output. */ _this._sig = new Signal({ context: _this.context, value: 0, }); /** * The output signal of the envelope */ _this.output = _this._sig; /** * Envelope has no input */ _this.input = undefined; var options = optionsFromArguments(Envelope.getDefaults(), arguments, ["attack", "decay", "sustain", "release"]); _this.attack = options.attack; _this.decay = options.decay; _this.sustain = options.sustain; _this.release = options.release; _this.attackCurve = options.attackCurve; _this.releaseCurve = options.releaseCurve; _this.decayCurve = options.decayCurve; return _this; } Envelope.getDefaults = function () { return Object.assign(ToneAudioNode.getDefaults(), { attack: 0.01, attackCurve: "linear", decay: 0.1, decayCurve: "exponential", release: 1, releaseCurve: "exponential", sustain: 0.5, }); }; Object.defineProperty(Envelope.prototype, "value", { /** * Read the current value of the envelope. Useful for * synchronizing visual output to the envelope. */ get: function () { return this.getValueAtTime(this.now()); }, enumerable: true, configurable: true }); /** * Get the curve * @param curve * @param direction In/Out * @return The curve name */ Envelope.prototype._getCurve = function (curve, direction) { if (isString(curve)) { return curve; } else { // look up the name in the curves array var curveName = void 0; for (curveName in EnvelopeCurves) { if (EnvelopeCurves[curveName][direction] === curve) { return curveName; } } // return the custom curve return curve; } }; /** * Assign a the curve to the given name using the direction * @param name * @param direction In/Out * @param curve */ Envelope.prototype._setCurve = function (name, direction, curve) { // check if it's a valid type if (isString(curve) && Reflect.has(EnvelopeCurves, curve)) { var curveDef = EnvelopeCurves[curve]; if (isObject(curveDef)) { if (name !== "_decayCurve") { this[name] = curveDef[direction]; } } else { this[name] = curveDef; } } else if (isArray(curve) && name !== "_decayCurve") { this[name] = curve; } else { throw new Error("Envelope: invalid curve: " + curve); } }; Object.defineProperty(Envelope.prototype, "attackCurve", { /** * The shape of the attack. * Can be any of these strings: * * "linear" * * "exponential" * * "sine" * * "cosine" * * "bounce" * * "ripple" * * "step" * * Can also be an array which describes the curve. Values * in the array are evenly subdivided and linearly * interpolated over the duration of the attack. * @example * import { Envelope } from "tone"; * const env = new Envelope(); * env.attackCurve = "linear"; * @example * import { Envelope } from "tone"; * const env = new Envelope(); * // can also be an array * env.attackCurve = [0, 0.2, 0.3, 0.4, 1]; */ get: function () { return this._getCurve(this._attackCurve, "In"); }, set: function (curve) { this._setCurve("_attackCurve", "In", curve); }, enumerable: true, configurable: true }); Object.defineProperty(Envelope.prototype, "releaseCurve", { /** * The shape of the release. See the attack curve types. * @example * import { Envelope } from "tone"; * const env = new Envelope(); * env.releaseCurve = "linear"; */ get: function () { return this._getCurve(this._releaseCurve, "Out"); }, set: function (curve) { this._setCurve("_releaseCurve", "Out", curve); }, enumerable: true, configurable: true }); Object.defineProperty(Envelope.prototype, "decayCurve", { /** * The shape of the decay either "linear" or "exponential" * @example * import { Envelope } from "tone"; * const env = new Envelope(); * env.decayCurve = "linear"; */ get: function () { return this._decayCurve; }, set: function (curve) { assert(["linear", "exponential"].some(function (c) { return c === curve; }), "Invalid envelope curve: " + curve); this._decayCurve = curve; }, enumerable: true, configurable: true }); /** * Trigger the attack/decay portion of the ADSR envelope. * @param time When the attack should start. * @param velocity The velocity of the envelope scales the vales. * number between 0-1 * @example * import { AmplitudeEnvelope, Oscillator } from "tone"; * const env = new AmplitudeEnvelope().toDestination(); * const osc = new Oscillator().connect(env).start(); * // trigger the attack 0.5 seconds from now with a velocity of 0.2 * env.triggerAttack("+0.5", 0.2); */ Envelope.prototype.triggerAttack = function (time, velocity) { if (velocity === void 0) { velocity = 1; } this.log("triggerAttack", time, velocity); time = this.toSeconds(time); var originalAttack = this.toSeconds(this.attack); var attack = originalAttack; var decay = this.toSeconds(this.decay); // check if it's not a complete attack var currentValue = this.getValueAtTime(time); if (currentValue > 0) { // subtract the current value from the attack time var attackRate = 1 / attack; var remainingDistance = 1 - currentValue; // the attack is now the remaining time attack = remainingDistance / attackRate; } // attack if (attack < this.sampleTime) { this._sig.cancelScheduledValues(time); // case where the attack time is 0 should set instantly this._sig.setValueAtTime(velocity, time); } else if (this._attackCurve === "linear") { this._sig.linearRampTo(velocity, attack, time); } else if (this._attackCurve === "exponential") { this._sig.targetRampTo(velocity, attack, time); } else { this._sig.cancelAndHoldAtTime(time); var curve = this._attackCurve; // find the starting position in the curve for (var i = 1; i < curve.length; i++) { // the starting index is between the two values if (curve[i - 1] <= currentValue && currentValue <= curve[i]) { curve = this._attackCurve.slice(i); // the first index is the current value curve[0] = currentValue; break; } } this._sig.setValueCurveAtTime(curve, time, attack, velocity); } // decay if (decay && this.sustain < 1) { var decayValue = velocity * this.sustain; var decayStart = time + attack; this.log("decay", decayStart); if (this._decayCurve === "linear") { this._sig.linearRampToValueAtTime(decayValue, decay + decayStart); } else { this._sig.exponentialApproachValueAtTime(decayValue, decayStart, decay); } } return this; }; /** * Triggers the release of the envelope. * @param time When the release portion of the envelope should start. * @example * import { AmplitudeEnvelope, Oscillator } from "tone"; * const env = new AmplitudeEnvelope().toDestination(); * const osc = new Oscillator().connect(env).start(); * env.triggerAttack(); * // trigger the release half a second after the attack * env.triggerRelease("+0.5"); */ Envelope.prototype.triggerRelease = function (time) { this.log("triggerRelease", time); time = this.toSeconds(time); var currentValue = this.getValueAtTime(time); if (currentValue > 0) { var release = this.toSeconds(this.release); if (release < this.sampleTime) { this._sig.setValueAtTime(0, time); } else if (this._releaseCurve === "linear") { this._sig.linearRampTo(0, release, time); } else if (this._releaseCurve === "exponential") { this._sig.targetRampTo(0, release, time); } else { 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); } } return this; }; /** * Get the scheduled value at the given time. This will * return the unconverted (raw) value. */ Envelope.prototype.getValueAtTime = function (time) { return this._sig.getValueAtTime(time); }; /** * triggerAttackRelease is shorthand for triggerAttack, then waiting * some duration, then triggerRelease. * @param duration The duration of the sustain. * @param time When the attack should be triggered. * @param velocity The velocity of the envelope. * @example * import { AmplitudeEnvelope, Oscillator } from "tone"; * const env = new AmplitudeEnvelope().toDestination(); * const osc = new Oscillator().connect(env).start(); * // trigger the release 0.5 seconds after the attack * env.triggerAttackRelease(0.5); */ Envelope.prototype.triggerAttackRelease = function (duration, time, velocity) { if (velocity === void 0) { velocity = 1; } time = this.toSeconds(time); this.triggerAttack(time, velocity); this.triggerRelease(time + this.toSeconds(duration)); return this; }; /** * Cancels all scheduled envelope changes after the given time. */ Envelope.prototype.cancel = function (after) { this._sig.cancelScheduledValues(this.toSeconds(after)); return this; }; /** * Connect the envelope to a destination node. */ Envelope.prototype.connect = function (destination, outputNumber, inputNumber) { if (outputNumber === void 0) { outputNumber = 0; } if (inputNumber === void 0) { inputNumber = 0; } connectSignal(this, destination, outputNumber, inputNumber); return this; }; /** * Render the envelope curve to an array of the given length. * Good for visualizing the envelope curve */ Envelope.prototype.asArray = function (length) { if (length === void 0) { length = 1024; } return tslib_1.__awaiter(this, void 0, void 0, function () { var duration, context, attackPortion, envelopeDuration, sustainTime, totalDuration, clone, buffer; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: duration = length / this.context.sampleRate; context = new OfflineContext(1, duration, this.context.sampleRate); attackPortion = this.toSeconds(this.attack) + this.toSeconds(this.decay); envelopeDuration = attackPortion + this.toSeconds(this.release); sustainTime = envelopeDuration * 0.1; totalDuration = envelopeDuration + sustainTime; clone = new this.constructor(Object.assign(this.get(), { attack: duration * this.toSeconds(this.attack) / totalDuration, decay: duration * this.toSeconds(this.decay) / totalDuration, release: duration * this.toSeconds(this.release) / totalDuration, context: context })); clone._sig.toDestination(); clone.triggerAttackRelease(duration * (attackPortion + sustainTime) / totalDuration, 0); return [4 /*yield*/, context.render()]; case 1: buffer = _a.sent(); return [2 /*return*/, buffer.getChannelData(0)]; } }); }); }; Envelope.prototype.dispose = function () { _super.prototype.dispose.call(this); this._sig.dispose(); return this; }; tslib_1.__decorate([ timeRange(0) ], Envelope.prototype, "attack", void 0); tslib_1.__decorate([ timeRange(0) ], Envelope.prototype, "decay", void 0); tslib_1.__decorate([ range(0, 1) ], Envelope.prototype, "sustain", void 0); tslib_1.__decorate([ timeRange(0) ], Envelope.prototype, "release", void 0); return Envelope; }(ToneAudioNode)); export { Envelope }; /** * Generate some complex envelope curves. */ var EnvelopeCurves = (function () { var curveLen = 128; var i; var k; // cosine curve var cosineCurve = []; for (i = 0; i < curveLen; i++) { cosineCurve[i] = Math.sin((i / (curveLen - 1)) * (Math.PI / 2)); } // ripple curve var rippleCurve = []; var rippleCurveFreq = 6.4; for (i = 0; i < curveLen - 1; i++) { k = (i / (curveLen - 1)); var sineWave = Math.sin(k * (Math.PI * 2) * rippleCurveFreq - Math.PI / 2) + 1; rippleCurve[i] = sineWave / 10 + k * 0.83; } rippleCurve[curveLen - 1] = 1; // stairs curve var stairsCurve = []; var steps = 5; for (i = 0; i < curveLen; i++) { stairsCurve[i] = Math.ceil((i / (curveLen - 1)) * steps) / steps; } // in-out easing curve var sineCurve = []; for (i = 0; i < curveLen; i++) { k = i / (curveLen - 1); sineCurve[i] = 0.5 * (1 - Math.cos(Math.PI * k)); } // a bounce curve var bounceCurve = []; for (i = 0; i < curveLen; i++) { k = i / (curveLen - 1); var freq = Math.pow(k, 3) * 4 + 0.2; var val = Math.cos(freq * Math.PI * 2 * k); bounceCurve[i] = Math.abs(val * (1 - k)); } /** * Invert a value curve to make it work for the release */ function invertCurve(curve) { var out = new Array(curve.length); for (var j = 0; j < curve.length; j++) { out[j] = 1 - curve[j]; } return out; } /** * reverse the curve */ function reverseCurve(curve) { return curve.slice(0).reverse(); } /** * attack and release curve arrays */ return { bounce: { In: invertCurve(bounceCurve), Out: bounceCurve, }, cosine: { In: cosineCurve, Out: reverseCurve(cosineCurve), }, exponential: "exponential", linear: "linear", ripple: { In: rippleCurve, Out: invertCurve(rippleCurve), }, sine: { In: sineCurve, Out: invertCurve(sineCurve), }, step: { In: stairsCurve, Out: invertCurve(stairsCurve), }, }; })(); //# sourceMappingURL=Envelope.js.map