tone
Version:
A Web Audio framework for making interactive music in the browser.
320 lines • 12.9 kB
JavaScript
import * as tslib_1 from "tslib";
import { MidiClass } from "../core/type/Midi";
import { deepMerge, omitFromObject, optionsFromArguments } from "../core/util/Defaults";
import { isArray, isNumber } from "../core/util/TypeCheck";
import { Instrument } from "./Instrument";
import { Synth } from "./Synth";
import { assert, warn } from "../core/util/Debug";
/**
* PolySynth handles voice creation and allocation for any
* instruments passed in as the second paramter. PolySynth is
* not a synthesizer by itself, it merely manages voices of
* one of the other types of synths, allowing any of the
* monophonic synthesizers to be polyphonic.
*
* @example
* import { PolySynth } from "tone";
* const synth = new PolySynth().toDestination();
* // set the attributes across all the voices using 'set'
* synth.set({ detune: -1200 });
* // play a chord
* synth.triggerAttackRelease(["C4", "E4", "A4"], 1);
* @category Instrument
*/
var PolySynth = /** @class */ (function (_super) {
tslib_1.__extends(PolySynth, _super);
function PolySynth() {
var _this = _super.call(this, optionsFromArguments(PolySynth.getDefaults(), arguments, ["voice", "options"])) || this;
_this.name = "PolySynth";
/**
* The voices which are not currently in use
*/
_this._availableVoices = [];
/**
* The currently active voices
*/
_this._activeVoices = [];
/**
* All of the allocated voices for this synth.
*/
_this._voices = [];
/**
* The GC timeout. Held so that it could be cancelled when the node is disposed.
*/
_this._gcTimeout = -1;
/**
* A moving average of the number of active voices
*/
_this._averageActiveVoices = 0;
var options = optionsFromArguments(PolySynth.getDefaults(), arguments, ["voice", "options"]);
// check against the old API (pre 14.3.0)
assert(!isNumber(options.voice), "DEPRECATED: The polyphony count is no longer the first argument.");
var defaults = options.voice.getDefaults();
_this.options = Object.assign(defaults, options.options);
_this.voice = options.voice;
_this.maxPolyphony = options.maxPolyphony;
// create the first voice
_this._dummyVoice = _this._getNextAvailableVoice();
// remove it from the voices list
var index = _this._voices.indexOf(_this._dummyVoice);
_this._voices.splice(index, 1);
// kick off the GC interval
_this._gcTimeout = _this.context.setInterval(_this._collectGarbage.bind(_this), 1);
return _this;
}
PolySynth.getDefaults = function () {
return Object.assign(Instrument.getDefaults(), {
maxPolyphony: 32,
options: {},
voice: Synth,
});
};
Object.defineProperty(PolySynth.prototype, "activeVoices", {
/**
* The number of active voices.
*/
get: function () {
return this._activeVoices.length;
},
enumerable: true,
configurable: true
});
/**
* Invoked when the source is done making sound, so that it can be
* readded to the pool of available voices
*/
PolySynth.prototype._makeVoiceAvailable = function (voice) {
this._availableVoices.push(voice);
// remove the midi note from 'active voices'
var activeVoiceIndex = this._activeVoices.findIndex(function (e) { return e.voice === voice; });
this._activeVoices.splice(activeVoiceIndex, 1);
};
/**
* Get an available voice from the pool of available voices.
* If one is not available and the maxPolyphony limit is reached,
* steal a voice, otherwise return null.
*/
PolySynth.prototype._getNextAvailableVoice = function () {
// if there are available voices, return the first one
if (this._availableVoices.length) {
return this._availableVoices.shift();
}
else if (this._voices.length < this.maxPolyphony) {
// otherwise if there is still more maxPolyphony, make a new voice
var voice = new this.voice(Object.assign(this.options, {
context: this.context,
onsilence: this._makeVoiceAvailable.bind(this),
}));
voice.connect(this.output);
this._voices.push(voice);
return voice;
}
else {
warn("Max polyphony exceeded. Note dropped.");
}
};
/**
* Occasionally check if there are any allocated voices which can be cleaned up.
*/
PolySynth.prototype._collectGarbage = function () {
this._averageActiveVoices = Math.max(this._averageActiveVoices * 0.95, this.activeVoices);
if (this._availableVoices.length && this._voices.length > Math.ceil(this._averageActiveVoices + 1)) {
// take off an available note
var firstAvail = this._availableVoices.shift();
var index = this._voices.indexOf(firstAvail);
this._voices.splice(index, 1);
if (!this.context.isOffline) {
firstAvail.dispose();
}
}
};
/**
* Internal method which triggers the attack
*/
PolySynth.prototype._triggerAttack = function (notes, time, velocity) {
var _this = this;
notes.forEach(function (note) {
var midiNote = new MidiClass(_this.context, note).toMidi();
var voice = _this._getNextAvailableVoice();
if (voice) {
voice.triggerAttack(note, time, velocity);
_this._activeVoices.push({
midi: midiNote, voice: voice, released: false,
});
_this.log("triggerAttack", note, time);
}
});
};
/**
* Internal method which triggers the release
*/
PolySynth.prototype._triggerRelease = function (notes, time) {
var _this = this;
notes.forEach(function (note) {
var midiNote = new MidiClass(_this.context, note).toMidi();
var event = _this._activeVoices.find(function (_a) {
var midi = _a.midi, released = _a.released;
return midi === midiNote && !released;
});
if (event) {
// trigger release on that note
event.voice.triggerRelease(time);
// mark it as released
event.released = true;
_this.log("triggerRelease", note, time);
}
});
};
/**
* Schedule the attack/release events. If the time is in the future, then it should set a timeout
* to wait for just-in-time scheduling
*/
PolySynth.prototype._scheduleEvent = function (type, notes, time, velocity) {
var _this = this;
assert(!this.disposed, "Synth was already disposed");
// if the notes are greater than this amount of time in the future, they should be scheduled with setTimeout
if (time <= this.now()) {
// do it immediately
if (type === "attack") {
this._triggerAttack(notes, time, velocity);
}
else {
this._triggerRelease(notes, time);
}
}
else {
// schedule it to start in the future
this.context.setTimeout(function () {
_this._scheduleEvent(type, notes, time, velocity);
}, time - this.now());
}
};
/**
* Trigger the attack portion of the note
* @param notes The notes to play. Accepts a single Frequency or an array of frequencies.
* @param time The start time of the note.
* @param velocity The velocity of the note.
* @example
* import { FMSynth, now, PolySynth } from "tone";
* const synth = new PolySynth(FMSynth).toDestination();
* // trigger a chord immediately with a velocity of 0.2
* synth.triggerAttack(["Ab3", "C4", "F5"], now(), 0.2);
*/
PolySynth.prototype.triggerAttack = function (notes, time, velocity) {
if (!Array.isArray(notes)) {
notes = [notes];
}
var computedTime = this.toSeconds(time);
this._scheduleEvent("attack", notes, computedTime, velocity);
return this;
};
/**
* Trigger the release of the note. Unlike monophonic instruments,
* a note (or array of notes) needs to be passed in as the first argument.
* @param notes The notes to play. Accepts a single Frequency or an array of frequencies.
* @param time When the release will be triggered.
* @example
* @example
* import { AMSynth, PolySynth } from "tone";
* const poly = new PolySynth(AMSynth).toDestination();
* poly.triggerAttack(["Ab3", "C4", "F5"]);
* // trigger the release of the given notes.
* poly.triggerRelease(["Ab3", "C4"], "+1");
* poly.triggerRelease("F5", "+3");
*/
PolySynth.prototype.triggerRelease = function (notes, time) {
if (!Array.isArray(notes)) {
notes = [notes];
}
var computedTime = this.toSeconds(time);
this._scheduleEvent("release", notes, computedTime);
return this;
};
/**
* Trigger the attack and release after the specified duration
* @param notes The notes to play. Accepts a single Frequency or an array of frequencies.
* @param duration the duration of the note
* @param time if no time is given, defaults to now
* @param velocity the velocity of the attack (0-1)
* @example
* import { AMSynth, PolySynth } from "tone";
* const poly = new PolySynth(AMSynth).toDestination();
* // can pass in an array of durations as well
* poly.triggerAttackRelease(["Eb3", "G4", "Bb4", "D5"], [4, 3, 2, 1]);
*/
PolySynth.prototype.triggerAttackRelease = function (notes, duration, time, velocity) {
var computedTime = this.toSeconds(time);
this.triggerAttack(notes, computedTime, velocity);
if (isArray(duration)) {
assert(isArray(notes), "If the duration is an array, the notes must also be an array");
notes = notes;
for (var i = 0; i < notes.length; i++) {
var d = duration[Math.min(i, duration.length - 1)];
var durationSeconds = this.toSeconds(d);
assert(durationSeconds > 0, "The duration must be greater than 0");
this.triggerRelease(notes[i], computedTime + durationSeconds);
}
}
else {
var durationSeconds = this.toSeconds(duration);
assert(durationSeconds > 0, "The duration must be greater than 0");
this.triggerRelease(notes, computedTime + durationSeconds);
}
return this;
};
PolySynth.prototype.sync = function () {
this._syncMethod("triggerAttack", 1);
this._syncMethod("triggerRelease", 1);
return this;
};
/**
* Set a member/attribute of the voices
* @example
* import { PolySynth } from "tone";
* const poly = new PolySynth().toDestination();
* // set all of the voices using an options object for the synth type
* poly.set({
* envelope: {
* attack: 0.25
* }
* });
* poly.triggerAttackRelease("Bb3", 0.2);
*/
PolySynth.prototype.set = function (options) {
// remove options which are controlled by the PolySynth
var sanitizedOptions = omitFromObject(options, ["onsilence", "context"]);
// store all of the options
this.options = deepMerge(this.options, sanitizedOptions);
this._voices.forEach(function (voice) { return voice.set(sanitizedOptions); });
this._dummyVoice.set(sanitizedOptions);
return this;
};
PolySynth.prototype.get = function () {
return this._dummyVoice.get();
};
/**
* Trigger the release portion of all the currently active voices immediately.
* Useful for silencing the synth.
*/
PolySynth.prototype.releaseAll = function () {
var now = this.now();
this._activeVoices.forEach(function (_a) {
var voice = _a.voice;
voice.triggerRelease(now);
});
this._activeVoices = [];
return this;
};
PolySynth.prototype.dispose = function () {
_super.prototype.dispose.call(this);
this._dummyVoice.dispose();
this._voices.forEach(function (v) { return v.dispose(); });
this._activeVoices = [];
this._availableVoices = [];
this.context.clearInterval(this._gcTimeout);
return this;
};
return PolySynth;
}(Instrument));
export { PolySynth };
//# sourceMappingURL=PolySynth.js.map