Tone.js/Tone/instrument/Instrument.ts

173 lines
5 KiB
TypeScript
Raw Normal View History

2019-07-18 18:07:25 +00:00
import { Volume } from "../component/channel/Volume";
import { Param } from "../core/context/Param";
import { OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../core/context/ToneAudioNode";
import { Decibels, Frequency, NormalRange, Time } from "../core/type/Units";
2019-07-18 18:07:25 +00:00
import { optionsFromArguments } from "../core/util/Defaults";
import { readOnly } from "../core/util/Interface";
export interface InstrumentOptions extends ToneAudioNodeOptions {
volume: Decibels;
}
/**
2019-09-14 20:39:18 +00:00
* Base-class for all instruments
2019-07-18 18:07:25 +00:00
*/
export abstract class Instrument<Options extends InstrumentOptions> extends ToneAudioNode<Options> {
/**
2019-09-14 20:39:18 +00:00
* The output and volume triming node
2019-07-18 18:07:25 +00:00
*/
private _volume: Volume;
output: OutputNode;
2019-07-22 20:16:13 +00:00
/**
* The instrument only has an output
*/
input: undefined;
2019-07-18 18:07:25 +00:00
/**
* The volume of the output in decibels.
* @example
2019-10-24 22:01:27 +00:00
* import { AMSynth } from "tone";
* const amSynth = new AMSynth().toDestination();
* amSynth.volume.value = -6;
* amSynth.triggerAttackRelease("G#3", 0.2);
2019-07-18 18:07:25 +00:00
*/
volume: Param<"decibels">;
2019-07-18 18:07:25 +00:00
/**
* Keep track of all events scheduled to the transport
* when the instrument is 'synced'
*/
private _scheduledEvents: number[] = [];
/**
* If the instrument is currently synced
*/
2019-11-17 18:09:19 +00:00
private _synced = false;
2019-07-18 18:07:25 +00:00
constructor(options?: Partial<InstrumentOptions>);
constructor() {
super(optionsFromArguments(Instrument.getDefaults(), arguments));
const options = optionsFromArguments(Instrument.getDefaults(), arguments);
this._volume = this.output = new Volume({
2019-07-18 18:07:25 +00:00
context: this.context,
volume: options.volume,
});
this.volume = this._volume.volume;
readOnly(this, "volume");
}
static getDefaults(): InstrumentOptions {
return Object.assign(ToneAudioNode.getDefaults(), {
volume: 0,
});
}
/**
* Sync the instrument to the Transport. All subsequent calls of
2019-10-24 22:01:27 +00:00
* [[triggerAttack]] and [[triggerRelease]] will be scheduled along the transport.
2019-07-18 18:07:25 +00:00
* @example
2019-10-24 22:01:27 +00:00
* import { FMSynth, Transport } from "tone";
* const fmSynth = new FMSynth().toDestination();
* fmSynth.volume.value = -6;
* fmSynth.sync();
* // schedule 3 notes when the transport first starts
* fmSynth.triggerAttackRelease("C4", "8n", 0);
* fmSynth.triggerAttackRelease("E4", "8n", "8n");
* fmSynth.triggerAttackRelease("G4", "8n", "4n");
* // start the transport to hear the notes
* Transport.start();
2019-07-18 18:07:25 +00:00
*/
sync(): this {
if (!this._synced) {
this._synced = true;
this._syncMethod("triggerAttack", 1);
this._syncMethod("triggerRelease", 0);
}
return this;
}
/**
* Wrap the given method so that it can be synchronized
* @param method Which method to wrap and sync
* @param timePosition What position the time argument appears in
*/
2019-07-22 20:16:13 +00:00
protected _syncMethod(method: string, timePosition: number): void {
2019-07-18 18:07:25 +00:00
const originalMethod = this["_original_" + method] = this[method];
this[method] = (...args: any[]) => {
const time = args[timePosition];
const id = this.context.transport.schedule((t) => {
args[timePosition] = t;
originalMethod.apply(this, args);
}, time);
this._scheduledEvents.push(id);
};
}
/**
* Unsync the instrument from the Transport
*/
unsync(): this {
this._scheduledEvents.forEach(id => this.context.transport.clear(id));
this._scheduledEvents = [];
if (this._synced) {
this._synced = false;
this.triggerAttack = this._original_triggerAttack;
this.triggerRelease = this._original_triggerRelease;
}
return this;
}
/**
2019-09-14 20:39:18 +00:00
* Trigger the attack and then the release after the duration.
* @param note The note to trigger.
* @param duration How long the note should be held for before
2019-09-14 20:39:18 +00:00
* triggering the release. This value must be greater than 0.
* @param time When the note should be triggered.
* @param velocity The velocity the note should be triggered at.
* @example
2019-10-24 22:01:27 +00:00
* import { Synth } from "tone";
* const synth = new Synth().toDestination();
* // trigger "C4" for the duration of an 8th note
2019-07-18 18:07:25 +00:00
* synth.triggerAttackRelease("C4", "8n");
*/
triggerAttackRelease(note: Frequency, duration: Time, time?: Time, velocity?: NormalRange): this {
const computedTime = this.toSeconds(time);
const computedDuration = this.toSeconds(duration);
this.triggerAttack(note, computedTime, velocity);
this.triggerRelease(computedTime + computedDuration);
return this;
}
/**
* Start the instrument's note.
* @param note the note to trigger
* @param time the time to trigger the ntoe
* @param velocity the velocity to trigger the note (betwee 0-1)
*/
abstract triggerAttack(note: Frequency, time?: Time, velocity?: NormalRange): this;
private _original_triggerAttack = this.triggerAttack;
/**
* Trigger the release phase of the current note.
* @param time when to trigger the release
2019-07-18 18:07:25 +00:00
*/
2019-07-22 20:16:13 +00:00
abstract triggerRelease(...args: any[]): this;
2019-07-18 18:07:25 +00:00
private _original_triggerRelease = this.triggerRelease;
/**
2019-09-14 20:39:18 +00:00
* clean up
* @returns {Instrument} this
2019-07-18 18:07:25 +00:00
*/
dispose(): this {
super.dispose();
2019-07-18 18:07:25 +00:00
this._volume.dispose();
this.unsync();
this._scheduledEvents = [];
return this;
}
}