1 | import { Monophonic, MonophonicOptions } from "./Monophonic.js";
|
2 | import { MonoSynth, MonoSynthOptions } from "./MonoSynth.js";
|
3 | import { Signal } from "../signal/Signal.js";
|
4 | import { readOnly, RecursivePartial } from "../core/util/Interface.js";
|
5 | import { LFO } from "../source/oscillator/LFO.js";
|
6 | import { Gain } from "../core/context/Gain.js";
|
7 | import { Multiply } from "../signal/Multiply.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 { Param } from "../core/context/Param.js";
|
21 |
|
22 | export interface DuoSynthOptions extends MonophonicOptions {
|
23 | voice0: Omit<MonoSynthOptions, keyof MonophonicOptions>;
|
24 | voice1: Omit<MonoSynthOptions, keyof MonophonicOptions>;
|
25 | harmonicity: Positive;
|
26 | vibratoRate: Frequency;
|
27 | vibratoAmount: Positive;
|
28 | }
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | export class DuoSynth extends Monophonic<DuoSynthOptions> {
|
39 | readonly name: string = "DuoSynth";
|
40 |
|
41 | readonly frequency: Signal<"frequency">;
|
42 | readonly detune: Signal<"cents">;
|
43 |
|
44 | |
45 |
|
46 |
|
47 | readonly voice0: MonoSynth;
|
48 |
|
49 | |
50 |
|
51 |
|
52 | readonly voice1: MonoSynth;
|
53 |
|
54 | |
55 |
|
56 |
|
57 | public vibratoAmount: Param<"normalRange">;
|
58 |
|
59 | |
60 |
|
61 |
|
62 | public vibratoRate: Signal<"frequency">;
|
63 |
|
64 | |
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | public harmonicity: Signal<"positive">;
|
74 |
|
75 | |
76 |
|
77 |
|
78 | private _vibrato: LFO;
|
79 |
|
80 | |
81 |
|
82 |
|
83 | private _vibratoGain: Gain<"normalRange">;
|
84 |
|
85 | constructor(options?: RecursivePartial<DuoSynthOptions>);
|
86 | constructor() {
|
87 | const options = optionsFromArguments(DuoSynth.getDefaults(), arguments);
|
88 | super(options);
|
89 |
|
90 | this.voice0 = new MonoSynth(
|
91 | Object.assign(options.voice0, {
|
92 | context: this.context,
|
93 | onsilence: () => this.onsilence(this),
|
94 | })
|
95 | );
|
96 | this.voice1 = new MonoSynth(
|
97 | Object.assign(options.voice1, {
|
98 | context: this.context,
|
99 | })
|
100 | );
|
101 |
|
102 | this.harmonicity = new Multiply({
|
103 | context: this.context,
|
104 | units: "positive",
|
105 | value: options.harmonicity,
|
106 | });
|
107 |
|
108 | this._vibrato = new LFO({
|
109 | frequency: options.vibratoRate,
|
110 | context: this.context,
|
111 | min: -50,
|
112 | max: 50,
|
113 | });
|
114 | // start the vibrato immediately
|
115 | this._vibrato.start();
|
116 | this.vibratoRate = this._vibrato.frequency;
|
117 | this._vibratoGain = new Gain({
|
118 | context: this.context,
|
119 | units: "normalRange",
|
120 | gain: options.vibratoAmount,
|
121 | });
|
122 | this.vibratoAmount = this._vibratoGain.gain;
|
123 |
|
124 | this.frequency = new Signal({
|
125 | context: this.context,
|
126 | units: "frequency",
|
127 | value: 440,
|
128 | });
|
129 | this.detune = new Signal({
|
130 | context: this.context,
|
131 | units: "cents",
|
132 | value: options.detune,
|
133 | });
|
134 |
|
135 | // control the two voices frequency
|
136 | this.frequency.connect(this.voice0.frequency);
|
137 | this.frequency.chain(this.harmonicity, this.voice1.frequency);
|
138 |
|
139 | this._vibrato.connect(this._vibratoGain);
|
140 | this._vibratoGain.fan(this.voice0.detune, this.voice1.detune);
|
141 |
|
142 | this.detune.fan(this.voice0.detune, this.voice1.detune);
|
143 |
|
144 | this.voice0.connect(this.output);
|
145 | this.voice1.connect(this.output);
|
146 |
|
147 | readOnly(this, [
|
148 | "voice0",
|
149 | "voice1",
|
150 | "frequency",
|
151 | "vibratoAmount",
|
152 | "vibratoRate",
|
153 | ]);
|
154 | }
|
155 |
|
156 | getLevelAtTime(time: Time): NormalRange {
|
157 | time = this.toSeconds(time);
|
158 | return (
|
159 | this.voice0.envelope.getValueAtTime(time) +
|
160 | this.voice1.envelope.getValueAtTime(time)
|
161 | );
|
162 | }
|
163 |
|
164 | static getDefaults(): DuoSynthOptions {
|
165 | return deepMerge(Monophonic.getDefaults(), {
|
166 | vibratoAmount: 0.5,
|
167 | vibratoRate: 5,
|
168 | harmonicity: 1.5,
|
169 | voice0: deepMerge(
|
170 | omitFromObject(
|
171 | MonoSynth.getDefaults(),
|
172 | Object.keys(Monophonic.getDefaults())
|
173 | ),
|
174 | {
|
175 | filterEnvelope: {
|
176 | attack: 0.01,
|
177 | decay: 0.0,
|
178 | sustain: 1,
|
179 | release: 0.5,
|
180 | },
|
181 | envelope: {
|
182 | attack: 0.01,
|
183 | decay: 0.0,
|
184 | sustain: 1,
|
185 | release: 0.5,
|
186 | },
|
187 | }
|
188 | ),
|
189 | voice1: deepMerge(
|
190 | omitFromObject(
|
191 | MonoSynth.getDefaults(),
|
192 | Object.keys(Monophonic.getDefaults())
|
193 | ),
|
194 | {
|
195 | filterEnvelope: {
|
196 | attack: 0.01,
|
197 | decay: 0.0,
|
198 | sustain: 1,
|
199 | release: 0.5,
|
200 | },
|
201 | envelope: {
|
202 | attack: 0.01,
|
203 | decay: 0.0,
|
204 | sustain: 1,
|
205 | release: 0.5,
|
206 | },
|
207 | }
|
208 | ),
|
209 | }) as unknown as DuoSynthOptions;
|
210 | }
|
211 | |
212 |
|
213 |
|
214 | protected _triggerEnvelopeAttack(time: Seconds, velocity: number): void {
|
215 |
|
216 | this.voice0._triggerEnvelopeAttack(time, velocity);
|
217 |
|
218 | this.voice1._triggerEnvelopeAttack(time, velocity);
|
219 | }
|
220 |
|
221 | |
222 |
|
223 |
|
224 | protected _triggerEnvelopeRelease(time: Seconds) {
|
225 |
|
226 | this.voice0._triggerEnvelopeRelease(time);
|
227 |
|
228 | this.voice1._triggerEnvelopeRelease(time);
|
229 | return this;
|
230 | }
|
231 |
|
232 | dispose(): this {
|
233 | super.dispose();
|
234 | this.voice0.dispose();
|
235 | this.voice1.dispose();
|
236 | this.frequency.dispose();
|
237 | this.detune.dispose();
|
238 | this._vibrato.dispose();
|
239 | this.vibratoRate.dispose();
|
240 | this._vibratoGain.dispose();
|
241 | this.harmonicity.dispose();
|
242 | return this;
|
243 | }
|
244 | }
|