UNPKG

12.8 kBJavaScriptView Raw
1import { TicksClass } from "../core/type/Ticks.js";
2import { TransportTimeClass } from "../core/type/TransportTime.js";
3import { defaultArg, optionsFromArguments } from "../core/util/Defaults.js";
4import { StateTimeline } from "../core/util/StateTimeline.js";
5import { isArray, isDefined, isObject, isUndef, } from "../core/util/TypeCheck.js";
6import { ToneEvent } from "./ToneEvent.js";
7/**
8 * Part is a collection ToneEvents which can be started/stopped and looped as a single unit.
9 *
10 * @example
11 * const synth = new Tone.Synth().toDestination();
12 * const part = new Tone.Part(((time, note) => {
13 * // the notes given as the second element in the array
14 * // will be passed in as the second argument
15 * synth.triggerAttackRelease(note, "8n", time);
16 * }), [[0, "C2"], ["0:2", "C3"], ["0:3:2", "G2"]]).start(0);
17 * Tone.Transport.start();
18 * @example
19 * const synth = new Tone.Synth().toDestination();
20 * // use an array of objects as long as the object has a "time" attribute
21 * const part = new Tone.Part(((time, value) => {
22 * // the value is an object which contains both the note and the velocity
23 * synth.triggerAttackRelease(value.note, "8n", time, value.velocity);
24 * }), [{ time: 0, note: "C3", velocity: 0.9 },
25 * { time: "0:2", note: "C4", velocity: 0.5 }
26 * ]).start(0);
27 * Tone.Transport.start();
28 * @category Event
29 */
30export class Part extends ToneEvent {
31 constructor() {
32 const options = optionsFromArguments(Part.getDefaults(), arguments, [
33 "callback",
34 "events",
35 ]);
36 super(options);
37 this.name = "Part";
38 /**
39 * Tracks the scheduled events
40 */
41 this._state = new StateTimeline("stopped");
42 /**
43 * The events that belong to this part
44 */
45 this._events = new Set();
46 // make sure things are assigned in the right order
47 this._state.increasing = true;
48 // add the events
49 options.events.forEach((event) => {
50 if (isArray(event)) {
51 this.add(event[0], event[1]);
52 }
53 else {
54 this.add(event);
55 }
56 });
57 }
58 static getDefaults() {
59 return Object.assign(ToneEvent.getDefaults(), {
60 events: [],
61 });
62 }
63 /**
64 * Start the part at the given time.
65 * @param time When to start the part.
66 * @param offset The offset from the start of the part to begin playing at.
67 */
68 start(time, offset) {
69 const ticks = this.toTicks(time);
70 if (this._state.getValueAtTime(ticks) !== "started") {
71 offset = defaultArg(offset, this._loop ? this._loopStart : 0);
72 if (this._loop) {
73 offset = defaultArg(offset, this._loopStart);
74 }
75 else {
76 offset = defaultArg(offset, 0);
77 }
78 const computedOffset = this.toTicks(offset);
79 this._state.add({
80 id: -1,
81 offset: computedOffset,
82 state: "started",
83 time: ticks,
84 });
85 this._forEach((event) => {
86 this._startNote(event, ticks, computedOffset);
87 });
88 }
89 return this;
90 }
91 /**
92 * Start the event in the given event at the correct time given
93 * the ticks and offset and looping.
94 * @param event
95 * @param ticks
96 * @param offset
97 */
98 _startNote(event, ticks, offset) {
99 ticks -= offset;
100 if (this._loop) {
101 if (event.startOffset >= this._loopStart &&
102 event.startOffset < this._loopEnd) {
103 if (event.startOffset < offset) {
104 // start it on the next loop
105 ticks += this._getLoopDuration();
106 }
107 event.start(new TicksClass(this.context, ticks));
108 }
109 else if (event.startOffset < this._loopStart &&
110 event.startOffset >= offset) {
111 event.loop = false;
112 event.start(new TicksClass(this.context, ticks));
113 }
114 }
115 else if (event.startOffset >= offset) {
116 event.start(new TicksClass(this.context, ticks));
117 }
118 }
119 get startOffset() {
120 return this._startOffset;
121 }
122 set startOffset(offset) {
123 this._startOffset = offset;
124 this._forEach((event) => {
125 event.startOffset += this._startOffset;
126 });
127 }
128 /**
129 * Stop the part at the given time.
130 * @param time When to stop the part.
131 */
132 stop(time) {
133 const ticks = this.toTicks(time);
134 this._state.cancel(ticks);
135 this._state.setStateAtTime("stopped", ticks);
136 this._forEach((event) => {
137 event.stop(time);
138 });
139 return this;
140 }
141 /**
142 * Get/Set an Event's value at the given time.
143 * If a value is passed in and no event exists at
144 * the given time, one will be created with that value.
145 * If two events are at the same time, the first one will
146 * be returned.
147 * @example
148 * const part = new Tone.Part();
149 * part.at("1m"); // returns the part at the first measure
150 * part.at("2m", "C2"); // set the value at "2m" to C2.
151 * // if an event didn't exist at that time, it will be created.
152 * @param time The time of the event to get or set.
153 * @param value If a value is passed in, the value of the event at the given time will be set to it.
154 */
155 at(time, value) {
156 const timeInTicks = new TransportTimeClass(this.context, time).toTicks();
157 const tickTime = new TicksClass(this.context, 1).toSeconds();
158 const iterator = this._events.values();
159 let result = iterator.next();
160 while (!result.done) {
161 const event = result.value;
162 if (Math.abs(timeInTicks - event.startOffset) < tickTime) {
163 if (isDefined(value)) {
164 event.value = value;
165 }
166 return event;
167 }
168 result = iterator.next();
169 }
170 // if there was no event at that time, create one
171 if (isDefined(value)) {
172 this.add(time, value);
173 // return the new event
174 return this.at(time);
175 }
176 else {
177 return null;
178 }
179 }
180 add(time, value) {
181 // extract the parameters
182 if (time instanceof Object && Reflect.has(time, "time")) {
183 value = time;
184 time = value.time;
185 }
186 const ticks = this.toTicks(time);
187 let event;
188 if (value instanceof ToneEvent) {
189 event = value;
190 event.callback = this._tick.bind(this);
191 }
192 else {
193 event = new ToneEvent({
194 callback: this._tick.bind(this),
195 context: this.context,
196 value,
197 });
198 }
199 // the start offset
200 event.startOffset = ticks;
201 // initialize the values
202 event.set({
203 humanize: this.humanize,
204 loop: this.loop,
205 loopEnd: this.loopEnd,
206 loopStart: this.loopStart,
207 playbackRate: this.playbackRate,
208 probability: this.probability,
209 });
210 this._events.add(event);
211 // start the note if it should be played right now
212 this._restartEvent(event);
213 return this;
214 }
215 /**
216 * Restart the given event
217 */
218 _restartEvent(event) {
219 this._state.forEach((stateEvent) => {
220 if (stateEvent.state === "started") {
221 this._startNote(event, stateEvent.time, stateEvent.offset);
222 }
223 else {
224 // stop the note
225 event.stop(new TicksClass(this.context, stateEvent.time));
226 }
227 });
228 }
229 remove(time, value) {
230 // extract the parameters
231 if (isObject(time) && time.hasOwnProperty("time")) {
232 value = time;
233 time = value.time;
234 }
235 time = this.toTicks(time);
236 this._events.forEach((event) => {
237 if (event.startOffset === time) {
238 if (isUndef(value) ||
239 (isDefined(value) && event.value === value)) {
240 this._events.delete(event);
241 event.dispose();
242 }
243 }
244 });
245 return this;
246 }
247 /**
248 * Remove all of the notes from the group.
249 */
250 clear() {
251 this._forEach((event) => event.dispose());
252 this._events.clear();
253 return this;
254 }
255 /**
256 * Cancel scheduled state change events: i.e. "start" and "stop".
257 * @param after The time after which to cancel the scheduled events.
258 */
259 cancel(after) {
260 this._forEach((event) => event.cancel(after));
261 this._state.cancel(this.toTicks(after));
262 return this;
263 }
264 /**
265 * Iterate over all of the events
266 */
267 _forEach(callback) {
268 if (this._events) {
269 this._events.forEach((event) => {
270 if (event instanceof Part) {
271 event._forEach(callback);
272 }
273 else {
274 callback(event);
275 }
276 });
277 }
278 return this;
279 }
280 /**
281 * Set the attribute of all of the events
282 * @param attr the attribute to set
283 * @param value The value to set it to
284 */
285 _setAll(attr, value) {
286 this._forEach((event) => {
287 event[attr] = value;
288 });
289 }
290 /**
291 * Internal tick method
292 * @param time The time of the event in seconds
293 */
294 _tick(time, value) {
295 if (!this.mute) {
296 this.callback(time, value);
297 }
298 }
299 /**
300 * Determine if the event should be currently looping
301 * given the loop boundries of this Part.
302 * @param event The event to test
303 */
304 _testLoopBoundries(event) {
305 if (this._loop &&
306 (event.startOffset < this._loopStart ||
307 event.startOffset >= this._loopEnd)) {
308 event.cancel(0);
309 }
310 else if (event.state === "stopped") {
311 // reschedule it if it's stopped
312 this._restartEvent(event);
313 }
314 }
315 get probability() {
316 return this._probability;
317 }
318 set probability(prob) {
319 this._probability = prob;
320 this._setAll("probability", prob);
321 }
322 get humanize() {
323 return this._humanize;
324 }
325 set humanize(variation) {
326 this._humanize = variation;
327 this._setAll("humanize", variation);
328 }
329 /**
330 * If the part should loop or not
331 * between Part.loopStart and
332 * Part.loopEnd. If set to true,
333 * the part will loop indefinitely,
334 * if set to a number greater than 1
335 * it will play a specific number of
336 * times, if set to false, 0 or 1, the
337 * part will only play once.
338 * @example
339 * const part = new Tone.Part();
340 * // loop the part 8 times
341 * part.loop = 8;
342 */
343 get loop() {
344 return this._loop;
345 }
346 set loop(loop) {
347 this._loop = loop;
348 this._forEach((event) => {
349 event.loopStart = this.loopStart;
350 event.loopEnd = this.loopEnd;
351 event.loop = loop;
352 this._testLoopBoundries(event);
353 });
354 }
355 /**
356 * The loopEnd point determines when it will
357 * loop if Part.loop is true.
358 */
359 get loopEnd() {
360 return new TicksClass(this.context, this._loopEnd).toSeconds();
361 }
362 set loopEnd(loopEnd) {
363 this._loopEnd = this.toTicks(loopEnd);
364 if (this._loop) {
365 this._forEach((event) => {
366 event.loopEnd = loopEnd;
367 this._testLoopBoundries(event);
368 });
369 }
370 }
371 /**
372 * The loopStart point determines when it will
373 * loop if Part.loop is true.
374 */
375 get loopStart() {
376 return new TicksClass(this.context, this._loopStart).toSeconds();
377 }
378 set loopStart(loopStart) {
379 this._loopStart = this.toTicks(loopStart);
380 if (this._loop) {
381 this._forEach((event) => {
382 event.loopStart = this.loopStart;
383 this._testLoopBoundries(event);
384 });
385 }
386 }
387 /**
388 * The playback rate of the part
389 */
390 get playbackRate() {
391 return this._playbackRate;
392 }
393 set playbackRate(rate) {
394 this._playbackRate = rate;
395 this._setAll("playbackRate", rate);
396 }
397 /**
398 * The number of scheduled notes in the part.
399 */
400 get length() {
401 return this._events.size;
402 }
403 dispose() {
404 super.dispose();
405 this.clear();
406 return this;
407 }
408}
409//# sourceMappingURL=Part.js.map
\No newline at end of file