Tone.js/Tone/instrument/MetalSynth.ts

269 lines
7.6 KiB
TypeScript

// tslint:disable: max-line-length
import { Envelope } from "../component/envelope/Envelope";
import { Filter } from "../component/filter/Filter";
import { Gain } from "../core/context/Gain";
import { Cents, Frequency, NormalRange, Positive, Time } from "../core/type/Units";
import { deepMerge, optionsFromArguments } from "../core/util/Defaults";
import { RecursivePartial } from "../core/util/Interface";
import { Multiply } from "../signal/Multiply";
import { Scale } from "../signal/Scale";
import { Signal } from "../signal/Signal";
import { FMOscillator } from "../source/oscillator/FMOscillator";
import { Monophonic } from "./Monophonic";
import { Synth, SynthOptions } from "./Synth";
interface MetalSynthOptions extends SynthOptions {
frequency: Frequency;
harmonicity: Positive;
modulationIndex: Positive;
octaves: number;
resonance: Frequency;
}
/**
* Inharmonic ratio of frequencies based on the Roland TR-808
* Taken from https://ccrma.stanford.edu/papers/tr-808-cymbal-physically-informed-circuit-bendable-digital-model
*/
const inharmRatios: number[] = [1.0, 1.483, 1.932, 2.546, 2.630, 3.897];
/**
* A highly inharmonic and spectrally complex source with a highpass filter
* and amplitude envelope which is good for making metalophone sounds.
* Based on CymbalSynth by [@polyrhythmatic](https://github.com/polyrhythmatic).
* Inspiration from [Sound on Sound](https://web.archive.org/web/20160610143924/https://www.soundonsound.com/sos/jul02/articles/synthsecrets0702.asp).
*/
export class MetalSynth extends Monophonic<MetalSynthOptions> {
readonly name = "MetalSynth";
/**
* The frequency of the cymbal
*/
frequency: Signal<Frequency>;
/**
* The array of FMOscillators
*/
private _oscillators: FMOscillator[] = [];
/**
* The frequency multipliers
*/
private _freqMultipliers: Multiply[] = [];
/**
* The amplitude for the body
*/
private _amplitue: Gain;
/**
* Highpass the output
*/
private _highpass: Filter;
/**
* The number of octaves the highpass
* filter frequency ramps
*/
private _octaves: number;
/**
* Scale the body envelope
* for the bandpass
*/
private _filterFreqScaler: Scale;
/**
* The envelope which is connected both to the
* amplitude and highpass filter's cutoff frequency
*/
readonly envelope: Envelope;
readonly detune: Signal<Cents>;
constructor(options?: RecursivePartial<MetalSynthOptions>)
constructor() {
super(optionsFromArguments(MetalSynth.getDefaults(), arguments));
const options = optionsFromArguments(MetalSynth.getDefaults(), arguments);
// not sure about setting this here -- but it was required because the abstract value was defined as non-optional in Monophonic
this.detune = new Signal<Cents>(0);
this.octaves = options.octaves;
this.frequency = new Signal(options.frequency);
// set the octaves
this._octaves = options.octaves;
this._amplitue = new Gain(0).connect(this.output);
this._highpass = new Filter({
Q: -3.0102999566398125,
type: "highpass",
}).connect(this._amplitue);
for (let i = 0; i < inharmRatios.length; i++) {
const osc = new FMOscillator({
harmonicity: options.harmonicity,
modulationIndex: options.modulationIndex,
modulationType: "square",
type: "square",
});
osc.connect(this._highpass);
this._oscillators[i] = osc;
const mult = new Multiply(inharmRatios[i]);
this._freqMultipliers[i] = mult;
this.frequency.chain(mult, osc.frequency);
}
this._filterFreqScaler = new Scale(this.toFrequency(options.resonance), 7000);
// tslint:disable:object-literal-sort-keys
this.envelope = new Envelope({
attack: options.envelope.attack,
attackCurve: "linear",
decay: options.envelope.decay,
sustain: 0,
release: options.envelope.release,
});
// tslint:enable:object-literal-sort-keys
this.envelope.chain(this._filterFreqScaler, this._highpass.frequency);
this.envelope.connect(this._amplitue.gain);
}
static getDefaults(): MetalSynthOptions {
return deepMerge(Synth.getDefaults(), {
envelope: {
attack: 0.001,
decay: 1.4,
release: 0.2,
},
frequency: 200,
harmonicity: 5.1,
modulationIndex: 32,
octaves: 1.5,
resonance: 4000,
});
}
/**
* Trigger the attack.
* @param time When the attack should be triggered.
* @param velocity The velocity that the envelope should be triggered at.
*/
public _triggerEnvelopeAttack(time: Time, velocity: NormalRange = 1): this {
this.envelope.triggerAttack(time, velocity);
this._oscillators.forEach(osc => osc.start(time));
if (this.envelope.sustain === 0) {
this._oscillators.forEach(osc => {
osc.stop(this.toSeconds(time) + this.toSeconds(this.envelope.attack) + this.toSeconds(this.envelope.decay));
});
}
return this;
}
/**
* Trigger the release of the envelope.
* @param time When the release should be triggered.
*/
public _triggerEnvelopeRelease(time: Time): this {
this.envelope.triggerRelease(time);
this._oscillators.forEach(osc => osc.stop(this.toSeconds(time) + this.toSeconds(this.envelope.release)));
return this;
}
/**
* Trigger the attack and release of the envelope after the given
* duration.
* @param duration The duration before triggering the release
* @param time When the attack should be triggered.
* @param velocity The velocity that the envelope should be triggered at.
*/
public triggerAttackRelease(duration: Time, time: Time, velocity: NormalRange = 1): this {
this.triggerAttack(this.toSeconds(time), velocity);
this.triggerRelease(this.toSeconds(time) + this.toSeconds(duration));
return this;
}
/**
* Sync the instrument to the Transport. All subsequent calls of
* [triggerAttack](#triggerattack) and [triggerRelease](#triggerrelease)
* will be scheduled along the transport.
* @example
* synth.sync()
* //schedule 3 notes when the transport first starts
* synth.triggerAttackRelease('8n', 0)
* synth.triggerAttackRelease('8n', '8n')
* synth.triggerAttackRelease('8n', '4n')
* //start the transport to hear the notes
* Transport.start()
*/
public sync(): this {
this._syncMethod("triggerRelease", 0);
this._syncMethod("triggerAttack", 0);
return this;
}
/**
* The modulationIndex of the oscillators which make up the source.
* see Tone.FMOscillator.modulationIndex
*/
public get modulationIndex(): number {
return this._oscillators[0].modulationIndex.value;
}
public set modulationIndex(val: number) {
this._oscillators.forEach(osc => (osc.modulationIndex.value = val));
}
/**
* The harmonicity of the oscillators which make up the source.
* see Tone.FMOscillator.harmonicity
*/
public get harmonicity(): number {
return this._oscillators[0].harmonicity.value;
}
public set harmonicity(val: number) {
this._oscillators.forEach(osc => (osc.harmonicity.value = val));
}
/**
* The frequency of the highpass filter attached to the envelope
*/
public get resonance(): number {
return this._filterFreqScaler.min;
}
public set resonance(val: number) {
this._filterFreqScaler.min = val;
this.octaves = this._octaves;
}
/**
* The number of octaves above the "resonance" frequency
* that the filter ramps during the attack/decay envelope
*/
public get octaves(): number {
return this._octaves;
}
public set octaves(val: number) {
this._octaves = val;
this._filterFreqScaler.max = this._filterFreqScaler.min * Math.pow(2, val);
}
dispose(): this {
super.dispose();
this._oscillators.forEach(osc => osc.dispose());
this._freqMultipliers.forEach(freqMult => freqMult.dispose());
this.frequency.dispose();
this._filterFreqScaler.dispose();
this._amplitue.dispose();
this.envelope.dispose();
this._highpass.dispose();
return this;
}
}