@e9g/buffered-audio-nodes-utils 0.1.0
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/dist/index.d.ts +121 -0
- package/dist/index.js +773 -0
- package/package.json +39 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ExecutionProvider } from 'buffered-audio-nodes-core';
|
|
2
|
+
|
|
3
|
+
declare function bandpass(channels: Array<Float32Array>, sampleRate: number, highPass?: number, lowPass?: number): void;
|
|
4
|
+
|
|
5
|
+
interface BiquadCoefficients {
|
|
6
|
+
fb: [number, number, number];
|
|
7
|
+
fa: [number, number, number];
|
|
8
|
+
}
|
|
9
|
+
declare function biquadFilter(samples: Float32Array, fb: [number, number, number], fa: [number, number, number]): Float32Array;
|
|
10
|
+
declare function zeroPhaseBiquadFilter(signal: Float32Array, coefficients: BiquadCoefficients): void;
|
|
11
|
+
declare function lowPassCoefficients(sampleRate: number, frequency: number): BiquadCoefficients;
|
|
12
|
+
declare function highPassCoefficients(sampleRate: number, frequency: number): BiquadCoefficients;
|
|
13
|
+
declare function bandPassCoefficients(sampleRate: number, centerFreq: number, quality: number): BiquadCoefficients;
|
|
14
|
+
declare function preFilterCoefficients(sampleRate: number): BiquadCoefficients;
|
|
15
|
+
declare function rlbFilterCoefficients(sampleRate: number): BiquadCoefficients;
|
|
16
|
+
|
|
17
|
+
declare function dbToLinear(db: number): number;
|
|
18
|
+
declare function linearToDb(linear: number): number;
|
|
19
|
+
|
|
20
|
+
declare function smoothEnvelope(envelope: Float32Array, windowSize: number, scratch?: Float32Array): void;
|
|
21
|
+
|
|
22
|
+
type FftBackend = "vkfft" | "fftw" | "js";
|
|
23
|
+
interface FftAddon {
|
|
24
|
+
batchFft(input: Float32Array, fftSize: number, batchCount: number): {
|
|
25
|
+
re: Float32Array;
|
|
26
|
+
im: Float32Array;
|
|
27
|
+
};
|
|
28
|
+
batchIfft(re: Float32Array, im: Float32Array, fftSize: number, batchCount: number): Float32Array;
|
|
29
|
+
}
|
|
30
|
+
declare function detectFftBackend(executionProviders: ReadonlyArray<ExecutionProvider>, options?: {
|
|
31
|
+
vkfftPath?: string;
|
|
32
|
+
fftwPath?: string;
|
|
33
|
+
}): FftBackend;
|
|
34
|
+
interface FftBackendConfig {
|
|
35
|
+
readonly backend: FftBackend;
|
|
36
|
+
readonly addonOptions: {
|
|
37
|
+
vkfftPath?: string;
|
|
38
|
+
fftwPath?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
declare function initFftBackend(executionProviders: ReadonlyArray<ExecutionProvider>, properties: {
|
|
42
|
+
vkfftAddonPath?: string;
|
|
43
|
+
fftwAddonPath?: string;
|
|
44
|
+
}): FftBackendConfig;
|
|
45
|
+
declare function getFftAddon(backend: FftBackend, options?: {
|
|
46
|
+
vkfftPath?: string;
|
|
47
|
+
fftwPath?: string;
|
|
48
|
+
}): FftAddon | null;
|
|
49
|
+
|
|
50
|
+
declare function interleave(samples: Array<Float32Array>, frames: number, channels: number): Float32Array;
|
|
51
|
+
declare function deinterleaveBuffer(buffer: Buffer, channels: number): Array<Float32Array>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mixed-radix FFT for non-power-of-2 sizes.
|
|
55
|
+
* Uses Cooley-Tukey decimation-in-time with radix-2, radix-3, and radix-5 butterflies.
|
|
56
|
+
* All state is per-instance (safe for concurrent use).
|
|
57
|
+
*/
|
|
58
|
+
declare class MixedRadixFft {
|
|
59
|
+
private readonly size;
|
|
60
|
+
private readonly radices;
|
|
61
|
+
private readonly permutation;
|
|
62
|
+
private readonly twiddleRe;
|
|
63
|
+
private readonly twiddleIm;
|
|
64
|
+
readonly frameRe: Float32Array;
|
|
65
|
+
readonly frameIm: Float32Array;
|
|
66
|
+
readonly outRe: Float32Array;
|
|
67
|
+
readonly outIm: Float32Array;
|
|
68
|
+
private readonly auxIm;
|
|
69
|
+
constructor(size: number);
|
|
70
|
+
fft(xRe: Float32Array, xIm: Float32Array, outRe: Float32Array, outIm: Float32Array): void;
|
|
71
|
+
ifft(xRe: Float32Array, xIm: Float32Array, outRe: Float32Array, outIm: Float32Array): void;
|
|
72
|
+
private radix2;
|
|
73
|
+
private radix3;
|
|
74
|
+
private radix5;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare function replaceChannel(chunk: {
|
|
78
|
+
readonly samples: Array<Float32Array>;
|
|
79
|
+
}, ch: number, newData: Float32Array, channels: number): Array<Float32Array>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resample audio channels by spawning ffmpeg directly.
|
|
83
|
+
* This avoids TransformStream nesting deadlocks when called from within _process().
|
|
84
|
+
*/
|
|
85
|
+
declare function resampleDirect(ffmpegPath: string, channels: Array<Float32Array>, sourceSampleRate: number, targetSampleRate: number): Promise<Array<Float32Array>>;
|
|
86
|
+
|
|
87
|
+
interface StftResult {
|
|
88
|
+
readonly real: Array<Float32Array>;
|
|
89
|
+
readonly imag: Array<Float32Array>;
|
|
90
|
+
readonly frames: number;
|
|
91
|
+
readonly fftSize: number;
|
|
92
|
+
}
|
|
93
|
+
interface StftOutput {
|
|
94
|
+
readonly real: Array<Float32Array>;
|
|
95
|
+
readonly imag: Array<Float32Array>;
|
|
96
|
+
}
|
|
97
|
+
declare function stft(signal: Float32Array, fftSize: number, hopSize: number, output?: StftOutput, backend?: FftBackend, fftAddonOptions?: {
|
|
98
|
+
vkfftPath?: string;
|
|
99
|
+
fftwPath?: string;
|
|
100
|
+
}): StftResult;
|
|
101
|
+
declare function istft(result: StftResult, hopSize: number, outputLength: number, backend?: FftBackend, fftAddonOptions?: {
|
|
102
|
+
vkfftPath?: string;
|
|
103
|
+
fftwPath?: string;
|
|
104
|
+
}): Float32Array;
|
|
105
|
+
declare function hanningWindow(size: number, periodic?: boolean): Float32Array;
|
|
106
|
+
interface FftWorkspace {
|
|
107
|
+
re: Float32Array;
|
|
108
|
+
im: Float32Array;
|
|
109
|
+
outRe: Float32Array;
|
|
110
|
+
outIm: Float32Array;
|
|
111
|
+
}
|
|
112
|
+
declare function createFftWorkspace(size: number): FftWorkspace;
|
|
113
|
+
declare function fft(input: Float32Array, workspace?: FftWorkspace): {
|
|
114
|
+
re: Float32Array;
|
|
115
|
+
im: Float32Array;
|
|
116
|
+
};
|
|
117
|
+
declare function ifft(re: Float32Array, im: Float32Array, workspace?: FftWorkspace): Float32Array;
|
|
118
|
+
declare function bitReverse(re: Float32Array, im: Float32Array, size: number): void;
|
|
119
|
+
declare function butterflyStages(re: Float32Array, im: Float32Array, size: number): void;
|
|
120
|
+
|
|
121
|
+
export { type BiquadCoefficients, type FftBackend, type FftBackendConfig, type FftWorkspace, MixedRadixFft, type StftOutput, type StftResult, bandPassCoefficients, bandpass, biquadFilter, bitReverse, butterflyStages, createFftWorkspace, dbToLinear, deinterleaveBuffer, detectFftBackend, fft, getFftAddon, hanningWindow, highPassCoefficients, ifft, initFftBackend, interleave, istft, linearToDb, lowPassCoefficients, preFilterCoefficients, replaceChannel, resampleDirect, rlbFilterCoefficients, smoothEnvelope, stft, zeroPhaseBiquadFilter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
|
|
4
|
+
// src/biquad.ts
|
|
5
|
+
function biquadFilter(samples, fb, fa) {
|
|
6
|
+
const output = new Float32Array(samples.length);
|
|
7
|
+
let x1 = 0;
|
|
8
|
+
let x2 = 0;
|
|
9
|
+
let y1 = 0;
|
|
10
|
+
let y2 = 0;
|
|
11
|
+
for (let index = 0; index < samples.length; index++) {
|
|
12
|
+
const x0 = samples[index] ?? 0;
|
|
13
|
+
const y0 = fb[0] * x0 + fb[1] * x1 + fb[2] * x2 - fa[1] * y1 - fa[2] * y2;
|
|
14
|
+
output[index] = y0;
|
|
15
|
+
x2 = x1;
|
|
16
|
+
x1 = x0;
|
|
17
|
+
y2 = y1;
|
|
18
|
+
y1 = y0;
|
|
19
|
+
}
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
function zeroPhaseBiquadFilter(signal, coefficients) {
|
|
23
|
+
const { fb, fa } = coefficients;
|
|
24
|
+
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
25
|
+
for (let index = 0; index < signal.length; index++) {
|
|
26
|
+
const x0 = signal[index] ?? 0;
|
|
27
|
+
const y0 = fb[0] * x0 + fb[1] * x1 + fb[2] * x2 - fa[1] * y1 - fa[2] * y2;
|
|
28
|
+
signal[index] = y0;
|
|
29
|
+
x2 = x1;
|
|
30
|
+
x1 = x0;
|
|
31
|
+
y2 = y1;
|
|
32
|
+
y1 = y0;
|
|
33
|
+
}
|
|
34
|
+
x1 = 0;
|
|
35
|
+
x2 = 0;
|
|
36
|
+
y1 = 0;
|
|
37
|
+
y2 = 0;
|
|
38
|
+
for (let index = signal.length - 1; index >= 0; index--) {
|
|
39
|
+
const x0 = signal[index] ?? 0;
|
|
40
|
+
const y0 = fb[0] * x0 + fb[1] * x1 + fb[2] * x2 - fa[1] * y1 - fa[2] * y2;
|
|
41
|
+
signal[index] = y0;
|
|
42
|
+
x2 = x1;
|
|
43
|
+
x1 = x0;
|
|
44
|
+
y2 = y1;
|
|
45
|
+
y1 = y0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function lowPassCoefficients(sampleRate, frequency) {
|
|
49
|
+
const w0 = 2 * Math.PI * frequency / sampleRate;
|
|
50
|
+
const cosW0 = Math.cos(w0);
|
|
51
|
+
const sinW0 = Math.sin(w0);
|
|
52
|
+
const alpha = sinW0 / Math.SQRT2;
|
|
53
|
+
const a0 = 1 + alpha;
|
|
54
|
+
return {
|
|
55
|
+
fb: [(1 - cosW0) / 2 / a0, (1 - cosW0) / a0, (1 - cosW0) / 2 / a0],
|
|
56
|
+
fa: [1, -2 * cosW0 / a0, (1 - alpha) / a0]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function highPassCoefficients(sampleRate, frequency) {
|
|
60
|
+
const w0 = 2 * Math.PI * frequency / sampleRate;
|
|
61
|
+
const cosW0 = Math.cos(w0);
|
|
62
|
+
const sinW0 = Math.sin(w0);
|
|
63
|
+
const alpha = sinW0 / Math.SQRT2;
|
|
64
|
+
const a0 = 1 + alpha;
|
|
65
|
+
return {
|
|
66
|
+
fb: [(1 + cosW0) / 2 / a0, -(1 + cosW0) / a0, (1 + cosW0) / 2 / a0],
|
|
67
|
+
fa: [1, -2 * cosW0 / a0, (1 - alpha) / a0]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function bandPassCoefficients(sampleRate, centerFreq, quality) {
|
|
71
|
+
const w0 = 2 * Math.PI * centerFreq / sampleRate;
|
|
72
|
+
const cosW0 = Math.cos(w0);
|
|
73
|
+
const sinW0 = Math.sin(w0);
|
|
74
|
+
const alpha = sinW0 / (2 * quality);
|
|
75
|
+
const a0 = 1 + alpha;
|
|
76
|
+
return {
|
|
77
|
+
fb: [alpha / a0, 0, -alpha / a0],
|
|
78
|
+
fa: [1, -2 * cosW0 / a0, (1 - alpha) / a0]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function preFilterCoefficients(sampleRate) {
|
|
82
|
+
if (sampleRate === 48e3) {
|
|
83
|
+
return {
|
|
84
|
+
fb: [1.53512485958697, -2.69169618940638, 1.19839281085285],
|
|
85
|
+
fa: [1, -1.69065929318241, 0.73248077421585]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const freq = 1681.974450955533;
|
|
89
|
+
const gain = 3.999843853973347;
|
|
90
|
+
const quality = 0.7071752369554196;
|
|
91
|
+
const kk = Math.tan(Math.PI * freq / sampleRate);
|
|
92
|
+
const vh = Math.pow(10, gain / 20);
|
|
93
|
+
const vb = Math.pow(vh, 0.4996667741545416);
|
|
94
|
+
const a0 = 1 + kk / quality + kk * kk;
|
|
95
|
+
return {
|
|
96
|
+
fb: [(vh + vb * kk / quality + kk * kk) / a0, 2 * (kk * kk - vh) / a0, (vh - vb * kk / quality + kk * kk) / a0],
|
|
97
|
+
fa: [1, 2 * (kk * kk - 1) / a0, (1 - kk / quality + kk * kk) / a0]
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function rlbFilterCoefficients(sampleRate) {
|
|
101
|
+
if (sampleRate === 48e3) {
|
|
102
|
+
return {
|
|
103
|
+
fb: [1, -2, 1],
|
|
104
|
+
fa: [1, -1.99004745483398, 0.99007225036621]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const freq = 38.13547087602444;
|
|
108
|
+
const quality = 0.5003270373238773;
|
|
109
|
+
const kk = Math.tan(Math.PI * freq / sampleRate);
|
|
110
|
+
const a0 = 1 + kk / quality + kk * kk;
|
|
111
|
+
return {
|
|
112
|
+
fb: [1 / a0, -2 / a0, 1 / a0],
|
|
113
|
+
fa: [1, 2 * (kk * kk - 1) / a0, (1 - kk / quality + kk * kk) / a0]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/bandpass.ts
|
|
118
|
+
function bandpass(channels, sampleRate, highPass, lowPass) {
|
|
119
|
+
if (!highPass && !lowPass) return;
|
|
120
|
+
for (const channel of channels) {
|
|
121
|
+
if (highPass) {
|
|
122
|
+
zeroPhaseBiquadFilter(channel, highPassCoefficients(sampleRate, highPass));
|
|
123
|
+
}
|
|
124
|
+
if (lowPass) {
|
|
125
|
+
zeroPhaseBiquadFilter(channel, lowPassCoefficients(sampleRate, lowPass));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/db.ts
|
|
131
|
+
function dbToLinear(db) {
|
|
132
|
+
if (db === -Infinity) return 0;
|
|
133
|
+
return Math.pow(10, db / 20);
|
|
134
|
+
}
|
|
135
|
+
function linearToDb(linear) {
|
|
136
|
+
return 20 * Math.log10(Math.max(linear, 1e-10));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/envelope.ts
|
|
140
|
+
function smoothEnvelope(envelope, windowSize, scratch) {
|
|
141
|
+
const halfWin = Math.floor(windowSize / 2);
|
|
142
|
+
const len = envelope.length;
|
|
143
|
+
const source = scratch ?? Float32Array.from(envelope);
|
|
144
|
+
if (scratch) {
|
|
145
|
+
source.set(envelope);
|
|
146
|
+
}
|
|
147
|
+
let sum = 0;
|
|
148
|
+
let count = 0;
|
|
149
|
+
for (let index = 0; index < Math.min(halfWin, len); index++) {
|
|
150
|
+
sum += source[index] ?? 0;
|
|
151
|
+
count++;
|
|
152
|
+
}
|
|
153
|
+
for (let index = 0; index < len; index++) {
|
|
154
|
+
const addIdx = index + halfWin;
|
|
155
|
+
if (addIdx < len) {
|
|
156
|
+
sum += source[addIdx] ?? 0;
|
|
157
|
+
count++;
|
|
158
|
+
}
|
|
159
|
+
const removeIdx = index - halfWin - 1;
|
|
160
|
+
if (removeIdx >= 0) {
|
|
161
|
+
sum -= source[removeIdx] ?? 0;
|
|
162
|
+
count--;
|
|
163
|
+
}
|
|
164
|
+
envelope[index] = sum / Math.max(count, 1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
var require2 = createRequire(import.meta.url);
|
|
168
|
+
function tryLoadVkfft(vkfftPath) {
|
|
169
|
+
if (!vkfftPath) return null;
|
|
170
|
+
try {
|
|
171
|
+
return require2(vkfftPath);
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function tryLoadFftw(fftwPath) {
|
|
177
|
+
if (!fftwPath) return null;
|
|
178
|
+
try {
|
|
179
|
+
return require2(fftwPath);
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function detectFftBackend(executionProviders, options) {
|
|
185
|
+
for (const provider of executionProviders) {
|
|
186
|
+
if (provider === "gpu") {
|
|
187
|
+
const vkfft = tryLoadVkfft(options?.vkfftPath);
|
|
188
|
+
if (vkfft) {
|
|
189
|
+
const device = vkfft.detectDevice();
|
|
190
|
+
if (device) {
|
|
191
|
+
return "vkfft";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (provider === "cpu-native") {
|
|
196
|
+
const fftw = tryLoadFftw(options?.fftwPath);
|
|
197
|
+
if (fftw) {
|
|
198
|
+
return "fftw";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (provider === "cpu") {
|
|
202
|
+
return "js";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return "js";
|
|
206
|
+
}
|
|
207
|
+
function initFftBackend(executionProviders, properties) {
|
|
208
|
+
const addonOptions = { vkfftPath: properties.vkfftAddonPath, fftwPath: properties.fftwAddonPath };
|
|
209
|
+
const backend = detectFftBackend(executionProviders, addonOptions);
|
|
210
|
+
return { backend, addonOptions };
|
|
211
|
+
}
|
|
212
|
+
function getFftAddon(backend, options) {
|
|
213
|
+
if (backend === "vkfft") return tryLoadVkfft(options?.vkfftPath);
|
|
214
|
+
if (backend === "fftw") return tryLoadFftw(options?.fftwPath);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/interleave.ts
|
|
219
|
+
function interleave(samples, frames, channels) {
|
|
220
|
+
const interleaved = new Float32Array(frames * channels);
|
|
221
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
222
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
223
|
+
interleaved[frame * channels + ch] = samples[ch]?.[frame] ?? 0;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return interleaved;
|
|
227
|
+
}
|
|
228
|
+
function deinterleaveBuffer(buffer, channels) {
|
|
229
|
+
const totalSamples = buffer.length / 4;
|
|
230
|
+
const frames = Math.floor(totalSamples / channels);
|
|
231
|
+
const result = [];
|
|
232
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
233
|
+
result.push(new Float32Array(frames));
|
|
234
|
+
}
|
|
235
|
+
const view = new Float32Array(buffer.buffer, buffer.byteOffset, totalSamples);
|
|
236
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
237
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
238
|
+
const channelArray = result[ch];
|
|
239
|
+
const value = view[frame * channels + ch];
|
|
240
|
+
if (channelArray && value !== void 0) {
|
|
241
|
+
channelArray[frame] = value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/mixed-radix-fft.ts
|
|
249
|
+
var MixedRadixFft = class {
|
|
250
|
+
constructor(size) {
|
|
251
|
+
this.size = size;
|
|
252
|
+
this.radices = factorize(size);
|
|
253
|
+
this.frameRe = new Float32Array(size);
|
|
254
|
+
this.frameIm = new Float32Array(size);
|
|
255
|
+
this.outRe = new Float32Array(size);
|
|
256
|
+
this.outIm = new Float32Array(size);
|
|
257
|
+
this.auxIm = new Float32Array(size);
|
|
258
|
+
this.permutation = computePermutation(size, this.radices);
|
|
259
|
+
const { twiddleRe, twiddleIm } = computeTwiddles(this.radices);
|
|
260
|
+
this.twiddleRe = twiddleRe;
|
|
261
|
+
this.twiddleIm = twiddleIm;
|
|
262
|
+
}
|
|
263
|
+
fft(xRe, xIm, outRe, outIm) {
|
|
264
|
+
const perm = this.permutation;
|
|
265
|
+
const nn = this.size;
|
|
266
|
+
for (let index = 0; index < nn; index++) {
|
|
267
|
+
const pp = perm[index] ?? 0;
|
|
268
|
+
outRe[index] = xRe[pp] ?? 0;
|
|
269
|
+
outIm[index] = xIm[pp] ?? 0;
|
|
270
|
+
}
|
|
271
|
+
let groupSize = 1;
|
|
272
|
+
let twOffset = 0;
|
|
273
|
+
for (const radix of this.radices) {
|
|
274
|
+
groupSize *= radix;
|
|
275
|
+
const subSize = groupSize / radix;
|
|
276
|
+
if (radix === 2) {
|
|
277
|
+
twOffset = this.radix2(outRe, outIm, nn, groupSize, subSize, twOffset);
|
|
278
|
+
} else if (radix === 3) {
|
|
279
|
+
twOffset = this.radix3(outRe, outIm, nn, groupSize, subSize, twOffset);
|
|
280
|
+
} else if (radix === 5) {
|
|
281
|
+
twOffset = this.radix5(outRe, outIm, nn, groupSize, subSize, twOffset);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
ifft(xRe, xIm, outRe, outIm) {
|
|
286
|
+
const auxIm = this.auxIm;
|
|
287
|
+
const nn = this.size;
|
|
288
|
+
for (let index = 0; index < nn; index++) {
|
|
289
|
+
auxIm[index] = -(xIm[index] ?? 0);
|
|
290
|
+
}
|
|
291
|
+
this.fft(xRe, auxIm, outRe, outIm);
|
|
292
|
+
for (let index = 0; index < nn; index++) {
|
|
293
|
+
outRe[index] = (outRe[index] ?? 0) / nn;
|
|
294
|
+
outIm[index] = -(outIm[index] ?? 0) / nn;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
radix2(outRe, outIm, nn, groupSize, subSize, twOffset) {
|
|
298
|
+
for (let group = 0; group < nn; group += groupSize) {
|
|
299
|
+
for (let ni = 0; ni < subSize; ni++) {
|
|
300
|
+
const idx0 = group + ni;
|
|
301
|
+
const idx1 = idx0 + subSize;
|
|
302
|
+
const twRe = ni === 0 ? 1 : this.twiddleRe[twOffset + ni - 1] ?? 0;
|
|
303
|
+
const twIm = ni === 0 ? 0 : this.twiddleIm[twOffset + ni - 1] ?? 0;
|
|
304
|
+
const tRe = (outRe[idx1] ?? 0) * twRe - (outIm[idx1] ?? 0) * twIm;
|
|
305
|
+
const tIm = (outRe[idx1] ?? 0) * twIm + (outIm[idx1] ?? 0) * twRe;
|
|
306
|
+
outRe[idx1] = (outRe[idx0] ?? 0) - tRe;
|
|
307
|
+
outIm[idx1] = (outIm[idx0] ?? 0) - tIm;
|
|
308
|
+
outRe[idx0] = (outRe[idx0] ?? 0) + tRe;
|
|
309
|
+
outIm[idx0] = (outIm[idx0] ?? 0) + tIm;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return twOffset + subSize - 1;
|
|
313
|
+
}
|
|
314
|
+
radix3(outRe, outIm, nn, groupSize, subSize, twOffset) {
|
|
315
|
+
const c3 = -0.5;
|
|
316
|
+
const s3 = -Math.sqrt(3) / 2;
|
|
317
|
+
for (let group = 0; group < nn; group += groupSize) {
|
|
318
|
+
for (let ni = 0; ni < subSize; ni++) {
|
|
319
|
+
const idx0 = group + ni;
|
|
320
|
+
const idx1 = idx0 + subSize;
|
|
321
|
+
const idx2 = idx0 + 2 * subSize;
|
|
322
|
+
let tw1Re, tw1Im, tw2Re, tw2Im;
|
|
323
|
+
if (ni === 0) {
|
|
324
|
+
tw1Re = 1;
|
|
325
|
+
tw1Im = 0;
|
|
326
|
+
tw2Re = 1;
|
|
327
|
+
tw2Im = 0;
|
|
328
|
+
} else {
|
|
329
|
+
tw1Re = this.twiddleRe[twOffset + ni - 1] ?? 0;
|
|
330
|
+
tw1Im = this.twiddleIm[twOffset + ni - 1] ?? 0;
|
|
331
|
+
tw2Re = this.twiddleRe[twOffset + subSize - 1 + ni - 1] ?? 0;
|
|
332
|
+
tw2Im = this.twiddleIm[twOffset + subSize - 1 + ni - 1] ?? 0;
|
|
333
|
+
}
|
|
334
|
+
const x1Re = (outRe[idx1] ?? 0) * tw1Re - (outIm[idx1] ?? 0) * tw1Im;
|
|
335
|
+
const x1Im = (outRe[idx1] ?? 0) * tw1Im + (outIm[idx1] ?? 0) * tw1Re;
|
|
336
|
+
const x2Re = (outRe[idx2] ?? 0) * tw2Re - (outIm[idx2] ?? 0) * tw2Im;
|
|
337
|
+
const x2Im = (outRe[idx2] ?? 0) * tw2Im + (outIm[idx2] ?? 0) * tw2Re;
|
|
338
|
+
const x0Re = outRe[idx0] ?? 0;
|
|
339
|
+
const x0Im = outIm[idx0] ?? 0;
|
|
340
|
+
const sumRe = x1Re + x2Re;
|
|
341
|
+
const sumIm = x1Im + x2Im;
|
|
342
|
+
const diffRe = x1Re - x2Re;
|
|
343
|
+
const diffIm = x1Im - x2Im;
|
|
344
|
+
outRe[idx0] = x0Re + sumRe;
|
|
345
|
+
outIm[idx0] = x0Im + sumIm;
|
|
346
|
+
outRe[idx1] = x0Re + c3 * sumRe - s3 * diffIm;
|
|
347
|
+
outIm[idx1] = x0Im + c3 * sumIm + s3 * diffRe;
|
|
348
|
+
outRe[idx2] = x0Re + c3 * sumRe + s3 * diffIm;
|
|
349
|
+
outIm[idx2] = x0Im + c3 * sumIm - s3 * diffRe;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return twOffset + 2 * (subSize - 1);
|
|
353
|
+
}
|
|
354
|
+
radix5(outRe, outIm, nn, groupSize, subSize, twOffset) {
|
|
355
|
+
const cos1 = Math.cos(2 * Math.PI / 5);
|
|
356
|
+
const cos2 = Math.cos(4 * Math.PI / 5);
|
|
357
|
+
const sin1 = -Math.sin(2 * Math.PI / 5);
|
|
358
|
+
const sin2 = -Math.sin(4 * Math.PI / 5);
|
|
359
|
+
for (let group = 0; group < nn; group += groupSize) {
|
|
360
|
+
for (let ni = 0; ni < subSize; ni++) {
|
|
361
|
+
const idx0 = group + ni;
|
|
362
|
+
const idx1 = idx0 + subSize;
|
|
363
|
+
const idx2 = idx0 + 2 * subSize;
|
|
364
|
+
const idx3 = idx0 + 3 * subSize;
|
|
365
|
+
const idx4 = idx0 + 4 * subSize;
|
|
366
|
+
let tw1Re, tw1Im;
|
|
367
|
+
let tw2Re, tw2Im;
|
|
368
|
+
let tw3Re, tw3Im;
|
|
369
|
+
let tw4Re, tw4Im;
|
|
370
|
+
if (ni === 0) {
|
|
371
|
+
tw1Re = 1;
|
|
372
|
+
tw1Im = 0;
|
|
373
|
+
tw2Re = 1;
|
|
374
|
+
tw2Im = 0;
|
|
375
|
+
tw3Re = 1;
|
|
376
|
+
tw3Im = 0;
|
|
377
|
+
tw4Re = 1;
|
|
378
|
+
tw4Im = 0;
|
|
379
|
+
} else {
|
|
380
|
+
tw1Re = this.twiddleRe[twOffset + ni - 1] ?? 0;
|
|
381
|
+
tw1Im = this.twiddleIm[twOffset + ni - 1] ?? 0;
|
|
382
|
+
tw2Re = this.twiddleRe[twOffset + subSize - 1 + ni - 1] ?? 0;
|
|
383
|
+
tw2Im = this.twiddleIm[twOffset + subSize - 1 + ni - 1] ?? 0;
|
|
384
|
+
tw3Re = this.twiddleRe[twOffset + 2 * (subSize - 1) + ni - 1] ?? 0;
|
|
385
|
+
tw3Im = this.twiddleIm[twOffset + 2 * (subSize - 1) + ni - 1] ?? 0;
|
|
386
|
+
tw4Re = this.twiddleRe[twOffset + 3 * (subSize - 1) + ni - 1] ?? 0;
|
|
387
|
+
tw4Im = this.twiddleIm[twOffset + 3 * (subSize - 1) + ni - 1] ?? 0;
|
|
388
|
+
}
|
|
389
|
+
const x0Re = outRe[idx0] ?? 0;
|
|
390
|
+
const x0Im = outIm[idx0] ?? 0;
|
|
391
|
+
const x1Re = (outRe[idx1] ?? 0) * tw1Re - (outIm[idx1] ?? 0) * tw1Im;
|
|
392
|
+
const x1Im = (outRe[idx1] ?? 0) * tw1Im + (outIm[idx1] ?? 0) * tw1Re;
|
|
393
|
+
const x2Re = (outRe[idx2] ?? 0) * tw2Re - (outIm[idx2] ?? 0) * tw2Im;
|
|
394
|
+
const x2Im = (outRe[idx2] ?? 0) * tw2Im + (outIm[idx2] ?? 0) * tw2Re;
|
|
395
|
+
const x3Re = (outRe[idx3] ?? 0) * tw3Re - (outIm[idx3] ?? 0) * tw3Im;
|
|
396
|
+
const x3Im = (outRe[idx3] ?? 0) * tw3Im + (outIm[idx3] ?? 0) * tw3Re;
|
|
397
|
+
const x4Re = (outRe[idx4] ?? 0) * tw4Re - (outIm[idx4] ?? 0) * tw4Im;
|
|
398
|
+
const x4Im = (outRe[idx4] ?? 0) * tw4Im + (outIm[idx4] ?? 0) * tw4Re;
|
|
399
|
+
const sum14Re = x1Re + x4Re;
|
|
400
|
+
const sum14Im = x1Im + x4Im;
|
|
401
|
+
const diff14Re = x1Re - x4Re;
|
|
402
|
+
const diff14Im = x1Im - x4Im;
|
|
403
|
+
const sum23Re = x2Re + x3Re;
|
|
404
|
+
const sum23Im = x2Im + x3Im;
|
|
405
|
+
const diff23Re = x2Re - x3Re;
|
|
406
|
+
const diff23Im = x2Im - x3Im;
|
|
407
|
+
outRe[idx0] = x0Re + sum14Re + sum23Re;
|
|
408
|
+
outIm[idx0] = x0Im + sum14Im + sum23Im;
|
|
409
|
+
outRe[idx1] = x0Re + cos1 * sum14Re + cos2 * sum23Re - sin1 * diff14Im - sin2 * diff23Im;
|
|
410
|
+
outIm[idx1] = x0Im + cos1 * sum14Im + cos2 * sum23Im + sin1 * diff14Re + sin2 * diff23Re;
|
|
411
|
+
outRe[idx2] = x0Re + cos2 * sum14Re + cos1 * sum23Re - sin2 * diff14Im + sin1 * diff23Im;
|
|
412
|
+
outIm[idx2] = x0Im + cos2 * sum14Im + cos1 * sum23Im + sin2 * diff14Re - sin1 * diff23Re;
|
|
413
|
+
outRe[idx3] = x0Re + cos2 * sum14Re + cos1 * sum23Re + sin2 * diff14Im - sin1 * diff23Im;
|
|
414
|
+
outIm[idx3] = x0Im + cos2 * sum14Im + cos1 * sum23Im - sin2 * diff14Re + sin1 * diff23Re;
|
|
415
|
+
outRe[idx4] = x0Re + cos1 * sum14Re + cos2 * sum23Re + sin1 * diff14Im + sin2 * diff23Im;
|
|
416
|
+
outIm[idx4] = x0Im + cos1 * sum14Im + cos2 * sum23Im - sin1 * diff14Re - sin2 * diff23Re;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return twOffset + 4 * (subSize - 1);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
function factorize(size) {
|
|
423
|
+
const factors = [];
|
|
424
|
+
let remaining = size;
|
|
425
|
+
for (const prime of [5, 3, 2]) {
|
|
426
|
+
while (remaining % prime === 0) {
|
|
427
|
+
factors.push(prime);
|
|
428
|
+
remaining /= prime;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (remaining !== 1) {
|
|
432
|
+
throw new Error(`MixedRadixFft: size ${size} has unsupported prime factor ${remaining} (only 2, 3, 5 supported)`);
|
|
433
|
+
}
|
|
434
|
+
factors.sort((lhs, rhs) => lhs - rhs);
|
|
435
|
+
return factors;
|
|
436
|
+
}
|
|
437
|
+
function computePermutation(size, radices) {
|
|
438
|
+
const permutation = new Uint16Array(size);
|
|
439
|
+
for (let index = 0; index < size; index++) {
|
|
440
|
+
let remainder = index;
|
|
441
|
+
let permuted = 0;
|
|
442
|
+
let base = size;
|
|
443
|
+
for (const radix of radices) {
|
|
444
|
+
base = base / radix;
|
|
445
|
+
const digit = remainder % radix;
|
|
446
|
+
remainder = Math.floor(remainder / radix);
|
|
447
|
+
permuted += digit * base;
|
|
448
|
+
}
|
|
449
|
+
permutation[index] = permuted;
|
|
450
|
+
}
|
|
451
|
+
return permutation;
|
|
452
|
+
}
|
|
453
|
+
function computeTwiddles(radices) {
|
|
454
|
+
let totalTwiddles = 0;
|
|
455
|
+
let groupSize = 1;
|
|
456
|
+
for (const radix of radices) {
|
|
457
|
+
groupSize *= radix;
|
|
458
|
+
totalTwiddles += (radix - 1) * (groupSize / radix);
|
|
459
|
+
}
|
|
460
|
+
const twiddleRe = new Float32Array(totalTwiddles);
|
|
461
|
+
const twiddleIm = new Float32Array(totalTwiddles);
|
|
462
|
+
let twOffset = 0;
|
|
463
|
+
groupSize = 1;
|
|
464
|
+
for (const radix of radices) {
|
|
465
|
+
groupSize *= radix;
|
|
466
|
+
const subSize = groupSize / radix;
|
|
467
|
+
for (let kk = 1; kk < radix; kk++) {
|
|
468
|
+
for (let ni = 0; ni < subSize; ni++) {
|
|
469
|
+
const angle = -2 * Math.PI * kk * ni / groupSize;
|
|
470
|
+
twiddleRe[twOffset] = Math.cos(angle);
|
|
471
|
+
twiddleIm[twOffset] = Math.sin(angle);
|
|
472
|
+
twOffset++;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { twiddleRe, twiddleIm };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/replace-channel.ts
|
|
480
|
+
function replaceChannel(chunk, ch, newData, channels) {
|
|
481
|
+
const frames = newData.length;
|
|
482
|
+
const result = [];
|
|
483
|
+
for (let writeCh = 0; writeCh < channels; writeCh++) {
|
|
484
|
+
result.push(writeCh === ch ? newData : chunk.samples[writeCh] ?? new Float32Array(frames));
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
function resampleDirect(ffmpegPath, channels, sourceSampleRate, targetSampleRate) {
|
|
489
|
+
if (sourceSampleRate === targetSampleRate) {
|
|
490
|
+
return Promise.resolve(channels.map((ch) => ch.slice()));
|
|
491
|
+
}
|
|
492
|
+
const numChannels = channels.length;
|
|
493
|
+
const frames = channels[0]?.length ?? 0;
|
|
494
|
+
if (frames === 0 || numChannels === 0) {
|
|
495
|
+
return Promise.resolve(channels);
|
|
496
|
+
}
|
|
497
|
+
return new Promise((resolve, reject) => {
|
|
498
|
+
const args = [
|
|
499
|
+
"-f",
|
|
500
|
+
"f32le",
|
|
501
|
+
"-ar",
|
|
502
|
+
String(sourceSampleRate),
|
|
503
|
+
"-ac",
|
|
504
|
+
String(numChannels),
|
|
505
|
+
"-i",
|
|
506
|
+
"pipe:0",
|
|
507
|
+
"-af",
|
|
508
|
+
`aresample=${targetSampleRate}:resampler=soxr:dither_method=triangular`,
|
|
509
|
+
"-f",
|
|
510
|
+
"f32le",
|
|
511
|
+
"-ar",
|
|
512
|
+
String(targetSampleRate),
|
|
513
|
+
"-ac",
|
|
514
|
+
String(numChannels),
|
|
515
|
+
"pipe:1"
|
|
516
|
+
];
|
|
517
|
+
const proc = spawn(ffmpegPath, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
518
|
+
const stdout = proc.stdout;
|
|
519
|
+
const stdin = proc.stdin;
|
|
520
|
+
const outputChunks = [];
|
|
521
|
+
const stderrChunks = [];
|
|
522
|
+
stdout.on("data", (chunk) => outputChunks.push(chunk));
|
|
523
|
+
proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
524
|
+
proc.on("error", (error) => {
|
|
525
|
+
reject(new Error(`Failed to spawn ffmpeg: ${error.message}`));
|
|
526
|
+
});
|
|
527
|
+
proc.on("close", (code) => {
|
|
528
|
+
if (code !== 0) {
|
|
529
|
+
const stderrOutput = Buffer.concat(stderrChunks).toString();
|
|
530
|
+
reject(new Error(`ffmpeg exited with code ${code}: ${stderrOutput}`));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const outputBuffer = Buffer.concat(outputChunks);
|
|
534
|
+
resolve(deinterleaveBuffer(outputBuffer, numChannels));
|
|
535
|
+
});
|
|
536
|
+
stdin.on("error", () => {
|
|
537
|
+
});
|
|
538
|
+
const interleaved = interleave(channels, frames, numChannels);
|
|
539
|
+
const buf = Buffer.from(interleaved.buffer, interleaved.byteOffset, interleaved.byteLength);
|
|
540
|
+
stdin.write(buf, () => stdin.end());
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/stft.ts
|
|
545
|
+
function stft(signal, fftSize, hopSize, output, backend, fftAddonOptions) {
|
|
546
|
+
const window = hanningWindow(fftSize);
|
|
547
|
+
const numFrames = Math.floor((signal.length - fftSize) / hopSize) + 1;
|
|
548
|
+
const halfSize = fftSize / 2 + 1;
|
|
549
|
+
const addon = backend ? getFftAddon(backend, fftAddonOptions) : null;
|
|
550
|
+
if (addon && numFrames > 0) {
|
|
551
|
+
const batchInput = new Float32Array(fftSize * numFrames);
|
|
552
|
+
for (let frame = 0; frame < numFrames; frame++) {
|
|
553
|
+
const offset = frame * hopSize;
|
|
554
|
+
for (let index = 0; index < fftSize; index++) {
|
|
555
|
+
batchInput[frame * fftSize + index] = (signal[offset + index] ?? 0) * (window[index] ?? 0);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const { re: batchRe, im: batchIm } = addon.batchFft(batchInput, fftSize, numFrames);
|
|
559
|
+
const real2 = output?.real ?? [];
|
|
560
|
+
const imag2 = output?.imag ?? [];
|
|
561
|
+
for (let frame = 0; frame < numFrames; frame++) {
|
|
562
|
+
const reSlice = batchRe.subarray(frame * halfSize, (frame + 1) * halfSize);
|
|
563
|
+
const imSlice = batchIm.subarray(frame * halfSize, (frame + 1) * halfSize);
|
|
564
|
+
if (output) {
|
|
565
|
+
output.real[frame]?.set(reSlice);
|
|
566
|
+
output.imag[frame]?.set(imSlice);
|
|
567
|
+
} else {
|
|
568
|
+
real2.push(Float32Array.from(reSlice));
|
|
569
|
+
imag2.push(Float32Array.from(imSlice));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return { real: real2, imag: imag2, frames: numFrames, fftSize };
|
|
573
|
+
}
|
|
574
|
+
const real = output?.real ?? [];
|
|
575
|
+
const imag = output?.imag ?? [];
|
|
576
|
+
const windowed = new Float32Array(fftSize);
|
|
577
|
+
const workspace = createFftWorkspace(fftSize);
|
|
578
|
+
for (let frame = 0; frame < numFrames; frame++) {
|
|
579
|
+
const offset = frame * hopSize;
|
|
580
|
+
for (let index = 0; index < fftSize; index++) {
|
|
581
|
+
windowed[index] = (signal[offset + index] ?? 0) * (window[index] ?? 0);
|
|
582
|
+
}
|
|
583
|
+
const { re, im } = fft(windowed, workspace);
|
|
584
|
+
if (output) {
|
|
585
|
+
output.real[frame]?.set(re.subarray(0, halfSize));
|
|
586
|
+
output.imag[frame]?.set(im.subarray(0, halfSize));
|
|
587
|
+
} else {
|
|
588
|
+
real.push(re.slice(0, halfSize));
|
|
589
|
+
imag.push(im.slice(0, halfSize));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return { real, imag, frames: numFrames, fftSize };
|
|
593
|
+
}
|
|
594
|
+
function istft(result, hopSize, outputLength, backend, fftAddonOptions) {
|
|
595
|
+
const { real, imag, frames, fftSize } = result;
|
|
596
|
+
const window = hanningWindow(fftSize);
|
|
597
|
+
const output = new Float32Array(outputLength);
|
|
598
|
+
const windowSum = new Float32Array(outputLength);
|
|
599
|
+
const halfSize = fftSize / 2 + 1;
|
|
600
|
+
const addon = backend ? getFftAddon(backend, fftAddonOptions) : null;
|
|
601
|
+
if (addon && frames > 0) {
|
|
602
|
+
const batchRe = new Float32Array(halfSize * frames);
|
|
603
|
+
const batchIm = new Float32Array(halfSize * frames);
|
|
604
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
605
|
+
const re = real[frame];
|
|
606
|
+
const im = imag[frame];
|
|
607
|
+
if (!re || !im) continue;
|
|
608
|
+
batchRe.set(re, frame * halfSize);
|
|
609
|
+
batchIm.set(im, frame * halfSize);
|
|
610
|
+
}
|
|
611
|
+
const timeDomainBatch = addon.batchIfft(batchRe, batchIm, fftSize, frames);
|
|
612
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
613
|
+
const offset = frame * hopSize;
|
|
614
|
+
for (let index = 0; index < fftSize; index++) {
|
|
615
|
+
const pos = offset + index;
|
|
616
|
+
if (pos < outputLength) {
|
|
617
|
+
output[pos] = (output[pos] ?? 0) + (timeDomainBatch[frame * fftSize + index] ?? 0) * (window[index] ?? 0);
|
|
618
|
+
windowSum[pos] = (windowSum[pos] ?? 0) + (window[index] ?? 0) * (window[index] ?? 0);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
const fullRe = new Float32Array(fftSize);
|
|
624
|
+
const fullIm = new Float32Array(fftSize);
|
|
625
|
+
const workspace = createFftWorkspace(fftSize);
|
|
626
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
627
|
+
const re = real[frame];
|
|
628
|
+
const im = imag[frame];
|
|
629
|
+
if (!re || !im) continue;
|
|
630
|
+
fullRe.fill(0);
|
|
631
|
+
fullIm.fill(0);
|
|
632
|
+
fullRe.set(re);
|
|
633
|
+
fullIm.set(im);
|
|
634
|
+
for (let index = 1; index < halfSize - 1; index++) {
|
|
635
|
+
fullRe[fftSize - index] = re[index] ?? 0;
|
|
636
|
+
fullIm[fftSize - index] = -(im[index] ?? 0);
|
|
637
|
+
}
|
|
638
|
+
const timeDomain = ifft(fullRe, fullIm, workspace);
|
|
639
|
+
const offset = frame * hopSize;
|
|
640
|
+
for (let index = 0; index < fftSize; index++) {
|
|
641
|
+
const pos = offset + index;
|
|
642
|
+
if (pos < outputLength) {
|
|
643
|
+
output[pos] = (output[pos] ?? 0) + (timeDomain[index] ?? 0) * (window[index] ?? 0);
|
|
644
|
+
windowSum[pos] = (windowSum[pos] ?? 0) + (window[index] ?? 0) * (window[index] ?? 0);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
for (let index = 0; index < outputLength; index++) {
|
|
650
|
+
const ws = windowSum[index] ?? 0;
|
|
651
|
+
if (ws > 1e-8) {
|
|
652
|
+
output[index] = (output[index] ?? 0) / ws;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return output;
|
|
656
|
+
}
|
|
657
|
+
var hanningWindowCache = /* @__PURE__ */ new Map();
|
|
658
|
+
function hanningWindow(size, periodic = true) {
|
|
659
|
+
const key = `${size}:${periodic ? "p" : "s"}`;
|
|
660
|
+
const cached = hanningWindowCache.get(key);
|
|
661
|
+
if (cached) return cached;
|
|
662
|
+
const window = new Float32Array(size);
|
|
663
|
+
const denominator = periodic ? size : size - 1;
|
|
664
|
+
for (let index = 0; index < size; index++) {
|
|
665
|
+
window[index] = 0.5 * (1 - Math.cos(2 * Math.PI * index / denominator));
|
|
666
|
+
}
|
|
667
|
+
hanningWindowCache.set(key, window);
|
|
668
|
+
return window;
|
|
669
|
+
}
|
|
670
|
+
function createFftWorkspace(size) {
|
|
671
|
+
return {
|
|
672
|
+
re: new Float32Array(size),
|
|
673
|
+
im: new Float32Array(size),
|
|
674
|
+
outRe: new Float32Array(size),
|
|
675
|
+
outIm: new Float32Array(size)
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function fft(input, workspace) {
|
|
679
|
+
const size = input.length;
|
|
680
|
+
const re = workspace ? workspace.re : new Float32Array(size);
|
|
681
|
+
const im = workspace ? workspace.im : new Float32Array(size);
|
|
682
|
+
re.set(input);
|
|
683
|
+
if (workspace) im.fill(0);
|
|
684
|
+
if (size <= 1) return { re, im };
|
|
685
|
+
bitReverse(re, im, size);
|
|
686
|
+
butterflyStages(re, im, size);
|
|
687
|
+
return { re, im };
|
|
688
|
+
}
|
|
689
|
+
function ifft(re, im, workspace) {
|
|
690
|
+
const size = re.length;
|
|
691
|
+
const outRe = workspace ? workspace.outRe : Float32Array.from(re);
|
|
692
|
+
const outIm = workspace ? workspace.outIm : new Float32Array(size);
|
|
693
|
+
if (workspace) outRe.set(re);
|
|
694
|
+
for (let index = 0; index < size; index++) {
|
|
695
|
+
outIm[index] = -(im[index] ?? 0);
|
|
696
|
+
}
|
|
697
|
+
bitReverse(outRe, outIm, size);
|
|
698
|
+
butterflyStages(outRe, outIm, size);
|
|
699
|
+
for (let index = 0; index < size; index++) {
|
|
700
|
+
outRe[index] = (outRe[index] ?? 0) / size;
|
|
701
|
+
}
|
|
702
|
+
return outRe;
|
|
703
|
+
}
|
|
704
|
+
function bitReverse(re, im, size) {
|
|
705
|
+
let rev = 0;
|
|
706
|
+
for (let index = 0; index < size - 1; index++) {
|
|
707
|
+
if (index < rev) {
|
|
708
|
+
const tempRe = re[index];
|
|
709
|
+
const tempIm = im[index];
|
|
710
|
+
re[index] = re[rev];
|
|
711
|
+
im[index] = im[rev];
|
|
712
|
+
re[rev] = tempRe;
|
|
713
|
+
im[rev] = tempIm;
|
|
714
|
+
}
|
|
715
|
+
let bit = size >> 1;
|
|
716
|
+
while (bit <= rev) {
|
|
717
|
+
rev -= bit;
|
|
718
|
+
bit >>= 1;
|
|
719
|
+
}
|
|
720
|
+
rev += bit;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
var twiddleCache = /* @__PURE__ */ new Map();
|
|
724
|
+
function getTwiddleFactors(size) {
|
|
725
|
+
let cached = twiddleCache.get(size);
|
|
726
|
+
if (cached) return cached;
|
|
727
|
+
const totalFactors = size / 2 * Math.log2(size);
|
|
728
|
+
const twRe = new Float32Array(totalFactors);
|
|
729
|
+
const twIm = new Float32Array(totalFactors);
|
|
730
|
+
let offset = 0;
|
|
731
|
+
for (let step = 2; step <= size; step *= 2) {
|
|
732
|
+
const halfStep = step / 2;
|
|
733
|
+
const angle = -2 * Math.PI / step;
|
|
734
|
+
for (let pair = 0; pair < halfStep; pair++) {
|
|
735
|
+
twRe[offset + pair] = Math.cos(angle * pair);
|
|
736
|
+
twIm[offset + pair] = Math.sin(angle * pair);
|
|
737
|
+
}
|
|
738
|
+
offset += halfStep;
|
|
739
|
+
}
|
|
740
|
+
cached = { re: twRe, im: twIm };
|
|
741
|
+
twiddleCache.set(size, cached);
|
|
742
|
+
return cached;
|
|
743
|
+
}
|
|
744
|
+
function butterflyStages(re, im, size) {
|
|
745
|
+
const twiddle = getTwiddleFactors(size);
|
|
746
|
+
const twRe = twiddle.re;
|
|
747
|
+
const twIm = twiddle.im;
|
|
748
|
+
let twOffset = 0;
|
|
749
|
+
for (let step = 2; step <= size; step *= 2) {
|
|
750
|
+
const halfStep = step / 2;
|
|
751
|
+
for (let group = 0; group < size; group += step) {
|
|
752
|
+
for (let pair = 0; pair < halfStep; pair++) {
|
|
753
|
+
const wr = twRe[twOffset + pair];
|
|
754
|
+
const wi = twIm[twOffset + pair];
|
|
755
|
+
const evenIdx = group + pair;
|
|
756
|
+
const oddIdx = group + pair + halfStep;
|
|
757
|
+
const oddRe = re[oddIdx];
|
|
758
|
+
const oddIm = im[oddIdx];
|
|
759
|
+
const evenRe = re[evenIdx];
|
|
760
|
+
const evenIm = im[evenIdx];
|
|
761
|
+
const tRe = oddRe * wr - oddIm * wi;
|
|
762
|
+
const tIm = oddRe * wi + oddIm * wr;
|
|
763
|
+
re[oddIdx] = evenRe - tRe;
|
|
764
|
+
im[oddIdx] = evenIm - tIm;
|
|
765
|
+
re[evenIdx] = evenRe + tRe;
|
|
766
|
+
im[evenIdx] = evenIm + tIm;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
twOffset += halfStep;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export { MixedRadixFft, bandPassCoefficients, bandpass, biquadFilter, bitReverse, butterflyStages, createFftWorkspace, dbToLinear, deinterleaveBuffer, detectFftBackend, fft, getFftAddon, hanningWindow, highPassCoefficients, ifft, initFftBackend, interleave, istft, linearToDb, lowPassCoefficients, preFilterCoefficients, replaceChannel, resampleDirect, rlbFilterCoefficients, smoothEnvelope, stft, zeroPhaseBiquadFilter };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@e9g/buffered-audio-nodes-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"author": "Matt Cavender",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"check": "concurrently \"eslint . --fix --cache --format ../../agent-eslint.js\" \"tsc --noEmit --pretty false 2>&1 | node ../../agent-tsc.js\"",
|
|
22
|
+
"check:verbose": "concurrently \"eslint .\" \"tsc --noEmit\"",
|
|
23
|
+
"lint": "eslint . --fix --cache --format ../../agent-eslint.js",
|
|
24
|
+
"lint:verbose": "eslint .",
|
|
25
|
+
"lint:fix": "eslint . --fix",
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"unit": "vitest run unit"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.3.5",
|
|
31
|
+
"concurrently": "*",
|
|
32
|
+
"tsup": "^8.0.0",
|
|
33
|
+
"typescript": "*",
|
|
34
|
+
"vitest": "^3.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@e9g/buffered-audio-nodes-core": "*"
|
|
38
|
+
}
|
|
39
|
+
}
|