@grame/faustwasm 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/assets/standalone/faust-ui/index.css +414 -0
- package/assets/standalone/faust-ui/index.css.map +1 -0
- package/assets/standalone/faust-ui/index.d.ts +1 -0
- package/assets/standalone/faust-ui/index.js +3478 -0
- package/assets/standalone/faust-ui/index.js.map +1 -0
- package/assets/standalone/faustwasm/index.d.ts +1252 -0
- package/assets/standalone/faustwasm/index.js +3772 -0
- package/assets/standalone/faustwasm/index.js.map +7 -0
- package/assets/standalone/index.html +73 -0
- package/assets/standalone/index.js +198 -0
- package/assets/standalone/types.d.ts +9 -0
- package/dist/cjs/index.d.ts +1252 -0
- package/dist/cjs/index.js +3752 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs-bundle/index.d.ts +1259 -0
- package/dist/cjs-bundle/index.js +11507 -0
- package/dist/cjs-bundle/index.js.map +7 -0
- package/dist/esm/index.d.ts +1252 -0
- package/dist/esm/index.js +3772 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/esm-bundle/index.d.ts +1259 -0
- package/dist/esm-bundle/index.js +11495 -0
- package/dist/esm-bundle/index.js.map +7 -0
- package/fileutils.js +40 -0
- package/libfaust-wasm/libfaust-wasm.d.cts +3 -0
- package/libfaust-wasm/libfaust-wasm.d.ts +3 -0
- package/libfaust-wasm/libfaust-wasm.data +0 -0
- package/libfaust-wasm/libfaust-wasm.data.d.ts +2 -0
- package/libfaust-wasm/libfaust-wasm.js +21 -0
- package/libfaust-wasm/libfaust-wasm.wasm +0 -0
- package/libfaust-wasm/libfaust-wasm.wasm.d.ts +2 -0
- package/library.md +91 -0
- package/package.json +43 -0
- package/postbuild-bundled.js +11 -0
- package/postbuild.js +63 -0
- package/prebuild-bundled.js +11 -0
- package/rsrc/overview.png +0 -0
- package/rsrc/overview.xlsx +0 -0
- package/scripts/faust2sndfile.js +43 -0
- package/scripts/faust2svg.js +17 -0
- package/scripts/faust2wasm.js +29 -0
- package/src/FaustAudioWorkletNode.ts +264 -0
- package/src/FaustAudioWorkletProcessor.ts +273 -0
- package/src/FaustCompiler.ts +252 -0
- package/src/FaustDspGenerator.ts +498 -0
- package/src/FaustDspInstance.ts +141 -0
- package/src/FaustFFTAudioWorkletProcessor.ts +554 -0
- package/src/FaustOfflineProcessor.ts +141 -0
- package/src/FaustScriptProcessorNode.ts +80 -0
- package/src/FaustSvgDiagrams.ts +37 -0
- package/src/FaustWasmInstantiator.ts +202 -0
- package/src/FaustWebAudioDsp.ts +1060 -0
- package/src/LibFaust.ts +57 -0
- package/src/WavDecoder.ts +207 -0
- package/src/WavEncoder.ts +204 -0
- package/src/copyWebStandaloneAssets.d.ts +3 -0
- package/src/copyWebStandaloneAssets.js +20 -0
- package/src/exports-bundle.ts +5 -0
- package/src/exports.ts +24 -0
- package/src/faust2svgFiles.d.ts +3 -0
- package/src/faust2svgFiles.js +43 -0
- package/src/faust2wasmFiles.d.ts +5 -0
- package/src/faust2wasmFiles.js +93 -0
- package/src/faust2wavFiles.d.ts +3 -0
- package/src/faust2wavFiles.js +58 -0
- package/src/index-bundle-iife.ts +7 -0
- package/src/index-bundle.ts +5 -0
- package/src/index.ts +5 -0
- package/src/instantiateFaustModule.ts +32 -0
- package/src/instantiateFaustModuleFromFile.ts +57 -0
- package/src/types.ts +234 -0
- package/test/clarinet.dsp +1 -0
- package/test/faustlive-wasm/faust-ui/index.css +414 -0
- package/test/faustlive-wasm/faust-ui/index.css.map +1 -0
- package/test/faustlive-wasm/faust-ui/index.d.ts +1 -0
- package/test/faustlive-wasm/faust-ui/index.js +3478 -0
- package/test/faustlive-wasm/faust-ui/index.js.map +1 -0
- package/test/faustlive-wasm/faustwasm/index.d.ts +1252 -0
- package/test/faustlive-wasm/faustwasm/index.js +3772 -0
- package/test/faustlive-wasm/faustwasm/index.js.map +7 -0
- package/test/faustlive-wasm/index.css +97 -0
- package/test/faustlive-wasm/index.html +130 -0
- package/test/faustlive-wasm/index.js +638 -0
- package/test/guitar.dsp +1 -0
- package/test/guitar1.dsp +2 -0
- package/test/libfaust-in-worklet/index.html +13 -0
- package/test/libfaust-in-worklet/index.js +40 -0
- package/test/mono.dsp +8 -0
- package/test/node/test.js +17 -0
- package/test/noise.dsp +3 -0
- package/test/organ.dsp +5 -0
- package/test/organ1.dsp +6 -0
- package/test/osc.dsp +4 -0
- package/test/osc2.dsp +5 -0
- package/test/osc3.dsp +4 -0
- package/test/p-dj.dsp +4 -0
- package/test/poly.dsp +13 -0
- package/test/rev.dsp +2 -0
- package/test/t1.dsp +1 -0
- package/test/web/fft.html +87 -0
- package/test/web/fft1.html +88 -0
- package/test/web/fft2.html +114 -0
- package/test/web/fftw.js +1907 -0
- package/test/web/index.html +12 -0
- package/test/web/index.js +235 -0
- package/test/web/kissfft.js +904 -0
- package/test/web/mono.html +46 -0
- package/test/web/poly-key.html +62 -0
- package/test/web/poly.html +59 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import type { FaustMonoDspInstance } from "./FaustDspInstance";
|
|
2
|
+
import type FaustWasmInstantiator from "./FaustWasmInstantiator";
|
|
3
|
+
import type { FaustBaseWebAudioDsp, FaustMonoWebAudioDsp, PlotHandler } from "./FaustWebAudioDsp";
|
|
4
|
+
import type { AudioParamDescriptor, AudioWorkletGlobalScope, LooseFaustDspFactory, FaustDspMeta, FaustUIItem, InterfaceFFT, TWindowFunction, Writeable, TypedArray, FFTUtils } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface FaustFFTOptionsData {
|
|
7
|
+
fftSize: number;
|
|
8
|
+
fftOverlap: number;
|
|
9
|
+
noIFFT: boolean;
|
|
10
|
+
/** Index number of the default window function, leave undefined or -1 for rectangular (no windowing) */
|
|
11
|
+
defaultWindowFunction: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Injected in the string to be compiled on AudioWorkletProcessor side
|
|
16
|
+
*/
|
|
17
|
+
export interface FaustFFTData {
|
|
18
|
+
processorName: string;
|
|
19
|
+
dspName: string;
|
|
20
|
+
dspMeta: FaustDspMeta;
|
|
21
|
+
fftOptions?: Partial<FaustFFTOptionsData>;
|
|
22
|
+
};
|
|
23
|
+
export interface FaustFFTAudioWorkletProcessorDependencies {
|
|
24
|
+
FaustBaseWebAudioDsp: typeof FaustBaseWebAudioDsp;
|
|
25
|
+
FaustMonoWebAudioDsp: typeof FaustMonoWebAudioDsp;
|
|
26
|
+
FaustWasmInstantiator: typeof FaustWasmInstantiator;
|
|
27
|
+
FFTUtils: typeof FFTUtils;
|
|
28
|
+
}
|
|
29
|
+
export interface FaustFFTAudioWorkletNodeOptions extends AudioWorkletNodeOptions {
|
|
30
|
+
processorOptions: FaustFFTAudioWorkletProcessorOptions;
|
|
31
|
+
}
|
|
32
|
+
export interface FaustFFTAudioWorkletProcessorOptions {
|
|
33
|
+
name: string;
|
|
34
|
+
sampleSize: number;
|
|
35
|
+
factory: LooseFaustDspFactory;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// Dynamic AudioWorkletProcessor code generator
|
|
40
|
+
const getFaustFFTAudioWorkletProcessor = (dependencies: FaustFFTAudioWorkletProcessorDependencies, faustData: FaustFFTData, register = true): typeof AudioWorkletProcessor => {
|
|
41
|
+
const { registerProcessor, AudioWorkletProcessor, sampleRate } = globalThis as unknown as AudioWorkletGlobalScope;
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
FaustBaseWebAudioDsp,
|
|
45
|
+
FaustWasmInstantiator,
|
|
46
|
+
FaustMonoWebAudioDsp,
|
|
47
|
+
FFTUtils
|
|
48
|
+
} = dependencies;
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
processorName,
|
|
52
|
+
dspName,
|
|
53
|
+
dspMeta,
|
|
54
|
+
fftOptions
|
|
55
|
+
} = faustData;
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
windowFunctions,
|
|
59
|
+
getFFT,
|
|
60
|
+
fftToSignal,
|
|
61
|
+
signalToFFT,
|
|
62
|
+
signalToNoFFT
|
|
63
|
+
} = FFTUtils;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ceil a number to multiple of another
|
|
67
|
+
*/
|
|
68
|
+
const ceil = (x: number, to: number): number => (Math.abs(to) < 1 ? Math.ceil(x * (1 / to)) / (1 / to) : Math.ceil(x / to) * to);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mod support wrapping with negative numbers
|
|
72
|
+
*/
|
|
73
|
+
const mod = (x: number, y: number): number => (x % y + y) % y;
|
|
74
|
+
|
|
75
|
+
const apply = (array: Writeable<ArrayLike<number>>, windowFunction: TWindowFunction) => {
|
|
76
|
+
for (let i = 0; i < array.length; i++) {
|
|
77
|
+
array[i] *= windowFunction(i, array.length);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const fftParamKeywords = ["/fftSize", "/fftHopSize", "/fftOverlap", "/windowFunction", "/noIFFT"];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Copy buffer to another, support negative offset index
|
|
85
|
+
*/
|
|
86
|
+
const setTypedArray = <T extends TypedArray = TypedArray>(to: T, from: T, offsetTo = 0, offsetFrom = 0) => {
|
|
87
|
+
const toLength = to.length;
|
|
88
|
+
const fromLength = from.length;
|
|
89
|
+
const spillLength = Math.min(toLength, fromLength);
|
|
90
|
+
let spilled = 0;
|
|
91
|
+
let $to = mod(offsetTo, toLength) || 0;
|
|
92
|
+
let $from = mod(offsetFrom, fromLength) || 0;
|
|
93
|
+
while (spilled < spillLength) {
|
|
94
|
+
const $spillLength = Math.min(spillLength - spilled, toLength - $to, fromLength - $from);
|
|
95
|
+
const $fromEnd = $from + $spillLength;
|
|
96
|
+
if ($from === 0 && $fromEnd === fromLength) to.set(from, $to);
|
|
97
|
+
else to.set(from.subarray($from, $fromEnd), $to);
|
|
98
|
+
$to = ($to + $spillLength) % toLength;
|
|
99
|
+
$from = $fromEnd % fromLength;
|
|
100
|
+
spilled += $spillLength;
|
|
101
|
+
}
|
|
102
|
+
return $to;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Analyse JSON to generate AudioParam parameters
|
|
106
|
+
const analyseParameters = (item: FaustUIItem): AudioParamDescriptor | null => {
|
|
107
|
+
const isFFTReserved = "address" in item && !!fftParamKeywords.find(k => item.address.endsWith(k));
|
|
108
|
+
if (isFFTReserved) return null;
|
|
109
|
+
if (item.type === "vslider" || item.type === "hslider" || item.type === "nentry") {
|
|
110
|
+
return { name: item.address, defaultValue: item.init || 0, minValue: item.min || 0, maxValue: item.max || 0 };
|
|
111
|
+
} else if (item.type === "button" || item.type === "checkbox") {
|
|
112
|
+
return { name: item.address, defaultValue: item.init || 0, minValue: 0, maxValue: 1 };
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Class for Faust FFT AudioWorkletProcessor
|
|
118
|
+
*/
|
|
119
|
+
class FaustFFTAudioWorkletProcessor extends AudioWorkletProcessor {
|
|
120
|
+
|
|
121
|
+
protected fDSPCode: FaustMonoWebAudioDsp;
|
|
122
|
+
|
|
123
|
+
protected paramValuesCache: Record<string, number> = {};
|
|
124
|
+
|
|
125
|
+
private dspInstance!: FaustMonoDspInstance;
|
|
126
|
+
private sampleSize!: number;
|
|
127
|
+
|
|
128
|
+
private destroyed = false;
|
|
129
|
+
/** Pointer of next start sample to write of the FFT input window */
|
|
130
|
+
private $inputWrite = 0;
|
|
131
|
+
/** Pointer of next start sample to read of the FFT input window */
|
|
132
|
+
private $inputRead = 0;
|
|
133
|
+
/** Pointer of next start sample to write of the FFT output window */
|
|
134
|
+
private $outputWrite = 0;
|
|
135
|
+
/** Pointer of next start sample to read of the FFT output window */
|
|
136
|
+
private $outputRead = 0;
|
|
137
|
+
/** Not perform in IFFT when reconstruct the audio signal */
|
|
138
|
+
private noIFFT = false;
|
|
139
|
+
/** audio data from input, array of channels */
|
|
140
|
+
private readonly fftInput: Float32Array[] = [];
|
|
141
|
+
/** audio data for output, array of channels */
|
|
142
|
+
private readonly fftOutput: Float32Array[] = [];
|
|
143
|
+
/** Generated from the current window function */
|
|
144
|
+
private window: Float32Array;
|
|
145
|
+
/** Generated from the current window's rolling sum square */
|
|
146
|
+
private windowSumSquare: Float32Array;
|
|
147
|
+
|
|
148
|
+
/** FFT constructor */
|
|
149
|
+
private FFT: typeof InterfaceFFT;
|
|
150
|
+
/** Real FFT interface */
|
|
151
|
+
private rfft: InterfaceFFT;
|
|
152
|
+
/** Faust param name of fftHopSize */
|
|
153
|
+
private fftHopSizeParam: string | undefined;
|
|
154
|
+
/** FFT Overlaps, 1 means no overlap */
|
|
155
|
+
private fftOverlap = 0;
|
|
156
|
+
private fftHopSize = 0;
|
|
157
|
+
private fftSize = 0;
|
|
158
|
+
private fftBufferSize = 0;
|
|
159
|
+
private fftProcessorZeros: Float32Array;
|
|
160
|
+
private noIFFTBuffer: Float32Array;
|
|
161
|
+
|
|
162
|
+
private fPlotHandler: PlotHandler | null = null;
|
|
163
|
+
private fCachedEvents: { type: string; data: any }[] = [];
|
|
164
|
+
private fBufferNum = 0;
|
|
165
|
+
get fftProcessorBufferSize() {
|
|
166
|
+
return this.fftSize / 2 + 1;
|
|
167
|
+
}
|
|
168
|
+
private windowFunction: TWindowFunction | null = null;
|
|
169
|
+
|
|
170
|
+
constructor(options: FaustFFTAudioWorkletNodeOptions) {
|
|
171
|
+
super(options);
|
|
172
|
+
|
|
173
|
+
// Setup port message handling
|
|
174
|
+
this.port.onmessage = (e: MessageEvent) => this.handleMessageAux(e);
|
|
175
|
+
|
|
176
|
+
const { parameterDescriptors } = (this.constructor as typeof AudioWorkletProcessor);
|
|
177
|
+
parameterDescriptors.forEach((pd) => {
|
|
178
|
+
this.paramValuesCache[pd.name] = pd.defaultValue || 0;
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const { factory, sampleSize } = options.processorOptions;
|
|
182
|
+
|
|
183
|
+
this.dspInstance = FaustWasmInstantiator.createSyncMonoDSPInstance(factory);
|
|
184
|
+
this.sampleSize = sampleSize;
|
|
185
|
+
|
|
186
|
+
// Init the FFT constructor and the Faust FFT Processor
|
|
187
|
+
this.initFFT();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async initFFT(): Promise<true> {
|
|
191
|
+
// Use injected function to instantiate the FFT constructor
|
|
192
|
+
this.FFT = await getFFT();
|
|
193
|
+
// Init Faust FFT Processor
|
|
194
|
+
await this.createFFTProcessor();
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static get parameterDescriptors() {
|
|
199
|
+
const params = [] as AudioParamDescriptor[];
|
|
200
|
+
// Analyse voice JSON to generate AudioParam parameters
|
|
201
|
+
const callback = (item: FaustUIItem) => {
|
|
202
|
+
const param = analyseParameters(item);
|
|
203
|
+
if (param) params.push(param);
|
|
204
|
+
}
|
|
205
|
+
FaustBaseWebAudioDsp.parseUI(dspMeta.ui, callback);
|
|
206
|
+
// Add to Faust parameters, FFT specified parameters
|
|
207
|
+
return [
|
|
208
|
+
...params,
|
|
209
|
+
{
|
|
210
|
+
defaultValue: fftOptions?.fftSize || 1024,
|
|
211
|
+
maxValue: 2 ** 32,
|
|
212
|
+
minValue: 2,
|
|
213
|
+
name: "fftSize"
|
|
214
|
+
}, {
|
|
215
|
+
defaultValue: fftOptions?.fftOverlap || 2,
|
|
216
|
+
maxValue: 32,
|
|
217
|
+
minValue: 1,
|
|
218
|
+
name: "fftOverlap"
|
|
219
|
+
}, {
|
|
220
|
+
defaultValue: typeof fftOptions?.defaultWindowFunction === "number" ? fftOptions.defaultWindowFunction + 1 : 0,
|
|
221
|
+
maxValue: windowFunctions?.length || 0,
|
|
222
|
+
minValue: 0,
|
|
223
|
+
name: "windowFunction"
|
|
224
|
+
}, {
|
|
225
|
+
defaultValue: +!!fftOptions?.noIFFT || 0,
|
|
226
|
+
maxValue: 1,
|
|
227
|
+
minValue: 0,
|
|
228
|
+
name: "noIFFT"
|
|
229
|
+
}
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
processFFT() {
|
|
234
|
+
// Get the number of samples that need to proceed, from the input r/w pointers
|
|
235
|
+
let samplesForFFT = mod(this.$inputWrite - this.$inputRead, this.fftBufferSize) || this.fftBufferSize;
|
|
236
|
+
// Start process, until no more enough samples
|
|
237
|
+
while (samplesForFFT >= this.fftSize) {
|
|
238
|
+
let fftProcessorOutputs: Float32Array[] = [];
|
|
239
|
+
// Faust processing, use a callback to avoid extra data copy
|
|
240
|
+
this.fDSPCode.compute((inputs) => {
|
|
241
|
+
// for each audio input channel, three Faust FFT input buffers can be generated (real, imag, FFT bin index)
|
|
242
|
+
for (let i = 0; i < Math.min(this.fftInput.length, Math.ceil(inputs.length / 3)); i++) {
|
|
243
|
+
// FFT forward, use a callback to avoid extra data copy
|
|
244
|
+
const ffted = this.rfft.forward((fftBuffer) => {
|
|
245
|
+
setTypedArray(fftBuffer, this.fftInput[i], 0, this.$inputRead);
|
|
246
|
+
// Windowing the input
|
|
247
|
+
for (let j = 0; j < fftBuffer.length; j++) {
|
|
248
|
+
fftBuffer[j] *= this.window[j];
|
|
249
|
+
}
|
|
250
|
+
// data for FFT (fftBuffer) is prepared
|
|
251
|
+
});
|
|
252
|
+
// write FFTed spectral data to three Faust FFT input buffers (real, imag, FFT bin index)
|
|
253
|
+
fftToSignal(ffted, inputs[i * 3], inputs[i * 3 + 1], inputs[i * 3 + 2]);
|
|
254
|
+
// Faust inputs are prepared
|
|
255
|
+
}
|
|
256
|
+
// If the Faust DSP has more inputs, fill them (zeros or real/imag, fill FFT bin indexes)
|
|
257
|
+
for (let i = this.fftInput.length * 3; i < inputs.length; i++) {
|
|
258
|
+
if (i % 3 === 2) inputs[i].forEach((v, j) => inputs[i][j] = j);
|
|
259
|
+
else inputs[i].fill(0);
|
|
260
|
+
}
|
|
261
|
+
}, (outputs) => {
|
|
262
|
+
// Get the Faust DSP outputs
|
|
263
|
+
fftProcessorOutputs = outputs as Float32Array[];
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Advance FFT input read pointers
|
|
267
|
+
this.$inputRead += this.fftHopSize;
|
|
268
|
+
this.$inputRead %= this.fftBufferSize;
|
|
269
|
+
|
|
270
|
+
samplesForFFT -= this.fftHopSize;
|
|
271
|
+
|
|
272
|
+
// Do inverse FFT on the processed data by Faust DSP, and write the reconstructed signal to the output buffer
|
|
273
|
+
for (let i = 0; i < this.fftOutput.length; i++) {
|
|
274
|
+
let iffted: Float32Array;
|
|
275
|
+
// If noIFFT option in enabled, then no need to do inverse IFFT, use the injected function to convert
|
|
276
|
+
if (this.noIFFT) {
|
|
277
|
+
iffted = this.noIFFTBuffer;
|
|
278
|
+
signalToNoFFT(fftProcessorOutputs[i * 2] || this.fftProcessorZeros, fftProcessorOutputs[i * 2 + 1] || this.fftProcessorZeros, iffted);
|
|
279
|
+
} else {
|
|
280
|
+
// FFT inverse, use a callback to avoid extra data copy
|
|
281
|
+
iffted = this.rfft.inverse((ifftBuffer) => {
|
|
282
|
+
// Convert the Faust DSP output (real/imag plans) to an array for inverse FFT
|
|
283
|
+
signalToFFT(fftProcessorOutputs[i * 2] || this.fftProcessorZeros, fftProcessorOutputs[i * 2 + 1] || this.fftProcessorZeros, ifftBuffer);
|
|
284
|
+
// ifftBuffer is prepared
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// Windowing the output
|
|
288
|
+
for (let j = 0; j < iffted.length; j++) {
|
|
289
|
+
iffted[j] *= this.window[j];
|
|
290
|
+
}
|
|
291
|
+
// Overlap-add, preparing the windowSumSquare array for reverse the windowing effect when output the audio
|
|
292
|
+
let $: number;
|
|
293
|
+
// First part, add the part that is overlaped with the previous window
|
|
294
|
+
for (let j = 0; j < iffted.length - this.fftHopSize; j++) {
|
|
295
|
+
$ = mod(this.$outputWrite + j, this.fftBufferSize);
|
|
296
|
+
this.fftOutput[i][$] += iffted[j];
|
|
297
|
+
if (i === 0) this.windowSumSquare[$] += this.noIFFT ? this.window[j] : this.window[j] ** 2;
|
|
298
|
+
}
|
|
299
|
+
// Second part, write directly to the output buffer
|
|
300
|
+
for (let j = iffted.length - this.fftHopSize; j < iffted.length; j++) {
|
|
301
|
+
$ = mod(this.$outputWrite + j, this.fftBufferSize);
|
|
302
|
+
this.fftOutput[i][$] = iffted[j];
|
|
303
|
+
if (i === 0) this.windowSumSquare[$] = this.noIFFT ? this.window[j] : this.window[j] ** 2;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Advance FFT output write pointers
|
|
307
|
+
this.$outputWrite += this.fftHopSize;
|
|
308
|
+
this.$outputWrite %= this.fftBufferSize;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: { [key: string]: Float32Array }) {
|
|
313
|
+
|
|
314
|
+
if (this.destroyed) return false;
|
|
315
|
+
if (!this.FFT) return true;
|
|
316
|
+
const input = inputs[0];
|
|
317
|
+
const output = outputs[0];
|
|
318
|
+
const inputChannels = input?.length || 0;
|
|
319
|
+
const outputChannels = output?.length || 0;
|
|
320
|
+
// if (input.length === 0) return true;
|
|
321
|
+
|
|
322
|
+
const bufferSize = input?.length ? Math.max(...input.map(c => c.length)) || 128 : 128;
|
|
323
|
+
|
|
324
|
+
// Reset FFT and related buffers if necessary (checks in the resetFFT method)
|
|
325
|
+
this.noIFFT = !!parameters.noIFFT[0];
|
|
326
|
+
this.resetFFT(~~parameters.fftSize[0], ~~parameters.fftOverlap[0], ~~parameters.windowFunction[0], inputChannels, outputChannels, bufferSize);
|
|
327
|
+
|
|
328
|
+
if (!this.fDSPCode) return true;
|
|
329
|
+
|
|
330
|
+
for (const path in parameters) {
|
|
331
|
+
if (!!fftParamKeywords.find(k => `/${path}`.endsWith(k))) continue;
|
|
332
|
+
const [paramValue] = parameters[path];
|
|
333
|
+
if (paramValue !== this.paramValuesCache[path]) {
|
|
334
|
+
this.fDSPCode.setParamValue(path, paramValue);
|
|
335
|
+
this.paramValuesCache[path] = paramValue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Write audio input into fftInput buffer, advance pointers
|
|
340
|
+
if (input?.length) {
|
|
341
|
+
let $inputWrite = 0;
|
|
342
|
+
for (let i = 0; i < input.length; i++) {
|
|
343
|
+
const inputWindow = this.fftInput[i];
|
|
344
|
+
const channel = input[i].length ? input[i] : new Float32Array(bufferSize);
|
|
345
|
+
$inputWrite = setTypedArray(inputWindow, channel, this.$inputWrite);
|
|
346
|
+
}
|
|
347
|
+
this.$inputWrite = $inputWrite;
|
|
348
|
+
} else {
|
|
349
|
+
this.$inputWrite += bufferSize;
|
|
350
|
+
this.$inputWrite %= this.fftBufferSize;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Do FFT if necessary
|
|
354
|
+
this.processFFT();
|
|
355
|
+
|
|
356
|
+
// Read from fftOutput buffer for audio output, applying windowSumSquare to reverse the doubled windowing effect
|
|
357
|
+
for (let i = 0; i < output.length; i++) {
|
|
358
|
+
setTypedArray(output[i], this.fftOutput[i], 0, this.$outputRead);
|
|
359
|
+
// let a = 0;
|
|
360
|
+
let div = 0;
|
|
361
|
+
for (let j = 0; j < bufferSize; j++) {
|
|
362
|
+
div = this.windowSumSquare[mod(this.$outputRead + j, this.fftBufferSize)];
|
|
363
|
+
output[i][j] /= div < 1e-8 ? 1 : div;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Advance pointers
|
|
367
|
+
this.$outputRead += bufferSize;
|
|
368
|
+
this.$outputRead %= this.fftBufferSize;
|
|
369
|
+
|
|
370
|
+
// plot
|
|
371
|
+
if (this.fPlotHandler) {
|
|
372
|
+
this.port.postMessage({ type: "plot", value: output, index: this.fBufferNum++, events: this.fCachedEvents });
|
|
373
|
+
this.fCachedEvents = [];
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
protected handleMessageAux(e: MessageEvent) { // use arrow function for binding
|
|
379
|
+
const msg = e.data;
|
|
380
|
+
|
|
381
|
+
switch (msg.type) {
|
|
382
|
+
// Generic MIDI message
|
|
383
|
+
case "midi": this.midiMessage(msg.data); break;
|
|
384
|
+
// Typed MIDI message
|
|
385
|
+
case "ctrlChange": this.ctrlChange(msg.data[0], msg.data[1], msg.data[2]); break;
|
|
386
|
+
case "pitchWheel": this.pitchWheel(msg.data[0], msg.data[1]); break;
|
|
387
|
+
// Generic data message
|
|
388
|
+
case "param": this.setParamValue(msg.data.path, msg.data.value); break;
|
|
389
|
+
// Plot handler set on demand
|
|
390
|
+
case "setPlotHandler": {
|
|
391
|
+
if (msg.data) {
|
|
392
|
+
this.fPlotHandler = (output, index, events) => {
|
|
393
|
+
if (events) this.fCachedEvents.push(...events);
|
|
394
|
+
};
|
|
395
|
+
} else {
|
|
396
|
+
this.fPlotHandler = null;
|
|
397
|
+
}
|
|
398
|
+
this.fDSPCode?.setPlotHandler(this.fPlotHandler);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case "start": {
|
|
402
|
+
this.fDSPCode?.start();
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "stop": {
|
|
406
|
+
this.fDSPCode?.stop();
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case "destroy": {
|
|
410
|
+
this.port.close();
|
|
411
|
+
this.destroy();
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
default:
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
protected setParamValue(path: string, value: number) {
|
|
420
|
+
this.fDSPCode?.setParamValue(path, value);
|
|
421
|
+
this.paramValuesCache[path] = value;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
protected midiMessage(data: number[] | Uint8Array) {
|
|
425
|
+
this.fDSPCode?.midiMessage(data);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
protected ctrlChange(channel: number, ctrl: number, value: number) {
|
|
429
|
+
this.fDSPCode?.ctrlChange(channel, ctrl, value);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
protected pitchWheel(channel: number, wheel: number) {
|
|
433
|
+
this.fDSPCode?.pitchWheel(channel, wheel);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
resetFFT(sizeIn: number, overlapIn: number, windowFunctionIn: number, inputChannels: number, outputChannels: number, bufferSize: number) {
|
|
437
|
+
const fftSize = ~~ceil(Math.max(2, sizeIn || 1024), 2);
|
|
438
|
+
const fftOverlap = ~~Math.min(fftSize, Math.max(1, overlapIn));
|
|
439
|
+
const fftHopSize = ~~Math.max(1, fftSize / fftOverlap);
|
|
440
|
+
const latency = fftSize - Math.min(fftHopSize, bufferSize);
|
|
441
|
+
let windowFunction: TWindowFunction | null = null;
|
|
442
|
+
|
|
443
|
+
// set the window function from the injected list
|
|
444
|
+
if (windowFunctionIn !== 0) {
|
|
445
|
+
windowFunction = typeof windowFunctions === "object" ? windowFunctions[~~windowFunctionIn - 1] || null : null;
|
|
446
|
+
}
|
|
447
|
+
const fftSizeChanged = fftSize !== this.fftSize;
|
|
448
|
+
|
|
449
|
+
const fftOverlapChanged = fftOverlap !== this.fftOverlap;
|
|
450
|
+
// Reset FFT vars if the size is changed
|
|
451
|
+
if (fftSizeChanged || fftOverlapChanged) {
|
|
452
|
+
this.fftSize = fftSize;
|
|
453
|
+
this.fftOverlap = fftOverlap;
|
|
454
|
+
this.fftHopSize = fftHopSize;
|
|
455
|
+
this.$inputWrite = 0;
|
|
456
|
+
this.$inputRead = 0;
|
|
457
|
+
this.$outputWrite = 0;
|
|
458
|
+
this.$outputRead = -latency;
|
|
459
|
+
this.fftBufferSize = Math.max(fftSize * 2 - this.fftHopSize, bufferSize * 2);
|
|
460
|
+
if (!fftSizeChanged && this.fftHopSizeParam) this.fDSPCode?.setParamValue(this.fftHopSizeParam, this.fftHopSize);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Reset the FFT interface and the Faust Processor
|
|
464
|
+
if (fftSizeChanged) {
|
|
465
|
+
this.rfft?.dispose();
|
|
466
|
+
this.rfft = new this.FFT(fftSize);
|
|
467
|
+
this.noIFFTBuffer = new Float32Array(this.fftSize);
|
|
468
|
+
this.createFFTProcessor();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Calculate a window from the window function, prepare the windowSumSquare buffer
|
|
472
|
+
if (fftSizeChanged || fftOverlapChanged || windowFunction !== this.windowFunction) {
|
|
473
|
+
this.windowFunction = windowFunction;
|
|
474
|
+
this.window = new Float32Array(fftSize);
|
|
475
|
+
this.window.fill(1);
|
|
476
|
+
if (windowFunction) apply(this.window, windowFunction);
|
|
477
|
+
this.windowSumSquare = new Float32Array(this.fftBufferSize);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Reset FFT I/O buffers if necessary
|
|
481
|
+
if (this.fftInput.length > inputChannels) {
|
|
482
|
+
this.fftInput.splice(inputChannels);
|
|
483
|
+
}
|
|
484
|
+
if (this.fftOutput.length > outputChannels) {
|
|
485
|
+
this.fftOutput.splice(outputChannels);
|
|
486
|
+
}
|
|
487
|
+
if (fftSizeChanged || fftOverlapChanged) {
|
|
488
|
+
for (let i = 0; i < inputChannels; i++) {
|
|
489
|
+
this.fftInput[i] = new Float32Array(this.fftBufferSize);
|
|
490
|
+
}
|
|
491
|
+
for (let i = 0; i < outputChannels; i++) {
|
|
492
|
+
this.fftOutput[i] = new Float32Array(this.fftBufferSize);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
if (this.fftInput.length < inputChannels) {
|
|
496
|
+
for (let i = this.fftInput.length; i < inputChannels; i++) {
|
|
497
|
+
this.fftInput[i] = new Float32Array(this.fftBufferSize);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (this.fftOutput.length < outputChannels) {
|
|
501
|
+
for (let i = this.fftOutput.length; i < outputChannels; i++) {
|
|
502
|
+
this.fftOutput[i] = new Float32Array(this.fftBufferSize);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async createFFTProcessor() {
|
|
508
|
+
this.fDSPCode?.stop();
|
|
509
|
+
this.fDSPCode?.destroy();
|
|
510
|
+
|
|
511
|
+
// Create Monophonic DSP
|
|
512
|
+
this.fDSPCode = new FaustMonoWebAudioDsp(this.dspInstance, sampleRate, this.sampleSize, this.fftProcessorBufferSize);
|
|
513
|
+
|
|
514
|
+
// Setup output handler
|
|
515
|
+
this.fDSPCode.setOutputParamHandler((path, value) => this.port.postMessage({ path, value, type: "param" }));
|
|
516
|
+
this.fDSPCode.setPlotHandler(this.fPlotHandler);
|
|
517
|
+
const params = this.fDSPCode.getParams();
|
|
518
|
+
this.fDSPCode.start();
|
|
519
|
+
// Write the cached parameters
|
|
520
|
+
for (const path in this.paramValuesCache) {
|
|
521
|
+
if (!!fftParamKeywords.find(k => `/${path}`.endsWith(k))) continue;
|
|
522
|
+
this.fDSPCode.setParamValue(path, this.paramValuesCache[path])
|
|
523
|
+
}
|
|
524
|
+
// Write the FFT reverved parameters
|
|
525
|
+
const fftSizeParam = params.find(s => s.endsWith("/fftSize"));
|
|
526
|
+
if (fftSizeParam) this.fDSPCode.setParamValue(fftSizeParam, this.fftSize);
|
|
527
|
+
this.fftHopSizeParam = params.find(s => s.endsWith("/fftHopSize"));
|
|
528
|
+
if (this.fftHopSizeParam) this.fDSPCode.setParamValue(this.fftHopSizeParam, this.fftHopSize);
|
|
529
|
+
// Prepare a array of zeros for furthur usage
|
|
530
|
+
this.fftProcessorZeros = new Float32Array(this.fftProcessorBufferSize);
|
|
531
|
+
}
|
|
532
|
+
destroy() {
|
|
533
|
+
this.fDSPCode?.stop();
|
|
534
|
+
this.fDSPCode?.destroy();
|
|
535
|
+
this.rfft?.dispose();
|
|
536
|
+
this.destroyed = true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const Processor = FaustFFTAudioWorkletProcessor;
|
|
542
|
+
if (register) {
|
|
543
|
+
try {
|
|
544
|
+
registerProcessor(processorName || dspName || "myfftdsp", Processor);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.warn(error);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return FaustFFTAudioWorkletProcessor;
|
|
551
|
+
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
export default getFaustFFTAudioWorkletProcessor;
|