UNPKG

7.41 kBPlain TextView Raw
1import { Envelope, EnvelopeOptions } from "../component/envelope/Envelope.js";
2import { Filter } from "../component/filter/Filter.js";
3import { Gain } from "../core/context/Gain.js";
4import {
5 ToneAudioNode,
6 ToneAudioNodeOptions,
7} from "../core/context/ToneAudioNode.js";
8import {
9 Frequency,
10 NormalRange,
11 Positive,
12 Seconds,
13 Time,
14} from "../core/type/Units.js";
15import {
16 deepMerge,
17 omitFromObject,
18 optionsFromArguments,
19} from "../core/util/Defaults.js";
20import { noOp, RecursivePartial } from "../core/util/Interface.js";
21import { Multiply } from "../signal/Multiply.js";
22import { Scale } from "../signal/Scale.js";
23import { Signal } from "../signal/Signal.js";
24import { FMOscillator } from "../source/oscillator/FMOscillator.js";
25import { Monophonic, MonophonicOptions } from "./Monophonic.js";
26
27export interface MetalSynthOptions extends MonophonicOptions {
28 harmonicity: Positive;
29 modulationIndex: Positive;
30 octaves: number;
31 resonance: Frequency;
32 envelope: Omit<EnvelopeOptions, keyof ToneAudioNodeOptions>;
33}
34
35/**
36 * Inharmonic ratio of frequencies based on the Roland TR-808
37 * Taken from https://ccrma.stanford.edu/papers/tr-808-cymbal-physically-informed-circuit-bendable-digital-model
38 */
39const inharmRatios: number[] = [1.0, 1.483, 1.932, 2.546, 2.63, 3.897];
40
41/**
42 * A highly inharmonic and spectrally complex source with a highpass filter
43 * and amplitude envelope which is good for making metallophone sounds.
44 * Based on CymbalSynth by [@polyrhythmatic](https://github.com/polyrhythmatic).
45 * @category Instrument
46 */
47export class MetalSynth extends Monophonic<MetalSynthOptions> {
48 readonly name: string = "MetalSynth";
49
50 /**
51 * The frequency of the cymbal
52 */
53 readonly frequency: Signal<"frequency">;
54
55 /**
56 * The detune applied to the oscillators
57 */
58 readonly detune: Signal<"cents">;
59
60 /**
61 * The array of FMOscillators
62 */
63 private _oscillators: FMOscillator[] = [];
64
65 /**
66 * The frequency multipliers
67 */
68 private _freqMultipliers: Multiply[] = [];
69
70 /**
71 * The gain node for the envelope.
72 */
73 private _amplitude: Gain;
74
75 /**
76 * Highpass the output
77 */
78 private _highpass: Filter;
79
80 /**
81 * The number of octaves the highpass
82 * filter frequency ramps
83 */
84 private _octaves: number;
85
86 /**
87 * Scale the body envelope for the highpass filter
88 */
89 private _filterFreqScaler: Scale;
90
91 /**
92 * The envelope which is connected both to the
93 * amplitude and a highpass filter's cutoff frequency.
94 * The lower-limit of the filter is controlled by the {@link resonance}
95 */
96 readonly envelope: Envelope;
97
98 constructor(options?: RecursivePartial<MetalSynthOptions>);
99 constructor() {
100 const options = optionsFromArguments(
101 MetalSynth.getDefaults(),
102 arguments
103 );
104 super(options);
105
106 this.detune = new Signal({
107 context: this.context,
108 units: "cents",
109 value: options.detune,
110 });
111
112 this.frequency = new Signal({
113 context: this.context,
114 units: "frequency",
115 });
116
117 this._amplitude = new Gain({
118 context: this.context,
119 gain: 0,
120 }).connect(this.output);
121
122 this._highpass = new Filter({
123 // Q: -3.0102999566398125,
124 Q: 0,
125 context: this.context,
126 type: "highpass",
127 }).connect(this._amplitude);
128
129 for (let i = 0; i < inharmRatios.length; i++) {
130 const osc = new FMOscillator({
131 context: this.context,
132 harmonicity: options.harmonicity,
133 modulationIndex: options.modulationIndex,
134 modulationType: "square",
135 onstop: i === 0 ? () => this.onsilence(this) : noOp,
136 type: "square",
137 });
138 osc.connect(this._highpass);
139 this._oscillators[i] = osc;
140
141 const mult = new Multiply({
142 context: this.context,
143 value: inharmRatios[i],
144 });
145 this._freqMultipliers[i] = mult;
146 this.frequency.chain(mult, osc.frequency);
147 this.detune.connect(osc.detune);
148 }
149
150 this._filterFreqScaler = new Scale({
151 context: this.context,
152 max: 7000,
153 min: this.toFrequency(options.resonance),
154 });
155
156 this.envelope = new Envelope({
157 attack: options.envelope.attack,
158 attackCurve: "linear",
159 context: this.context,
160 decay: options.envelope.decay,
161 release: options.envelope.release,
162 sustain: 0,
163 });
164
165 this.envelope.chain(this._filterFreqScaler, this._highpass.frequency);
166 this.envelope.connect(this._amplitude.gain);
167 // set the octaves
168 this._octaves = options.octaves;
169 this.octaves = options.octaves;
170 }
171
172 static getDefaults(): MetalSynthOptions {
173 return deepMerge(Monophonic.getDefaults(), {
174 envelope: Object.assign(
175 omitFromObject(
176 Envelope.getDefaults(),
177 Object.keys(ToneAudioNode.getDefaults())
178 ),
179 {
180 attack: 0.001,
181 decay: 1.4,
182 release: 0.2,
183 }
184 ),
185 harmonicity: 5.1,
186 modulationIndex: 32,
187 octaves: 1.5,
188 resonance: 4000,
189 });
190 }
191
192 /**
193 * Trigger the attack.
194 * @param time When the attack should be triggered.
195 * @param velocity The velocity that the envelope should be triggered at.
196 */
197 protected _triggerEnvelopeAttack(
198 time: Seconds,
199 velocity: NormalRange = 1
200 ): this {
201 this.envelope.triggerAttack(time, velocity);
202 this._oscillators.forEach((osc) => osc.start(time));
203 if (this.envelope.sustain === 0) {
204 this._oscillators.forEach((osc) => {
205 osc.stop(
206 time +
207 this.toSeconds(this.envelope.attack) +
208 this.toSeconds(this.envelope.decay)
209 );
210 });
211 }
212 return this;
213 }
214
215 /**
216 * Trigger the release of the envelope.
217 * @param time When the release should be triggered.
218 */
219 protected _triggerEnvelopeRelease(time: Seconds): this {
220 this.envelope.triggerRelease(time);
221 this._oscillators.forEach((osc) =>
222 osc.stop(time + this.toSeconds(this.envelope.release))
223 );
224 return this;
225 }
226
227 getLevelAtTime(time: Time): NormalRange {
228 time = this.toSeconds(time);
229 return this.envelope.getValueAtTime(time);
230 }
231
232 /**
233 * The modulationIndex of the oscillators which make up the source.
234 * see {@link FMOscillator.modulationIndex}
235 * @min 1
236 * @max 100
237 */
238 get modulationIndex(): number {
239 return this._oscillators[0].modulationIndex.value;
240 }
241 set modulationIndex(val) {
242 this._oscillators.forEach((osc) => (osc.modulationIndex.value = val));
243 }
244
245 /**
246 * The harmonicity of the oscillators which make up the source.
247 * see Tone.FMOscillator.harmonicity
248 * @min 0.1
249 * @max 10
250 */
251 get harmonicity(): number {
252 return this._oscillators[0].harmonicity.value;
253 }
254 set harmonicity(val) {
255 this._oscillators.forEach((osc) => (osc.harmonicity.value = val));
256 }
257
258 /**
259 * The lower level of the highpass filter which is attached to the envelope.
260 * This value should be between [0, 7000]
261 * @min 0
262 * @max 7000
263 */
264 get resonance(): Frequency {
265 return this._filterFreqScaler.min;
266 }
267 set resonance(val) {
268 this._filterFreqScaler.min = this.toFrequency(val);
269 this.octaves = this._octaves;
270 }
271
272 /**
273 * The number of octaves above the "resonance" frequency
274 * that the filter ramps during the attack/decay envelope
275 * @min 0
276 * @max 8
277 */
278 get octaves(): number {
279 return this._octaves;
280 }
281 set octaves(val) {
282 this._octaves = val;
283 this._filterFreqScaler.max =
284 this._filterFreqScaler.min * Math.pow(2, val);
285 }
286
287 dispose(): this {
288 super.dispose();
289 this._oscillators.forEach((osc) => osc.dispose());
290 this._freqMultipliers.forEach((freqMult) => freqMult.dispose());
291 this.frequency.dispose();
292 this.detune.dispose();
293 this._filterFreqScaler.dispose();
294 this._amplitude.dispose();
295 this.envelope.dispose();
296 this._highpass.dispose();
297 return this;
298 }
299}