UNPKG

10.1 kBJavaScriptView Raw
1import "../core/clock/Transport.js";
2import { ToneWithContext, } from "../core/context/ToneWithContext.js";
3import { TicksClass } from "../core/type/Ticks.js";
4import { defaultArg, optionsFromArguments } from "../core/util/Defaults.js";
5import { noOp } from "../core/util/Interface.js";
6import { StateTimeline, } from "../core/util/StateTimeline.js";
7import { isBoolean, isNumber } from "../core/util/TypeCheck.js";
8/**
9 * ToneEvent abstracts away this.context.transport.schedule and provides a schedulable
10 * callback for a single or repeatable events along the timeline.
11 *
12 * @example
13 * const synth = new Tone.PolySynth().toDestination();
14 * const chordEvent = new Tone.ToneEvent(((time, chord) => {
15 * // the chord as well as the exact time of the event
16 * // are passed in as arguments to the callback function
17 * synth.triggerAttackRelease(chord, 0.5, time);
18 * }), ["D4", "E4", "F4"]);
19 * // start the chord at the beginning of the transport timeline
20 * chordEvent.start();
21 * // loop it every measure for 8 measures
22 * chordEvent.loop = 8;
23 * chordEvent.loopEnd = "1m";
24 * @category Event
25 */
26export class ToneEvent extends ToneWithContext {
27 constructor() {
28 const options = optionsFromArguments(ToneEvent.getDefaults(), arguments, ["callback", "value"]);
29 super(options);
30 this.name = "ToneEvent";
31 /**
32 * Tracks the scheduled events
33 */
34 this._state = new StateTimeline("stopped");
35 /**
36 * A delay time from when the event is scheduled to start
37 */
38 this._startOffset = 0;
39 this._loop = options.loop;
40 this.callback = options.callback;
41 this.value = options.value;
42 this._loopStart = this.toTicks(options.loopStart);
43 this._loopEnd = this.toTicks(options.loopEnd);
44 this._playbackRate = options.playbackRate;
45 this._probability = options.probability;
46 this._humanize = options.humanize;
47 this.mute = options.mute;
48 this._playbackRate = options.playbackRate;
49 this._state.increasing = true;
50 // schedule the events for the first time
51 this._rescheduleEvents();
52 }
53 static getDefaults() {
54 return Object.assign(ToneWithContext.getDefaults(), {
55 callback: noOp,
56 humanize: false,
57 loop: false,
58 loopEnd: "1m",
59 loopStart: 0,
60 mute: false,
61 playbackRate: 1,
62 probability: 1,
63 value: null,
64 });
65 }
66 /**
67 * Reschedule all of the events along the timeline
68 * with the updated values.
69 * @param after Only reschedules events after the given time.
70 */
71 _rescheduleEvents(after = -1) {
72 // if no argument is given, schedules all of the events
73 this._state.forEachFrom(after, (event) => {
74 let duration;
75 if (event.state === "started") {
76 if (event.id !== -1) {
77 this.context.transport.clear(event.id);
78 }
79 const startTick = event.time +
80 Math.round(this.startOffset / this._playbackRate);
81 if (this._loop === true ||
82 (isNumber(this._loop) && this._loop > 1)) {
83 duration = Infinity;
84 if (isNumber(this._loop)) {
85 duration = this._loop * this._getLoopDuration();
86 }
87 const nextEvent = this._state.getAfter(startTick);
88 if (nextEvent !== null) {
89 duration = Math.min(duration, nextEvent.time - startTick);
90 }
91 if (duration !== Infinity) {
92 duration = new TicksClass(this.context, duration);
93 }
94 const interval = new TicksClass(this.context, this._getLoopDuration());
95 event.id = this.context.transport.scheduleRepeat(this._tick.bind(this), interval, new TicksClass(this.context, startTick), duration);
96 }
97 else {
98 event.id = this.context.transport.schedule(this._tick.bind(this), new TicksClass(this.context, startTick));
99 }
100 }
101 });
102 }
103 /**
104 * Returns the playback state of the note, either "started" or "stopped".
105 */
106 get state() {
107 return this._state.getValueAtTime(this.context.transport.ticks);
108 }
109 /**
110 * The start from the scheduled start time.
111 */
112 get startOffset() {
113 return this._startOffset;
114 }
115 set startOffset(offset) {
116 this._startOffset = offset;
117 }
118 /**
119 * The probability of the notes being triggered.
120 */
121 get probability() {
122 return this._probability;
123 }
124 set probability(prob) {
125 this._probability = prob;
126 }
127 /**
128 * If set to true, will apply small random variation
129 * to the callback time. If the value is given as a time, it will randomize
130 * by that amount.
131 * @example
132 * const event = new Tone.ToneEvent();
133 * event.humanize = true;
134 */
135 get humanize() {
136 return this._humanize;
137 }
138 set humanize(variation) {
139 this._humanize = variation;
140 }
141 /**
142 * Start the note at the given time.
143 * @param time When the event should start.
144 */
145 start(time) {
146 const ticks = this.toTicks(time);
147 if (this._state.getValueAtTime(ticks) === "stopped") {
148 this._state.add({
149 id: -1,
150 state: "started",
151 time: ticks,
152 });
153 this._rescheduleEvents(ticks);
154 }
155 return this;
156 }
157 /**
158 * Stop the Event at the given time.
159 * @param time When the event should stop.
160 */
161 stop(time) {
162 this.cancel(time);
163 const ticks = this.toTicks(time);
164 if (this._state.getValueAtTime(ticks) === "started") {
165 this._state.setStateAtTime("stopped", ticks, { id: -1 });
166 const previousEvent = this._state.getBefore(ticks);
167 let rescheduleTime = ticks;
168 if (previousEvent !== null) {
169 rescheduleTime = previousEvent.time;
170 }
171 this._rescheduleEvents(rescheduleTime);
172 }
173 return this;
174 }
175 /**
176 * Cancel all scheduled events greater than or equal to the given time
177 * @param time The time after which events will be cancel.
178 */
179 cancel(time) {
180 time = defaultArg(time, -Infinity);
181 const ticks = this.toTicks(time);
182 this._state.forEachFrom(ticks, (event) => {
183 this.context.transport.clear(event.id);
184 });
185 this._state.cancel(ticks);
186 return this;
187 }
188 /**
189 * The callback function invoker. Also
190 * checks if the Event is done playing
191 * @param time The time of the event in seconds
192 */
193 _tick(time) {
194 const ticks = this.context.transport.getTicksAtTime(time);
195 if (!this.mute && this._state.getValueAtTime(ticks) === "started") {
196 if (this.probability < 1 && Math.random() > this.probability) {
197 return;
198 }
199 if (this.humanize) {
200 let variation = 0.02;
201 if (!isBoolean(this.humanize)) {
202 variation = this.toSeconds(this.humanize);
203 }
204 time += (Math.random() * 2 - 1) * variation;
205 }
206 this.callback(time, this.value);
207 }
208 }
209 /**
210 * Get the duration of the loop.
211 */
212 _getLoopDuration() {
213 return (this._loopEnd - this._loopStart) / this._playbackRate;
214 }
215 /**
216 * If the note should loop or not
217 * between ToneEvent.loopStart and
218 * ToneEvent.loopEnd. If set to true,
219 * the event will loop indefinitely,
220 * if set to a number greater than 1
221 * it will play a specific number of
222 * times, if set to false, 0 or 1, the
223 * part will only play once.
224 */
225 get loop() {
226 return this._loop;
227 }
228 set loop(loop) {
229 this._loop = loop;
230 this._rescheduleEvents();
231 }
232 /**
233 * The playback rate of the event. Defaults to 1.
234 * @example
235 * const note = new Tone.ToneEvent();
236 * note.loop = true;
237 * // repeat the note twice as fast
238 * note.playbackRate = 2;
239 */
240 get playbackRate() {
241 return this._playbackRate;
242 }
243 set playbackRate(rate) {
244 this._playbackRate = rate;
245 this._rescheduleEvents();
246 }
247 /**
248 * The loopEnd point is the time the event will loop
249 * if ToneEvent.loop is true.
250 */
251 get loopEnd() {
252 return new TicksClass(this.context, this._loopEnd).toSeconds();
253 }
254 set loopEnd(loopEnd) {
255 this._loopEnd = this.toTicks(loopEnd);
256 if (this._loop) {
257 this._rescheduleEvents();
258 }
259 }
260 /**
261 * The time when the loop should start.
262 */
263 get loopStart() {
264 return new TicksClass(this.context, this._loopStart).toSeconds();
265 }
266 set loopStart(loopStart) {
267 this._loopStart = this.toTicks(loopStart);
268 if (this._loop) {
269 this._rescheduleEvents();
270 }
271 }
272 /**
273 * The current progress of the loop interval.
274 * Returns 0 if the event is not started yet or
275 * it is not set to loop.
276 */
277 get progress() {
278 if (this._loop) {
279 const ticks = this.context.transport.ticks;
280 const lastEvent = this._state.get(ticks);
281 if (lastEvent !== null && lastEvent.state === "started") {
282 const loopDuration = this._getLoopDuration();
283 const progress = (ticks - lastEvent.time) % loopDuration;
284 return progress / loopDuration;
285 }
286 else {
287 return 0;
288 }
289 }
290 else {
291 return 0;
292 }
293 }
294 dispose() {
295 super.dispose();
296 this.cancel();
297 this._state.dispose();
298 return this;
299 }
300}
301//# sourceMappingURL=ToneEvent.js.map
\No newline at end of file