1 | import { StereoEffect, StereoEffectOptions } from "./StereoEffect.js";
|
2 | import { Frequency, NormalRange } from "../core/type/Units.js";
|
3 | import { optionsFromArguments } from "../core/util/Defaults.js";
|
4 | import { readOnly } from "../core/util/Interface.js";
|
5 | import { Signal } from "../signal/Signal.js";
|
6 | import { LowpassCombFilter } from "../component/filter/LowpassCombFilter.js";
|
7 |
|
8 | export interface FreeverbOptions extends StereoEffectOptions {
|
9 | dampening: Frequency;
|
10 | roomSize: NormalRange;
|
11 | }
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const combFilterTunings = [
|
17 | 1557 / 44100,
|
18 | 1617 / 44100,
|
19 | 1491 / 44100,
|
20 | 1422 / 44100,
|
21 | 1277 / 44100,
|
22 | 1356 / 44100,
|
23 | 1188 / 44100,
|
24 | 1116 / 44100,
|
25 | ];
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | const allpassFilterFrequencies = [225, 556, 441, 341];
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | export class Freeverb extends StereoEffect<FreeverbOptions> {
|
45 | readonly name: string = "Freeverb";
|
46 |
|
47 | |
48 |
|
49 |
|
50 | readonly roomSize: Signal<"normalRange">;
|
51 |
|
52 | |
53 |
|
54 |
|
55 | private _combFilters: LowpassCombFilter[] = [];
|
56 |
|
57 | |
58 |
|
59 |
|
60 | private _allpassFiltersL: BiquadFilterNode[] = [];
|
61 |
|
62 | |
63 |
|
64 |
|
65 | private _allpassFiltersR: BiquadFilterNode[] = [];
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 | constructor(roomSize?: NormalRange, dampening?: Frequency);
|
72 | constructor(options?: Partial<FreeverbOptions>);
|
73 | constructor() {
|
74 | const options = optionsFromArguments(
|
75 | Freeverb.getDefaults(),
|
76 | arguments,
|
77 | ["roomSize", "dampening"]
|
78 | );
|
79 | super(options);
|
80 |
|
81 | this.roomSize = new Signal({
|
82 | context: this.context,
|
83 | value: options.roomSize,
|
84 | units: "normalRange",
|
85 | });
|
86 |
|
87 | // make the allpass filters on the right
|
88 | this._allpassFiltersL = allpassFilterFrequencies.map((freq) => {
|
89 | const allpassL = this.context.createBiquadFilter();
|
90 | allpassL.type = "allpass";
|
91 | allpassL.frequency.value = freq;
|
92 | return allpassL;
|
93 | });
|
94 |
|
95 | // make the allpass filters on the left
|
96 | this._allpassFiltersR = allpassFilterFrequencies.map((freq) => {
|
97 | const allpassR = this.context.createBiquadFilter();
|
98 | allpassR.type = "allpass";
|
99 | allpassR.frequency.value = freq;
|
100 | return allpassR;
|
101 | });
|
102 |
|
103 | // make the comb filters
|
104 | this._combFilters = combFilterTunings.map((delayTime, index) => {
|
105 | const lfpf = new LowpassCombFilter({
|
106 | context: this.context,
|
107 | dampening: options.dampening,
|
108 | delayTime,
|
109 | });
|
110 | if (index < combFilterTunings.length / 2) {
|
111 | this.connectEffectLeft(lfpf, ...this._allpassFiltersL);
|
112 | } else {
|
113 | this.connectEffectRight(lfpf, ...this._allpassFiltersR);
|
114 | }
|
115 | this.roomSize.connect(lfpf.resonance);
|
116 | return lfpf;
|
117 | });
|
118 |
|
119 | readOnly(this, ["roomSize"]);
|
120 | }
|
121 |
|
122 | static getDefaults(): FreeverbOptions {
|
123 | return Object.assign(StereoEffect.getDefaults(), {
|
124 | roomSize: 0.7,
|
125 | dampening: 3000,
|
126 | });
|
127 | }
|
128 |
|
129 | /**
|
130 | * The amount of dampening of the reverberant signal.
|
131 | */
|
132 |
|
133 | get dampening(): Frequency {
|
134 | return this._combFilters[0].dampening;
|
135 | }
|
136 | set dampening(d) {
|
137 | this._combFilters.forEach((c) => (c.dampening = d));
|
138 | }
|
139 |
|
140 | dispose(): this {
|
141 | super.dispose();
|
142 | this._allpassFiltersL.forEach((al) => al.disconnect());
|
143 | this._allpassFiltersR.forEach((ar) => ar.disconnect());
|
144 | this._combFilters.forEach((cf) => cf.dispose());
|
145 | this.roomSize.dispose();
|
146 | return this;
|
147 | }
|
148 | }
|