2024-05-03 18:31:14 +00:00
|
|
|
import { TicksClass } from "../core/type/Ticks.js";
|
|
|
|
import {
|
|
|
|
NormalRange,
|
|
|
|
Positive,
|
|
|
|
Seconds,
|
|
|
|
Ticks,
|
|
|
|
Time,
|
|
|
|
TransportTime,
|
|
|
|
} from "../core/type/Units.js";
|
|
|
|
import { omitFromObject, optionsFromArguments } from "../core/util/Defaults.js";
|
|
|
|
import { isArray, isString } from "../core/util/TypeCheck.js";
|
|
|
|
import { Part } from "./Part.js";
|
|
|
|
import { ToneEvent, ToneEventCallback, ToneEventOptions } from "./ToneEvent.js";
|
2019-07-25 01:53:35 +00:00
|
|
|
|
2024-04-30 13:44:57 +00:00
|
|
|
type SequenceEventDescription<T> = Array<T | SequenceEventDescription<T>>;
|
2019-07-25 01:53:35 +00:00
|
|
|
|
2019-07-26 15:50:29 +00:00
|
|
|
interface SequenceOptions<T> extends Omit<ToneEventOptions<T>, "value"> {
|
2019-07-25 01:53:35 +00:00
|
|
|
loopStart: number;
|
|
|
|
loopEnd: number;
|
|
|
|
subdivision: Time;
|
2019-07-26 15:50:29 +00:00
|
|
|
events: SequenceEventDescription<T>;
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A sequence is an alternate notation of a part. Instead
|
|
|
|
* of passing in an array of [time, event] pairs, pass
|
|
|
|
* in an array of events which will be spaced at the
|
|
|
|
* given subdivision. Sub-arrays will subdivide that beat
|
|
|
|
* by the number of items are in the array.
|
2022-04-23 16:23:52 +00:00
|
|
|
* Sequence notation inspiration from [Tidal Cycles](http://tidalcycles.org/)
|
2019-07-25 01:53:35 +00:00
|
|
|
* @example
|
2020-04-17 02:24:18 +00:00
|
|
|
* const synth = new Tone.Synth().toDestination();
|
|
|
|
* const seq = new Tone.Sequence((time, note) => {
|
2019-10-24 22:01:27 +00:00
|
|
|
* synth.triggerAttackRelease(note, 0.1, time);
|
|
|
|
* // subdivisions are given as subarrays
|
|
|
|
* }, ["C4", ["E4", "D4", "E4"], "G4", ["A4", "G4"]]).start(0);
|
2020-04-17 02:24:18 +00:00
|
|
|
* Tone.Transport.start();
|
2019-09-16 14:49:30 +00:00
|
|
|
* @category Event
|
2019-07-25 01:53:35 +00:00
|
|
|
*/
|
2019-07-26 15:50:29 +00:00
|
|
|
export class Sequence<ValueType = any> extends ToneEvent<ValueType> {
|
2019-09-04 23:18:44 +00:00
|
|
|
readonly name: string = "Sequence";
|
2019-07-25 01:53:35 +00:00
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* The subdivison of each note
|
2019-07-25 01:53:35 +00:00
|
|
|
*/
|
|
|
|
private _subdivision: Ticks;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The object responsible for scheduling all of the events
|
|
|
|
*/
|
|
|
|
private _part: Part = new Part({
|
|
|
|
callback: this._seqCallback.bind(this),
|
|
|
|
context: this.context,
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* private reference to all of the sequence proxies
|
|
|
|
*/
|
2019-12-17 16:58:45 +00:00
|
|
|
private _events: SequenceEventDescription<ValueType> = [];
|
2019-07-25 01:53:35 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The proxied array
|
|
|
|
*/
|
2019-12-17 16:58:45 +00:00
|
|
|
private _eventsArray: SequenceEventDescription<ValueType> = [];
|
2019-07-25 01:53:35 +00:00
|
|
|
|
2019-08-27 15:58:40 +00:00
|
|
|
/**
|
|
|
|
* @param callback The callback to invoke with every note
|
2024-04-30 13:44:57 +00:00
|
|
|
* @param events The sequence of events
|
2019-08-27 15:58:40 +00:00
|
|
|
* @param subdivision The subdivision between which events are placed.
|
|
|
|
*/
|
2019-07-25 01:53:35 +00:00
|
|
|
constructor(
|
2019-07-26 15:50:29 +00:00
|
|
|
callback?: ToneEventCallback<ValueType>,
|
|
|
|
events?: SequenceEventDescription<ValueType>,
|
2024-05-03 18:31:14 +00:00
|
|
|
subdivision?: Time
|
2019-07-25 01:53:35 +00:00
|
|
|
);
|
2019-07-26 15:50:29 +00:00
|
|
|
constructor(options?: Partial<SequenceOptions<ValueType>>);
|
2019-07-25 01:53:35 +00:00
|
|
|
constructor() {
|
2024-05-03 18:31:14 +00:00
|
|
|
const options = optionsFromArguments(
|
|
|
|
Sequence.getDefaults(),
|
|
|
|
arguments,
|
|
|
|
["callback", "events", "subdivision"]
|
|
|
|
);
|
2024-05-06 14:55:55 +00:00
|
|
|
super(options);
|
2019-07-25 01:53:35 +00:00
|
|
|
|
|
|
|
this._subdivision = this.toTicks(options.subdivision);
|
|
|
|
|
|
|
|
this.events = options.events;
|
|
|
|
|
|
|
|
// set all of the values
|
|
|
|
this.loop = options.loop;
|
|
|
|
this.loopStart = options.loopStart;
|
|
|
|
this.loopEnd = options.loopEnd;
|
|
|
|
this.playbackRate = options.playbackRate;
|
|
|
|
this.probability = options.probability;
|
|
|
|
this.humanize = options.humanize;
|
|
|
|
this.mute = options.mute;
|
|
|
|
this.playbackRate = options.playbackRate;
|
|
|
|
}
|
|
|
|
|
2019-07-26 15:50:29 +00:00
|
|
|
static getDefaults(): SequenceOptions<any> {
|
2024-05-03 18:31:14 +00:00
|
|
|
return Object.assign(
|
|
|
|
omitFromObject(ToneEvent.getDefaults(), ["value"]),
|
|
|
|
{
|
|
|
|
events: [],
|
|
|
|
loop: true,
|
|
|
|
loopEnd: 0,
|
|
|
|
loopStart: 0,
|
|
|
|
subdivision: "8n",
|
|
|
|
}
|
|
|
|
);
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The internal callback for when an event is invoked
|
|
|
|
*/
|
|
|
|
private _seqCallback(time: Seconds, value: any): void {
|
2021-01-19 04:27:55 +00:00
|
|
|
if (value !== null && !this.mute) {
|
2019-07-25 01:53:35 +00:00
|
|
|
this.callback(time, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The sequence
|
|
|
|
*/
|
|
|
|
get events(): any[] {
|
|
|
|
return this._events;
|
|
|
|
}
|
|
|
|
set events(s) {
|
|
|
|
this.clear();
|
|
|
|
this._eventsArray = s;
|
|
|
|
this._events = this._createSequence(this._eventsArray);
|
|
|
|
this._eventsUpdated();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* Start the part at the given time.
|
2019-08-30 16:06:38 +00:00
|
|
|
* @param time When to start the part.
|
|
|
|
* @param offset The offset index to start at
|
2019-07-25 01:53:35 +00:00
|
|
|
*/
|
|
|
|
start(time?: TransportTime, offset?: number): this {
|
|
|
|
this._part.start(time, offset ? this._indexTime(offset) : offset);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* Stop the part at the given time.
|
2019-08-30 16:06:38 +00:00
|
|
|
* @param time When to stop the part.
|
2019-07-25 01:53:35 +00:00
|
|
|
*/
|
|
|
|
stop(time?: TransportTime): this {
|
|
|
|
this._part.stop(time);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* The subdivision of the sequence. This can only be
|
|
|
|
* set in the constructor. The subdivision is the
|
|
|
|
* interval between successive steps.
|
2019-07-25 01:53:35 +00:00
|
|
|
*/
|
|
|
|
get subdivision(): Seconds {
|
|
|
|
return new TicksClass(this.context, this._subdivision).toSeconds();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a sequence proxy which can be monitored to create subsequences
|
|
|
|
*/
|
|
|
|
private _createSequence(array: any[]): any[] {
|
|
|
|
return new Proxy(array, {
|
|
|
|
get: (target: any[], property: PropertyKey): any => {
|
|
|
|
// property is index in this case
|
|
|
|
return target[property];
|
|
|
|
},
|
2024-05-03 18:31:14 +00:00
|
|
|
set: (
|
|
|
|
target: any[],
|
|
|
|
property: PropertyKey,
|
|
|
|
value: any
|
|
|
|
): boolean => {
|
2019-07-25 01:53:35 +00:00
|
|
|
if (isString(property) && isFinite(parseInt(property, 10))) {
|
|
|
|
if (isArray(value)) {
|
|
|
|
target[property] = this._createSequence(value);
|
|
|
|
} else {
|
|
|
|
target[property] = value;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
target[property] = value;
|
|
|
|
}
|
|
|
|
this._eventsUpdated();
|
|
|
|
// return true to accept the changes
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When the sequence has changed, all of the events need to be recreated
|
|
|
|
*/
|
|
|
|
private _eventsUpdated(): void {
|
|
|
|
this._part.clear();
|
2024-05-03 18:31:14 +00:00
|
|
|
this._rescheduleSequence(
|
|
|
|
this._eventsArray,
|
|
|
|
this._subdivision,
|
|
|
|
this.startOffset
|
|
|
|
);
|
2019-07-25 01:53:35 +00:00
|
|
|
// update the loopEnd
|
|
|
|
this.loopEnd = this.loopEnd;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* reschedule all of the events that need to be rescheduled
|
|
|
|
*/
|
2024-05-03 18:31:14 +00:00
|
|
|
private _rescheduleSequence(
|
|
|
|
sequence: any[],
|
|
|
|
subdivision: Ticks,
|
|
|
|
startOffset: Ticks
|
|
|
|
): void {
|
2019-07-25 01:53:35 +00:00
|
|
|
sequence.forEach((value, index) => {
|
2024-05-03 18:31:14 +00:00
|
|
|
const eventOffset = index * subdivision + startOffset;
|
2019-07-25 01:53:35 +00:00
|
|
|
if (isArray(value)) {
|
2024-05-03 18:31:14 +00:00
|
|
|
this._rescheduleSequence(
|
|
|
|
value,
|
|
|
|
subdivision / value.length,
|
|
|
|
eventOffset
|
|
|
|
);
|
2019-07-25 01:53:35 +00:00
|
|
|
} else {
|
2024-05-03 18:31:14 +00:00
|
|
|
const startTime = new TicksClass(
|
|
|
|
this.context,
|
|
|
|
eventOffset,
|
|
|
|
"i"
|
|
|
|
).toSeconds();
|
2019-07-25 01:53:35 +00:00
|
|
|
this._part.add(startTime, value);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* Get the time of the index given the Sequence's subdivision
|
2019-08-30 16:06:38 +00:00
|
|
|
* @param index
|
|
|
|
* @return The time of that index
|
2019-07-25 01:53:35 +00:00
|
|
|
*/
|
|
|
|
private _indexTime(index: number): Seconds {
|
2024-05-03 18:31:14 +00:00
|
|
|
return new TicksClass(
|
|
|
|
this.context,
|
|
|
|
index * this._subdivision + this.startOffset
|
|
|
|
).toSeconds();
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear all of the events
|
|
|
|
*/
|
|
|
|
clear(): this {
|
|
|
|
this._part.clear();
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose(): this {
|
|
|
|
super.dispose();
|
|
|
|
this._part.dispose();
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2019-09-14 22:06:46 +00:00
|
|
|
//-------------------------------------
|
2019-07-25 01:53:35 +00:00
|
|
|
// PROXY CALLS
|
2019-09-14 22:06:46 +00:00
|
|
|
//-------------------------------------
|
2019-07-25 01:53:35 +00:00
|
|
|
|
|
|
|
get loop(): boolean | number {
|
|
|
|
return this._part.loop;
|
|
|
|
}
|
|
|
|
set loop(l) {
|
2019-11-21 18:19:44 +00:00
|
|
|
this._part.loop = l;
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The index at which the sequence should start looping
|
|
|
|
*/
|
|
|
|
get loopStart(): number {
|
|
|
|
return this._loopStart;
|
|
|
|
}
|
|
|
|
set loopStart(index) {
|
|
|
|
this._loopStart = index;
|
2019-11-21 18:19:44 +00:00
|
|
|
this._part.loopStart = this._indexTime(index);
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The index at which the sequence should end looping
|
|
|
|
*/
|
|
|
|
get loopEnd(): number {
|
|
|
|
return this._loopEnd;
|
|
|
|
}
|
|
|
|
set loopEnd(index) {
|
|
|
|
this._loopEnd = index;
|
2019-11-21 18:19:44 +00:00
|
|
|
if (index === 0) {
|
|
|
|
this._part.loopEnd = this._indexTime(this._eventsArray.length);
|
|
|
|
} else {
|
|
|
|
this._part.loopEnd = this._indexTime(index);
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get startOffset(): Ticks {
|
|
|
|
return this._part.startOffset;
|
|
|
|
}
|
|
|
|
set startOffset(start) {
|
2019-11-21 18:19:44 +00:00
|
|
|
this._part.startOffset = start;
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
get playbackRate(): Positive {
|
|
|
|
return this._part.playbackRate;
|
|
|
|
}
|
|
|
|
set playbackRate(rate) {
|
2019-11-21 18:19:44 +00:00
|
|
|
this._part.playbackRate = rate;
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
get probability(): NormalRange {
|
|
|
|
return this._part.probability;
|
|
|
|
}
|
|
|
|
set probability(prob) {
|
2019-11-21 18:19:44 +00:00
|
|
|
this._part.probability = prob;
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
2019-12-22 06:31:27 +00:00
|
|
|
get progress(): NormalRange {
|
|
|
|
return this._part.progress;
|
|
|
|
}
|
|
|
|
|
2019-07-25 01:53:35 +00:00
|
|
|
get humanize(): boolean | Time {
|
|
|
|
return this._part.humanize;
|
|
|
|
}
|
|
|
|
set humanize(variation) {
|
2019-11-21 18:19:44 +00:00
|
|
|
this._part.humanize = variation;
|
2019-07-25 01:53:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The number of scheduled events
|
|
|
|
*/
|
|
|
|
get length(): number {
|
|
|
|
return this._part.length;
|
|
|
|
}
|
|
|
|
}
|