1 | import { Envelope, EnvelopeOptions } from "../component/envelope/Envelope.js";
|
2 | import { Filter } from "../component/filter/Filter.js";
|
3 | import { Gain } from "../core/context/Gain.js";
|
4 | import {
|
5 | ToneAudioNode,
|
6 | ToneAudioNodeOptions,
|
7 | } from "../core/context/ToneAudioNode.js";
|
8 | import {
|
9 | Frequency,
|
10 | NormalRange,
|
11 | Positive,
|
12 | Seconds,
|
13 | Time,
|
14 | } from "../core/type/Units.js";
|
15 | import {
|
16 | deepMerge,
|
17 | omitFromObject,
|
18 | optionsFromArguments,
|
19 | } from "../core/util/Defaults.js";
|
20 | import { noOp, RecursivePartial } from "../core/util/Interface.js";
|
21 | import { Multiply } from "../signal/Multiply.js";
|
22 | import { Scale } from "../signal/Scale.js";
|
23 | import { Signal } from "../signal/Signal.js";
|
24 | import { FMOscillator } from "../source/oscillator/FMOscillator.js";
|
25 | import { Monophonic, MonophonicOptions } from "./Monophonic.js";
|
26 |
|
27 | export 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 |
|
37 |
|
38 |
|
39 | const inharmRatios: number[] = [1.0, 1.483, 1.932, 2.546, 2.63, 3.897];
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | export class MetalSynth extends Monophonic<MetalSynthOptions> {
|
48 | readonly name: string = "MetalSynth";
|
49 |
|
50 | |
51 |
|
52 |
|
53 | readonly frequency: Signal<"frequency">;
|
54 |
|
55 | |
56 |
|
57 |
|
58 | readonly detune: Signal<"cents">;
|
59 |
|
60 | |
61 |
|
62 |
|
63 | private _oscillators: FMOscillator[] = [];
|
64 |
|
65 | |
66 |
|
67 |
|
68 | private _freqMultipliers: Multiply[] = [];
|
69 |
|
70 | |
71 |
|
72 |
|
73 | private _amplitude: Gain;
|
74 |
|
75 | |
76 |
|
77 |
|
78 | private _highpass: Filter;
|
79 |
|
80 | |
81 |
|
82 |
|
83 |
|
84 | private _octaves: number;
|
85 |
|
86 | |
87 |
|
88 |
|
89 | private _filterFreqScaler: Scale;
|
90 |
|
91 | |
92 |
|
93 |
|
94 |
|
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 |
|
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 |
|
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 |
|
194 |
|
195 |
|
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 |
|
217 |
|
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 |
|
234 |
|
235 |
|
236 |
|
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 |
|
247 |
|
248 |
|
249 |
|
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 |
|
260 |
|
261 |
|
262 |
|
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 |
|
274 |
|
275 |
|
276 |
|
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 | }
|