2019-08-12 04:16:03 +00:00
|
|
|
import { MidiClass } from "../core/type/Midi";
|
2019-08-12 17:18:18 +00:00
|
|
|
import { Frequency, MidiNote, NormalRange, Seconds, Time } from "../core/type/Units";
|
2019-08-12 04:16:03 +00:00
|
|
|
import { deepMerge, optionsFromArguments } from "../core/util/Defaults";
|
|
|
|
import { RecursivePartial } from "../core/util/Interface";
|
2019-09-04 15:59:30 +00:00
|
|
|
import { isArray, isNumber } from "../core/util/TypeCheck";
|
2019-08-12 04:16:03 +00:00
|
|
|
import { Instrument, InstrumentOptions } from "./Instrument";
|
2019-08-16 19:21:09 +00:00
|
|
|
import { MembraneSynth, MembraneSynthOptions } from "./MembraneSynth";
|
2019-09-04 22:29:11 +00:00
|
|
|
import { MetalSynth, MetalSynthOptions } from "./MetalSynth";
|
2019-08-16 19:21:09 +00:00
|
|
|
import { Monophonic } from "./Monophonic";
|
2019-08-12 04:16:03 +00:00
|
|
|
import { Synth, SynthOptions } from "./Synth";
|
|
|
|
|
|
|
|
type VoiceConstructor<V> = {
|
2019-09-14 22:12:44 +00:00
|
|
|
getDefaults: () => VoiceOptions<V>;
|
2019-08-12 04:16:03 +00:00
|
|
|
} & (new (...args: any[]) => V);
|
|
|
|
|
|
|
|
type OmitMonophonicOptions<T> = Omit<T, "context" | "onsilence">;
|
|
|
|
|
|
|
|
type VoiceOptions<T> =
|
2019-08-16 19:21:09 +00:00
|
|
|
T extends MembraneSynth ? MembraneSynthOptions :
|
2019-09-14 22:12:44 +00:00
|
|
|
T extends MetalSynth ? MetalSynthOptions :
|
|
|
|
T extends Synth ? SynthOptions :
|
|
|
|
never;
|
2019-08-12 04:16:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The settable synth options. excludes monophonic options.
|
|
|
|
*/
|
2019-09-16 14:15:23 +00:00
|
|
|
type PartialVoiceOptions<T> = RecursivePartial<OmitMonophonicOptions<VoiceOptions<T>>>;
|
2019-08-12 04:16:03 +00:00
|
|
|
|
2019-09-04 22:29:11 +00:00
|
|
|
export interface PolySynthOptions<Voice> extends InstrumentOptions {
|
2019-09-03 23:29:59 +00:00
|
|
|
maxPolyphony: number;
|
2019-08-12 04:16:03 +00:00
|
|
|
voice: VoiceConstructor<Voice>;
|
|
|
|
options: PartialVoiceOptions<Voice>;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
2019-08-27 15:57:00 +00:00
|
|
|
* @example
|
2019-09-03 23:29:59 +00:00
|
|
|
* var synth = new PolySynth(Tone.Synth, {
|
2019-08-12 04:16:03 +00:00
|
|
|
* oscillator : {
|
|
|
|
* type : "square"
|
|
|
|
* }
|
|
|
|
* }).toMaster();
|
|
|
|
* //set the attributes using the set interface
|
|
|
|
* synth.set("detune", -1200);
|
|
|
|
* //play a chord
|
|
|
|
* synth.triggerAttackRelease(["C4", "E4", "A4"], "4n");
|
2019-09-16 14:15:23 +00:00
|
|
|
* @category Instrument
|
2019-08-12 04:16:03 +00:00
|
|
|
*/
|
|
|
|
export class PolySynth<Voice extends Monophonic<any> = Synth> extends Instrument<VoiceOptions<Voice>> {
|
|
|
|
|
2019-09-04 23:18:44 +00:00
|
|
|
readonly name: string = "PolySynth";
|
2019-08-12 04:16:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The voices which are not currently in use
|
|
|
|
*/
|
|
|
|
private _availableVoices: Voice[] = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The currently active voices
|
|
|
|
*/
|
2019-09-14 22:12:44 +00:00
|
|
|
private _activeVoices: Array<{midi: MidiNote; voice: Voice}> = [];
|
2019-08-12 04:16:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* All of the allocated voices for this synth.
|
|
|
|
*/
|
|
|
|
private _voices: Voice[] = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The options that are set on the synth.
|
|
|
|
*/
|
|
|
|
private options: VoiceOptions<Voice>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The polyphony limit.
|
|
|
|
*/
|
2019-09-03 23:29:59 +00:00
|
|
|
maxPolyphony: number;
|
2019-08-12 04:16:03 +00:00
|
|
|
|
2019-09-03 23:29:59 +00:00
|
|
|
/**
|
|
|
|
* The voice constructor
|
|
|
|
*/
|
2019-08-12 04:16:03 +00:00
|
|
|
private readonly voice: VoiceConstructor<Voice>;
|
|
|
|
|
2019-08-27 15:57:00 +00:00
|
|
|
/**
|
2019-09-03 23:29:59 +00:00
|
|
|
* The GC timeout. Held so that it could be cancelled when the node is disposed.
|
|
|
|
*/
|
|
|
|
private _gcTimeout: number = -1;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A moving average of the number of active voices
|
|
|
|
*/
|
|
|
|
private _averageActiveVoices: number = 0;
|
|
|
|
|
|
|
|
/**
|
2019-08-27 15:57:00 +00:00
|
|
|
* @param voice The constructor of the voices
|
|
|
|
* @param options The options object to set the synth voice
|
|
|
|
*/
|
2019-08-12 04:16:03 +00:00
|
|
|
constructor(
|
|
|
|
voice?: VoiceConstructor<Voice>,
|
|
|
|
options?: PartialVoiceOptions<Voice>,
|
|
|
|
);
|
|
|
|
constructor(options?: Partial<PolySynthOptions<Voice>>);
|
|
|
|
constructor() {
|
|
|
|
|
2019-09-03 23:29:59 +00:00
|
|
|
super(optionsFromArguments(PolySynth.getDefaults(), arguments, ["voice", "options"]));
|
|
|
|
const options = optionsFromArguments(PolySynth.getDefaults(), arguments, ["voice", "options"]);
|
2019-08-12 04:16:03 +00:00
|
|
|
|
2019-09-04 15:59:30 +00:00
|
|
|
// check against the old API (pre 14.3.0)
|
|
|
|
this.assert(!isNumber(options.voice), "DEPRECATED: The polyphony count is no longer the first argument.");
|
|
|
|
|
2019-08-12 04:16:03 +00:00
|
|
|
const defaults = options.voice.getDefaults();
|
|
|
|
this.options = Object.assign(defaults, options.options) as VoiceOptions<Voice>;
|
|
|
|
this.voice = options.voice as unknown as VoiceConstructor<Voice>;
|
2019-09-03 23:29:59 +00:00
|
|
|
this.maxPolyphony = options.maxPolyphony;
|
|
|
|
|
|
|
|
// kick off the GC interval
|
2019-09-06 18:55:20 +00:00
|
|
|
this._gcTimeout = this.context.setInterval(this._collectGarbage.bind(this), 1);
|
2019-08-12 04:16:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static getDefaults(): PolySynthOptions<Synth> {
|
|
|
|
return Object.assign(Instrument.getDefaults(), {
|
2019-09-03 23:29:59 +00:00
|
|
|
maxPolyphony: 32,
|
2019-08-12 04:16:03 +00:00
|
|
|
options: {},
|
|
|
|
voice: Synth,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The number of active voices.
|
|
|
|
*/
|
|
|
|
get activeVoices(): number {
|
2019-08-14 14:50:01 +00:00
|
|
|
return this._activeVoices.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If there is a voice active on that note, return it
|
|
|
|
*/
|
|
|
|
private _getActiveVoice(note: MidiNote): Voice | undefined {
|
2019-09-14 22:12:44 +00:00
|
|
|
const event = this._activeVoices.find(({ midi }) => midi === note);
|
2019-08-14 14:50:01 +00:00
|
|
|
if (event) {
|
|
|
|
return event.voice;
|
|
|
|
}
|
2019-08-12 04:16:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invoked when the source is done making sound, so that it can be
|
|
|
|
* readded to the pool of available voices
|
|
|
|
*/
|
|
|
|
private _makeVoiceAvailable(voice: Voice): void {
|
|
|
|
this._availableVoices.push(voice);
|
|
|
|
// remove the midi note from 'active voices'
|
2019-08-14 14:50:01 +00:00
|
|
|
const activeVoiceIndex = this._activeVoices.findIndex((e) => e.voice === voice);
|
|
|
|
this._activeVoices.splice(activeVoiceIndex, 1);
|
2019-08-12 04:16:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an available voice from the pool of available voices.
|
2019-09-03 23:29:59 +00:00
|
|
|
* If one is not available and the maxPolyphony limit is reached,
|
2019-08-12 04:16:03 +00:00
|
|
|
* steal a voice, otherwise return null.
|
|
|
|
*/
|
|
|
|
private _getNextAvailableVoice(): Voice | undefined {
|
|
|
|
// if there are available voices, return the first one
|
|
|
|
if (this._availableVoices.length) {
|
|
|
|
return this._availableVoices.shift();
|
2019-09-03 23:29:59 +00:00
|
|
|
} else if (this._voices.length < this.maxPolyphony) {
|
|
|
|
// otherwise if there is still more maxPolyphony, make a new voice
|
2019-08-12 04:16:03 +00:00
|
|
|
const 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;
|
2019-08-12 17:21:55 +00:00
|
|
|
} else {
|
|
|
|
console.warn("Max polyphony exceeded. Note dropped.");
|
2019-08-12 04:16:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-03 23:29:59 +00:00
|
|
|
/**
|
|
|
|
* Occasionally check if there are any allocated voices which can be cleaned up.
|
|
|
|
*/
|
|
|
|
private _collectGarbage(): void {
|
|
|
|
this._averageActiveVoices = Math.max(this._averageActiveVoices * 0.95, this.activeVoices);
|
|
|
|
if (this._availableVoices.length && this._voices.length > this._averageActiveVoices) {
|
|
|
|
// take off an available note
|
|
|
|
const firstAvail = this._availableVoices.shift() as Voice;
|
|
|
|
const index = this._voices.indexOf(firstAvail);
|
|
|
|
this._voices.splice(index, 1);
|
|
|
|
firstAvail.dispose();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-12 04:16:03 +00:00
|
|
|
/**
|
2019-08-12 17:18:18 +00:00
|
|
|
* Internal method which triggers the attack
|
2019-08-12 04:16:03 +00:00
|
|
|
*/
|
2019-08-12 17:18:18 +00:00
|
|
|
private _triggerAttack(notes: Frequency[], time: Seconds, velocity?: NormalRange): void {
|
2019-08-12 04:16:03 +00:00
|
|
|
notes.forEach(note => {
|
|
|
|
const midiNote = new MidiClass(this.context, note).toMidi();
|
2019-08-14 14:50:01 +00:00
|
|
|
// let voice: Voice | undefined;
|
2019-08-12 17:18:18 +00:00
|
|
|
// if there's already a note at that voice, reuse it
|
2019-08-14 14:50:01 +00:00
|
|
|
let voice = this._getActiveVoice(midiNote);
|
|
|
|
// if it has a note, and that note is still active
|
|
|
|
if (voice && voice.getLevelAtTime(time) > 0) {
|
|
|
|
const activeVoiceIndex = this._activeVoices.findIndex((e) => e.voice === voice);
|
|
|
|
this._activeVoices.splice(activeVoiceIndex, 1);
|
2019-08-12 04:16:03 +00:00
|
|
|
} else {
|
2019-08-12 17:18:18 +00:00
|
|
|
// otherwise get the next available voice
|
2019-08-12 04:16:03 +00:00
|
|
|
voice = this._getNextAvailableVoice();
|
|
|
|
}
|
|
|
|
if (voice) {
|
2019-08-12 17:18:18 +00:00
|
|
|
voice.triggerAttack(note, time, velocity);
|
2019-08-14 14:50:01 +00:00
|
|
|
this._activeVoices.unshift({
|
|
|
|
midi: midiNote, voice,
|
|
|
|
});
|
|
|
|
this.log("triggerAttack", note, time);
|
2019-08-12 04:16:03 +00:00
|
|
|
}
|
|
|
|
});
|
2019-08-12 17:18:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method which triggers the release
|
|
|
|
*/
|
|
|
|
private _triggerRelease(notes: Frequency[], time: Seconds): void {
|
|
|
|
notes.forEach(note => {
|
|
|
|
const midiNote = new MidiClass(this.context, note).toMidi();
|
2019-08-14 14:50:01 +00:00
|
|
|
const voice = this._getActiveVoice(midiNote);
|
|
|
|
if (voice) {
|
2019-08-12 17:18:18 +00:00
|
|
|
// trigger release on that note
|
|
|
|
voice.triggerRelease(time);
|
2019-08-14 14:50:01 +00:00
|
|
|
this.log("triggerRelease", note, time);
|
2019-08-12 17:18:18 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
private _scheduleEvent(type: "attack" | "release", notes: Frequency[], time: Seconds, velocity?: NormalRange): void {
|
2019-08-14 14:50:01 +00:00
|
|
|
this.assert(!this.disposed, "Synth was already disposed");
|
2019-08-12 17:18:18 +00:00
|
|
|
// 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(() => {
|
|
|
|
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
|
|
|
|
* //trigger a chord immediately with a velocity of 0.2
|
|
|
|
* poly.triggerAttack(["Ab3", "C4", "F5"], undefined, 0.2);
|
|
|
|
*/
|
|
|
|
triggerAttack(notes: Frequency | Frequency[], time?: Time, velocity?: NormalRange): this {
|
|
|
|
|
|
|
|
if (!Array.isArray(notes)) {
|
|
|
|
notes = [notes];
|
|
|
|
}
|
|
|
|
const computedTime = this.toSeconds(time);
|
|
|
|
this._scheduleEvent("attack", notes, computedTime, velocity);
|
2019-08-12 04:16:03 +00:00
|
|
|
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
|
|
|
|
* poly.triggerRelease(["Ab3", "C4", "F5"], "+2n");
|
|
|
|
*/
|
|
|
|
triggerRelease(notes: Frequency | Frequency[], time?: Time): this {
|
|
|
|
if (!Array.isArray(notes)) {
|
|
|
|
notes = [notes];
|
|
|
|
}
|
|
|
|
const computedTime = this.toSeconds(time);
|
2019-08-12 17:18:18 +00:00
|
|
|
this._scheduleEvent("release", notes, computedTime);
|
2019-08-12 04:16:03 +00:00
|
|
|
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
|
|
|
|
* //trigger a chord for a duration of a half note
|
|
|
|
* poly.triggerAttackRelease(["Eb3", "G4", "C5"], "2n");
|
|
|
|
* @example
|
|
|
|
* //can pass in an array of durations as well
|
|
|
|
* poly.triggerAttackRelease(["Eb3", "G4", "C5"], ["2n", "4n", "4n"]);
|
|
|
|
*/
|
|
|
|
triggerAttackRelease(
|
|
|
|
notes: Frequency | Frequency[],
|
|
|
|
duration: Time | Time[],
|
|
|
|
time?: Time,
|
|
|
|
velocity?: NormalRange,
|
|
|
|
): this {
|
|
|
|
const computedTime = this.toSeconds(time);
|
|
|
|
this.triggerAttack(notes, computedTime, velocity);
|
|
|
|
if (isArray(duration)) {
|
|
|
|
this.assert(isArray(notes), "If the duration is an array, the notes must also be an array");
|
|
|
|
notes = notes as Frequency[];
|
|
|
|
for (let i = 0; i < notes.length; i++) {
|
|
|
|
const d = duration[Math.min(i, duration.length - 1)];
|
2019-08-12 17:18:18 +00:00
|
|
|
const durationSeconds = this.toSeconds(d);
|
|
|
|
this.assert(durationSeconds > 0, "The duration must be greater than 0");
|
|
|
|
this.triggerRelease(notes[i], computedTime + durationSeconds);
|
2019-08-12 04:16:03 +00:00
|
|
|
}
|
|
|
|
} else {
|
2019-08-12 17:18:18 +00:00
|
|
|
const durationSeconds = this.toSeconds(duration);
|
|
|
|
this.assert(durationSeconds > 0, "The duration must be greater than 0");
|
|
|
|
this.triggerRelease(notes, computedTime + durationSeconds);
|
2019-08-12 04:16:03 +00:00
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sync the instrument to the Transport. All subsequent calls of
|
|
|
|
* {@link triggerAttack} and {@link triggerRelease} will be scheduled along the transport.
|
|
|
|
* @example
|
|
|
|
* synth.sync()
|
|
|
|
* //schedule 3 notes when the transport first starts
|
|
|
|
* synth.triggerAttackRelease('8n', 0)
|
|
|
|
* synth.triggerAttackRelease('8n', '8n')
|
|
|
|
* synth.triggerAttackRelease('8n', '4n')
|
|
|
|
* //start the transport to hear the notes
|
|
|
|
* Transport.start()
|
|
|
|
*/
|
|
|
|
sync(): this {
|
|
|
|
this._syncMethod("triggerAttack", 1);
|
|
|
|
this._syncMethod("triggerRelease", 1);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set a member/attribute of the voices
|
|
|
|
* @example
|
|
|
|
* poly.set({
|
|
|
|
* "filter" : {
|
|
|
|
* "type" : "highpass"
|
|
|
|
* },
|
|
|
|
* "envelope" : {
|
|
|
|
* "attack" : 0.25
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
*/
|
|
|
|
set(options: RecursivePartial<VoiceOptions<Voice>>): this {
|
|
|
|
this.options = deepMerge(this.options, options);
|
|
|
|
this._voices.forEach(voice => voice.set(this.options));
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* Get the synth's attributes.
|
2019-08-12 04:16:03 +00:00
|
|
|
*/
|
|
|
|
get(): VoiceOptions<Voice> {
|
2019-09-03 23:29:59 +00:00
|
|
|
// return a clone of the options
|
|
|
|
return Object.assign({}, this.options);
|
2019-08-12 04:16:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-08-13 23:54:11 +00:00
|
|
|
* Trigger the release portion of all the currently active voices immediately.
|
|
|
|
* Useful for silencing the synth.
|
2019-08-12 04:16:03 +00:00
|
|
|
*/
|
2019-08-13 23:54:11 +00:00
|
|
|
releaseAll(): this {
|
|
|
|
const now = this.now();
|
2019-09-14 22:12:44 +00:00
|
|
|
this._activeVoices.forEach(({ voice }) => {
|
2019-08-13 23:54:11 +00:00
|
|
|
voice.triggerRelease(now);
|
2019-08-12 04:16:03 +00:00
|
|
|
});
|
2019-08-14 14:50:01 +00:00
|
|
|
this._activeVoices = [];
|
2019-08-12 04:16:03 +00:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose(): this {
|
|
|
|
super.dispose();
|
|
|
|
this._voices.forEach(v => v.dispose());
|
2019-08-14 14:50:01 +00:00
|
|
|
this._activeVoices = [];
|
2019-08-12 04:16:03 +00:00
|
|
|
this._availableVoices = [];
|
2019-09-06 18:55:20 +00:00
|
|
|
this.context.clearInterval(this._gcTimeout);
|
2019-08-12 04:16:03 +00:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|