1 | import { MidiClass } from "../core/type/Midi.js";
|
2 | import { deepMerge, omitFromObject, optionsFromArguments, } from "../core/util/Defaults.js";
|
3 | import { isArray, isNumber } from "../core/util/TypeCheck.js";
|
4 | import { Instrument } from "./Instrument.js";
|
5 | import { Monophonic } from "./Monophonic.js";
|
6 | import { Synth } from "./Synth.js";
|
7 | import { assert, warn } from "../core/util/Debug.js";
|
8 | /**
|
9 | * PolySynth handles voice creation and allocation for any
|
10 | * instruments passed in as the second parameter. PolySynth is
|
11 | * not a synthesizer by itself, it merely manages voices of
|
12 | * one of the other types of synths, allowing any of the
|
13 | * monophonic synthesizers to be polyphonic.
|
14 | *
|
15 | * @example
|
16 | * const synth = new Tone.PolySynth().toDestination();
|
17 | * // set the attributes across all the voices using 'set'
|
18 | * synth.set({ detune: -1200 });
|
19 | * // play a chord
|
20 | * synth.triggerAttackRelease(["C4", "E4", "A4"], 1);
|
21 | * @category Instrument
|
22 | */
|
23 | export class PolySynth extends Instrument {
|
24 | constructor() {
|
25 | const options = optionsFromArguments(PolySynth.getDefaults(), arguments, ["voice", "options"]);
|
26 | super(options);
|
27 | this.name = "PolySynth";
|
28 | /**
|
29 | * The voices which are not currently in use
|
30 | */
|
31 | this._availableVoices = [];
|
32 | /**
|
33 | * The currently active voices
|
34 | */
|
35 | this._activeVoices = [];
|
36 | /**
|
37 | * All of the allocated voices for this synth.
|
38 | */
|
39 | this._voices = [];
|
40 | /**
|
41 | * The GC timeout. Held so that it could be cancelled when the node is disposed.
|
42 | */
|
43 | this._gcTimeout = -1;
|
44 | /**
|
45 | * A moving average of the number of active voices
|
46 | */
|
47 | this._averageActiveVoices = 0;
|
48 | /**
|
49 | * The release which is scheduled to the timeline.
|
50 | */
|
51 | this._syncedRelease = (time) => this.releaseAll(time);
|
52 | // check against the old API (pre 14.3.0)
|
53 | assert(!isNumber(options.voice), "DEPRECATED: The polyphony count is no longer the first argument.");
|
54 | const defaults = options.voice.getDefaults();
|
55 | this.options = Object.assign(defaults, options.options);
|
56 | this.voice = options.voice;
|
57 | this.maxPolyphony = options.maxPolyphony;
|
58 | // create the first voice
|
59 | this._dummyVoice = this._getNextAvailableVoice();
|
60 | // remove it from the voices list
|
61 | const index = this._voices.indexOf(this._dummyVoice);
|
62 | this._voices.splice(index, 1);
|
63 | // kick off the GC interval
|
64 | this._gcTimeout = this.context.setInterval(this._collectGarbage.bind(this), 1);
|
65 | }
|
66 | static getDefaults() {
|
67 | return Object.assign(Instrument.getDefaults(), {
|
68 | maxPolyphony: 32,
|
69 | options: {},
|
70 | voice: Synth,
|
71 | });
|
72 | }
|
73 | /**
|
74 | * The number of active voices.
|
75 | */
|
76 | get activeVoices() {
|
77 | return this._activeVoices.length;
|
78 | }
|
79 | /**
|
80 | * Invoked when the source is done making sound, so that it can be
|
81 | * readded to the pool of available voices
|
82 | */
|
83 | _makeVoiceAvailable(voice) {
|
84 | this._availableVoices.push(voice);
|
85 | // remove the midi note from 'active voices'
|
86 | const activeVoiceIndex = this._activeVoices.findIndex((e) => e.voice === voice);
|
87 | this._activeVoices.splice(activeVoiceIndex, 1);
|
88 | }
|
89 | /**
|
90 | * Get an available voice from the pool of available voices.
|
91 | * If one is not available and the maxPolyphony limit is reached,
|
92 | * steal a voice, otherwise return null.
|
93 | */
|
94 | _getNextAvailableVoice() {
|
95 | // if there are available voices, return the first one
|
96 | if (this._availableVoices.length) {
|
97 | return this._availableVoices.shift();
|
98 | }
|
99 | else if (this._voices.length < this.maxPolyphony) {
|
100 | // otherwise if there is still more maxPolyphony, make a new voice
|
101 | const voice = new this.voice(Object.assign(this.options, {
|
102 | context: this.context,
|
103 | onsilence: this._makeVoiceAvailable.bind(this),
|
104 | }));
|
105 | assert(voice instanceof Monophonic, "Voice must extend Monophonic class");
|
106 | voice.connect(this.output);
|
107 | this._voices.push(voice);
|
108 | return voice;
|
109 | }
|
110 | else {
|
111 | warn("Max polyphony exceeded. Note dropped.");
|
112 | }
|
113 | }
|
114 | /**
|
115 | * Occasionally check if there are any allocated voices which can be cleaned up.
|
116 | */
|
117 | _collectGarbage() {
|
118 | this._averageActiveVoices = Math.max(this._averageActiveVoices * 0.95, this.activeVoices);
|
119 | if (this._availableVoices.length &&
|
120 | this._voices.length > Math.ceil(this._averageActiveVoices + 1)) {
|
121 | // take off an available note
|
122 | const firstAvail = this._availableVoices.shift();
|
123 | const index = this._voices.indexOf(firstAvail);
|
124 | this._voices.splice(index, 1);
|
125 | if (!this.context.isOffline) {
|
126 | firstAvail.dispose();
|
127 | }
|
128 | }
|
129 | }
|
130 | /**
|
131 | * Internal method which triggers the attack
|
132 | */
|
133 | _triggerAttack(notes, time, velocity) {
|
134 | notes.forEach((note) => {
|
135 | const midiNote = new MidiClass(this.context, note).toMidi();
|
136 | const voice = this._getNextAvailableVoice();
|
137 | if (voice) {
|
138 | voice.triggerAttack(note, time, velocity);
|
139 | this._activeVoices.push({
|
140 | midi: midiNote,
|
141 | voice,
|
142 | released: false,
|
143 | });
|
144 | this.log("triggerAttack", note, time);
|
145 | }
|
146 | });
|
147 | }
|
148 | /**
|
149 | * Internal method which triggers the release
|
150 | */
|
151 | _triggerRelease(notes, time) {
|
152 | notes.forEach((note) => {
|
153 | const midiNote = new MidiClass(this.context, note).toMidi();
|
154 | const event = this._activeVoices.find(({ midi, released }) => midi === midiNote && !released);
|
155 | if (event) {
|
156 | // trigger release on that note
|
157 | event.voice.triggerRelease(time);
|
158 | // mark it as released
|
159 | event.released = true;
|
160 | this.log("triggerRelease", note, time);
|
161 | }
|
162 | });
|
163 | }
|
164 | /**
|
165 | * Schedule the attack/release events. If the time is in the future, then it should set a timeout
|
166 | * to wait for just-in-time scheduling
|
167 | */
|
168 | _scheduleEvent(type, notes, time, velocity) {
|
169 | assert(!this.disposed, "Synth was already disposed");
|
170 | // if the notes are greater than this amount of time in the future, they should be scheduled with setTimeout
|
171 | if (time <= this.now()) {
|
172 | // do it immediately
|
173 | if (type === "attack") {
|
174 | this._triggerAttack(notes, time, velocity);
|
175 | }
|
176 | else {
|
177 | this._triggerRelease(notes, time);
|
178 | }
|
179 | }
|
180 | else {
|
181 | // schedule it to start in the future
|
182 | this.context.setTimeout(() => {
|
183 | if (!this.disposed) {
|
184 | this._scheduleEvent(type, notes, time, velocity);
|
185 | }
|
186 | }, time - this.now());
|
187 | }
|
188 | }
|
189 | /**
|
190 | * Trigger the attack portion of the note
|
191 | * @param notes The notes to play. Accepts a single Frequency or an array of frequencies.
|
192 | * @param time The start time of the note.
|
193 | * @param velocity The velocity of the note.
|
194 | * @example
|
195 | * const synth = new Tone.PolySynth(Tone.FMSynth).toDestination();
|
196 | * // trigger a chord immediately with a velocity of 0.2
|
197 | * synth.triggerAttack(["Ab3", "C4", "F5"], Tone.now(), 0.2);
|
198 | */
|
199 | triggerAttack(notes, time, velocity) {
|
200 | if (!Array.isArray(notes)) {
|
201 | notes = [notes];
|
202 | }
|
203 | const computedTime = this.toSeconds(time);
|
204 | this._scheduleEvent("attack", notes, computedTime, velocity);
|
205 | return this;
|
206 | }
|
207 | /**
|
208 | * Trigger the release of the note. Unlike monophonic instruments,
|
209 | * a note (or array of notes) needs to be passed in as the first argument.
|
210 | * @param notes The notes to play. Accepts a single Frequency or an array of frequencies.
|
211 | * @param time When the release will be triggered.
|
212 | * @example
|
213 | * const poly = new Tone.PolySynth(Tone.AMSynth).toDestination();
|
214 | * poly.triggerAttack(["Ab3", "C4", "F5"]);
|
215 | * // trigger the release of the given notes.
|
216 | * poly.triggerRelease(["Ab3", "C4"], "+1");
|
217 | * poly.triggerRelease("F5", "+3");
|
218 | */
|
219 | triggerRelease(notes, time) {
|
220 | if (!Array.isArray(notes)) {
|
221 | notes = [notes];
|
222 | }
|
223 | const computedTime = this.toSeconds(time);
|
224 | this._scheduleEvent("release", notes, computedTime);
|
225 | return this;
|
226 | }
|
227 | /**
|
228 | * Trigger the attack and release after the specified duration
|
229 | * @param notes The notes to play. Accepts a single Frequency or an array of frequencies.
|
230 | * @param duration the duration of the note
|
231 | * @param time if no time is given, defaults to now
|
232 | * @param velocity the velocity of the attack (0-1)
|
233 | * @example
|
234 | * const poly = new Tone.PolySynth(Tone.AMSynth).toDestination();
|
235 | * // can pass in an array of durations as well
|
236 | * poly.triggerAttackRelease(["Eb3", "G4", "Bb4", "D5"], [4, 3, 2, 1]);
|
237 | */
|
238 | triggerAttackRelease(notes, duration, time, velocity) {
|
239 | const computedTime = this.toSeconds(time);
|
240 | this.triggerAttack(notes, computedTime, velocity);
|
241 | if (isArray(duration)) {
|
242 | assert(isArray(notes), "If the duration is an array, the notes must also be an array");
|
243 | notes = notes;
|
244 | for (let i = 0; i < notes.length; i++) {
|
245 | const d = duration[Math.min(i, duration.length - 1)];
|
246 | const durationSeconds = this.toSeconds(d);
|
247 | assert(durationSeconds > 0, "The duration must be greater than 0");
|
248 | this.triggerRelease(notes[i], computedTime + durationSeconds);
|
249 | }
|
250 | }
|
251 | else {
|
252 | const durationSeconds = this.toSeconds(duration);
|
253 | assert(durationSeconds > 0, "The duration must be greater than 0");
|
254 | this.triggerRelease(notes, computedTime + durationSeconds);
|
255 | }
|
256 | return this;
|
257 | }
|
258 | sync() {
|
259 | if (this._syncState()) {
|
260 | this._syncMethod("triggerAttack", 1);
|
261 | this._syncMethod("triggerRelease", 1);
|
262 | // make sure that the sound doesn't play after its been stopped
|
263 | this.context.transport.on("stop", this._syncedRelease);
|
264 | this.context.transport.on("pause", this._syncedRelease);
|
265 | this.context.transport.on("loopEnd", this._syncedRelease);
|
266 | }
|
267 | return this;
|
268 | }
|
269 | /**
|
270 | * Set a member/attribute of the voices
|
271 | * @example
|
272 | * const poly = new Tone.PolySynth().toDestination();
|
273 | * // set all of the voices using an options object for the synth type
|
274 | * poly.set({
|
275 | * envelope: {
|
276 | * attack: 0.25
|
277 | * }
|
278 | * });
|
279 | * poly.triggerAttackRelease("Bb3", 0.2);
|
280 | */
|
281 | set(options) {
|
282 | // remove options which are controlled by the PolySynth
|
283 | const sanitizedOptions = omitFromObject(options, [
|
284 | "onsilence",
|
285 | "context",
|
286 | ]);
|
287 | // store all of the options
|
288 | this.options = deepMerge(this.options, sanitizedOptions);
|
289 | this._voices.forEach((voice) => voice.set(sanitizedOptions));
|
290 | this._dummyVoice.set(sanitizedOptions);
|
291 | return this;
|
292 | }
|
293 | get() {
|
294 | return this._dummyVoice.get();
|
295 | }
|
296 | /**
|
297 | * Trigger the release portion of all the currently active voices immediately.
|
298 | * Useful for silencing the synth.
|
299 | */
|
300 | releaseAll(time) {
|
301 | const computedTime = this.toSeconds(time);
|
302 | this._activeVoices.forEach(({ voice }) => {
|
303 | voice.triggerRelease(computedTime);
|
304 | });
|
305 | return this;
|
306 | }
|
307 | dispose() {
|
308 | super.dispose();
|
309 | this._dummyVoice.dispose();
|
310 | this._voices.forEach((v) => v.dispose());
|
311 | this._activeVoices = [];
|
312 | this._availableVoices = [];
|
313 | this.context.clearInterval(this._gcTimeout);
|
314 | return this;
|
315 | }
|
316 | }
|
317 | //# sourceMappingURL=PolySynth.js.map |
\ | No newline at end of file |