UNPKG

12.3 kBJavaScriptView Raw
1import { MidiClass } from "../core/type/Midi.js";
2import { deepMerge, omitFromObject, optionsFromArguments, } from "../core/util/Defaults.js";
3import { isArray, isNumber } from "../core/util/TypeCheck.js";
4import { Instrument } from "./Instrument.js";
5import { Monophonic } from "./Monophonic.js";
6import { Synth } from "./Synth.js";
7import { 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 */
23export 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