UNPKG

5.69 kBPlain TextView Raw
1import { Volume } from "../component/channel/Volume.js";
2import { Param } from "../core/context/Param.js";
3import {
4 OutputNode,
5 ToneAudioNode,
6 ToneAudioNodeOptions,
7} from "../core/context/ToneAudioNode.js";
8import { Decibels, Frequency, NormalRange, Time } from "../core/type/Units.js";
9import { optionsFromArguments } from "../core/util/Defaults.js";
10import { readOnly } from "../core/util/Interface.js";
11
12export interface InstrumentOptions extends ToneAudioNodeOptions {
13 volume: Decibels;
14}
15
16/**
17 * Base-class for all instruments
18 */
19export abstract class Instrument<
20 Options extends InstrumentOptions,
21> extends ToneAudioNode<Options> {
22 /**
23 * The output and volume triming node
24 */
25 private _volume: Volume;
26 output: OutputNode;
27
28 /**
29 * The instrument only has an output
30 */
31 input: undefined;
32
33 /**
34 * The volume of the output in decibels.
35 * @example
36 * const amSynth = new Tone.AMSynth().toDestination();
37 * amSynth.volume.value = -6;
38 * amSynth.triggerAttackRelease("G#3", 0.2);
39 */
40 volume: Param<"decibels">;
41
42 /**
43 * Keep track of all events scheduled to the transport
44 * when the instrument is 'synced'
45 */
46 private _scheduledEvents: number[] = [];
47
48 /**
49 * If the instrument is currently synced
50 */
51 private _synced = false;
52
53 constructor(options?: Partial<InstrumentOptions>);
54 constructor() {
55 const options = optionsFromArguments(
56 Instrument.getDefaults(),
57 arguments
58 );
59 super(options);
60
61 this._volume = this.output = new Volume({
62 context: this.context,
63 volume: options.volume,
64 });
65 this.volume = this._volume.volume;
66 readOnly(this, "volume");
67 }
68
69 static getDefaults(): InstrumentOptions {
70 return Object.assign(ToneAudioNode.getDefaults(), {
71 volume: 0,
72 });
73 }
74
75 /**
76 * Sync the instrument to the Transport. All subsequent calls of
77 * {@link triggerAttack} and {@link triggerRelease} will be scheduled along the transport.
78 * @example
79 * const fmSynth = new Tone.FMSynth().toDestination();
80 * fmSynth.volume.value = -6;
81 * fmSynth.sync();
82 * // schedule 3 notes when the transport first starts
83 * fmSynth.triggerAttackRelease("C4", "8n", 0);
84 * fmSynth.triggerAttackRelease("E4", "8n", "8n");
85 * fmSynth.triggerAttackRelease("G4", "8n", "4n");
86 * // start the transport to hear the notes
87 * Tone.Transport.start();
88 */
89 sync(): this {
90 if (this._syncState()) {
91 this._syncMethod("triggerAttack", 1);
92 this._syncMethod("triggerRelease", 0);
93
94 this.context.transport.on("stop", this._syncedRelease);
95 this.context.transport.on("pause", this._syncedRelease);
96 this.context.transport.on("loopEnd", this._syncedRelease);
97 }
98 return this;
99 }
100
101 /**
102 * set _sync
103 */
104 protected _syncState(): boolean {
105 let changed = false;
106 if (!this._synced) {
107 this._synced = true;
108 changed = true;
109 }
110 return changed;
111 }
112
113 /**
114 * Wrap the given method so that it can be synchronized
115 * @param method Which method to wrap and sync
116 * @param timePosition What position the time argument appears in
117 */
118 protected _syncMethod(method: string, timePosition: number): void {
119 const originalMethod = (this["_original_" + method] = this[method]);
120 this[method] = (...args: any[]) => {
121 const time = args[timePosition];
122 const id = this.context.transport.schedule((t) => {
123 args[timePosition] = t;
124 originalMethod.apply(this, args);
125 }, time);
126 this._scheduledEvents.push(id);
127 };
128 }
129
130 /**
131 * Unsync the instrument from the Transport
132 */
133 unsync(): this {
134 this._scheduledEvents.forEach((id) => this.context.transport.clear(id));
135 this._scheduledEvents = [];
136 if (this._synced) {
137 this._synced = false;
138 this.triggerAttack = this._original_triggerAttack;
139 this.triggerRelease = this._original_triggerRelease;
140
141 this.context.transport.off("stop", this._syncedRelease);
142 this.context.transport.off("pause", this._syncedRelease);
143 this.context.transport.off("loopEnd", this._syncedRelease);
144 }
145 return this;
146 }
147
148 /**
149 * Trigger the attack and then the release after the duration.
150 * @param note The note to trigger.
151 * @param duration How long the note should be held for before
152 * triggering the release. This value must be greater than 0.
153 * @param time When the note should be triggered.
154 * @param velocity The velocity the note should be triggered at.
155 * @example
156 * const synth = new Tone.Synth().toDestination();
157 * // trigger "C4" for the duration of an 8th note
158 * synth.triggerAttackRelease("C4", "8n");
159 */
160 triggerAttackRelease(
161 note: Frequency,
162 duration: Time,
163 time?: Time,
164 velocity?: NormalRange
165 ): this {
166 const computedTime = this.toSeconds(time);
167 const computedDuration = this.toSeconds(duration);
168 this.triggerAttack(note, computedTime, velocity);
169 this.triggerRelease(computedTime + computedDuration);
170 return this;
171 }
172
173 /**
174 * Start the instrument's note.
175 * @param note the note to trigger
176 * @param time the time to trigger the note
177 * @param velocity the velocity to trigger the note (between 0-1)
178 */
179 abstract triggerAttack(
180 note: Frequency,
181 time?: Time,
182 velocity?: NormalRange
183 ): this;
184 private _original_triggerAttack = this.triggerAttack;
185
186 /**
187 * Trigger the release phase of the current note.
188 * @param time when to trigger the release
189 */
190 abstract triggerRelease(...args: any[]): this;
191 private _original_triggerRelease = this.triggerRelease;
192
193 /**
194 * The release which is scheduled to the timeline.
195 */
196 protected _syncedRelease = (time: number) =>
197 this._original_triggerRelease(time);
198
199 /**
200 * clean up
201 * @returns {Instrument} this
202 */
203 dispose(): this {
204 super.dispose();
205 this._volume.dispose();
206 this.unsync();
207 this._scheduledEvents = [];
208 return this;
209 }
210}