@grame/faustwasm 0.12.2 → 0.13.1

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.
Files changed (81) hide show
  1. package/.prettierrc +5 -0
  2. package/README.md +21 -3
  3. package/assets/standalone/faustwasm/index.d.ts +38 -16
  4. package/assets/standalone/faustwasm/index.js +1593 -404
  5. package/assets/standalone/faustwasm/index.js.map +2 -2
  6. package/dist/cjs/index.d.ts +38 -16
  7. package/dist/cjs/index.js +1592 -404
  8. package/dist/cjs/index.js.map +2 -2
  9. package/dist/cjs-bundle/index.d.ts +38 -16
  10. package/dist/cjs-bundle/index.js +1595 -406
  11. package/dist/cjs-bundle/index.js.map +2 -2
  12. package/dist/esm/index.d.ts +38 -16
  13. package/dist/esm/index.js +1593 -404
  14. package/dist/esm/index.js.map +2 -2
  15. package/dist/esm-bundle/index.d.ts +38 -16
  16. package/dist/esm-bundle/index.js +1595 -406
  17. package/dist/esm-bundle/index.js.map +2 -2
  18. package/eslint.config.js +11 -0
  19. package/package.json +57 -51
  20. package/sound.cpp +163 -0
  21. package/sound.dsp +4 -0
  22. package/sound.json +1 -0
  23. package/sound.wasm +0 -0
  24. package/src/FaustAudioWorkletCommunicator.ts +150 -143
  25. package/src/FaustAudioWorkletNode.ts +173 -75
  26. package/src/FaustAudioWorkletProcessor.ts +218 -90
  27. package/src/FaustCmajor.ts +9 -3
  28. package/src/FaustCompiler.ts +112 -35
  29. package/src/FaustDspGenerator.ts +589 -117
  30. package/src/FaustDspInstance.ts +58 -24
  31. package/src/FaustFFTAudioWorkletProcessor.ts +822 -612
  32. package/src/FaustOfflineProcessor.ts +187 -56
  33. package/src/FaustScriptProcessorNode.ts +156 -49
  34. package/src/FaustSensors.ts +426 -96
  35. package/src/FaustSvgDiagrams.ts +18 -5
  36. package/src/FaustWasmInstantiator.ts +224 -70
  37. package/src/FaustWebAudioDsp.ts +1060 -349
  38. package/src/LibFaust.ts +13 -4
  39. package/src/SoundfileReader.ts +175 -131
  40. package/src/WavDecoder.ts +42 -20
  41. package/src/WavEncoder.ts +35 -21
  42. package/src/copyWebStandaloneAssets.d.ts +20 -5
  43. package/src/copyWebStandaloneAssets.js +203 -75
  44. package/src/exports-bundle.ts +5 -5
  45. package/src/exports.ts +24 -24
  46. package/src/faust2CmajorFiles.d.ts +7 -3
  47. package/src/faust2cmajorFiles.js +10 -8
  48. package/src/faust2svgFiles.d.ts +7 -3
  49. package/src/faust2svgFiles.js +9 -7
  50. package/src/faust2wasmFiles.d.ts +10 -5
  51. package/src/faust2wasmFiles.js +34 -17
  52. package/src/faust2wavFiles.d.ts +12 -3
  53. package/src/faust2wavFiles.js +25 -12
  54. package/src/index-bundle-iife.ts +2 -2
  55. package/src/index-bundle.ts +1 -1
  56. package/src/index.ts +1 -1
  57. package/src/instantiateFaustModule.ts +43 -36
  58. package/src/instantiateFaustModuleFromFile.ts +30 -14
  59. package/src/types.ts +101 -25
  60. package/test/faustlive-wasm/faustwasm/index.d.ts +38 -16
  61. package/test/faustlive-wasm/faustwasm/index.js +1593 -404
  62. package/test/faustlive-wasm/faustwasm/index.js.map +2 -2
  63. package/test/web/create-faust-node.html +53 -0
  64. package/test/web/create-faust-node.js +181 -0
  65. package/test/organ/create-node.js +0 -252
  66. package/test/organ/dsp-meta.json +0 -131
  67. package/test/organ/dsp-module.wasm +0 -0
  68. package/test/organ/faust-pwa.js +0 -344
  69. package/test/organ/faust-ui/index.css +0 -442
  70. package/test/organ/faust-ui/index.css.map +0 -1
  71. package/test/organ/faust-ui/index.d.ts +0 -1
  72. package/test/organ/faust-ui/index.js +0 -3756
  73. package/test/organ/faust-ui/index.js.map +0 -1
  74. package/test/organ/faustwasm/index.d.ts +0 -1705
  75. package/test/organ/faustwasm/index.js +0 -4773
  76. package/test/organ/faustwasm/index.js.map +0 -7
  77. package/test/organ/icon.png +0 -0
  78. package/test/organ/index.html +0 -76
  79. package/test/organ/index.js +0 -69
  80. package/test/organ/manifest.json +0 -17
  81. package/test/organ/service-worker.js +0 -165
@@ -1,612 +1,822 @@
1
- import type { FaustAudioWorkletProcessorCommunicator } from "./FaustAudioWorkletCommunicator";
2
- import type { FaustMonoDspInstance } from "./FaustDspInstance";
3
- import type FaustWasmInstantiator from "./FaustWasmInstantiator";
4
- import type { FaustBaseWebAudioDsp, FaustMonoWebAudioDsp, PlotHandler } from "./FaustWebAudioDsp";
5
- import type { AudioParamDescriptor, AudioWorkletGlobalScope, LooseFaustDspFactory, FaustDspMeta, FaustUIItem, InterfaceFFT, TWindowFunction, Writeable, TypedArray, FFTUtils } from "./types";
6
- import type { AudioWorkletGlobalScope as WamAudioWorkletGlobalScope, WamParamMgrSDKBaseModuleScope } from "@webaudiomodules/sdk-parammgr";
7
-
8
- export interface FaustFFTOptionsData {
9
- fftSize: number;
10
- fftOverlap: number;
11
- noIFFT: boolean;
12
- /** Index number of the default window function, leave undefined or -1 for rectangular (no windowing) */
13
- defaultWindowFunction: number;
14
- }
15
-
16
- /**
17
- * Injected in the string to be compiled on AudioWorkletProcessor side
18
- */
19
- export interface FaustFFTData {
20
- processorName: string;
21
- dspName: string;
22
- dspMeta: FaustDspMeta;
23
- fftOptions?: Partial<FaustFFTOptionsData>;
24
- };
25
- export interface FaustFFTAudioWorkletProcessorDependencies {
26
- FaustBaseWebAudioDsp: typeof FaustBaseWebAudioDsp;
27
- FaustMonoWebAudioDsp: typeof FaustMonoWebAudioDsp;
28
- FaustWasmInstantiator: typeof FaustWasmInstantiator;
29
- FaustAudioWorkletProcessorCommunicator: typeof FaustAudioWorkletProcessorCommunicator;
30
- FFTUtils: typeof FFTUtils;
31
- }
32
- export interface FaustFFTAudioWorkletNodeOptions extends AudioWorkletNodeOptions {
33
- processorOptions: FaustFFTAudioWorkletProcessorOptions;
34
- }
35
- export interface FaustFFTAudioWorkletProcessorOptions {
36
- name: string;
37
- sampleSize: number;
38
- factory: LooseFaustDspFactory;
39
- // for WAMs
40
- moduleId?: string;
41
- instanceId?: string;
42
- }
43
-
44
-
45
- // Dynamic AudioWorkletProcessor code generator
46
- const getFaustFFTAudioWorkletProcessor = (dependencies: FaustFFTAudioWorkletProcessorDependencies, faustData: FaustFFTData, register = true): typeof AudioWorkletProcessor => {
47
- const { registerProcessor, AudioWorkletProcessor, sampleRate } = globalThis as unknown as AudioWorkletGlobalScope;
48
-
49
- const {
50
- FaustBaseWebAudioDsp,
51
- FaustWasmInstantiator,
52
- FaustMonoWebAudioDsp,
53
- FaustAudioWorkletProcessorCommunicator,
54
- FFTUtils
55
- } = dependencies;
56
-
57
- const {
58
- processorName,
59
- dspName,
60
- dspMeta,
61
- fftOptions
62
- } = faustData;
63
-
64
- const {
65
- windowFunctions,
66
- getFFT,
67
- fftToSignal,
68
- signalToFFT,
69
- signalToNoFFT
70
- } = FFTUtils;
71
-
72
- /**
73
- * Ceil a number to multiple of another
74
- */
75
- const ceil = (x: number, to: number): number => (Math.abs(to) < 1 ? Math.ceil(x * (1 / to)) / (1 / to) : Math.ceil(x / to) * to);
76
-
77
- /**
78
- * Mod support wrapping with negative numbers
79
- */
80
- const mod = (x: number, y: number): number => (x % y + y) % y;
81
-
82
- const apply = (array: Writeable<ArrayLike<number>>, windowFunction: TWindowFunction) => {
83
- for (let i = 0; i < array.length; i++) {
84
- array[i] *= windowFunction(i, array.length);
85
- }
86
- };
87
-
88
- const fftParamKeywords = ["/fftSize", "/fftHopSize", "/fftOverlap", "/windowFunction", "/noIFFT"];
89
-
90
- /**
91
- * Copy buffer to another, support negative offset index
92
- */
93
- const setTypedArray = <T extends TypedArray = TypedArray>(to: T, from: T, offsetTo = 0, offsetFrom = 0) => {
94
- const toLength = to.length;
95
- const fromLength = from.length;
96
- const spillLength = Math.min(toLength, fromLength);
97
- let spilled = 0;
98
- let $to = mod(offsetTo, toLength) || 0;
99
- let $from = mod(offsetFrom, fromLength) || 0;
100
- while (spilled < spillLength) {
101
- const $spillLength = Math.min(spillLength - spilled, toLength - $to, fromLength - $from);
102
- const $fromEnd = $from + $spillLength;
103
- if ($from === 0 && $fromEnd === fromLength) to.set(from, $to);
104
- else to.set(from.subarray($from, $fromEnd), $to);
105
- $to = ($to + $spillLength) % toLength;
106
- $from = $fromEnd % fromLength;
107
- spilled += $spillLength;
108
- }
109
- return $to;
110
- };
111
-
112
- // Analyse JSON to generate AudioParam parameters
113
- const analyseParameters = (item: FaustUIItem): AudioParamDescriptor | null => {
114
- const isFFTReserved = "address" in item && !!fftParamKeywords.find(k => item.address.endsWith(k));
115
- if (isFFTReserved) return null;
116
- if (item.type === "vslider" || item.type === "hslider" || item.type === "nentry") {
117
- return { name: item.address, defaultValue: item.init || 0, minValue: item.min || 0, maxValue: item.max || 0 };
118
- } else if (item.type === "button" || item.type === "checkbox") {
119
- return { name: item.address, defaultValue: item.init || 0, minValue: 0, maxValue: 1 };
120
- }
121
- return null;
122
- }
123
- /**
124
- * Class for Faust FFT AudioWorkletProcessor
125
- */
126
- class FaustFFTAudioWorkletProcessor extends AudioWorkletProcessor {
127
-
128
- protected fDSPCode: FaustMonoWebAudioDsp;
129
-
130
- protected paramValuesCache: Record<string, number> = {};
131
-
132
- protected wamInfo?: { moduleId: string; instanceId: string };
133
- protected communicator: FaustAudioWorkletProcessorCommunicator;
134
-
135
- private dspInstance!: FaustMonoDspInstance;
136
- private sampleSize!: number;
137
-
138
- private destroyed = false;
139
- /** Pointer of next start sample to write of the FFT input window */
140
- private $inputWrite = 0;
141
- /** Pointer of next start sample to read of the FFT input window */
142
- private $inputRead = 0;
143
- /** Pointer of next start sample to write of the FFT output window */
144
- private $outputWrite = 0;
145
- /** Pointer of next start sample to read of the FFT output window */
146
- private $outputRead = 0;
147
- /** Not perform in IFFT when reconstruct the audio signal */
148
- private noIFFT = false;
149
- /** audio data from input, array of channels */
150
- private readonly fftInput: Float32Array[] = [];
151
- /** audio data for output, array of channels */
152
- private readonly fftOutput: Float32Array[] = [];
153
- /** Generated from the current window function */
154
- private window: Float32Array;
155
- /** Generated from the current window's rolling sum square */
156
- private windowSumSquare: Float32Array;
157
-
158
- /** FFT constructor */
159
- private FFT: typeof InterfaceFFT;
160
- /** Real FFT interface */
161
- private rfft: InterfaceFFT;
162
- /** Faust param name of fftHopSize */
163
- private fftHopSizeParam: string | undefined;
164
- /** FFT Overlaps, 1 means no overlap */
165
- private fftOverlap = 0;
166
- private fftHopSize = 0;
167
- private fftSize = 0;
168
- private fftBufferSize = 0;
169
- private fftProcessorZeros: Float32Array;
170
- private noIFFTBuffer: Float32Array;
171
-
172
- private fPlotHandler: PlotHandler | null = null;
173
- private fCachedEvents: { type: string; data: any }[] = [];
174
- private fBufferNum = 0;
175
- private soundfiles: LooseFaustDspFactory["soundfiles"] = {};
176
- get fftProcessorBufferSize() {
177
- return this.fftSize / 2 + 1;
178
- }
179
- private windowFunction: TWindowFunction | null = null;
180
-
181
- constructor(options: FaustFFTAudioWorkletNodeOptions) {
182
- super(options);
183
-
184
- // Setup port message handling
185
- this.port.addEventListener("message", this.handleMessageAux);
186
- this.port.start();
187
- this.communicator = new FaustAudioWorkletProcessorCommunicator(this.port);
188
-
189
- const { parameterDescriptors } = (this.constructor as typeof AudioWorkletProcessor);
190
- parameterDescriptors.forEach((pd) => {
191
- this.paramValuesCache[pd.name] = pd.defaultValue || 0;
192
- })
193
-
194
- const { factory, sampleSize } = options.processorOptions;
195
-
196
- this.dspInstance = FaustWasmInstantiator.createSyncMonoDSPInstance(factory);
197
- this.sampleSize = sampleSize;
198
- this.soundfiles = factory.soundfiles;
199
-
200
- // Init the FFT constructor and the Faust FFT Processor
201
- this.initFFT();
202
-
203
- const { moduleId, instanceId } = options.processorOptions;
204
- if (!moduleId || !instanceId) return;
205
- this.wamInfo = { moduleId, instanceId };
206
- }
207
-
208
- async initFFT(): Promise<true> {
209
- // Use injected function to instantiate the FFT constructor
210
- this.FFT = await getFFT();
211
- // Init Faust FFT Processor
212
- await this.createFFTProcessor();
213
- return true;
214
- }
215
-
216
- static get parameterDescriptors() {
217
- const params = [] as AudioParamDescriptor[];
218
- // Analyse voice JSON to generate AudioParam parameters
219
- const callback = (item: FaustUIItem) => {
220
- const param = analyseParameters(item);
221
- if (param) params.push(param);
222
- }
223
- FaustBaseWebAudioDsp.parseUI(dspMeta.ui, callback);
224
- // Add to Faust parameters, FFT specified parameters
225
- return [
226
- ...params,
227
- {
228
- defaultValue: fftOptions?.fftSize || 1024,
229
- maxValue: 2 ** 32,
230
- minValue: 2,
231
- name: "fftSize"
232
- }, {
233
- defaultValue: fftOptions?.fftOverlap || 2,
234
- maxValue: 32,
235
- minValue: 1,
236
- name: "fftOverlap"
237
- }, {
238
- defaultValue: typeof fftOptions?.defaultWindowFunction === "number" ? fftOptions.defaultWindowFunction + 1 : 0,
239
- maxValue: windowFunctions?.length || 0,
240
- minValue: 0,
241
- name: "windowFunction"
242
- }, {
243
- defaultValue: +!!fftOptions?.noIFFT || 0,
244
- maxValue: 1,
245
- minValue: 0,
246
- name: "noIFFT"
247
- }
248
- ];
249
- }
250
-
251
- setupWamEventHandler() {
252
- if (!this.wamInfo) return;
253
- const { moduleId, instanceId } = this.wamInfo;
254
- const { webAudioModules } = (globalThis as unknown as WamAudioWorkletGlobalScope);
255
- const ModuleScope = webAudioModules.getModuleScope(moduleId) as WamParamMgrSDKBaseModuleScope;
256
- const paramMgrProcessor = ModuleScope?.paramMgrProcessors?.[instanceId];
257
- if (!paramMgrProcessor) return;
258
- if (paramMgrProcessor.handleEvent) return;
259
- paramMgrProcessor.handleEvent = (event) => {
260
- if (event.type === "wam-midi") this.midiMessage(event.data.bytes);
261
- };
262
- }
263
-
264
- processFFT() {
265
- // Get the number of samples that need to proceed, from the input r/w pointers
266
- let samplesForFFT = mod(this.$inputWrite - this.$inputRead, this.fftBufferSize) || this.fftBufferSize;
267
- // Start process, until no more enough samples
268
- while (samplesForFFT >= this.fftSize) {
269
- let fftProcessorOutputs: Float32Array[] = [];
270
- // Faust processing, use a callback to avoid extra data copy
271
- this.fDSPCode.compute((inputs) => {
272
- // for each audio input channel, three Faust FFT input buffers can be generated (real, imag, FFT bin index)
273
- for (let i = 0; i < Math.min(this.fftInput.length, Math.ceil(inputs.length / 3)); i++) {
274
- // FFT forward, use a callback to avoid extra data copy
275
- const ffted = this.rfft.forward((fftBuffer) => {
276
- setTypedArray(fftBuffer, this.fftInput[i], 0, this.$inputRead);
277
- // Windowing the input
278
- for (let j = 0; j < fftBuffer.length; j++) {
279
- fftBuffer[j] *= this.window[j];
280
- }
281
- // data for FFT (fftBuffer) is prepared
282
- });
283
- // write FFTed spectral data to three Faust FFT input buffers (real, imag, FFT bin index)
284
- fftToSignal(ffted, inputs[i * 3], inputs[i * 3 + 1], inputs[i * 3 + 2]);
285
- // Faust inputs are prepared
286
- }
287
- // If the Faust DSP has more inputs, fill them (zeros or real/imag, fill FFT bin indexes)
288
- for (let i = this.fftInput.length * 3; i < inputs.length; i++) {
289
- if (i % 3 === 2) inputs[i].forEach((v, j) => inputs[i][j] = j);
290
- else inputs[i].fill(0);
291
- }
292
- }, (outputs) => {
293
- // Get the Faust DSP outputs
294
- fftProcessorOutputs = outputs as Float32Array[];
295
- });
296
-
297
- // Advance FFT input read pointers
298
- this.$inputRead += this.fftHopSize;
299
- this.$inputRead %= this.fftBufferSize;
300
-
301
- samplesForFFT -= this.fftHopSize;
302
-
303
- // Do inverse FFT on the processed data by Faust DSP, and write the reconstructed signal to the output buffer
304
- for (let i = 0; i < this.fftOutput.length; i++) {
305
- let iffted: Float32Array;
306
- // If noIFFT option in enabled, then no need to do inverse IFFT, use the injected function to convert
307
- if (this.noIFFT) {
308
- iffted = this.noIFFTBuffer;
309
- signalToNoFFT(fftProcessorOutputs[i * 2] || this.fftProcessorZeros, fftProcessorOutputs[i * 2 + 1] || this.fftProcessorZeros, iffted);
310
- } else {
311
- // FFT inverse, use a callback to avoid extra data copy
312
- iffted = this.rfft.inverse((ifftBuffer) => {
313
- // Convert the Faust DSP output (real/imag plans) to an array for inverse FFT
314
- signalToFFT(fftProcessorOutputs[i * 2] || this.fftProcessorZeros, fftProcessorOutputs[i * 2 + 1] || this.fftProcessorZeros, ifftBuffer);
315
- // ifftBuffer is prepared
316
- });
317
- }
318
- // Windowing the output
319
- for (let j = 0; j < iffted.length; j++) {
320
- iffted[j] *= this.window[j];
321
- }
322
- // Overlap-add, preparing the windowSumSquare array for reverse the windowing effect when output the audio
323
- let $: number;
324
- // First part, add the part that is overlaped with the previous window
325
- for (let j = 0; j < iffted.length - this.fftHopSize; j++) {
326
- $ = mod(this.$outputWrite + j, this.fftBufferSize);
327
- this.fftOutput[i][$] += iffted[j];
328
- if (i === 0) this.windowSumSquare[$] += this.noIFFT ? this.window[j] : this.window[j] ** 2;
329
- }
330
- // Second part, write directly to the output buffer
331
- for (let j = iffted.length - this.fftHopSize; j < iffted.length; j++) {
332
- $ = mod(this.$outputWrite + j, this.fftBufferSize);
333
- this.fftOutput[i][$] = iffted[j];
334
- if (i === 0) this.windowSumSquare[$] = this.noIFFT ? this.window[j] : this.window[j] ** 2;
335
- }
336
- }
337
- // Advance FFT output write pointers
338
- this.$outputWrite += this.fftHopSize;
339
- this.$outputWrite %= this.fftBufferSize;
340
- }
341
- }
342
-
343
- process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: { [key: string]: Float32Array }) {
344
-
345
- if (this.destroyed) return false;
346
- if (!this.FFT) return true;
347
- const input = inputs[0];
348
- const output = outputs[0];
349
- const inputChannels = input?.length || 0;
350
- const outputChannels = output?.length || 0;
351
- // if (input.length === 0) return true;
352
-
353
- const bufferSize = input?.length ? Math.max(...input.map(c => c.length)) || 128 : 128;
354
-
355
- // Reset FFT and related buffers if necessary (checks in the resetFFT method)
356
- this.noIFFT = !!parameters.noIFFT[0];
357
- this.resetFFT(~~parameters.fftSize[0], ~~parameters.fftOverlap[0], ~~parameters.windowFunction[0], inputChannels, outputChannels, bufferSize);
358
-
359
- if (!this.fDSPCode) return true;
360
-
361
- for (const path in parameters) {
362
- if (!!fftParamKeywords.find(k => `/${path}`.endsWith(k))) continue;
363
- const [paramValue] = parameters[path];
364
- if (paramValue !== this.paramValuesCache[path]) {
365
- // Set value and update the cache
366
- this.setParamValue(path, paramValue);
367
- }
368
- }
369
- if (this.communicator.getNewAccDataAvailable()) {
370
- const acc = this.communicator.getAcc();
371
- if (acc) {
372
- this.communicator.setNewAccDataAvailable(false);
373
- const { invert, ...data } = acc;
374
- this.propagateAcc(data, invert);
375
- }
376
- }
377
- if (this.communicator.getNewGyrDataAvailable()) {
378
- const gyr = this.communicator.getGyr();
379
- if (gyr) {
380
- this.communicator.setNewGyrDataAvailable(false);
381
- this.propagateGyr(gyr);
382
- }
383
- }
384
-
385
- // Write audio input into fftInput buffer, advance pointers
386
- if (input?.length) {
387
- let $inputWrite = 0;
388
- for (let i = 0; i < input.length; i++) {
389
- const inputWindow = this.fftInput[i];
390
- const channel = input[i].length ? input[i] : new Float32Array(bufferSize);
391
- $inputWrite = setTypedArray(inputWindow, channel, this.$inputWrite);
392
- }
393
- this.$inputWrite = $inputWrite;
394
- } else {
395
- this.$inputWrite += bufferSize;
396
- this.$inputWrite %= this.fftBufferSize;
397
- }
398
-
399
- // Do FFT if necessary
400
- this.processFFT();
401
-
402
- // Read from fftOutput buffer for audio output, applying windowSumSquare to reverse the doubled windowing effect
403
- for (let i = 0; i < output.length; i++) {
404
- setTypedArray(output[i], this.fftOutput[i], 0, this.$outputRead);
405
- // let a = 0;
406
- let div = 0;
407
- for (let j = 0; j < bufferSize; j++) {
408
- div = this.windowSumSquare[mod(this.$outputRead + j, this.fftBufferSize)];
409
- output[i][j] /= div < 1e-8 ? 1 : div;
410
- }
411
- }
412
- // Advance pointers
413
- this.$outputRead += bufferSize;
414
- this.$outputRead %= this.fftBufferSize;
415
-
416
- // plot
417
- if (this.fPlotHandler) {
418
- this.port.postMessage({ type: "plot", value: output, index: this.fBufferNum++, events: this.fCachedEvents });
419
- this.fCachedEvents = [];
420
- }
421
- return true;
422
- }
423
-
424
- protected handleMessageAux = (e: MessageEvent) => { // use arrow function for binding
425
- const msg = e.data;
426
-
427
- switch (msg.type) {
428
- // Generic MIDI message
429
- case "midi": this.midiMessage(msg.data); break;
430
- // Typed MIDI message
431
- case "ctrlChange": this.ctrlChange(msg.data[0], msg.data[1], msg.data[2]); break;
432
- case "pitchWheel": this.pitchWheel(msg.data[0], msg.data[1]); break;
433
- // Generic data message
434
- case "param": this.setParamValue(msg.data.path, msg.data.value); break;
435
- // Plot handler set on demand
436
- case "setPlotHandler": {
437
- if (msg.data) {
438
- this.fPlotHandler = (output, index, events) => {
439
- if (events) this.fCachedEvents.push(...events);
440
- };
441
- } else {
442
- this.fPlotHandler = null;
443
- }
444
- this.fDSPCode?.setPlotHandler(this.fPlotHandler);
445
- break;
446
- }
447
- case "setupWamEventHandler": {
448
- this.setupWamEventHandler();
449
- break;
450
- }
451
- case "start": {
452
- this.fDSPCode?.start();
453
- break;
454
- }
455
- case "stop": {
456
- this.fDSPCode?.stop();
457
- break;
458
- }
459
- case "destroy": {
460
- this.port.close();
461
- this.destroy();
462
- break;
463
- }
464
- default:
465
- break;
466
- }
467
- }
468
-
469
- protected setParamValue(path: string, value: number) {
470
- this.fDSPCode?.setParamValue(path, value);
471
- this.paramValuesCache[path] = value;
472
- }
473
-
474
- protected midiMessage(data: number[] | Uint8Array) {
475
- this.fDSPCode?.midiMessage(data);
476
- }
477
-
478
- protected ctrlChange(channel: number, ctrl: number, value: number) {
479
- this.fDSPCode?.ctrlChange(channel, ctrl, value);
480
- }
481
-
482
- protected pitchWheel(channel: number, wheel: number) {
483
- this.fDSPCode?.pitchWheel(channel, wheel);
484
- }
485
-
486
- protected propagateAcc(accelerationIncludingGravity: NonNullable<DeviceMotionEvent["accelerationIncludingGravity"]>, invert: boolean = false) {
487
- this.fDSPCode.propagateAcc(accelerationIncludingGravity, invert);
488
- }
489
-
490
- protected propagateGyr(event: Pick<DeviceOrientationEvent, "alpha" | "beta" | "gamma">) {
491
- this.fDSPCode.propagateGyr(event);
492
- }
493
-
494
- resetFFT(sizeIn: number, overlapIn: number, windowFunctionIn: number, inputChannels: number, outputChannels: number, bufferSize: number) {
495
- const fftSize = ~~ceil(Math.max(2, sizeIn || 1024), 2);
496
- const fftOverlap = ~~Math.min(fftSize, Math.max(1, overlapIn));
497
- const fftHopSize = ~~Math.max(1, fftSize / fftOverlap);
498
- const latency = fftSize - Math.min(fftHopSize, bufferSize);
499
- let windowFunction: TWindowFunction | null = null;
500
-
501
- // set the window function from the injected list
502
- if (windowFunctionIn !== 0) {
503
- windowFunction = typeof windowFunctions === "object" ? windowFunctions[~~windowFunctionIn - 1] || null : null;
504
- }
505
- const fftSizeChanged = fftSize !== this.fftSize;
506
-
507
- const fftOverlapChanged = fftOverlap !== this.fftOverlap;
508
- // Reset FFT vars if the size is changed
509
- if (fftSizeChanged || fftOverlapChanged) {
510
- this.fftSize = fftSize;
511
- this.fftOverlap = fftOverlap;
512
- this.fftHopSize = fftHopSize;
513
- this.$inputWrite = 0;
514
- this.$inputRead = 0;
515
- this.$outputWrite = 0;
516
- this.$outputRead = -latency;
517
- this.fftBufferSize = Math.max(fftSize * 2 - this.fftHopSize, bufferSize * 2);
518
- if (!fftSizeChanged && this.fftHopSizeParam) this.fDSPCode?.setParamValue(this.fftHopSizeParam, this.fftHopSize);
519
- }
520
-
521
- // Reset the FFT interface and the Faust Processor
522
- if (fftSizeChanged) {
523
- this.rfft?.dispose();
524
- this.rfft = new this.FFT(fftSize);
525
- this.noIFFTBuffer = new Float32Array(this.fftSize);
526
- this.createFFTProcessor();
527
- }
528
-
529
- // Calculate a window from the window function, prepare the windowSumSquare buffer
530
- if (fftSizeChanged || fftOverlapChanged || windowFunction !== this.windowFunction) {
531
- this.windowFunction = windowFunction;
532
- this.window = new Float32Array(fftSize);
533
- this.window.fill(1);
534
- if (windowFunction) apply(this.window, windowFunction);
535
- this.windowSumSquare = new Float32Array(this.fftBufferSize);
536
- }
537
-
538
- // Reset FFT I/O buffers if necessary
539
- if (this.fftInput.length > inputChannels) {
540
- this.fftInput.splice(inputChannels);
541
- }
542
- if (this.fftOutput.length > outputChannels) {
543
- this.fftOutput.splice(outputChannels);
544
- }
545
- if (fftSizeChanged || fftOverlapChanged) {
546
- for (let i = 0; i < inputChannels; i++) {
547
- this.fftInput[i] = new Float32Array(this.fftBufferSize);
548
- }
549
- for (let i = 0; i < outputChannels; i++) {
550
- this.fftOutput[i] = new Float32Array(this.fftBufferSize);
551
- }
552
- } else {
553
- if (this.fftInput.length < inputChannels) {
554
- for (let i = this.fftInput.length; i < inputChannels; i++) {
555
- this.fftInput[i] = new Float32Array(this.fftBufferSize);
556
- }
557
- }
558
- if (this.fftOutput.length < outputChannels) {
559
- for (let i = this.fftOutput.length; i < outputChannels; i++) {
560
- this.fftOutput[i] = new Float32Array(this.fftBufferSize);
561
- }
562
- }
563
- }
564
- }
565
- async createFFTProcessor() {
566
- this.fDSPCode?.stop();
567
- this.fDSPCode?.destroy();
568
-
569
- // Create Monophonic DSP
570
- this.fDSPCode = new FaustMonoWebAudioDsp(this.dspInstance, sampleRate, this.sampleSize, this.fftProcessorBufferSize, this.soundfiles);
571
-
572
- // Setup output handler
573
- this.fDSPCode.setOutputParamHandler((path, value) => this.port.postMessage({ path, value, type: "param" }));
574
- this.fDSPCode.setPlotHandler(this.fPlotHandler);
575
- const params = this.fDSPCode.getParams();
576
- this.fDSPCode.start();
577
- // Write the cached parameters
578
- for (const path in this.paramValuesCache) {
579
- if (!!fftParamKeywords.find(k => `/${path}`.endsWith(k))) continue;
580
- this.fDSPCode.setParamValue(path, this.paramValuesCache[path])
581
- }
582
- // Write the FFT reverved parameters
583
- const fftSizeParam = params.find(s => s.endsWith("/fftSize"));
584
- if (fftSizeParam) this.fDSPCode.setParamValue(fftSizeParam, this.fftSize);
585
- this.fftHopSizeParam = params.find(s => s.endsWith("/fftHopSize"));
586
- if (this.fftHopSizeParam) this.fDSPCode.setParamValue(this.fftHopSizeParam, this.fftHopSize);
587
- // Prepare a array of zeros for furthur usage
588
- this.fftProcessorZeros = new Float32Array(this.fftProcessorBufferSize);
589
- }
590
- destroy() {
591
- this.fDSPCode?.stop();
592
- this.fDSPCode?.destroy();
593
- this.rfft?.dispose();
594
- this.destroyed = true;
595
- }
596
-
597
- }
598
-
599
- const Processor = FaustFFTAudioWorkletProcessor;
600
- if (register) {
601
- try {
602
- registerProcessor(processorName || dspName || "myfftdsp", Processor);
603
- } catch (error) {
604
- console.warn(error);
605
- }
606
- }
607
-
608
- return FaustFFTAudioWorkletProcessor;
609
-
610
- };
611
-
612
- export default getFaustFFTAudioWorkletProcessor;
1
+ import type { FaustAudioWorkletProcessorCommunicator } from './FaustAudioWorkletCommunicator';
2
+ import type { FaustMonoDspInstance } from './FaustDspInstance';
3
+ import type FaustWasmInstantiator from './FaustWasmInstantiator';
4
+ import type {
5
+ FaustBaseWebAudioDsp,
6
+ FaustMonoWebAudioDsp,
7
+ PlotHandler
8
+ } from './FaustWebAudioDsp';
9
+ import type {
10
+ AudioParamDescriptor,
11
+ AudioWorkletGlobalScope,
12
+ LooseFaustDspFactory,
13
+ FaustDspMeta,
14
+ FaustUIItem,
15
+ InterfaceFFT,
16
+ TWindowFunction,
17
+ Writeable,
18
+ TypedArray,
19
+ FFTUtils
20
+ } from './types';
21
+ import type {
22
+ AudioWorkletGlobalScope as WamAudioWorkletGlobalScope,
23
+ WamParamMgrSDKBaseModuleScope
24
+ } from '@webaudiomodules/sdk-parammgr';
25
+
26
+ export interface FaustFFTOptionsData {
27
+ fftSize: number;
28
+ fftOverlap: number;
29
+ noIFFT: boolean;
30
+ /** Index number of the default window function, leave undefined or -1 for rectangular (no windowing) */
31
+ defaultWindowFunction: number;
32
+ }
33
+
34
+ /**
35
+ * Injected in the string to be compiled on AudioWorkletProcessor side
36
+ */
37
+ export interface FaustFFTData {
38
+ processorName: string;
39
+ dspName: string;
40
+ dspMeta: FaustDspMeta;
41
+ fftOptions?: Partial<FaustFFTOptionsData>;
42
+ }
43
+ export interface FaustFFTAudioWorkletProcessorDependencies {
44
+ FaustBaseWebAudioDsp: typeof FaustBaseWebAudioDsp;
45
+ FaustMonoWebAudioDsp: typeof FaustMonoWebAudioDsp;
46
+ FaustWasmInstantiator: typeof FaustWasmInstantiator;
47
+ FaustAudioWorkletProcessorCommunicator: typeof FaustAudioWorkletProcessorCommunicator;
48
+ FFTUtils: typeof FFTUtils;
49
+ }
50
+ export interface FaustFFTAudioWorkletNodeOptions
51
+ extends AudioWorkletNodeOptions {
52
+ processorOptions: FaustFFTAudioWorkletProcessorOptions;
53
+ }
54
+ export interface FaustFFTAudioWorkletProcessorOptions {
55
+ name: string;
56
+ sampleSize: number;
57
+ factory: LooseFaustDspFactory;
58
+ // for WAMs
59
+ moduleId?: string;
60
+ instanceId?: string;
61
+ }
62
+
63
+ // Dynamic AudioWorkletProcessor code generator
64
+ const getFaustFFTAudioWorkletProcessor = (
65
+ dependencies: FaustFFTAudioWorkletProcessorDependencies,
66
+ faustData: FaustFFTData,
67
+ register = true
68
+ ): typeof AudioWorkletProcessor => {
69
+ const { registerProcessor, AudioWorkletProcessor, sampleRate } =
70
+ globalThis as unknown as AudioWorkletGlobalScope;
71
+
72
+ const {
73
+ FaustBaseWebAudioDsp,
74
+ FaustWasmInstantiator,
75
+ FaustMonoWebAudioDsp,
76
+ FaustAudioWorkletProcessorCommunicator,
77
+ FFTUtils
78
+ } = dependencies;
79
+
80
+ const { processorName, dspName, dspMeta, fftOptions } = faustData;
81
+
82
+ const { windowFunctions, getFFT, fftToSignal, signalToFFT, signalToNoFFT } =
83
+ FFTUtils;
84
+
85
+ /**
86
+ * Ceil a number to multiple of another
87
+ */
88
+ const ceil = (x: number, to: number): number =>
89
+ Math.abs(to) < 1
90
+ ? Math.ceil(x * (1 / to)) / (1 / to)
91
+ : Math.ceil(x / to) * to;
92
+
93
+ /**
94
+ * Mod support wrapping with negative numbers
95
+ */
96
+ const mod = (x: number, y: number): number => ((x % y) + y) % y;
97
+
98
+ const apply = (
99
+ array: Writeable<ArrayLike<number>>,
100
+ windowFunction: TWindowFunction
101
+ ) => {
102
+ for (let i = 0; i < array.length; i++) {
103
+ array[i] *= windowFunction(i, array.length);
104
+ }
105
+ };
106
+
107
+ const fftParamKeywords = [
108
+ '/fftSize',
109
+ '/fftHopSize',
110
+ '/fftOverlap',
111
+ '/windowFunction',
112
+ '/noIFFT'
113
+ ];
114
+
115
+ /**
116
+ * Copy buffer to another, support negative offset index
117
+ */
118
+ const setTypedArray = <T extends TypedArray = TypedArray>(
119
+ to: T,
120
+ from: T,
121
+ offsetTo = 0,
122
+ offsetFrom = 0
123
+ ) => {
124
+ const toLength = to.length;
125
+ const fromLength = from.length;
126
+ const spillLength = Math.min(toLength, fromLength);
127
+ let spilled = 0;
128
+ let $to = mod(offsetTo, toLength) || 0;
129
+ let $from = mod(offsetFrom, fromLength) || 0;
130
+ while (spilled < spillLength) {
131
+ const $spillLength = Math.min(
132
+ spillLength - spilled,
133
+ toLength - $to,
134
+ fromLength - $from
135
+ );
136
+ const $fromEnd = $from + $spillLength;
137
+ if ($from === 0 && $fromEnd === fromLength) to.set(from, $to);
138
+ else to.set(from.subarray($from, $fromEnd), $to);
139
+ $to = ($to + $spillLength) % toLength;
140
+ $from = $fromEnd % fromLength;
141
+ spilled += $spillLength;
142
+ }
143
+ return $to;
144
+ };
145
+
146
+ // Analyse JSON to generate AudioParam parameters
147
+ const analyseParameters = (
148
+ item: FaustUIItem
149
+ ): AudioParamDescriptor | null => {
150
+ const isFFTReserved =
151
+ 'address' in item &&
152
+ !!fftParamKeywords.find((k) => item.address.endsWith(k));
153
+ if (isFFTReserved) return null;
154
+ if (
155
+ item.type === 'vslider' ||
156
+ item.type === 'hslider' ||
157
+ item.type === 'nentry'
158
+ ) {
159
+ return {
160
+ name: item.address,
161
+ defaultValue: item.init || 0,
162
+ minValue: item.min || 0,
163
+ maxValue: item.max || 0
164
+ };
165
+ } else if (item.type === 'button' || item.type === 'checkbox') {
166
+ return {
167
+ name: item.address,
168
+ defaultValue: item.init || 0,
169
+ minValue: 0,
170
+ maxValue: 1
171
+ };
172
+ }
173
+ return null;
174
+ };
175
+ /**
176
+ * Class for Faust FFT AudioWorkletProcessor
177
+ */
178
+ class FaustFFTAudioWorkletProcessor extends AudioWorkletProcessor {
179
+ protected fDSPCode: FaustMonoWebAudioDsp;
180
+
181
+ protected paramValuesCache: Record<string, number> = {};
182
+
183
+ protected wamInfo?: { moduleId: string; instanceId: string };
184
+ protected communicator: FaustAudioWorkletProcessorCommunicator;
185
+
186
+ private dspInstance!: FaustMonoDspInstance;
187
+ private sampleSize!: number;
188
+
189
+ private destroyed = false;
190
+ /** Pointer of next start sample to write of the FFT input window */
191
+ private $inputWrite = 0;
192
+ /** Pointer of next start sample to read of the FFT input window */
193
+ private $inputRead = 0;
194
+ /** Pointer of next start sample to write of the FFT output window */
195
+ private $outputWrite = 0;
196
+ /** Pointer of next start sample to read of the FFT output window */
197
+ private $outputRead = 0;
198
+ /** Not perform in IFFT when reconstruct the audio signal */
199
+ private noIFFT = false;
200
+ /** audio data from input, array of channels */
201
+ private readonly fftInput: Float32Array[] = [];
202
+ /** audio data for output, array of channels */
203
+ private readonly fftOutput: Float32Array[] = [];
204
+ /** Generated from the current window function */
205
+ private window: Float32Array;
206
+ /** Generated from the current window's rolling sum square */
207
+ private windowSumSquare: Float32Array;
208
+
209
+ /** FFT constructor */
210
+ private FFT: typeof InterfaceFFT;
211
+ /** Real FFT interface */
212
+ private rfft: InterfaceFFT;
213
+ /** Faust param name of fftHopSize */
214
+ private fftHopSizeParam: string | undefined;
215
+ /** FFT Overlaps, 1 means no overlap */
216
+ private fftOverlap = 0;
217
+ private fftHopSize = 0;
218
+ private fftSize = 0;
219
+ private fftBufferSize = 0;
220
+ private fftProcessorZeros: Float32Array;
221
+ private noIFFTBuffer: Float32Array;
222
+
223
+ private fPlotHandler: PlotHandler | null = null;
224
+ private fCachedEvents: { type: string; data: any }[] = [];
225
+ private fBufferNum = 0;
226
+ private soundfiles: LooseFaustDspFactory['soundfiles'] = {};
227
+ get fftProcessorBufferSize() {
228
+ return this.fftSize / 2 + 1;
229
+ }
230
+ private windowFunction: TWindowFunction | null = null;
231
+
232
+ constructor(options: FaustFFTAudioWorkletNodeOptions) {
233
+ super(options);
234
+
235
+ // Setup port message handling
236
+ this.port.addEventListener('message', this.handleMessageAux);
237
+ this.port.start();
238
+ this.communicator = new FaustAudioWorkletProcessorCommunicator(
239
+ this.port
240
+ );
241
+
242
+ const { parameterDescriptors } = this
243
+ .constructor as typeof AudioWorkletProcessor;
244
+ parameterDescriptors.forEach((pd) => {
245
+ this.paramValuesCache[pd.name] = pd.defaultValue || 0;
246
+ });
247
+
248
+ const { factory, sampleSize } = options.processorOptions;
249
+
250
+ this.dspInstance =
251
+ FaustWasmInstantiator.createSyncMonoDSPInstance(factory);
252
+ this.sampleSize = sampleSize;
253
+ this.soundfiles = factory.soundfiles;
254
+
255
+ // Init the FFT constructor and the Faust FFT Processor
256
+ this.initFFT();
257
+
258
+ const { moduleId, instanceId } = options.processorOptions;
259
+ if (!moduleId || !instanceId) return;
260
+ this.wamInfo = { moduleId, instanceId };
261
+ }
262
+
263
+ async initFFT(): Promise<true> {
264
+ // Use injected function to instantiate the FFT constructor
265
+ this.FFT = await getFFT();
266
+ // Init Faust FFT Processor
267
+ await this.createFFTProcessor();
268
+ return true;
269
+ }
270
+
271
+ static get parameterDescriptors() {
272
+ const params = [] as AudioParamDescriptor[];
273
+ // Analyse voice JSON to generate AudioParam parameters
274
+ const callback = (item: FaustUIItem) => {
275
+ const param = analyseParameters(item);
276
+ if (param) params.push(param);
277
+ };
278
+ FaustBaseWebAudioDsp.parseUI(dspMeta.ui, callback);
279
+ // Add to Faust parameters, FFT specified parameters
280
+ return [
281
+ ...params,
282
+ {
283
+ defaultValue: fftOptions?.fftSize || 1024,
284
+ maxValue: 2 ** 32,
285
+ minValue: 2,
286
+ name: 'fftSize'
287
+ },
288
+ {
289
+ defaultValue: fftOptions?.fftOverlap || 2,
290
+ maxValue: 32,
291
+ minValue: 1,
292
+ name: 'fftOverlap'
293
+ },
294
+ {
295
+ defaultValue:
296
+ typeof fftOptions?.defaultWindowFunction === 'number'
297
+ ? fftOptions.defaultWindowFunction + 1
298
+ : 0,
299
+ maxValue: windowFunctions?.length || 0,
300
+ minValue: 0,
301
+ name: 'windowFunction'
302
+ },
303
+ {
304
+ defaultValue: +!!fftOptions?.noIFFT || 0,
305
+ maxValue: 1,
306
+ minValue: 0,
307
+ name: 'noIFFT'
308
+ }
309
+ ];
310
+ }
311
+
312
+ setupWamEventHandler() {
313
+ if (!this.wamInfo) return;
314
+ const { moduleId, instanceId } = this.wamInfo;
315
+ const { webAudioModules } =
316
+ globalThis as unknown as WamAudioWorkletGlobalScope;
317
+ const ModuleScope = webAudioModules.getModuleScope(
318
+ moduleId
319
+ ) as WamParamMgrSDKBaseModuleScope;
320
+ const paramMgrProcessor =
321
+ ModuleScope?.paramMgrProcessors?.[instanceId];
322
+ if (!paramMgrProcessor) return;
323
+ if (paramMgrProcessor.handleEvent) return;
324
+ paramMgrProcessor.handleEvent = (event) => {
325
+ if (event.type === 'wam-midi')
326
+ this.midiMessage(event.data.bytes);
327
+ };
328
+ }
329
+
330
+ processFFT() {
331
+ // Get the number of samples that need to proceed, from the input r/w pointers
332
+ let samplesForFFT =
333
+ mod(this.$inputWrite - this.$inputRead, this.fftBufferSize) ||
334
+ this.fftBufferSize;
335
+ // Start process, until no more enough samples
336
+ while (samplesForFFT >= this.fftSize) {
337
+ let fftProcessorOutputs: Float32Array[] = [];
338
+ // Faust processing, use a callback to avoid extra data copy
339
+ this.fDSPCode.compute(
340
+ (inputs) => {
341
+ // for each audio input channel, three Faust FFT input buffers can be generated (real, imag, FFT bin index)
342
+ for (
343
+ let i = 0;
344
+ i <
345
+ Math.min(
346
+ this.fftInput.length,
347
+ Math.ceil(inputs.length / 3)
348
+ );
349
+ i++
350
+ ) {
351
+ // FFT forward, use a callback to avoid extra data copy
352
+ const ffted = this.rfft.forward((fftBuffer) => {
353
+ setTypedArray(
354
+ fftBuffer,
355
+ this.fftInput[i],
356
+ 0,
357
+ this.$inputRead
358
+ );
359
+ // Windowing the input
360
+ for (let j = 0; j < fftBuffer.length; j++) {
361
+ fftBuffer[j] *= this.window[j];
362
+ }
363
+ // data for FFT (fftBuffer) is prepared
364
+ });
365
+ // write FFTed spectral data to three Faust FFT input buffers (real, imag, FFT bin index)
366
+ fftToSignal(
367
+ ffted,
368
+ inputs[i * 3],
369
+ inputs[i * 3 + 1],
370
+ inputs[i * 3 + 2]
371
+ );
372
+ // Faust inputs are prepared
373
+ }
374
+ // If the Faust DSP has more inputs, fill them (zeros or real/imag, fill FFT bin indexes)
375
+ for (
376
+ let i = this.fftInput.length * 3;
377
+ i < inputs.length;
378
+ i++
379
+ ) {
380
+ if (i % 3 === 2)
381
+ inputs[i].forEach((v, j) => (inputs[i][j] = j));
382
+ else inputs[i].fill(0);
383
+ }
384
+ },
385
+ (outputs) => {
386
+ // Get the Faust DSP outputs
387
+ fftProcessorOutputs = outputs as Float32Array[];
388
+ }
389
+ );
390
+
391
+ // Advance FFT input read pointers
392
+ this.$inputRead += this.fftHopSize;
393
+ this.$inputRead %= this.fftBufferSize;
394
+
395
+ samplesForFFT -= this.fftHopSize;
396
+
397
+ // Do inverse FFT on the processed data by Faust DSP, and write the reconstructed signal to the output buffer
398
+ for (let i = 0; i < this.fftOutput.length; i++) {
399
+ let iffted: Float32Array;
400
+ // If noIFFT option in enabled, then no need to do inverse IFFT, use the injected function to convert
401
+ if (this.noIFFT) {
402
+ iffted = this.noIFFTBuffer;
403
+ signalToNoFFT(
404
+ fftProcessorOutputs[i * 2] ||
405
+ this.fftProcessorZeros,
406
+ fftProcessorOutputs[i * 2 + 1] ||
407
+ this.fftProcessorZeros,
408
+ iffted
409
+ );
410
+ } else {
411
+ // FFT inverse, use a callback to avoid extra data copy
412
+ iffted = this.rfft.inverse((ifftBuffer) => {
413
+ // Convert the Faust DSP output (real/imag plans) to an array for inverse FFT
414
+ signalToFFT(
415
+ fftProcessorOutputs[i * 2] ||
416
+ this.fftProcessorZeros,
417
+ fftProcessorOutputs[i * 2 + 1] ||
418
+ this.fftProcessorZeros,
419
+ ifftBuffer
420
+ );
421
+ // ifftBuffer is prepared
422
+ });
423
+ }
424
+ // Windowing the output
425
+ for (let j = 0; j < iffted.length; j++) {
426
+ iffted[j] *= this.window[j];
427
+ }
428
+ // Overlap-add, preparing the windowSumSquare array for reverse the windowing effect when output the audio
429
+ let $: number;
430
+ // First part, add the part that is overlaped with the previous window
431
+ for (let j = 0; j < iffted.length - this.fftHopSize; j++) {
432
+ $ = mod(this.$outputWrite + j, this.fftBufferSize);
433
+ this.fftOutput[i][$] += iffted[j];
434
+ if (i === 0)
435
+ this.windowSumSquare[$] += this.noIFFT
436
+ ? this.window[j]
437
+ : this.window[j] ** 2;
438
+ }
439
+ // Second part, write directly to the output buffer
440
+ for (
441
+ let j = iffted.length - this.fftHopSize;
442
+ j < iffted.length;
443
+ j++
444
+ ) {
445
+ $ = mod(this.$outputWrite + j, this.fftBufferSize);
446
+ this.fftOutput[i][$] = iffted[j];
447
+ if (i === 0)
448
+ this.windowSumSquare[$] = this.noIFFT
449
+ ? this.window[j]
450
+ : this.window[j] ** 2;
451
+ }
452
+ }
453
+ // Advance FFT output write pointers
454
+ this.$outputWrite += this.fftHopSize;
455
+ this.$outputWrite %= this.fftBufferSize;
456
+ }
457
+ }
458
+
459
+ process(
460
+ inputs: Float32Array[][],
461
+ outputs: Float32Array[][],
462
+ parameters: { [key: string]: Float32Array }
463
+ ) {
464
+ if (this.destroyed) return false;
465
+ if (!this.FFT) return true;
466
+ const input = inputs[0];
467
+ const output = outputs[0];
468
+ const inputChannels = input?.length || 0;
469
+ const outputChannels = output?.length || 0;
470
+ // if (input.length === 0) return true;
471
+
472
+ const bufferSize = input?.length
473
+ ? Math.max(...input.map((c) => c.length)) || 128
474
+ : 128;
475
+
476
+ // Reset FFT and related buffers if necessary (checks in the resetFFT method)
477
+ this.noIFFT = !!parameters.noIFFT[0];
478
+ this.resetFFT(
479
+ ~~parameters.fftSize[0],
480
+ ~~parameters.fftOverlap[0],
481
+ ~~parameters.windowFunction[0],
482
+ inputChannels,
483
+ outputChannels,
484
+ bufferSize
485
+ );
486
+
487
+ if (!this.fDSPCode) return true;
488
+
489
+ for (const path in parameters) {
490
+ if (fftParamKeywords.find((k) => `/${path}`.endsWith(k)))
491
+ continue;
492
+ const [paramValue] = parameters[path];
493
+ if (paramValue !== this.paramValuesCache[path]) {
494
+ // Set value and update the cache
495
+ this.setParamValue(path, paramValue);
496
+ }
497
+ }
498
+ if (this.communicator.getNewAccDataAvailable()) {
499
+ const acc = this.communicator.getAcc();
500
+ if (acc) {
501
+ this.communicator.setNewAccDataAvailable(false);
502
+ const { invert, ...data } = acc;
503
+ this.propagateAcc(data, invert);
504
+ }
505
+ }
506
+ if (this.communicator.getNewGyrDataAvailable()) {
507
+ const gyr = this.communicator.getGyr();
508
+ if (gyr) {
509
+ this.communicator.setNewGyrDataAvailable(false);
510
+ this.propagateGyr(gyr);
511
+ }
512
+ }
513
+
514
+ // Write audio input into fftInput buffer, advance pointers
515
+ if (input?.length) {
516
+ let $inputWrite = 0;
517
+ for (let i = 0; i < input.length; i++) {
518
+ const inputWindow = this.fftInput[i];
519
+ const channel = input[i].length
520
+ ? input[i]
521
+ : new Float32Array(bufferSize);
522
+ $inputWrite = setTypedArray(
523
+ inputWindow,
524
+ channel,
525
+ this.$inputWrite
526
+ );
527
+ }
528
+ this.$inputWrite = $inputWrite;
529
+ } else {
530
+ this.$inputWrite += bufferSize;
531
+ this.$inputWrite %= this.fftBufferSize;
532
+ }
533
+
534
+ // Do FFT if necessary
535
+ this.processFFT();
536
+
537
+ // Read from fftOutput buffer for audio output, applying windowSumSquare to reverse the doubled windowing effect
538
+ for (let i = 0; i < output.length; i++) {
539
+ setTypedArray(
540
+ output[i],
541
+ this.fftOutput[i],
542
+ 0,
543
+ this.$outputRead
544
+ );
545
+ // let a = 0;
546
+ let div = 0;
547
+ for (let j = 0; j < bufferSize; j++) {
548
+ div =
549
+ this.windowSumSquare[
550
+ mod(this.$outputRead + j, this.fftBufferSize)
551
+ ];
552
+ output[i][j] /= div < 1e-8 ? 1 : div;
553
+ }
554
+ }
555
+ // Advance pointers
556
+ this.$outputRead += bufferSize;
557
+ this.$outputRead %= this.fftBufferSize;
558
+
559
+ // plot
560
+ if (this.fPlotHandler) {
561
+ this.port.postMessage({
562
+ type: 'plot',
563
+ value: output,
564
+ index: this.fBufferNum++,
565
+ events: this.fCachedEvents
566
+ });
567
+ this.fCachedEvents = [];
568
+ }
569
+ return true;
570
+ }
571
+
572
+ protected handleMessageAux = (e: MessageEvent) => {
573
+ // use arrow function for binding
574
+ const msg = e.data;
575
+
576
+ switch (msg.type) {
577
+ // Generic MIDI message
578
+ case 'midi':
579
+ this.midiMessage(msg.data);
580
+ break;
581
+ // Typed MIDI message
582
+ case 'ctrlChange':
583
+ this.ctrlChange(msg.data[0], msg.data[1], msg.data[2]);
584
+ break;
585
+ case 'pitchWheel':
586
+ this.pitchWheel(msg.data[0], msg.data[1]);
587
+ break;
588
+ // Generic data message
589
+ case 'param':
590
+ this.setParamValue(msg.data.path, msg.data.value);
591
+ break;
592
+ // Plot handler set on demand
593
+ case 'setPlotHandler': {
594
+ if (msg.data) {
595
+ this.fPlotHandler = (output, index, events) => {
596
+ if (events) this.fCachedEvents.push(...events);
597
+ };
598
+ } else {
599
+ this.fPlotHandler = null;
600
+ }
601
+ this.fDSPCode?.setPlotHandler(this.fPlotHandler);
602
+ break;
603
+ }
604
+ case 'setupWamEventHandler': {
605
+ this.setupWamEventHandler();
606
+ break;
607
+ }
608
+ case 'start': {
609
+ this.fDSPCode?.start();
610
+ break;
611
+ }
612
+ case 'stop': {
613
+ this.fDSPCode?.stop();
614
+ break;
615
+ }
616
+ case 'destroy': {
617
+ this.port.close();
618
+ this.destroy();
619
+ break;
620
+ }
621
+ default:
622
+ break;
623
+ }
624
+ };
625
+
626
+ protected setParamValue(path: string, value: number) {
627
+ this.fDSPCode?.setParamValue(path, value);
628
+ this.paramValuesCache[path] = value;
629
+ }
630
+
631
+ protected midiMessage(data: number[] | Uint8Array) {
632
+ this.fDSPCode?.midiMessage(data);
633
+ }
634
+
635
+ protected ctrlChange(channel: number, ctrl: number, value: number) {
636
+ this.fDSPCode?.ctrlChange(channel, ctrl, value);
637
+ }
638
+
639
+ protected pitchWheel(channel: number, wheel: number) {
640
+ this.fDSPCode?.pitchWheel(channel, wheel);
641
+ }
642
+
643
+ protected propagateAcc(
644
+ accelerationIncludingGravity: NonNullable<
645
+ DeviceMotionEvent['accelerationIncludingGravity']
646
+ >,
647
+ invert: boolean = false
648
+ ) {
649
+ this.fDSPCode.propagateAcc(accelerationIncludingGravity, invert);
650
+ }
651
+
652
+ protected propagateGyr(
653
+ event: Pick<DeviceOrientationEvent, 'alpha' | 'beta' | 'gamma'>
654
+ ) {
655
+ this.fDSPCode.propagateGyr(event);
656
+ }
657
+
658
+ resetFFT(
659
+ sizeIn: number,
660
+ overlapIn: number,
661
+ windowFunctionIn: number,
662
+ inputChannels: number,
663
+ outputChannels: number,
664
+ bufferSize: number
665
+ ) {
666
+ const fftSize = ~~ceil(Math.max(2, sizeIn || 1024), 2);
667
+ const fftOverlap = ~~Math.min(fftSize, Math.max(1, overlapIn));
668
+ const fftHopSize = ~~Math.max(1, fftSize / fftOverlap);
669
+ const latency = fftSize - Math.min(fftHopSize, bufferSize);
670
+ let windowFunction: TWindowFunction | null = null;
671
+
672
+ // set the window function from the injected list
673
+ if (windowFunctionIn !== 0) {
674
+ windowFunction =
675
+ typeof windowFunctions === 'object'
676
+ ? windowFunctions[~~windowFunctionIn - 1] || null
677
+ : null;
678
+ }
679
+ const fftSizeChanged = fftSize !== this.fftSize;
680
+
681
+ const fftOverlapChanged = fftOverlap !== this.fftOverlap;
682
+ // Reset FFT vars if the size is changed
683
+ if (fftSizeChanged || fftOverlapChanged) {
684
+ this.fftSize = fftSize;
685
+ this.fftOverlap = fftOverlap;
686
+ this.fftHopSize = fftHopSize;
687
+ this.$inputWrite = 0;
688
+ this.$inputRead = 0;
689
+ this.$outputWrite = 0;
690
+ this.$outputRead = -latency;
691
+ this.fftBufferSize = Math.max(
692
+ fftSize * 2 - this.fftHopSize,
693
+ bufferSize * 2
694
+ );
695
+ if (!fftSizeChanged && this.fftHopSizeParam)
696
+ this.fDSPCode?.setParamValue(
697
+ this.fftHopSizeParam,
698
+ this.fftHopSize
699
+ );
700
+ }
701
+
702
+ // Reset the FFT interface and the Faust Processor
703
+ if (fftSizeChanged) {
704
+ this.rfft?.dispose();
705
+ this.rfft = new this.FFT(fftSize);
706
+ this.noIFFTBuffer = new Float32Array(this.fftSize);
707
+ this.createFFTProcessor();
708
+ }
709
+
710
+ // Calculate a window from the window function, prepare the windowSumSquare buffer
711
+ if (
712
+ fftSizeChanged ||
713
+ fftOverlapChanged ||
714
+ windowFunction !== this.windowFunction
715
+ ) {
716
+ this.windowFunction = windowFunction;
717
+ this.window = new Float32Array(fftSize);
718
+ this.window.fill(1);
719
+ if (windowFunction) apply(this.window, windowFunction);
720
+ this.windowSumSquare = new Float32Array(this.fftBufferSize);
721
+ }
722
+
723
+ // Reset FFT I/O buffers if necessary
724
+ if (this.fftInput.length > inputChannels) {
725
+ this.fftInput.splice(inputChannels);
726
+ }
727
+ if (this.fftOutput.length > outputChannels) {
728
+ this.fftOutput.splice(outputChannels);
729
+ }
730
+ if (fftSizeChanged || fftOverlapChanged) {
731
+ for (let i = 0; i < inputChannels; i++) {
732
+ this.fftInput[i] = new Float32Array(this.fftBufferSize);
733
+ }
734
+ for (let i = 0; i < outputChannels; i++) {
735
+ this.fftOutput[i] = new Float32Array(this.fftBufferSize);
736
+ }
737
+ } else {
738
+ if (this.fftInput.length < inputChannels) {
739
+ for (let i = this.fftInput.length; i < inputChannels; i++) {
740
+ this.fftInput[i] = new Float32Array(this.fftBufferSize);
741
+ }
742
+ }
743
+ if (this.fftOutput.length < outputChannels) {
744
+ for (
745
+ let i = this.fftOutput.length;
746
+ i < outputChannels;
747
+ i++
748
+ ) {
749
+ this.fftOutput[i] = new Float32Array(
750
+ this.fftBufferSize
751
+ );
752
+ }
753
+ }
754
+ }
755
+ }
756
+ async createFFTProcessor() {
757
+ this.fDSPCode?.stop();
758
+ this.fDSPCode?.destroy();
759
+
760
+ // Create Monophonic DSP
761
+ this.fDSPCode = new FaustMonoWebAudioDsp(
762
+ this.dspInstance,
763
+ sampleRate,
764
+ this.sampleSize,
765
+ this.fftProcessorBufferSize,
766
+ this.soundfiles
767
+ );
768
+
769
+ // Setup output handler
770
+ this.fDSPCode.setOutputParamHandler((path, value) =>
771
+ this.port.postMessage({ path, value, type: 'param' })
772
+ );
773
+ this.fDSPCode.setPlotHandler(this.fPlotHandler);
774
+ const params = this.fDSPCode.getParams();
775
+ this.fDSPCode.start();
776
+ // Write the cached parameters
777
+ for (const path in this.paramValuesCache) {
778
+ if (fftParamKeywords.find((k) => `/${path}`.endsWith(k)))
779
+ continue;
780
+ this.fDSPCode.setParamValue(path, this.paramValuesCache[path]);
781
+ }
782
+ // Write the FFT reverved parameters
783
+ const fftSizeParam = params.find((s) => s.endsWith('/fftSize'));
784
+ if (fftSizeParam)
785
+ this.fDSPCode.setParamValue(fftSizeParam, this.fftSize);
786
+ this.fftHopSizeParam = params.find((s) =>
787
+ s.endsWith('/fftHopSize')
788
+ );
789
+ if (this.fftHopSizeParam)
790
+ this.fDSPCode.setParamValue(
791
+ this.fftHopSizeParam,
792
+ this.fftHopSize
793
+ );
794
+ // Prepare a array of zeros for furthur usage
795
+ this.fftProcessorZeros = new Float32Array(
796
+ this.fftProcessorBufferSize
797
+ );
798
+ }
799
+ destroy() {
800
+ this.fDSPCode?.stop();
801
+ this.fDSPCode?.destroy();
802
+ this.rfft?.dispose();
803
+ this.destroyed = true;
804
+ }
805
+ }
806
+
807
+ const Processor = FaustFFTAudioWorkletProcessor;
808
+ if (register) {
809
+ try {
810
+ registerProcessor(
811
+ processorName || dspName || 'myfftdsp',
812
+ Processor
813
+ );
814
+ } catch (error) {
815
+ console.warn(error);
816
+ }
817
+ }
818
+
819
+ return FaustFFTAudioWorkletProcessor;
820
+ };
821
+
822
+ export default getFaustFFTAudioWorkletProcessor;