@buffered-audio/nodes 0.18.2 → 0.20.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/README.md +47 -59
- package/dist/chunk-YJWZP2OD.js +2067 -0
- package/dist/cli.js +18 -19
- package/dist/index.d.ts +143 -221
- package/dist/index.js +1067 -3243
- package/dist/nlm-worker.d.ts +2 -0
- package/dist/nlm-worker.js +13 -0
- package/package.json +7 -6
|
@@ -0,0 +1,2067 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
11
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
var __copyProps = (to, from, except, desc) => {
|
|
18
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
19
|
+
for (let key of __getOwnPropNames(from))
|
|
20
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
21
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
22
|
+
}
|
|
23
|
+
return to;
|
|
24
|
+
};
|
|
25
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
26
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
27
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
28
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
29
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
30
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
31
|
+
mod
|
|
32
|
+
));
|
|
33
|
+
var AmplitudeHistogramAccumulator = class {
|
|
34
|
+
constructor(bucketCount) {
|
|
35
|
+
this.bucketMax = 0;
|
|
36
|
+
this.totalSamples = 0;
|
|
37
|
+
this.pendingZeros = 0;
|
|
38
|
+
this.finalized = false;
|
|
39
|
+
if (!Number.isInteger(bucketCount) || bucketCount <= 0) {
|
|
40
|
+
throw new Error(`AmplitudeHistogramAccumulator: bucketCount must be a positive integer, got ${String(bucketCount)}`);
|
|
41
|
+
}
|
|
42
|
+
this.bucketCount = bucketCount;
|
|
43
|
+
this.buckets = new Uint32Array(bucketCount);
|
|
44
|
+
}
|
|
45
|
+
// Consumes `frames` samples per channel; throws if any channel has fewer than `frames` samples or if `finalize` was already called.
|
|
46
|
+
push(channels, frames) {
|
|
47
|
+
if (this.finalized) {
|
|
48
|
+
throw new Error("AmplitudeHistogramAccumulator: push() called after finalize()");
|
|
49
|
+
}
|
|
50
|
+
if (frames <= 0) return;
|
|
51
|
+
for (let channelIndex = 0; channelIndex < channels.length; channelIndex++) {
|
|
52
|
+
const channel = channels[channelIndex];
|
|
53
|
+
if (channel === void 0 || channel.length < frames) {
|
|
54
|
+
throw new Error(`AmplitudeHistogramAccumulator: channel ${channelIndex} has ${channel?.length ?? 0} samples, fewer than the requested ${frames}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let chunkMax = 0;
|
|
58
|
+
for (const channel of channels) {
|
|
59
|
+
for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
|
|
60
|
+
const value = Math.abs(channel[frameIndex] ?? 0);
|
|
61
|
+
if (value > chunkMax) chunkMax = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (chunkMax > this.bucketMax) {
|
|
65
|
+
this.rebucket(chunkMax);
|
|
66
|
+
}
|
|
67
|
+
if (this.bucketMax === 0) {
|
|
68
|
+
const chunkSamples = channels.length * frames;
|
|
69
|
+
this.pendingZeros += chunkSamples;
|
|
70
|
+
this.totalSamples += chunkSamples;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const scale = this.bucketCount / this.bucketMax;
|
|
74
|
+
const lastBucket = this.bucketCount - 1;
|
|
75
|
+
for (const channel of channels) {
|
|
76
|
+
for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
|
|
77
|
+
const value = Math.abs(channel[frameIndex] ?? 0);
|
|
78
|
+
let bucketIndex = Math.floor(value * scale);
|
|
79
|
+
if (bucketIndex < 0) bucketIndex = 0;
|
|
80
|
+
else if (bucketIndex > lastBucket) bucketIndex = lastBucket;
|
|
81
|
+
this.buckets[bucketIndex] = (this.buckets[bucketIndex] ?? 0) + 1;
|
|
82
|
+
this.totalSamples += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finalize() {
|
|
87
|
+
if (this.cachedResult !== void 0) return this.cachedResult;
|
|
88
|
+
this.finalized = true;
|
|
89
|
+
if (this.totalSamples === 0 || this.bucketMax === 0) {
|
|
90
|
+
this.cachedResult = { buckets: this.buckets, bucketMax: 0, median: 0 };
|
|
91
|
+
return this.cachedResult;
|
|
92
|
+
}
|
|
93
|
+
const target = this.totalSamples / 2;
|
|
94
|
+
const bucketWidth = this.bucketMax / this.bucketCount;
|
|
95
|
+
let cumulative = 0;
|
|
96
|
+
let median = 0;
|
|
97
|
+
for (let bucketIndex = 0; bucketIndex < this.bucketCount; bucketIndex++) {
|
|
98
|
+
const count = this.buckets[bucketIndex] ?? 0;
|
|
99
|
+
const next = cumulative + count;
|
|
100
|
+
if (next >= target) {
|
|
101
|
+
const fraction = count > 0 ? (target - cumulative) / count : 0;
|
|
102
|
+
median = (bucketIndex + fraction) * bucketWidth;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
cumulative = next;
|
|
106
|
+
}
|
|
107
|
+
this.cachedResult = { buckets: this.buckets, bucketMax: this.bucketMax, median };
|
|
108
|
+
return this.cachedResult;
|
|
109
|
+
}
|
|
110
|
+
rebucket(newMax) {
|
|
111
|
+
if (this.bucketMax === 0) {
|
|
112
|
+
if (this.pendingZeros > 0) {
|
|
113
|
+
this.buckets[0] = (this.buckets[0] ?? 0) + this.pendingZeros;
|
|
114
|
+
this.pendingZeros = 0;
|
|
115
|
+
}
|
|
116
|
+
this.bucketMax = newMax;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const oldBuckets = this.buckets;
|
|
120
|
+
const oldMax = this.bucketMax;
|
|
121
|
+
const newBuckets = new Uint32Array(this.bucketCount);
|
|
122
|
+
const lastBucket = this.bucketCount - 1;
|
|
123
|
+
const oldWidth = oldMax / this.bucketCount;
|
|
124
|
+
const newScale = this.bucketCount / newMax;
|
|
125
|
+
for (let oldIndex = 0; oldIndex < this.bucketCount; oldIndex++) {
|
|
126
|
+
const count = oldBuckets[oldIndex] ?? 0;
|
|
127
|
+
if (count === 0) continue;
|
|
128
|
+
const center = (oldIndex + 0.5) * oldWidth;
|
|
129
|
+
let newIndex = Math.floor(center * newScale);
|
|
130
|
+
if (newIndex < 0) newIndex = 0;
|
|
131
|
+
else if (newIndex > lastBucket) newIndex = lastBucket;
|
|
132
|
+
newBuckets[newIndex] = (newBuckets[newIndex] ?? 0) + count;
|
|
133
|
+
}
|
|
134
|
+
this.buckets = newBuckets;
|
|
135
|
+
this.bucketMax = newMax;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
function zeroPhaseBiquadFilter(signal, coefficients) {
|
|
139
|
+
const { fb, fa } = coefficients;
|
|
140
|
+
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
141
|
+
for (let index = 0; index < signal.length; index++) {
|
|
142
|
+
const x0 = signal[index] ?? 0;
|
|
143
|
+
const y0 = fb[0] * x0 + fb[1] * x1 + fb[2] * x2 - fa[1] * y1 - fa[2] * y2;
|
|
144
|
+
signal[index] = y0;
|
|
145
|
+
x2 = x1;
|
|
146
|
+
x1 = x0;
|
|
147
|
+
y2 = y1;
|
|
148
|
+
y1 = y0;
|
|
149
|
+
}
|
|
150
|
+
x1 = 0;
|
|
151
|
+
x2 = 0;
|
|
152
|
+
y1 = 0;
|
|
153
|
+
y2 = 0;
|
|
154
|
+
for (let index = signal.length - 1; index >= 0; index--) {
|
|
155
|
+
const x0 = signal[index] ?? 0;
|
|
156
|
+
const y0 = fb[0] * x0 + fb[1] * x1 + fb[2] * x2 - fa[1] * y1 - fa[2] * y2;
|
|
157
|
+
signal[index] = y0;
|
|
158
|
+
x2 = x1;
|
|
159
|
+
x1 = x0;
|
|
160
|
+
y2 = y1;
|
|
161
|
+
y1 = y0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function lowPassCoefficients(sampleRate, frequency, quality = Math.SQRT1_2) {
|
|
165
|
+
const w0 = 2 * Math.PI * frequency / sampleRate;
|
|
166
|
+
const cosW0 = Math.cos(w0);
|
|
167
|
+
const sinW0 = Math.sin(w0);
|
|
168
|
+
const alpha = sinW0 / (2 * quality);
|
|
169
|
+
const a0 = 1 + alpha;
|
|
170
|
+
return {
|
|
171
|
+
fb: [(1 - cosW0) / 2 / a0, (1 - cosW0) / a0, (1 - cosW0) / 2 / a0],
|
|
172
|
+
fa: [1, -2 * cosW0 / a0, (1 - alpha) / a0]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function highPassCoefficients(sampleRate, frequency, quality = Math.SQRT1_2) {
|
|
176
|
+
const w0 = 2 * Math.PI * frequency / sampleRate;
|
|
177
|
+
const cosW0 = Math.cos(w0);
|
|
178
|
+
const sinW0 = Math.sin(w0);
|
|
179
|
+
const alpha = sinW0 / (2 * quality);
|
|
180
|
+
const a0 = 1 + alpha;
|
|
181
|
+
return {
|
|
182
|
+
fb: [(1 + cosW0) / 2 / a0, -(1 + cosW0) / a0, (1 + cosW0) / 2 / a0],
|
|
183
|
+
fa: [1, -2 * cosW0 / a0, (1 - alpha) / a0]
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function preFilterCoefficients(sampleRate) {
|
|
187
|
+
if (sampleRate === 48e3) {
|
|
188
|
+
return {
|
|
189
|
+
fb: [1.53512485958697, -2.69169618940638, 1.19839281085285],
|
|
190
|
+
fa: [1, -1.69065929318241, 0.73248077421585]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const freq = 1681.974450955533;
|
|
194
|
+
const gain = 3.999843853973347;
|
|
195
|
+
const quality = 0.7071752369554196;
|
|
196
|
+
const kk = Math.tan(Math.PI * freq / sampleRate);
|
|
197
|
+
const vh = Math.pow(10, gain / 20);
|
|
198
|
+
const vb = Math.pow(vh, 0.4996667741545416);
|
|
199
|
+
const a0 = 1 + kk / quality + kk * kk;
|
|
200
|
+
return {
|
|
201
|
+
fb: [(vh + vb * kk / quality + kk * kk) / a0, 2 * (kk * kk - vh) / a0, (vh - vb * kk / quality + kk * kk) / a0],
|
|
202
|
+
fa: [1, 2 * (kk * kk - 1) / a0, (1 - kk / quality + kk * kk) / a0]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function rlbFilterCoefficients(sampleRate) {
|
|
206
|
+
if (sampleRate === 48e3) {
|
|
207
|
+
return {
|
|
208
|
+
fb: [1, -2, 1],
|
|
209
|
+
fa: [1, -1.99004745483398, 0.99007225036621]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const freq = 38.13547087602444;
|
|
213
|
+
const quality = 0.5003270373238773;
|
|
214
|
+
const kk = Math.tan(Math.PI * freq / sampleRate);
|
|
215
|
+
const a0 = 1 + kk / quality + kk * kk;
|
|
216
|
+
return {
|
|
217
|
+
fb: [1 / a0, -2 / a0, 1 / a0],
|
|
218
|
+
fa: [1, 2 * (kk * kk - 1) / a0, (1 - kk / quality + kk * kk) / a0]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function bandpass(channels, sampleRate, highPass, lowPass) {
|
|
222
|
+
if (!highPass && !lowPass) return;
|
|
223
|
+
for (const channel of channels) {
|
|
224
|
+
if (highPass) {
|
|
225
|
+
zeroPhaseBiquadFilter(channel, highPassCoefficients(sampleRate, highPass));
|
|
226
|
+
}
|
|
227
|
+
if (lowPass) {
|
|
228
|
+
zeroPhaseBiquadFilter(channel, lowPassCoefficients(sampleRate, lowPass));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var BidirectionalIir = class {
|
|
233
|
+
constructor(options) {
|
|
234
|
+
this.smoothingMs = options.smoothingMs;
|
|
235
|
+
this.sampleRate = options.sampleRate;
|
|
236
|
+
const samplePeriod = 1 / this.sampleRate;
|
|
237
|
+
const tauBidirectional = this.smoothingMs / 1e3 * Math.SQRT2;
|
|
238
|
+
this.alphaBidirectional = tauBidirectional > 0 ? 1 - Math.exp(-samplePeriod / tauBidirectional) : 1;
|
|
239
|
+
const tauCausal = this.smoothingMs / 1e3;
|
|
240
|
+
this.alphaCausal = tauCausal > 0 ? 1 - Math.exp(-samplePeriod / tauCausal) : 1;
|
|
241
|
+
}
|
|
242
|
+
applyBidirectional(input) {
|
|
243
|
+
const output = Float32Array.from(input);
|
|
244
|
+
if (this.smoothingMs <= 0) return output;
|
|
245
|
+
const alpha = this.alphaBidirectional;
|
|
246
|
+
const oneMinusAlpha = 1 - alpha;
|
|
247
|
+
let y = output[0] ?? 0;
|
|
248
|
+
for (let index = 0; index < output.length; index++) {
|
|
249
|
+
const x = output[index] ?? 0;
|
|
250
|
+
y = alpha * x + oneMinusAlpha * y;
|
|
251
|
+
output[index] = y;
|
|
252
|
+
}
|
|
253
|
+
y = output[output.length - 1] ?? 0;
|
|
254
|
+
for (let index = output.length - 1; index >= 0; index--) {
|
|
255
|
+
const x = output[index] ?? 0;
|
|
256
|
+
y = alpha * x + oneMinusAlpha * y;
|
|
257
|
+
output[index] = y;
|
|
258
|
+
}
|
|
259
|
+
return output;
|
|
260
|
+
}
|
|
261
|
+
applyCausal(input, state) {
|
|
262
|
+
const output = Float32Array.from(input);
|
|
263
|
+
if (this.smoothingMs <= 0) return output;
|
|
264
|
+
const alpha = this.alphaCausal;
|
|
265
|
+
const oneMinusAlpha = 1 - alpha;
|
|
266
|
+
let y = state.value;
|
|
267
|
+
for (let index = 0; index < output.length; index++) {
|
|
268
|
+
const x = output[index] ?? 0;
|
|
269
|
+
y = alpha * x + oneMinusAlpha * y;
|
|
270
|
+
output[index] = y;
|
|
271
|
+
}
|
|
272
|
+
state.value = y;
|
|
273
|
+
return output;
|
|
274
|
+
}
|
|
275
|
+
applyForwardPass(input, state) {
|
|
276
|
+
const output = Float32Array.from(input);
|
|
277
|
+
if (this.smoothingMs <= 0) return output;
|
|
278
|
+
const alpha = this.alphaBidirectional;
|
|
279
|
+
const oneMinusAlpha = 1 - alpha;
|
|
280
|
+
let y = state.value;
|
|
281
|
+
for (let index = 0; index < output.length; index++) {
|
|
282
|
+
const x = output[index] ?? 0;
|
|
283
|
+
y = alpha * x + oneMinusAlpha * y;
|
|
284
|
+
output[index] = y;
|
|
285
|
+
}
|
|
286
|
+
state.value = y;
|
|
287
|
+
return output;
|
|
288
|
+
}
|
|
289
|
+
applyBackwardPassInPlace(buffer) {
|
|
290
|
+
if (this.smoothingMs <= 0) return;
|
|
291
|
+
if (buffer.length === 0) return;
|
|
292
|
+
const alpha = this.alphaBidirectional;
|
|
293
|
+
const oneMinusAlpha = 1 - alpha;
|
|
294
|
+
let y = buffer[buffer.length - 1] ?? 0;
|
|
295
|
+
for (let index = buffer.length - 1; index >= 0; index--) {
|
|
296
|
+
const x = buffer[index] ?? 0;
|
|
297
|
+
y = alpha * x + oneMinusAlpha * y;
|
|
298
|
+
buffer[index] = y;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
var computeRingSize = (blockSize, blockStep) => Math.max(1, Math.ceil(blockSize / blockStep));
|
|
303
|
+
var BlockSumAccumulator = class {
|
|
304
|
+
constructor(blockSize, blockStep) {
|
|
305
|
+
this.closedBlockSums = [];
|
|
306
|
+
this.samplesProcessed = 0;
|
|
307
|
+
this.nextBlockToOpen = 0;
|
|
308
|
+
this.nextBlockToClose = 0;
|
|
309
|
+
this.finalized = false;
|
|
310
|
+
if (blockSize <= 0) {
|
|
311
|
+
throw new Error(`BlockSumAccumulator: blockSize must be positive, got ${blockSize}`);
|
|
312
|
+
}
|
|
313
|
+
if (blockStep <= 0) {
|
|
314
|
+
throw new Error(`BlockSumAccumulator: blockStep must be positive, got ${blockStep}`);
|
|
315
|
+
}
|
|
316
|
+
this.blockSize = blockSize;
|
|
317
|
+
this.blockStep = blockStep;
|
|
318
|
+
this.ringSize = computeRingSize(blockSize, blockStep);
|
|
319
|
+
this.activeBlockSums = new Float64Array(this.ringSize);
|
|
320
|
+
}
|
|
321
|
+
// Consumes `frames` per-frame sums from `perFrameSums[0]` (needs length >= `frames`); block-boundary accounting advances as if appended to one contiguous stream.
|
|
322
|
+
push(perFrameSums, frames) {
|
|
323
|
+
if (this.finalized) {
|
|
324
|
+
throw new Error("BlockSumAccumulator: push() called after finalize()");
|
|
325
|
+
}
|
|
326
|
+
if (frames <= 0) return;
|
|
327
|
+
if (perFrameSums.length < frames) {
|
|
328
|
+
throw new Error(`BlockSumAccumulator: perFrameSums has ${perFrameSums.length} entries, fewer than the requested ${frames}`);
|
|
329
|
+
}
|
|
330
|
+
const blockSize = this.blockSize;
|
|
331
|
+
const blockStep = this.blockStep;
|
|
332
|
+
const ringSize = this.ringSize;
|
|
333
|
+
const activeBlockSums = this.activeBlockSums;
|
|
334
|
+
let samplesProcessed = this.samplesProcessed;
|
|
335
|
+
let nextBlockToOpen = this.nextBlockToOpen;
|
|
336
|
+
let nextBlockToClose = this.nextBlockToClose;
|
|
337
|
+
let nextOpenAt = nextBlockToOpen * blockStep;
|
|
338
|
+
let nextCloseAt = nextBlockToClose * blockStep + blockSize;
|
|
339
|
+
for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
|
|
340
|
+
const sampleContribution = perFrameSums[frameIndex] ?? 0;
|
|
341
|
+
while (samplesProcessed >= nextOpenAt) {
|
|
342
|
+
activeBlockSums[nextBlockToOpen % ringSize] = 0;
|
|
343
|
+
nextBlockToOpen++;
|
|
344
|
+
nextOpenAt += blockStep;
|
|
345
|
+
}
|
|
346
|
+
let slot = nextBlockToClose % ringSize;
|
|
347
|
+
for (let blockIndex = nextBlockToClose; blockIndex < nextBlockToOpen; blockIndex++) {
|
|
348
|
+
activeBlockSums[slot] = (activeBlockSums[slot] ?? 0) + sampleContribution;
|
|
349
|
+
slot++;
|
|
350
|
+
if (slot === ringSize) slot = 0;
|
|
351
|
+
}
|
|
352
|
+
samplesProcessed++;
|
|
353
|
+
while (samplesProcessed >= nextCloseAt) {
|
|
354
|
+
this.closedBlockSums.push(activeBlockSums[nextBlockToClose % ringSize] ?? 0);
|
|
355
|
+
nextBlockToClose++;
|
|
356
|
+
nextCloseAt += blockStep;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
this.samplesProcessed = samplesProcessed;
|
|
360
|
+
this.nextBlockToOpen = nextBlockToOpen;
|
|
361
|
+
this.nextBlockToClose = nextBlockToClose;
|
|
362
|
+
}
|
|
363
|
+
finalize() {
|
|
364
|
+
this.finalized = true;
|
|
365
|
+
return this.closedBlockSums;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
function dbToLinear(db) {
|
|
369
|
+
if (db === -Infinity) return 0;
|
|
370
|
+
return Math.pow(10, db / 20);
|
|
371
|
+
}
|
|
372
|
+
function linearToDb(linear) {
|
|
373
|
+
return 20 * Math.log10(Math.max(linear, 1e-10));
|
|
374
|
+
}
|
|
375
|
+
var require2 = createRequire(import.meta.url);
|
|
376
|
+
function tryLoadVkfft(vkfftPath) {
|
|
377
|
+
if (!vkfftPath) return null;
|
|
378
|
+
try {
|
|
379
|
+
return require2(vkfftPath);
|
|
380
|
+
} catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function tryLoadFftw(fftwPath) {
|
|
385
|
+
if (!fftwPath) return null;
|
|
386
|
+
try {
|
|
387
|
+
return require2(fftwPath);
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function detectFftBackend(executionProviders, options) {
|
|
393
|
+
for (const provider of executionProviders) {
|
|
394
|
+
if (provider === "gpu") {
|
|
395
|
+
const vkfft = tryLoadVkfft(options?.vkfftPath);
|
|
396
|
+
if (vkfft) {
|
|
397
|
+
const device = vkfft.detectDevice();
|
|
398
|
+
if (device) {
|
|
399
|
+
return "vkfft";
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (provider === "cpu-native") {
|
|
404
|
+
const fftw = tryLoadFftw(options?.fftwPath);
|
|
405
|
+
if (fftw) {
|
|
406
|
+
return "fftw";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (provider === "cpu") {
|
|
410
|
+
return "js";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return "js";
|
|
414
|
+
}
|
|
415
|
+
function initFftBackend(executionProviders, properties) {
|
|
416
|
+
const addonOptions = { vkfftPath: properties.vkfftAddonPath, fftwPath: properties.fftwAddonPath };
|
|
417
|
+
const backend = detectFftBackend(executionProviders, addonOptions);
|
|
418
|
+
return { backend, addonOptions };
|
|
419
|
+
}
|
|
420
|
+
function getFftAddon(backend, options) {
|
|
421
|
+
if (backend === "vkfft") return tryLoadVkfft(options?.vkfftPath);
|
|
422
|
+
if (backend === "fftw") return tryLoadFftw(options?.fftwPath);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
function interleave(samples, frames, channels) {
|
|
426
|
+
const interleaved = new Float32Array(frames * channels);
|
|
427
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
428
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
429
|
+
interleaved[frame * channels + ch] = samples[ch]?.[frame] ?? 0;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return interleaved;
|
|
433
|
+
}
|
|
434
|
+
function deinterleaveBuffer(buffer, channels) {
|
|
435
|
+
const totalSamples = buffer.length / 4;
|
|
436
|
+
const frames = Math.floor(totalSamples / channels);
|
|
437
|
+
const result = [];
|
|
438
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
439
|
+
result.push(new Float32Array(frames));
|
|
440
|
+
}
|
|
441
|
+
const view = new Float32Array(buffer.buffer, buffer.byteOffset, totalSamples);
|
|
442
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
443
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
444
|
+
const channelArray = result[ch];
|
|
445
|
+
const value = view[frame * channels + ch];
|
|
446
|
+
if (channelArray && value !== void 0) {
|
|
447
|
+
channelArray[frame] = value;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
var KWeightedSquaredSum = class {
|
|
454
|
+
constructor(sampleRate, channelCount, channelWeights) {
|
|
455
|
+
if (channelCount <= 0) {
|
|
456
|
+
throw new Error(`KWeightedSquaredSum: channelCount must be positive, got ${channelCount}`);
|
|
457
|
+
}
|
|
458
|
+
const weights = channelWeights ?? new Array(channelCount).fill(1);
|
|
459
|
+
if (weights.length !== channelCount) {
|
|
460
|
+
throw new Error(`KWeightedSquaredSum: channelWeights length ${weights.length} does not match channel count ${channelCount}`);
|
|
461
|
+
}
|
|
462
|
+
this.channelCount = channelCount;
|
|
463
|
+
const preFilter = preFilterCoefficients(sampleRate);
|
|
464
|
+
const rlbFilter = rlbFilterCoefficients(sampleRate);
|
|
465
|
+
this.preB0 = preFilter.fb[0];
|
|
466
|
+
this.preB1 = preFilter.fb[1];
|
|
467
|
+
this.preB2 = preFilter.fb[2];
|
|
468
|
+
this.preA1 = preFilter.fa[1];
|
|
469
|
+
this.preA2 = preFilter.fa[2];
|
|
470
|
+
this.rlbB0 = rlbFilter.fb[0];
|
|
471
|
+
this.rlbB1 = rlbFilter.fb[1];
|
|
472
|
+
this.rlbB2 = rlbFilter.fb[2];
|
|
473
|
+
this.rlbA1 = rlbFilter.fa[1];
|
|
474
|
+
this.rlbA2 = rlbFilter.fa[2];
|
|
475
|
+
this.preX1 = new Float64Array(channelCount);
|
|
476
|
+
this.preX2 = new Float64Array(channelCount);
|
|
477
|
+
this.preY1 = new Float64Array(channelCount);
|
|
478
|
+
this.preY2 = new Float64Array(channelCount);
|
|
479
|
+
this.rlbX1 = new Float64Array(channelCount);
|
|
480
|
+
this.rlbX2 = new Float64Array(channelCount);
|
|
481
|
+
this.rlbY1 = new Float64Array(channelCount);
|
|
482
|
+
this.rlbY2 = new Float64Array(channelCount);
|
|
483
|
+
this.weights = new Float64Array(channelCount);
|
|
484
|
+
for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
|
|
485
|
+
this.weights[channelIndex] = weights[channelIndex] ?? 1;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// `channels[c]` and `output` need >= `frames` entries from index 0 (oversized OK); `output[i]` gets the K-weighted channel-weighted squared sum at frame `i`; biquad state advances as if appended contiguously.
|
|
489
|
+
push(channels, frames, output) {
|
|
490
|
+
if (channels.length !== this.channelCount) {
|
|
491
|
+
throw new Error(`KWeightedSquaredSum: push got ${channels.length} channels, expected ${this.channelCount}`);
|
|
492
|
+
}
|
|
493
|
+
if (frames <= 0) return;
|
|
494
|
+
for (let channelIndex = 0; channelIndex < this.channelCount; channelIndex++) {
|
|
495
|
+
const channel = channels[channelIndex] ?? new Float32Array(0);
|
|
496
|
+
if (channel.length < frames) {
|
|
497
|
+
throw new Error(`KWeightedSquaredSum: channel ${channelIndex} has ${channel.length} samples, fewer than the requested ${frames}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (output.length < frames) {
|
|
501
|
+
throw new Error(`KWeightedSquaredSum: output buffer has ${output.length} entries, fewer than the requested ${frames}`);
|
|
502
|
+
}
|
|
503
|
+
const channelCount = this.channelCount;
|
|
504
|
+
const weights = this.weights;
|
|
505
|
+
const preB0 = this.preB0;
|
|
506
|
+
const preB1 = this.preB1;
|
|
507
|
+
const preB2 = this.preB2;
|
|
508
|
+
const preA1 = this.preA1;
|
|
509
|
+
const preA2 = this.preA2;
|
|
510
|
+
const rlbB0 = this.rlbB0;
|
|
511
|
+
const rlbB1 = this.rlbB1;
|
|
512
|
+
const rlbB2 = this.rlbB2;
|
|
513
|
+
const rlbA1 = this.rlbA1;
|
|
514
|
+
const rlbA2 = this.rlbA2;
|
|
515
|
+
for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
|
|
516
|
+
const channel = channels[channelIndex] ?? channels[0] ?? new Float32Array(0);
|
|
517
|
+
const weight = weights[channelIndex] ?? 1;
|
|
518
|
+
let px1 = this.preX1[channelIndex] ?? 0;
|
|
519
|
+
let px2 = this.preX2[channelIndex] ?? 0;
|
|
520
|
+
let py1 = this.preY1[channelIndex] ?? 0;
|
|
521
|
+
let py2 = this.preY2[channelIndex] ?? 0;
|
|
522
|
+
let rx1 = this.rlbX1[channelIndex] ?? 0;
|
|
523
|
+
let rx2 = this.rlbX2[channelIndex] ?? 0;
|
|
524
|
+
let ry1 = this.rlbY1[channelIndex] ?? 0;
|
|
525
|
+
let ry2 = this.rlbY2[channelIndex] ?? 0;
|
|
526
|
+
if (channelIndex === 0) {
|
|
527
|
+
for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
|
|
528
|
+
const x0 = channel[frameIndex] ?? 0;
|
|
529
|
+
const preY = preB0 * x0 + preB1 * px1 + preB2 * px2 - preA1 * py1 - preA2 * py2;
|
|
530
|
+
px2 = px1;
|
|
531
|
+
px1 = x0;
|
|
532
|
+
py2 = py1;
|
|
533
|
+
py1 = preY;
|
|
534
|
+
const rlbY = rlbB0 * preY + rlbB1 * rx1 + rlbB2 * rx2 - rlbA1 * ry1 - rlbA2 * ry2;
|
|
535
|
+
rx2 = rx1;
|
|
536
|
+
rx1 = preY;
|
|
537
|
+
ry2 = ry1;
|
|
538
|
+
ry1 = rlbY;
|
|
539
|
+
output[frameIndex] = weight * rlbY * rlbY;
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
for (let frameIndex = 0; frameIndex < frames; frameIndex++) {
|
|
543
|
+
const x0 = channel[frameIndex] ?? 0;
|
|
544
|
+
const preY = preB0 * x0 + preB1 * px1 + preB2 * px2 - preA1 * py1 - preA2 * py2;
|
|
545
|
+
px2 = px1;
|
|
546
|
+
px1 = x0;
|
|
547
|
+
py2 = py1;
|
|
548
|
+
py1 = preY;
|
|
549
|
+
const rlbY = rlbB0 * preY + rlbB1 * rx1 + rlbB2 * rx2 - rlbA1 * ry1 - rlbA2 * ry2;
|
|
550
|
+
rx2 = rx1;
|
|
551
|
+
rx1 = preY;
|
|
552
|
+
ry2 = ry1;
|
|
553
|
+
ry1 = rlbY;
|
|
554
|
+
output[frameIndex] = (output[frameIndex] ?? 0) + weight * rlbY * rlbY;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
this.preX1[channelIndex] = px1;
|
|
558
|
+
this.preX2[channelIndex] = px2;
|
|
559
|
+
this.preY1[channelIndex] = py1;
|
|
560
|
+
this.preY2[channelIndex] = py2;
|
|
561
|
+
this.rlbX1[channelIndex] = rx1;
|
|
562
|
+
this.rlbX2[channelIndex] = rx2;
|
|
563
|
+
this.rlbY1[channelIndex] = ry1;
|
|
564
|
+
this.rlbY2[channelIndex] = ry2;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
var LUFS_OFFSET = -0.691;
|
|
569
|
+
var ABSOLUTE_GATE_LUFS = -70;
|
|
570
|
+
var RELATIVE_GATE_OFFSET_LU = -10;
|
|
571
|
+
var BLOCK_DURATION_SECONDS = 0.4;
|
|
572
|
+
var BLOCK_STEP_SECONDS = 0.1;
|
|
573
|
+
var SHORT_TERM_BLOCK_DURATION_SECONDS = 3;
|
|
574
|
+
var POWER_FLOOR = 1e-10;
|
|
575
|
+
var LRA_ABSOLUTE_GATE_LUFS = -70;
|
|
576
|
+
var LRA_RELATIVE_GATE_OFFSET_LU = -20;
|
|
577
|
+
var LRA_LOW_PERCENTILE = 0.1;
|
|
578
|
+
var LRA_HIGH_PERCENTILE = 0.95;
|
|
579
|
+
function applyBs1770Gating(closedBlockSums, blockSize) {
|
|
580
|
+
const blockCount = closedBlockSums.length;
|
|
581
|
+
if (blockCount === 0) return -Infinity;
|
|
582
|
+
const absoluteThresholdPower = Math.pow(10, (ABSOLUTE_GATE_LUFS - LUFS_OFFSET) / 10);
|
|
583
|
+
let absoluteSurvivorCount = 0;
|
|
584
|
+
let absoluteSum = 0;
|
|
585
|
+
for (let blockIndex = 0; blockIndex < blockCount; blockIndex++) {
|
|
586
|
+
const power = (closedBlockSums[blockIndex] ?? 0) / blockSize;
|
|
587
|
+
if (power > absoluteThresholdPower) {
|
|
588
|
+
absoluteSum += power;
|
|
589
|
+
absoluteSurvivorCount++;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (absoluteSurvivorCount === 0) return -Infinity;
|
|
593
|
+
const absoluteMean = absoluteSum / absoluteSurvivorCount;
|
|
594
|
+
const relativeThresholdLufs = LUFS_OFFSET + 10 * Math.log10(absoluteMean) + RELATIVE_GATE_OFFSET_LU;
|
|
595
|
+
const relativeThresholdPower = Math.pow(10, (relativeThresholdLufs - LUFS_OFFSET) / 10);
|
|
596
|
+
let relativeSurvivorCount = 0;
|
|
597
|
+
let relativeSum = 0;
|
|
598
|
+
for (let blockIndex = 0; blockIndex < blockCount; blockIndex++) {
|
|
599
|
+
const power = (closedBlockSums[blockIndex] ?? 0) / blockSize;
|
|
600
|
+
if (power > absoluteThresholdPower && power > relativeThresholdPower) {
|
|
601
|
+
relativeSum += power;
|
|
602
|
+
relativeSurvivorCount++;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (relativeSurvivorCount === 0) return -Infinity;
|
|
606
|
+
const integratedMean = relativeSum / relativeSurvivorCount;
|
|
607
|
+
return LUFS_OFFSET + 10 * Math.log10(integratedMean);
|
|
608
|
+
}
|
|
609
|
+
var IntegratedLufsAccumulator = class {
|
|
610
|
+
constructor(sampleRate, channelCount, channelWeights) {
|
|
611
|
+
this.outputBuffer = new Float64Array(0);
|
|
612
|
+
this.finalized = false;
|
|
613
|
+
if (channelCount <= 0) {
|
|
614
|
+
throw new Error(`IntegratedLufsAccumulator: channelCount must be positive, got ${channelCount}`);
|
|
615
|
+
}
|
|
616
|
+
if (channelWeights !== void 0 && channelWeights.length !== channelCount) {
|
|
617
|
+
throw new Error(`IntegratedLufsAccumulator: channelWeights length ${channelWeights.length} does not match channel count ${channelCount}`);
|
|
618
|
+
}
|
|
619
|
+
this.blockSize = Math.round(BLOCK_DURATION_SECONDS * sampleRate);
|
|
620
|
+
const blockStep = Math.round(BLOCK_STEP_SECONDS * sampleRate);
|
|
621
|
+
this.kw = new KWeightedSquaredSum(sampleRate, channelCount, channelWeights);
|
|
622
|
+
this.blocks = new BlockSumAccumulator(this.blockSize, blockStep);
|
|
623
|
+
}
|
|
624
|
+
// `channels[c]` needs >= `frames` valid samples from index 0 (oversized OK); state advances as if appended to one contiguous buffer.
|
|
625
|
+
push(channels, frames) {
|
|
626
|
+
if (this.finalized) {
|
|
627
|
+
throw new Error("IntegratedLufsAccumulator: push() called after finalize()");
|
|
628
|
+
}
|
|
629
|
+
if (frames <= 0) return;
|
|
630
|
+
if (this.outputBuffer.length < frames) {
|
|
631
|
+
this.outputBuffer = new Float64Array(frames);
|
|
632
|
+
}
|
|
633
|
+
this.kw.push(channels, frames, this.outputBuffer);
|
|
634
|
+
this.blocks.push(this.outputBuffer, frames);
|
|
635
|
+
}
|
|
636
|
+
finalize() {
|
|
637
|
+
this.finalized = true;
|
|
638
|
+
return applyBs1770Gating(this.blocks.finalize(), this.blockSize);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
function computeLraFromShortTerm(shortTermLoudness) {
|
|
642
|
+
const absoluteGated = [];
|
|
643
|
+
for (let index = 0; index < shortTermLoudness.length; index++) {
|
|
644
|
+
const value = shortTermLoudness[index] ?? 0;
|
|
645
|
+
if (value > LRA_ABSOLUTE_GATE_LUFS) {
|
|
646
|
+
absoluteGated.push(value);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (absoluteGated.length < 2) return 0;
|
|
650
|
+
let absoluteSum = 0;
|
|
651
|
+
for (let index = 0; index < absoluteGated.length; index++) {
|
|
652
|
+
absoluteSum += Math.pow(10, (absoluteGated[index] ?? 0) / 10);
|
|
653
|
+
}
|
|
654
|
+
const absoluteMean = absoluteSum / absoluteGated.length;
|
|
655
|
+
const relativeThreshold = 10 * Math.log10(absoluteMean) + LRA_RELATIVE_GATE_OFFSET_LU;
|
|
656
|
+
const relativeGated = [];
|
|
657
|
+
for (let index = 0; index < absoluteGated.length; index++) {
|
|
658
|
+
const value = absoluteGated[index] ?? 0;
|
|
659
|
+
if (value > relativeThreshold) {
|
|
660
|
+
relativeGated.push(value);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (relativeGated.length < 2) return 0;
|
|
664
|
+
relativeGated.sort((lhs, rhs) => lhs - rhs);
|
|
665
|
+
const lowIndex = Math.floor(relativeGated.length * LRA_LOW_PERCENTILE);
|
|
666
|
+
const highIndex = Math.min(Math.ceil(relativeGated.length * LRA_HIGH_PERCENTILE) - 1, relativeGated.length - 1);
|
|
667
|
+
return (relativeGated[highIndex] ?? 0) - (relativeGated[lowIndex] ?? 0);
|
|
668
|
+
}
|
|
669
|
+
function getLraConsideredStats(shortTerm) {
|
|
670
|
+
const aboveAbsolute = [];
|
|
671
|
+
for (let index = 0; index < shortTerm.length; index++) {
|
|
672
|
+
const lufs = shortTerm[index] ?? 0;
|
|
673
|
+
if (lufs > LRA_ABSOLUTE_GATE_LUFS) {
|
|
674
|
+
aboveAbsolute.push(lufs);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (aboveAbsolute.length === 0) {
|
|
678
|
+
return { min: Number.POSITIVE_INFINITY, median: Number.POSITIVE_INFINITY };
|
|
679
|
+
}
|
|
680
|
+
let absoluteSum = 0;
|
|
681
|
+
for (let index = 0; index < aboveAbsolute.length; index++) {
|
|
682
|
+
absoluteSum += Math.pow(10, (aboveAbsolute[index] ?? 0) / 10);
|
|
683
|
+
}
|
|
684
|
+
const absoluteMean = absoluteSum / aboveAbsolute.length;
|
|
685
|
+
const relativeThreshold = 10 * Math.log10(absoluteMean) + LRA_RELATIVE_GATE_OFFSET_LU;
|
|
686
|
+
const considered = [];
|
|
687
|
+
for (let index = 0; index < aboveAbsolute.length; index++) {
|
|
688
|
+
const lufs = aboveAbsolute[index] ?? 0;
|
|
689
|
+
if (lufs > relativeThreshold) {
|
|
690
|
+
considered.push(lufs);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (considered.length === 0) {
|
|
694
|
+
return { min: Number.POSITIVE_INFINITY, median: Number.POSITIVE_INFINITY };
|
|
695
|
+
}
|
|
696
|
+
considered.sort((left, right) => left - right);
|
|
697
|
+
const min = considered[0] ?? Number.POSITIVE_INFINITY;
|
|
698
|
+
const middleIndex = considered.length >> 1;
|
|
699
|
+
const median = considered.length % 2 === 1 ? considered[middleIndex] ?? Number.POSITIVE_INFINITY : ((considered[middleIndex - 1] ?? 0) + (considered[middleIndex] ?? 0)) / 2;
|
|
700
|
+
return { min, median };
|
|
701
|
+
}
|
|
702
|
+
var LoudnessAccumulator = class {
|
|
703
|
+
constructor(sampleRate, channelCount, channelWeights) {
|
|
704
|
+
this.outputBuffer = new Float64Array(0);
|
|
705
|
+
this.finalized = false;
|
|
706
|
+
if (channelCount <= 0) {
|
|
707
|
+
throw new Error(`LoudnessAccumulator: channelCount must be positive, got ${channelCount}`);
|
|
708
|
+
}
|
|
709
|
+
if (channelWeights !== void 0 && channelWeights.length !== channelCount) {
|
|
710
|
+
throw new Error(`LoudnessAccumulator: channelWeights length ${channelWeights.length} does not match channel count ${channelCount}`);
|
|
711
|
+
}
|
|
712
|
+
this.blockSize400 = Math.round(BLOCK_DURATION_SECONDS * sampleRate);
|
|
713
|
+
this.blockSize3s = Math.round(SHORT_TERM_BLOCK_DURATION_SECONDS * sampleRate);
|
|
714
|
+
const blockStep = Math.round(BLOCK_STEP_SECONDS * sampleRate);
|
|
715
|
+
this.kw = new KWeightedSquaredSum(sampleRate, channelCount, channelWeights);
|
|
716
|
+
this.blocks400 = new BlockSumAccumulator(this.blockSize400, blockStep);
|
|
717
|
+
this.blocks3s = new BlockSumAccumulator(this.blockSize3s, blockStep);
|
|
718
|
+
}
|
|
719
|
+
push(channels, frames) {
|
|
720
|
+
if (this.finalized) {
|
|
721
|
+
throw new Error("LoudnessAccumulator: push() called after finalize()");
|
|
722
|
+
}
|
|
723
|
+
if (frames <= 0) return;
|
|
724
|
+
if (this.outputBuffer.length < frames) {
|
|
725
|
+
this.outputBuffer = new Float64Array(frames);
|
|
726
|
+
}
|
|
727
|
+
this.kw.push(channels, frames, this.outputBuffer);
|
|
728
|
+
this.blocks400.push(this.outputBuffer, frames);
|
|
729
|
+
this.blocks3s.push(this.outputBuffer, frames);
|
|
730
|
+
}
|
|
731
|
+
finalize() {
|
|
732
|
+
if (this.cachedResult !== void 0) return this.cachedResult;
|
|
733
|
+
this.finalized = true;
|
|
734
|
+
const closed400 = this.blocks400.finalize();
|
|
735
|
+
const closed3s = this.blocks3s.finalize();
|
|
736
|
+
const blockSize400 = this.blockSize400;
|
|
737
|
+
const blockSize3s = this.blockSize3s;
|
|
738
|
+
const momentary = new Array(closed400.length);
|
|
739
|
+
for (let index = 0; index < closed400.length; index++) {
|
|
740
|
+
const sum = closed400[index] ?? 0;
|
|
741
|
+
momentary[index] = LUFS_OFFSET + 10 * Math.log10(Math.max(sum / blockSize400, POWER_FLOOR));
|
|
742
|
+
}
|
|
743
|
+
const shortTerm = new Array(closed3s.length);
|
|
744
|
+
for (let index = 0; index < closed3s.length; index++) {
|
|
745
|
+
const sum = closed3s[index] ?? 0;
|
|
746
|
+
shortTerm[index] = LUFS_OFFSET + 10 * Math.log10(Math.max(sum / blockSize3s, POWER_FLOOR));
|
|
747
|
+
}
|
|
748
|
+
const integrated = applyBs1770Gating(closed400, blockSize400);
|
|
749
|
+
const range = computeLraFromShortTerm(shortTerm);
|
|
750
|
+
this.cachedResult = { integrated, momentary, shortTerm, range };
|
|
751
|
+
return this.cachedResult;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
var MixedRadixFft = class {
|
|
755
|
+
constructor(size) {
|
|
756
|
+
this.size = size;
|
|
757
|
+
this.radices = factorize(size);
|
|
758
|
+
this.frameRe = new Float32Array(size);
|
|
759
|
+
this.frameIm = new Float32Array(size);
|
|
760
|
+
this.outRe = new Float32Array(size);
|
|
761
|
+
this.outIm = new Float32Array(size);
|
|
762
|
+
this.auxIm = new Float32Array(size);
|
|
763
|
+
this.permutation = computePermutation(size, this.radices);
|
|
764
|
+
const { twiddleRe, twiddleIm } = computeTwiddles(this.radices);
|
|
765
|
+
this.twiddleRe = twiddleRe;
|
|
766
|
+
this.twiddleIm = twiddleIm;
|
|
767
|
+
}
|
|
768
|
+
fft(xRe, xIm, outRe, outIm) {
|
|
769
|
+
const perm = this.permutation;
|
|
770
|
+
const nn = this.size;
|
|
771
|
+
for (let index = 0; index < nn; index++) {
|
|
772
|
+
const pp = perm[index] ?? 0;
|
|
773
|
+
outRe[index] = xRe[pp] ?? 0;
|
|
774
|
+
outIm[index] = xIm[pp] ?? 0;
|
|
775
|
+
}
|
|
776
|
+
let groupSize = 1;
|
|
777
|
+
let twOffset = 0;
|
|
778
|
+
for (const radix of this.radices) {
|
|
779
|
+
groupSize *= radix;
|
|
780
|
+
const subSize = groupSize / radix;
|
|
781
|
+
if (radix === 2) {
|
|
782
|
+
twOffset = this.radix2(outRe, outIm, nn, groupSize, subSize, twOffset);
|
|
783
|
+
} else if (radix === 3) {
|
|
784
|
+
twOffset = this.radix3(outRe, outIm, nn, groupSize, subSize, twOffset);
|
|
785
|
+
} else if (radix === 5) {
|
|
786
|
+
twOffset = this.radix5(outRe, outIm, nn, groupSize, subSize, twOffset);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
ifft(xRe, xIm, outRe, outIm) {
|
|
791
|
+
const auxIm = this.auxIm;
|
|
792
|
+
const nn = this.size;
|
|
793
|
+
for (let index = 0; index < nn; index++) {
|
|
794
|
+
auxIm[index] = -(xIm[index] ?? 0);
|
|
795
|
+
}
|
|
796
|
+
this.fft(xRe, auxIm, outRe, outIm);
|
|
797
|
+
for (let index = 0; index < nn; index++) {
|
|
798
|
+
outRe[index] = (outRe[index] ?? 0) / nn;
|
|
799
|
+
outIm[index] = -(outIm[index] ?? 0) / nn;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
radix2(outRe, outIm, nn, groupSize, subSize, twOffset) {
|
|
803
|
+
for (let group = 0; group < nn; group += groupSize) {
|
|
804
|
+
for (let ni = 0; ni < subSize; ni++) {
|
|
805
|
+
const idx0 = group + ni;
|
|
806
|
+
const idx1 = idx0 + subSize;
|
|
807
|
+
const twRe = ni === 0 ? 1 : this.twiddleRe[twOffset + ni - 1] ?? 0;
|
|
808
|
+
const twIm = ni === 0 ? 0 : this.twiddleIm[twOffset + ni - 1] ?? 0;
|
|
809
|
+
const tRe = (outRe[idx1] ?? 0) * twRe - (outIm[idx1] ?? 0) * twIm;
|
|
810
|
+
const tIm = (outRe[idx1] ?? 0) * twIm + (outIm[idx1] ?? 0) * twRe;
|
|
811
|
+
outRe[idx1] = (outRe[idx0] ?? 0) - tRe;
|
|
812
|
+
outIm[idx1] = (outIm[idx0] ?? 0) - tIm;
|
|
813
|
+
outRe[idx0] = (outRe[idx0] ?? 0) + tRe;
|
|
814
|
+
outIm[idx0] = (outIm[idx0] ?? 0) + tIm;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return twOffset + subSize - 1;
|
|
818
|
+
}
|
|
819
|
+
radix3(outRe, outIm, nn, groupSize, subSize, twOffset) {
|
|
820
|
+
const c3 = -0.5;
|
|
821
|
+
const s3 = -Math.sqrt(3) / 2;
|
|
822
|
+
for (let group = 0; group < nn; group += groupSize) {
|
|
823
|
+
for (let ni = 0; ni < subSize; ni++) {
|
|
824
|
+
const idx0 = group + ni;
|
|
825
|
+
const idx1 = idx0 + subSize;
|
|
826
|
+
const idx2 = idx0 + 2 * subSize;
|
|
827
|
+
let tw1Re, tw1Im, tw2Re, tw2Im;
|
|
828
|
+
if (ni === 0) {
|
|
829
|
+
tw1Re = 1;
|
|
830
|
+
tw1Im = 0;
|
|
831
|
+
tw2Re = 1;
|
|
832
|
+
tw2Im = 0;
|
|
833
|
+
} else {
|
|
834
|
+
tw1Re = this.twiddleRe[twOffset + ni - 1] ?? 0;
|
|
835
|
+
tw1Im = this.twiddleIm[twOffset + ni - 1] ?? 0;
|
|
836
|
+
tw2Re = this.twiddleRe[twOffset + subSize - 1 + ni - 1] ?? 0;
|
|
837
|
+
tw2Im = this.twiddleIm[twOffset + subSize - 1 + ni - 1] ?? 0;
|
|
838
|
+
}
|
|
839
|
+
const x1Re = (outRe[idx1] ?? 0) * tw1Re - (outIm[idx1] ?? 0) * tw1Im;
|
|
840
|
+
const x1Im = (outRe[idx1] ?? 0) * tw1Im + (outIm[idx1] ?? 0) * tw1Re;
|
|
841
|
+
const x2Re = (outRe[idx2] ?? 0) * tw2Re - (outIm[idx2] ?? 0) * tw2Im;
|
|
842
|
+
const x2Im = (outRe[idx2] ?? 0) * tw2Im + (outIm[idx2] ?? 0) * tw2Re;
|
|
843
|
+
const x0Re = outRe[idx0] ?? 0;
|
|
844
|
+
const x0Im = outIm[idx0] ?? 0;
|
|
845
|
+
const sumRe = x1Re + x2Re;
|
|
846
|
+
const sumIm = x1Im + x2Im;
|
|
847
|
+
const diffRe = x1Re - x2Re;
|
|
848
|
+
const diffIm = x1Im - x2Im;
|
|
849
|
+
outRe[idx0] = x0Re + sumRe;
|
|
850
|
+
outIm[idx0] = x0Im + sumIm;
|
|
851
|
+
outRe[idx1] = x0Re + c3 * sumRe - s3 * diffIm;
|
|
852
|
+
outIm[idx1] = x0Im + c3 * sumIm + s3 * diffRe;
|
|
853
|
+
outRe[idx2] = x0Re + c3 * sumRe + s3 * diffIm;
|
|
854
|
+
outIm[idx2] = x0Im + c3 * sumIm - s3 * diffRe;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return twOffset + 2 * (subSize - 1);
|
|
858
|
+
}
|
|
859
|
+
radix5(outRe, outIm, nn, groupSize, subSize, twOffset) {
|
|
860
|
+
const cos1 = Math.cos(2 * Math.PI / 5);
|
|
861
|
+
const cos2 = Math.cos(4 * Math.PI / 5);
|
|
862
|
+
const sin1 = -Math.sin(2 * Math.PI / 5);
|
|
863
|
+
const sin2 = -Math.sin(4 * Math.PI / 5);
|
|
864
|
+
for (let group = 0; group < nn; group += groupSize) {
|
|
865
|
+
for (let ni = 0; ni < subSize; ni++) {
|
|
866
|
+
const idx0 = group + ni;
|
|
867
|
+
const idx1 = idx0 + subSize;
|
|
868
|
+
const idx2 = idx0 + 2 * subSize;
|
|
869
|
+
const idx3 = idx0 + 3 * subSize;
|
|
870
|
+
const idx4 = idx0 + 4 * subSize;
|
|
871
|
+
let tw1Re, tw1Im;
|
|
872
|
+
let tw2Re, tw2Im;
|
|
873
|
+
let tw3Re, tw3Im;
|
|
874
|
+
let tw4Re, tw4Im;
|
|
875
|
+
if (ni === 0) {
|
|
876
|
+
tw1Re = 1;
|
|
877
|
+
tw1Im = 0;
|
|
878
|
+
tw2Re = 1;
|
|
879
|
+
tw2Im = 0;
|
|
880
|
+
tw3Re = 1;
|
|
881
|
+
tw3Im = 0;
|
|
882
|
+
tw4Re = 1;
|
|
883
|
+
tw4Im = 0;
|
|
884
|
+
} else {
|
|
885
|
+
tw1Re = this.twiddleRe[twOffset + ni - 1] ?? 0;
|
|
886
|
+
tw1Im = this.twiddleIm[twOffset + ni - 1] ?? 0;
|
|
887
|
+
tw2Re = this.twiddleRe[twOffset + subSize - 1 + ni - 1] ?? 0;
|
|
888
|
+
tw2Im = this.twiddleIm[twOffset + subSize - 1 + ni - 1] ?? 0;
|
|
889
|
+
tw3Re = this.twiddleRe[twOffset + 2 * (subSize - 1) + ni - 1] ?? 0;
|
|
890
|
+
tw3Im = this.twiddleIm[twOffset + 2 * (subSize - 1) + ni - 1] ?? 0;
|
|
891
|
+
tw4Re = this.twiddleRe[twOffset + 3 * (subSize - 1) + ni - 1] ?? 0;
|
|
892
|
+
tw4Im = this.twiddleIm[twOffset + 3 * (subSize - 1) + ni - 1] ?? 0;
|
|
893
|
+
}
|
|
894
|
+
const x0Re = outRe[idx0] ?? 0;
|
|
895
|
+
const x0Im = outIm[idx0] ?? 0;
|
|
896
|
+
const x1Re = (outRe[idx1] ?? 0) * tw1Re - (outIm[idx1] ?? 0) * tw1Im;
|
|
897
|
+
const x1Im = (outRe[idx1] ?? 0) * tw1Im + (outIm[idx1] ?? 0) * tw1Re;
|
|
898
|
+
const x2Re = (outRe[idx2] ?? 0) * tw2Re - (outIm[idx2] ?? 0) * tw2Im;
|
|
899
|
+
const x2Im = (outRe[idx2] ?? 0) * tw2Im + (outIm[idx2] ?? 0) * tw2Re;
|
|
900
|
+
const x3Re = (outRe[idx3] ?? 0) * tw3Re - (outIm[idx3] ?? 0) * tw3Im;
|
|
901
|
+
const x3Im = (outRe[idx3] ?? 0) * tw3Im + (outIm[idx3] ?? 0) * tw3Re;
|
|
902
|
+
const x4Re = (outRe[idx4] ?? 0) * tw4Re - (outIm[idx4] ?? 0) * tw4Im;
|
|
903
|
+
const x4Im = (outRe[idx4] ?? 0) * tw4Im + (outIm[idx4] ?? 0) * tw4Re;
|
|
904
|
+
const sum14Re = x1Re + x4Re;
|
|
905
|
+
const sum14Im = x1Im + x4Im;
|
|
906
|
+
const diff14Re = x1Re - x4Re;
|
|
907
|
+
const diff14Im = x1Im - x4Im;
|
|
908
|
+
const sum23Re = x2Re + x3Re;
|
|
909
|
+
const sum23Im = x2Im + x3Im;
|
|
910
|
+
const diff23Re = x2Re - x3Re;
|
|
911
|
+
const diff23Im = x2Im - x3Im;
|
|
912
|
+
outRe[idx0] = x0Re + sum14Re + sum23Re;
|
|
913
|
+
outIm[idx0] = x0Im + sum14Im + sum23Im;
|
|
914
|
+
outRe[idx1] = x0Re + cos1 * sum14Re + cos2 * sum23Re - sin1 * diff14Im - sin2 * diff23Im;
|
|
915
|
+
outIm[idx1] = x0Im + cos1 * sum14Im + cos2 * sum23Im + sin1 * diff14Re + sin2 * diff23Re;
|
|
916
|
+
outRe[idx2] = x0Re + cos2 * sum14Re + cos1 * sum23Re - sin2 * diff14Im + sin1 * diff23Im;
|
|
917
|
+
outIm[idx2] = x0Im + cos2 * sum14Im + cos1 * sum23Im + sin2 * diff14Re - sin1 * diff23Re;
|
|
918
|
+
outRe[idx3] = x0Re + cos2 * sum14Re + cos1 * sum23Re + sin2 * diff14Im - sin1 * diff23Im;
|
|
919
|
+
outIm[idx3] = x0Im + cos2 * sum14Im + cos1 * sum23Im - sin2 * diff14Re + sin1 * diff23Re;
|
|
920
|
+
outRe[idx4] = x0Re + cos1 * sum14Re + cos2 * sum23Re + sin1 * diff14Im + sin2 * diff23Im;
|
|
921
|
+
outIm[idx4] = x0Im + cos1 * sum14Im + cos2 * sum23Im - sin1 * diff14Re - sin2 * diff23Re;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return twOffset + 4 * (subSize - 1);
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
function factorize(size) {
|
|
928
|
+
const factors = [];
|
|
929
|
+
let remaining = size;
|
|
930
|
+
for (const prime of [5, 3, 2]) {
|
|
931
|
+
while (remaining % prime === 0) {
|
|
932
|
+
factors.push(prime);
|
|
933
|
+
remaining /= prime;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (remaining !== 1) {
|
|
937
|
+
throw new Error(`MixedRadixFft: size ${size} has unsupported prime factor ${remaining} (only 2, 3, 5 supported)`);
|
|
938
|
+
}
|
|
939
|
+
factors.sort((lhs, rhs) => lhs - rhs);
|
|
940
|
+
return factors;
|
|
941
|
+
}
|
|
942
|
+
function computePermutation(size, radices) {
|
|
943
|
+
const permutation = new Uint16Array(size);
|
|
944
|
+
for (let index = 0; index < size; index++) {
|
|
945
|
+
let remainder = index;
|
|
946
|
+
let permuted = 0;
|
|
947
|
+
let base = size;
|
|
948
|
+
for (const radix of radices) {
|
|
949
|
+
base = base / radix;
|
|
950
|
+
const digit = remainder % radix;
|
|
951
|
+
remainder = Math.floor(remainder / radix);
|
|
952
|
+
permuted += digit * base;
|
|
953
|
+
}
|
|
954
|
+
permutation[index] = permuted;
|
|
955
|
+
}
|
|
956
|
+
return permutation;
|
|
957
|
+
}
|
|
958
|
+
function computeTwiddles(radices) {
|
|
959
|
+
let totalTwiddles = 0;
|
|
960
|
+
let groupSize = 1;
|
|
961
|
+
for (const radix of radices) {
|
|
962
|
+
groupSize *= radix;
|
|
963
|
+
totalTwiddles += (radix - 1) * (groupSize / radix - 1);
|
|
964
|
+
}
|
|
965
|
+
const twiddleRe = new Float32Array(totalTwiddles);
|
|
966
|
+
const twiddleIm = new Float32Array(totalTwiddles);
|
|
967
|
+
let twOffset = 0;
|
|
968
|
+
groupSize = 1;
|
|
969
|
+
for (const radix of radices) {
|
|
970
|
+
groupSize *= radix;
|
|
971
|
+
const subSize = groupSize / radix;
|
|
972
|
+
for (let kk = 1; kk < radix; kk++) {
|
|
973
|
+
for (let ni = 1; ni < subSize; ni++) {
|
|
974
|
+
const angle = -2 * Math.PI * kk * ni / groupSize;
|
|
975
|
+
twiddleRe[twOffset] = Math.cos(angle);
|
|
976
|
+
twiddleIm[twOffset] = Math.sin(angle);
|
|
977
|
+
twOffset++;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return { twiddleRe, twiddleIm };
|
|
982
|
+
}
|
|
983
|
+
var STDERR_CAP_BYTES = 64 * 1024;
|
|
984
|
+
var ResampleStream = class {
|
|
985
|
+
constructor(ffmpegPath, options) {
|
|
986
|
+
this.chunks = [];
|
|
987
|
+
this.chunkedBytes = 0;
|
|
988
|
+
this.stdoutEnded = false;
|
|
989
|
+
this.exited = false;
|
|
990
|
+
this.stderr = "";
|
|
991
|
+
this.closed = false;
|
|
992
|
+
const { sourceSampleRate, targetSampleRate, channels } = options;
|
|
993
|
+
if (channels <= 0) throw new Error(`ResampleStream: channels must be > 0, got ${String(channels)}`);
|
|
994
|
+
if (sourceSampleRate <= 0) throw new Error(`ResampleStream: sourceSampleRate must be > 0, got ${String(sourceSampleRate)}`);
|
|
995
|
+
if (targetSampleRate <= 0) throw new Error(`ResampleStream: targetSampleRate must be > 0, got ${String(targetSampleRate)}`);
|
|
996
|
+
this.channels = channels;
|
|
997
|
+
this.bytesPerFrame = channels * 4;
|
|
998
|
+
const args = [
|
|
999
|
+
"-f",
|
|
1000
|
+
"f32le",
|
|
1001
|
+
"-ar",
|
|
1002
|
+
String(sourceSampleRate),
|
|
1003
|
+
"-ac",
|
|
1004
|
+
String(channels),
|
|
1005
|
+
"-i",
|
|
1006
|
+
"pipe:0",
|
|
1007
|
+
"-af",
|
|
1008
|
+
`aresample=${String(targetSampleRate)}:resampler=soxr:dither_method=triangular`,
|
|
1009
|
+
"-f",
|
|
1010
|
+
"f32le",
|
|
1011
|
+
"-ar",
|
|
1012
|
+
String(targetSampleRate),
|
|
1013
|
+
"-ac",
|
|
1014
|
+
String(channels),
|
|
1015
|
+
"pipe:1"
|
|
1016
|
+
];
|
|
1017
|
+
this.child = spawn(ffmpegPath, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1018
|
+
this.child.stdout.on("data", (bytes) => this.onStdoutData(bytes));
|
|
1019
|
+
this.child.stdout.once("end", () => this.onStdoutEnd());
|
|
1020
|
+
this.child.stderr.on("data", (bytes) => this.onStderrData(bytes));
|
|
1021
|
+
this.child.on("error", (error) => this.onExit(error));
|
|
1022
|
+
this.child.once("exit", (code) => {
|
|
1023
|
+
if (code !== null && code !== 0) {
|
|
1024
|
+
const detail = this.stderr ? `: ${this.stderr.slice(0, 1024)}` : "";
|
|
1025
|
+
this.onExit(new Error(`ffmpeg exited ${String(code)}${detail}`));
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
this.onExit();
|
|
1029
|
+
});
|
|
1030
|
+
this.child.stdin.on("error", (error) => {
|
|
1031
|
+
if (error.code === "EPIPE") return;
|
|
1032
|
+
this.exitError ?? (this.exitError = error);
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
async write(samples) {
|
|
1036
|
+
if (this.closed) throw new Error("ResampleStream: write after close");
|
|
1037
|
+
if (this.exitError) throw this.exitError;
|
|
1038
|
+
const frames = samples[0]?.length ?? 0;
|
|
1039
|
+
if (frames === 0) return;
|
|
1040
|
+
const interleaved = interleave(samples, frames, this.channels);
|
|
1041
|
+
const buf = Buffer.from(interleaved.buffer, interleaved.byteOffset, interleaved.byteLength);
|
|
1042
|
+
if (this.pendingDrain) await this.pendingDrain;
|
|
1043
|
+
const ok = this.child.stdin.write(buf);
|
|
1044
|
+
if (!ok) {
|
|
1045
|
+
this.pendingDrain = new Promise((resolve) => {
|
|
1046
|
+
this.child.stdin.once("drain", () => {
|
|
1047
|
+
this.pendingDrain = void 0;
|
|
1048
|
+
resolve();
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async read(frames) {
|
|
1054
|
+
if (this.closed) throw new Error("ResampleStream: read after close");
|
|
1055
|
+
if (frames <= 0) return this.emptyChannels();
|
|
1056
|
+
if (this.pendingRead) throw new Error("ResampleStream: concurrent read");
|
|
1057
|
+
if (this.chunkedBytes >= this.bytesPerFrame) return this.drainOutput(frames);
|
|
1058
|
+
if (this.exitError) throw this.exitError;
|
|
1059
|
+
if (this.stdoutEnded && this.exited) return this.emptyChannels();
|
|
1060
|
+
return new Promise((resolve, reject) => {
|
|
1061
|
+
this.pendingRead = { resolve, reject, frames };
|
|
1062
|
+
this.maybeSatisfyPendingRead();
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
async end() {
|
|
1066
|
+
if (this.closed) return;
|
|
1067
|
+
if (this.pendingDrain) await this.pendingDrain;
|
|
1068
|
+
if (!this.child.stdin.writableEnded) this.child.stdin.end();
|
|
1069
|
+
}
|
|
1070
|
+
async close() {
|
|
1071
|
+
if (this.closed) return;
|
|
1072
|
+
this.closed = true;
|
|
1073
|
+
if (this.pendingRead) {
|
|
1074
|
+
this.pendingRead.reject(new Error("ResampleStream: close while read pending"));
|
|
1075
|
+
this.pendingRead = void 0;
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
if (!this.child.stdin.writableEnded) {
|
|
1079
|
+
try {
|
|
1080
|
+
this.child.stdin.end();
|
|
1081
|
+
} catch {
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
} finally {
|
|
1085
|
+
if (!this.exited && this.child.exitCode === null && !this.child.killed) {
|
|
1086
|
+
this.child.kill("SIGTERM");
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (!this.exited) {
|
|
1090
|
+
await new Promise((resolve) => {
|
|
1091
|
+
let settled = false;
|
|
1092
|
+
const settle = () => {
|
|
1093
|
+
if (!settled) {
|
|
1094
|
+
settled = true;
|
|
1095
|
+
resolve();
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
const timeout = setTimeout(() => {
|
|
1099
|
+
this.child.kill("SIGKILL");
|
|
1100
|
+
settle();
|
|
1101
|
+
}, 5e3);
|
|
1102
|
+
this.child.once("exit", () => {
|
|
1103
|
+
clearTimeout(timeout);
|
|
1104
|
+
settle();
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
onStdoutData(bytes) {
|
|
1110
|
+
if (bytes.length > 0) {
|
|
1111
|
+
this.chunks.push(bytes);
|
|
1112
|
+
this.chunkedBytes += bytes.length;
|
|
1113
|
+
}
|
|
1114
|
+
this.maybeSatisfyPendingRead();
|
|
1115
|
+
}
|
|
1116
|
+
onStdoutEnd() {
|
|
1117
|
+
this.stdoutEnded = true;
|
|
1118
|
+
this.maybeSatisfyPendingRead();
|
|
1119
|
+
}
|
|
1120
|
+
onStderrData(bytes) {
|
|
1121
|
+
if (this.stderr.length >= STDERR_CAP_BYTES) return;
|
|
1122
|
+
const remaining = STDERR_CAP_BYTES - this.stderr.length;
|
|
1123
|
+
const text = bytes.toString("utf8");
|
|
1124
|
+
this.stderr += text.length > remaining ? text.slice(0, remaining) : text;
|
|
1125
|
+
}
|
|
1126
|
+
onExit(error) {
|
|
1127
|
+
this.exited = true;
|
|
1128
|
+
if (error) this.exitError ?? (this.exitError = error);
|
|
1129
|
+
this.maybeSatisfyPendingRead();
|
|
1130
|
+
}
|
|
1131
|
+
maybeSatisfyPendingRead() {
|
|
1132
|
+
if (!this.pendingRead) return;
|
|
1133
|
+
const { frames, resolve, reject } = this.pendingRead;
|
|
1134
|
+
if (this.chunkedBytes >= this.bytesPerFrame) {
|
|
1135
|
+
this.pendingRead = void 0;
|
|
1136
|
+
resolve(this.drainOutput(frames));
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (this.stdoutEnded && this.exited) {
|
|
1140
|
+
this.pendingRead = void 0;
|
|
1141
|
+
if (this.exitError) {
|
|
1142
|
+
reject(this.exitError);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
resolve(this.emptyChannels());
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
drainOutput(frames) {
|
|
1149
|
+
const wantBytes = frames * this.bytesPerFrame;
|
|
1150
|
+
const available = Math.min(this.chunkedBytes, wantBytes);
|
|
1151
|
+
const completeFrames = Math.floor(available / this.bytesPerFrame);
|
|
1152
|
+
if (completeFrames === 0) return this.emptyChannels();
|
|
1153
|
+
const completeBytes = completeFrames * this.bytesPerFrame;
|
|
1154
|
+
const aligned = Buffer.allocUnsafe(completeBytes);
|
|
1155
|
+
let written = 0;
|
|
1156
|
+
while (written < completeBytes) {
|
|
1157
|
+
const head = this.chunks[0];
|
|
1158
|
+
if (!head) break;
|
|
1159
|
+
const remaining = completeBytes - written;
|
|
1160
|
+
if (head.length <= remaining) {
|
|
1161
|
+
head.copy(aligned, written, 0, head.length);
|
|
1162
|
+
written += head.length;
|
|
1163
|
+
this.chunks.shift();
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
head.copy(aligned, written, 0, remaining);
|
|
1167
|
+
this.chunks[0] = head.subarray(remaining);
|
|
1168
|
+
written += remaining;
|
|
1169
|
+
}
|
|
1170
|
+
this.chunkedBytes -= completeBytes;
|
|
1171
|
+
return deinterleaveBuffer(aligned, this.channels);
|
|
1172
|
+
}
|
|
1173
|
+
emptyChannels() {
|
|
1174
|
+
const out = [];
|
|
1175
|
+
for (let ch = 0; ch < this.channels; ch++) out.push(new Float32Array(0));
|
|
1176
|
+
return out;
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
var SlidingWindowMaxStream = class {
|
|
1180
|
+
constructor(halfWidth) {
|
|
1181
|
+
this.dequeHead = 0;
|
|
1182
|
+
this.dequeTail = 0;
|
|
1183
|
+
this.consumedFrames = 0;
|
|
1184
|
+
this.emittedFrames = 0;
|
|
1185
|
+
if (halfWidth < 0 || !Number.isFinite(halfWidth)) {
|
|
1186
|
+
throw new RangeError(`SlidingWindowMaxStream: halfWidth must be a non-negative finite number, got ${halfWidth}`);
|
|
1187
|
+
}
|
|
1188
|
+
this.halfWidth = halfWidth;
|
|
1189
|
+
const ringCapacity = 2 * halfWidth + 1;
|
|
1190
|
+
this.lookAhead = new Float32Array(ringCapacity);
|
|
1191
|
+
this.deque = new Int32Array(ringCapacity);
|
|
1192
|
+
}
|
|
1193
|
+
push(chunk, isFinal) {
|
|
1194
|
+
const chunkLength = chunk.length;
|
|
1195
|
+
const halfWidth = this.halfWidth;
|
|
1196
|
+
const ringSize = this.lookAhead.length;
|
|
1197
|
+
const dequeCapacity = this.deque.length;
|
|
1198
|
+
const totalAfter = this.consumedFrames + chunkLength;
|
|
1199
|
+
const targetEmittedAfter = isFinal ? totalAfter : Math.max(0, totalAfter - halfWidth);
|
|
1200
|
+
const emitCount = Math.max(0, targetEmittedAfter - this.emittedFrames);
|
|
1201
|
+
const output = new Float32Array(emitCount);
|
|
1202
|
+
let outputCursor = 0;
|
|
1203
|
+
for (let chunkIdx = 0; chunkIdx < chunkLength; chunkIdx++) {
|
|
1204
|
+
const inputIdx = this.consumedFrames;
|
|
1205
|
+
const value = chunk[chunkIdx] ?? 0;
|
|
1206
|
+
this.lookAhead[inputIdx % ringSize] = value;
|
|
1207
|
+
while (this.dequeTail > this.dequeHead) {
|
|
1208
|
+
const tailIdx = this.deque[(this.dequeTail - 1) % dequeCapacity] ?? 0;
|
|
1209
|
+
const tailValue = this.lookAhead[tailIdx % ringSize] ?? 0;
|
|
1210
|
+
if (tailValue > value) break;
|
|
1211
|
+
this.dequeTail--;
|
|
1212
|
+
}
|
|
1213
|
+
this.deque[this.dequeTail % dequeCapacity] = inputIdx;
|
|
1214
|
+
this.dequeTail++;
|
|
1215
|
+
this.consumedFrames++;
|
|
1216
|
+
const outputIdx = inputIdx - halfWidth;
|
|
1217
|
+
if (outputIdx < 0) continue;
|
|
1218
|
+
const leftEdge = Math.max(0, outputIdx - halfWidth);
|
|
1219
|
+
while (this.dequeTail > this.dequeHead && (this.deque[this.dequeHead % dequeCapacity] ?? 0) < leftEdge) {
|
|
1220
|
+
this.dequeHead++;
|
|
1221
|
+
}
|
|
1222
|
+
const frontIdx = this.deque[this.dequeHead % dequeCapacity] ?? 0;
|
|
1223
|
+
output[outputCursor] = this.lookAhead[frontIdx % ringSize] ?? 0;
|
|
1224
|
+
outputCursor++;
|
|
1225
|
+
this.emittedFrames++;
|
|
1226
|
+
}
|
|
1227
|
+
if (isFinal) {
|
|
1228
|
+
const finalLength = this.consumedFrames;
|
|
1229
|
+
while (this.emittedFrames < finalLength) {
|
|
1230
|
+
const outputIdx = this.emittedFrames;
|
|
1231
|
+
const leftEdge = Math.max(0, outputIdx - halfWidth);
|
|
1232
|
+
while (this.dequeTail > this.dequeHead && (this.deque[this.dequeHead % dequeCapacity] ?? 0) < leftEdge) {
|
|
1233
|
+
this.dequeHead++;
|
|
1234
|
+
}
|
|
1235
|
+
if (this.dequeTail === this.dequeHead) {
|
|
1236
|
+
output[outputCursor] = 0;
|
|
1237
|
+
} else {
|
|
1238
|
+
const frontIdx = this.deque[this.dequeHead % dequeCapacity] ?? 0;
|
|
1239
|
+
output[outputCursor] = this.lookAhead[frontIdx % ringSize] ?? 0;
|
|
1240
|
+
}
|
|
1241
|
+
outputCursor++;
|
|
1242
|
+
this.emittedFrames++;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return output;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
var SlidingWindowMinStream = class {
|
|
1249
|
+
constructor(halfWidth) {
|
|
1250
|
+
this.dequeHead = 0;
|
|
1251
|
+
this.dequeTail = 0;
|
|
1252
|
+
this.consumedFrames = 0;
|
|
1253
|
+
this.emittedFrames = 0;
|
|
1254
|
+
if (halfWidth < 0 || !Number.isFinite(halfWidth)) {
|
|
1255
|
+
throw new RangeError(`SlidingWindowMinStream: halfWidth must be a non-negative finite number, got ${halfWidth}`);
|
|
1256
|
+
}
|
|
1257
|
+
this.halfWidth = halfWidth;
|
|
1258
|
+
const ringCapacity = 2 * halfWidth + 1;
|
|
1259
|
+
this.lookAhead = new Float32Array(ringCapacity);
|
|
1260
|
+
this.deque = new Int32Array(ringCapacity);
|
|
1261
|
+
}
|
|
1262
|
+
push(chunk, isFinal) {
|
|
1263
|
+
const chunkLength = chunk.length;
|
|
1264
|
+
const halfWidth = this.halfWidth;
|
|
1265
|
+
const ringSize = this.lookAhead.length;
|
|
1266
|
+
const dequeCapacity = this.deque.length;
|
|
1267
|
+
const totalAfter = this.consumedFrames + chunkLength;
|
|
1268
|
+
const targetEmittedAfter = isFinal ? totalAfter : Math.max(0, totalAfter - halfWidth);
|
|
1269
|
+
const emitCount = Math.max(0, targetEmittedAfter - this.emittedFrames);
|
|
1270
|
+
const output = new Float32Array(emitCount);
|
|
1271
|
+
let outputCursor = 0;
|
|
1272
|
+
for (let chunkIdx = 0; chunkIdx < chunkLength; chunkIdx++) {
|
|
1273
|
+
const inputIdx = this.consumedFrames;
|
|
1274
|
+
const value = chunk[chunkIdx] ?? 0;
|
|
1275
|
+
this.lookAhead[inputIdx % ringSize] = value;
|
|
1276
|
+
while (this.dequeTail > this.dequeHead) {
|
|
1277
|
+
const tailIdx = this.deque[(this.dequeTail - 1) % dequeCapacity] ?? 0;
|
|
1278
|
+
const tailValue = this.lookAhead[tailIdx % ringSize] ?? 0;
|
|
1279
|
+
if (tailValue < value) break;
|
|
1280
|
+
this.dequeTail--;
|
|
1281
|
+
}
|
|
1282
|
+
this.deque[this.dequeTail % dequeCapacity] = inputIdx;
|
|
1283
|
+
this.dequeTail++;
|
|
1284
|
+
this.consumedFrames++;
|
|
1285
|
+
const outputIdx = inputIdx - halfWidth;
|
|
1286
|
+
if (outputIdx < 0) continue;
|
|
1287
|
+
const leftEdge = Math.max(0, outputIdx - halfWidth);
|
|
1288
|
+
while (this.dequeTail > this.dequeHead && (this.deque[this.dequeHead % dequeCapacity] ?? 0) < leftEdge) {
|
|
1289
|
+
this.dequeHead++;
|
|
1290
|
+
}
|
|
1291
|
+
const frontIdx = this.deque[this.dequeHead % dequeCapacity] ?? 0;
|
|
1292
|
+
output[outputCursor] = this.lookAhead[frontIdx % ringSize] ?? 0;
|
|
1293
|
+
outputCursor++;
|
|
1294
|
+
this.emittedFrames++;
|
|
1295
|
+
}
|
|
1296
|
+
if (isFinal) {
|
|
1297
|
+
const finalLength = this.consumedFrames;
|
|
1298
|
+
while (this.emittedFrames < finalLength) {
|
|
1299
|
+
const outputIdx = this.emittedFrames;
|
|
1300
|
+
const leftEdge = Math.max(0, outputIdx - halfWidth);
|
|
1301
|
+
while (this.dequeTail > this.dequeHead && (this.deque[this.dequeHead % dequeCapacity] ?? 0) < leftEdge) {
|
|
1302
|
+
this.dequeHead++;
|
|
1303
|
+
}
|
|
1304
|
+
if (this.dequeTail === this.dequeHead) {
|
|
1305
|
+
output[outputCursor] = 0;
|
|
1306
|
+
} else {
|
|
1307
|
+
const frontIdx = this.deque[this.dequeHead % dequeCapacity] ?? 0;
|
|
1308
|
+
output[outputCursor] = this.lookAhead[frontIdx % ringSize] ?? 0;
|
|
1309
|
+
}
|
|
1310
|
+
outputCursor++;
|
|
1311
|
+
this.emittedFrames++;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return output;
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
var batchInputCache = /* @__PURE__ */ new Map();
|
|
1318
|
+
function getBatchInput(fftSize, numFrames) {
|
|
1319
|
+
const needed = fftSize * numFrames;
|
|
1320
|
+
const cached = batchInputCache.get(fftSize);
|
|
1321
|
+
if (cached && cached.length >= needed) return cached;
|
|
1322
|
+
const grown = new Float32Array(needed);
|
|
1323
|
+
batchInputCache.set(fftSize, grown);
|
|
1324
|
+
return grown;
|
|
1325
|
+
}
|
|
1326
|
+
var batchTimeCache = /* @__PURE__ */ new Map();
|
|
1327
|
+
function getBatchTime(fftSize, numFrames) {
|
|
1328
|
+
const needed = fftSize * numFrames;
|
|
1329
|
+
const cached = batchTimeCache.get(fftSize);
|
|
1330
|
+
if (cached && cached.length >= needed) return cached;
|
|
1331
|
+
const grown = new Float32Array(needed);
|
|
1332
|
+
batchTimeCache.set(fftSize, grown);
|
|
1333
|
+
return grown;
|
|
1334
|
+
}
|
|
1335
|
+
function stft(signal, fftSize, hopSize, output, backend, fftAddonOptions) {
|
|
1336
|
+
const window = hanningWindow(fftSize);
|
|
1337
|
+
const numFrames = Math.floor((signal.length - fftSize) / hopSize) + 1;
|
|
1338
|
+
const halfSize = fftSize / 2 + 1;
|
|
1339
|
+
const addon = backend ? getFftAddon(backend, fftAddonOptions) : null;
|
|
1340
|
+
const real = output?.real ?? (numFrames > 0 ? new Float32Array(halfSize * numFrames) : new Float32Array(0));
|
|
1341
|
+
const imag = output?.imag ?? (numFrames > 0 ? new Float32Array(halfSize * numFrames) : new Float32Array(0));
|
|
1342
|
+
if (numFrames <= 0) {
|
|
1343
|
+
return { real, imag, frames: 0, fftSize };
|
|
1344
|
+
}
|
|
1345
|
+
if (addon) {
|
|
1346
|
+
const batchInput = getBatchInput(fftSize, numFrames);
|
|
1347
|
+
for (let frame = 0; frame < numFrames; frame++) {
|
|
1348
|
+
const offset = frame * hopSize;
|
|
1349
|
+
for (let index = 0; index < fftSize; index++) {
|
|
1350
|
+
batchInput[frame * fftSize + index] = (signal[offset + index] ?? 0) * (window[index] ?? 0);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (typeof addon.batchFftInto === "function") {
|
|
1354
|
+
addon.batchFftInto(batchInput.subarray(0, fftSize * numFrames), real.subarray(0, halfSize * numFrames), imag.subarray(0, halfSize * numFrames), fftSize, numFrames);
|
|
1355
|
+
} else {
|
|
1356
|
+
const { re: batchRe, im: batchIm } = addon.batchFft(batchInput.subarray(0, fftSize * numFrames), fftSize, numFrames);
|
|
1357
|
+
real.set(batchRe.subarray(0, halfSize * numFrames));
|
|
1358
|
+
imag.set(batchIm.subarray(0, halfSize * numFrames));
|
|
1359
|
+
}
|
|
1360
|
+
return { real, imag, frames: numFrames, fftSize };
|
|
1361
|
+
}
|
|
1362
|
+
const windowed = new Float32Array(fftSize);
|
|
1363
|
+
const workspace = createFftWorkspace(fftSize);
|
|
1364
|
+
for (let frame = 0; frame < numFrames; frame++) {
|
|
1365
|
+
const offset = frame * hopSize;
|
|
1366
|
+
for (let index = 0; index < fftSize; index++) {
|
|
1367
|
+
windowed[index] = (signal[offset + index] ?? 0) * (window[index] ?? 0);
|
|
1368
|
+
}
|
|
1369
|
+
const { re, im } = fft(windowed, workspace);
|
|
1370
|
+
const dstOffset = frame * halfSize;
|
|
1371
|
+
for (let bin = 0; bin < halfSize; bin++) {
|
|
1372
|
+
real[dstOffset + bin] = re[bin] ?? 0;
|
|
1373
|
+
imag[dstOffset + bin] = im[bin] ?? 0;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return { real, imag, frames: numFrames, fftSize };
|
|
1377
|
+
}
|
|
1378
|
+
function istft(result, hopSize, outputLength, backend, fftAddonOptions) {
|
|
1379
|
+
const { real, imag, frames, fftSize } = result;
|
|
1380
|
+
const window = hanningWindow(fftSize);
|
|
1381
|
+
const output = new Float32Array(outputLength);
|
|
1382
|
+
const windowSum = new Float32Array(outputLength);
|
|
1383
|
+
const halfSize = fftSize / 2 + 1;
|
|
1384
|
+
const addon = backend ? getFftAddon(backend, fftAddonOptions) : null;
|
|
1385
|
+
if (addon && frames > 0) {
|
|
1386
|
+
const reView = real.subarray(0, halfSize * frames);
|
|
1387
|
+
const imView = imag.subarray(0, halfSize * frames);
|
|
1388
|
+
let timeDomainBatch;
|
|
1389
|
+
if (typeof addon.batchIfftInto === "function") {
|
|
1390
|
+
const batchTime = getBatchTime(fftSize, frames);
|
|
1391
|
+
addon.batchIfftInto(reView, imView, batchTime.subarray(0, fftSize * frames), fftSize, frames);
|
|
1392
|
+
timeDomainBatch = batchTime;
|
|
1393
|
+
} else {
|
|
1394
|
+
timeDomainBatch = addon.batchIfft(reView, imView, fftSize, frames);
|
|
1395
|
+
}
|
|
1396
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
1397
|
+
const offset = frame * hopSize;
|
|
1398
|
+
for (let index = 0; index < fftSize; index++) {
|
|
1399
|
+
const pos = offset + index;
|
|
1400
|
+
if (pos < outputLength) {
|
|
1401
|
+
output[pos] = (output[pos] ?? 0) + (timeDomainBatch[frame * fftSize + index] ?? 0) * (window[index] ?? 0);
|
|
1402
|
+
windowSum[pos] = (windowSum[pos] ?? 0) + (window[index] ?? 0) * (window[index] ?? 0);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
} else {
|
|
1407
|
+
const fullRe = new Float32Array(fftSize);
|
|
1408
|
+
const fullIm = new Float32Array(fftSize);
|
|
1409
|
+
const workspace = createFftWorkspace(fftSize);
|
|
1410
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
1411
|
+
const srcOffset = frame * halfSize;
|
|
1412
|
+
fullRe.fill(0);
|
|
1413
|
+
fullIm.fill(0);
|
|
1414
|
+
for (let bin = 0; bin < halfSize; bin++) {
|
|
1415
|
+
fullRe[bin] = real[srcOffset + bin] ?? 0;
|
|
1416
|
+
fullIm[bin] = imag[srcOffset + bin] ?? 0;
|
|
1417
|
+
}
|
|
1418
|
+
for (let index = 1; index < halfSize - 1; index++) {
|
|
1419
|
+
fullRe[fftSize - index] = real[srcOffset + index] ?? 0;
|
|
1420
|
+
fullIm[fftSize - index] = -(imag[srcOffset + index] ?? 0);
|
|
1421
|
+
}
|
|
1422
|
+
const timeDomain = ifft(fullRe, fullIm, workspace);
|
|
1423
|
+
const offset = frame * hopSize;
|
|
1424
|
+
for (let index = 0; index < fftSize; index++) {
|
|
1425
|
+
const pos = offset + index;
|
|
1426
|
+
if (pos < outputLength) {
|
|
1427
|
+
output[pos] = (output[pos] ?? 0) + (timeDomain[index] ?? 0) * (window[index] ?? 0);
|
|
1428
|
+
windowSum[pos] = (windowSum[pos] ?? 0) + (window[index] ?? 0) * (window[index] ?? 0);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
for (let index = 0; index < outputLength; index++) {
|
|
1434
|
+
const ws = windowSum[index] ?? 0;
|
|
1435
|
+
if (ws > 1e-8) {
|
|
1436
|
+
output[index] = (output[index] ?? 0) / ws;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return output;
|
|
1440
|
+
}
|
|
1441
|
+
var hanningWindowCache = /* @__PURE__ */ new Map();
|
|
1442
|
+
function hanningWindow(size, periodic = true) {
|
|
1443
|
+
const key = `${size}:${periodic ? "p" : "s"}`;
|
|
1444
|
+
const cached = hanningWindowCache.get(key);
|
|
1445
|
+
if (cached) return cached;
|
|
1446
|
+
const window = new Float32Array(size);
|
|
1447
|
+
const denominator = periodic ? size : size - 1;
|
|
1448
|
+
for (let index = 0; index < size; index++) {
|
|
1449
|
+
window[index] = 0.5 * (1 - Math.cos(2 * Math.PI * index / denominator));
|
|
1450
|
+
}
|
|
1451
|
+
hanningWindowCache.set(key, window);
|
|
1452
|
+
return window;
|
|
1453
|
+
}
|
|
1454
|
+
function createFftWorkspace(size) {
|
|
1455
|
+
return {
|
|
1456
|
+
re: new Float32Array(size),
|
|
1457
|
+
im: new Float32Array(size),
|
|
1458
|
+
outRe: new Float32Array(size),
|
|
1459
|
+
outIm: new Float32Array(size)
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
function fft(input, workspace) {
|
|
1463
|
+
const size = input.length;
|
|
1464
|
+
const re = workspace ? workspace.re : new Float32Array(size);
|
|
1465
|
+
const im = workspace ? workspace.im : new Float32Array(size);
|
|
1466
|
+
re.set(input);
|
|
1467
|
+
if (workspace) im.fill(0);
|
|
1468
|
+
if (size <= 1) return { re, im };
|
|
1469
|
+
bitReverse(re, im, size);
|
|
1470
|
+
butterflyStages(re, im, size);
|
|
1471
|
+
return { re, im };
|
|
1472
|
+
}
|
|
1473
|
+
function ifft(re, im, workspace) {
|
|
1474
|
+
const size = re.length;
|
|
1475
|
+
const outRe = workspace ? workspace.outRe : Float32Array.from(re);
|
|
1476
|
+
const outIm = workspace ? workspace.outIm : new Float32Array(size);
|
|
1477
|
+
if (workspace) outRe.set(re);
|
|
1478
|
+
for (let index = 0; index < size; index++) {
|
|
1479
|
+
outIm[index] = -(im[index] ?? 0);
|
|
1480
|
+
}
|
|
1481
|
+
bitReverse(outRe, outIm, size);
|
|
1482
|
+
butterflyStages(outRe, outIm, size);
|
|
1483
|
+
for (let index = 0; index < size; index++) {
|
|
1484
|
+
outRe[index] = (outRe[index] ?? 0) / size;
|
|
1485
|
+
}
|
|
1486
|
+
return outRe;
|
|
1487
|
+
}
|
|
1488
|
+
function bitReverse(re, im, size) {
|
|
1489
|
+
let rev = 0;
|
|
1490
|
+
for (let index = 0; index < size - 1; index++) {
|
|
1491
|
+
if (index < rev) {
|
|
1492
|
+
const tempRe = re[index];
|
|
1493
|
+
const tempIm = im[index];
|
|
1494
|
+
re[index] = re[rev];
|
|
1495
|
+
im[index] = im[rev];
|
|
1496
|
+
re[rev] = tempRe;
|
|
1497
|
+
im[rev] = tempIm;
|
|
1498
|
+
}
|
|
1499
|
+
let bit = size >> 1;
|
|
1500
|
+
while (bit <= rev) {
|
|
1501
|
+
rev -= bit;
|
|
1502
|
+
bit >>= 1;
|
|
1503
|
+
}
|
|
1504
|
+
rev += bit;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
var twiddleCache = /* @__PURE__ */ new Map();
|
|
1508
|
+
function getTwiddleFactors(size) {
|
|
1509
|
+
let cached = twiddleCache.get(size);
|
|
1510
|
+
if (cached) return cached;
|
|
1511
|
+
const totalFactors = size / 2 * Math.log2(size);
|
|
1512
|
+
const twRe = new Float32Array(totalFactors);
|
|
1513
|
+
const twIm = new Float32Array(totalFactors);
|
|
1514
|
+
let offset = 0;
|
|
1515
|
+
for (let step = 2; step <= size; step *= 2) {
|
|
1516
|
+
const halfStep = step / 2;
|
|
1517
|
+
const angle = -2 * Math.PI / step;
|
|
1518
|
+
for (let pair = 0; pair < halfStep; pair++) {
|
|
1519
|
+
twRe[offset + pair] = Math.cos(angle * pair);
|
|
1520
|
+
twIm[offset + pair] = Math.sin(angle * pair);
|
|
1521
|
+
}
|
|
1522
|
+
offset += halfStep;
|
|
1523
|
+
}
|
|
1524
|
+
cached = { re: twRe, im: twIm };
|
|
1525
|
+
twiddleCache.set(size, cached);
|
|
1526
|
+
return cached;
|
|
1527
|
+
}
|
|
1528
|
+
function butterflyStages(re, im, size) {
|
|
1529
|
+
const twiddle = getTwiddleFactors(size);
|
|
1530
|
+
const twRe = twiddle.re;
|
|
1531
|
+
const twIm = twiddle.im;
|
|
1532
|
+
let twOffset = 0;
|
|
1533
|
+
for (let step = 2; step <= size; step *= 2) {
|
|
1534
|
+
const halfStep = step / 2;
|
|
1535
|
+
for (let group = 0; group < size; group += step) {
|
|
1536
|
+
for (let pair = 0; pair < halfStep; pair++) {
|
|
1537
|
+
const wr = twRe[twOffset + pair];
|
|
1538
|
+
const wi = twIm[twOffset + pair];
|
|
1539
|
+
const evenIdx = group + pair;
|
|
1540
|
+
const oddIdx = group + pair + halfStep;
|
|
1541
|
+
const oddRe = re[oddIdx];
|
|
1542
|
+
const oddIm = im[oddIdx];
|
|
1543
|
+
const evenRe = re[evenIdx];
|
|
1544
|
+
const evenIm = im[evenIdx];
|
|
1545
|
+
const tRe = oddRe * wr - oddIm * wi;
|
|
1546
|
+
const tIm = oddRe * wi + oddIm * wr;
|
|
1547
|
+
re[oddIdx] = evenRe - tRe;
|
|
1548
|
+
im[oddIdx] = evenIm - tIm;
|
|
1549
|
+
re[evenIdx] = evenRe + tRe;
|
|
1550
|
+
im[evenIdx] = evenIm + tIm;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
twOffset += halfStep;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
var TAPS_PER_PHASE_4X = 12;
|
|
1557
|
+
var HISTORY_LENGTH_4X = TAPS_PER_PHASE_4X;
|
|
1558
|
+
var P1T0 = 0.001708984375;
|
|
1559
|
+
var P1T1 = 0.010986328125;
|
|
1560
|
+
var P1T2 = -0.0196533203125;
|
|
1561
|
+
var P1T3 = 0.033203125;
|
|
1562
|
+
var P1T4 = -0.0594482421875;
|
|
1563
|
+
var P1T5 = 0.1373291015625;
|
|
1564
|
+
var P1T6 = 0.97216796875;
|
|
1565
|
+
var P1T7 = -0.102294921875;
|
|
1566
|
+
var P1T8 = 0.047607421875;
|
|
1567
|
+
var P1T9 = -0.026611328125;
|
|
1568
|
+
var P1T10 = 0.014892578125;
|
|
1569
|
+
var P1T11 = -0.00830078125;
|
|
1570
|
+
var P2T0 = -0.0291748046875;
|
|
1571
|
+
var P2T1 = 0.029296875;
|
|
1572
|
+
var P2T2 = -0.0517578125;
|
|
1573
|
+
var P2T3 = 0.089111328125;
|
|
1574
|
+
var P2T4 = -0.16650390625;
|
|
1575
|
+
var P2T5 = 0.465087890625;
|
|
1576
|
+
var P2T6 = 0.77978515625;
|
|
1577
|
+
var P2T7 = -0.2003173828125;
|
|
1578
|
+
var P2T8 = 0.1015625;
|
|
1579
|
+
var P2T9 = -0.0582275390625;
|
|
1580
|
+
var P2T10 = 0.0330810546875;
|
|
1581
|
+
var P2T11 = -0.0189208984375;
|
|
1582
|
+
var P3T0 = -0.0189208984375;
|
|
1583
|
+
var P3T1 = 0.0330810546875;
|
|
1584
|
+
var P3T2 = -0.0582275390625;
|
|
1585
|
+
var P3T3 = 0.1015625;
|
|
1586
|
+
var P3T4 = -0.2003173828125;
|
|
1587
|
+
var P3T5 = 0.77978515625;
|
|
1588
|
+
var P3T6 = 0.465087890625;
|
|
1589
|
+
var P3T7 = -0.16650390625;
|
|
1590
|
+
var P3T8 = 0.089111328125;
|
|
1591
|
+
var P3T9 = -0.0517578125;
|
|
1592
|
+
var P3T10 = 0.029296875;
|
|
1593
|
+
var P3T11 = -0.0291748046875;
|
|
1594
|
+
var TruePeakUpsampler = class {
|
|
1595
|
+
constructor(factor = 4) {
|
|
1596
|
+
this.work = new Float64Array(0);
|
|
1597
|
+
if (factor !== 4) {
|
|
1598
|
+
throw new Error(`TruePeakUpsampler: factor ${factor} is not yet implemented; only 4\xD7 (BS.1770-4 Annex 1) is supported`);
|
|
1599
|
+
}
|
|
1600
|
+
this.factor = factor;
|
|
1601
|
+
this.history = new Float64Array(HISTORY_LENGTH_4X);
|
|
1602
|
+
}
|
|
1603
|
+
// `outputScratch` (>= input.length × factor) avoids the per-call allocation; the returned view aliases it.
|
|
1604
|
+
upsample(input, outputScratch) {
|
|
1605
|
+
const factor = this.factor;
|
|
1606
|
+
const inputLength = input.length;
|
|
1607
|
+
const outputLength = inputLength * factor;
|
|
1608
|
+
const output = outputScratch !== void 0 && outputScratch.length >= outputLength ? outputScratch.subarray(0, outputLength) : new Float32Array(outputLength);
|
|
1609
|
+
const history = this.history;
|
|
1610
|
+
const historyLength = HISTORY_LENGTH_4X;
|
|
1611
|
+
const workLength = historyLength + inputLength;
|
|
1612
|
+
if (this.work.length < workLength) {
|
|
1613
|
+
this.work = new Float64Array(workLength);
|
|
1614
|
+
}
|
|
1615
|
+
const work = this.work;
|
|
1616
|
+
work.set(history, 0);
|
|
1617
|
+
for (let inIdx = 0; inIdx < inputLength; inIdx++) {
|
|
1618
|
+
work[historyLength + inIdx] = input[inIdx] ?? 0;
|
|
1619
|
+
}
|
|
1620
|
+
for (let inIdx = 0; inIdx < inputLength; inIdx++) {
|
|
1621
|
+
const currentIdx = historyLength + inIdx;
|
|
1622
|
+
const outOffset = inIdx * factor;
|
|
1623
|
+
output[outOffset] = input[inIdx] ?? 0;
|
|
1624
|
+
const v0 = work[currentIdx] ?? 0;
|
|
1625
|
+
const v1 = work[currentIdx - 1] ?? 0;
|
|
1626
|
+
const v2 = work[currentIdx - 2] ?? 0;
|
|
1627
|
+
const v3 = work[currentIdx - 3] ?? 0;
|
|
1628
|
+
const v4 = work[currentIdx - 4] ?? 0;
|
|
1629
|
+
const v5 = work[currentIdx - 5] ?? 0;
|
|
1630
|
+
const v6 = work[currentIdx - 6] ?? 0;
|
|
1631
|
+
const v7 = work[currentIdx - 7] ?? 0;
|
|
1632
|
+
const v8 = work[currentIdx - 8] ?? 0;
|
|
1633
|
+
const v9 = work[currentIdx - 9] ?? 0;
|
|
1634
|
+
const v10 = work[currentIdx - 10] ?? 0;
|
|
1635
|
+
const v11 = work[currentIdx - 11] ?? 0;
|
|
1636
|
+
let acc1 = 0;
|
|
1637
|
+
acc1 += P1T0 * v0;
|
|
1638
|
+
acc1 += P1T1 * v1;
|
|
1639
|
+
acc1 += P1T2 * v2;
|
|
1640
|
+
acc1 += P1T3 * v3;
|
|
1641
|
+
acc1 += P1T4 * v4;
|
|
1642
|
+
acc1 += P1T5 * v5;
|
|
1643
|
+
acc1 += P1T6 * v6;
|
|
1644
|
+
acc1 += P1T7 * v7;
|
|
1645
|
+
acc1 += P1T8 * v8;
|
|
1646
|
+
acc1 += P1T9 * v9;
|
|
1647
|
+
acc1 += P1T10 * v10;
|
|
1648
|
+
acc1 += P1T11 * v11;
|
|
1649
|
+
let acc2 = 0;
|
|
1650
|
+
acc2 += P2T0 * v0;
|
|
1651
|
+
acc2 += P2T1 * v1;
|
|
1652
|
+
acc2 += P2T2 * v2;
|
|
1653
|
+
acc2 += P2T3 * v3;
|
|
1654
|
+
acc2 += P2T4 * v4;
|
|
1655
|
+
acc2 += P2T5 * v5;
|
|
1656
|
+
acc2 += P2T6 * v6;
|
|
1657
|
+
acc2 += P2T7 * v7;
|
|
1658
|
+
acc2 += P2T8 * v8;
|
|
1659
|
+
acc2 += P2T9 * v9;
|
|
1660
|
+
acc2 += P2T10 * v10;
|
|
1661
|
+
acc2 += P2T11 * v11;
|
|
1662
|
+
let acc3 = 0;
|
|
1663
|
+
acc3 += P3T0 * v0;
|
|
1664
|
+
acc3 += P3T1 * v1;
|
|
1665
|
+
acc3 += P3T2 * v2;
|
|
1666
|
+
acc3 += P3T3 * v3;
|
|
1667
|
+
acc3 += P3T4 * v4;
|
|
1668
|
+
acc3 += P3T5 * v5;
|
|
1669
|
+
acc3 += P3T6 * v6;
|
|
1670
|
+
acc3 += P3T7 * v7;
|
|
1671
|
+
acc3 += P3T8 * v8;
|
|
1672
|
+
acc3 += P3T9 * v9;
|
|
1673
|
+
acc3 += P3T10 * v10;
|
|
1674
|
+
acc3 += P3T11 * v11;
|
|
1675
|
+
output[outOffset + 1] = acc1;
|
|
1676
|
+
output[outOffset + 2] = acc2;
|
|
1677
|
+
output[outOffset + 3] = acc3;
|
|
1678
|
+
}
|
|
1679
|
+
if (inputLength >= historyLength) {
|
|
1680
|
+
history.set(work.subarray(workLength - historyLength, workLength), 0);
|
|
1681
|
+
} else if (inputLength > 0) {
|
|
1682
|
+
history.copyWithin(0, inputLength);
|
|
1683
|
+
history.set(work.subarray(historyLength, workLength), historyLength - inputLength);
|
|
1684
|
+
}
|
|
1685
|
+
return output;
|
|
1686
|
+
}
|
|
1687
|
+
reset() {
|
|
1688
|
+
this.history.fill(0);
|
|
1689
|
+
this.work = new Float64Array(0);
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
var DEFAULT_OVERSAMPLE_FACTOR = 4;
|
|
1693
|
+
var TruePeakAccumulator = class {
|
|
1694
|
+
constructor(_sampleRate, channelCount, oversampleFactor = DEFAULT_OVERSAMPLE_FACTOR) {
|
|
1695
|
+
this.upsampleScratch = new Float32Array(0);
|
|
1696
|
+
this.runningMax = 0;
|
|
1697
|
+
if (channelCount <= 0) {
|
|
1698
|
+
throw new Error(`TruePeakAccumulator: channelCount must be positive, got ${channelCount}`);
|
|
1699
|
+
}
|
|
1700
|
+
this.channelCount = channelCount;
|
|
1701
|
+
const upsamplers = [];
|
|
1702
|
+
for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
|
|
1703
|
+
upsamplers.push(new TruePeakUpsampler(oversampleFactor));
|
|
1704
|
+
}
|
|
1705
|
+
this.upsamplers = upsamplers;
|
|
1706
|
+
}
|
|
1707
|
+
// `channels[c]` needs >= `frames` valid samples from index 0 (oversized OK); advances per-channel upsampler state as if appended contiguously.
|
|
1708
|
+
push(channels, frames) {
|
|
1709
|
+
if (channels.length !== this.channelCount) {
|
|
1710
|
+
throw new Error(`TruePeakAccumulator: push got ${channels.length} channels, expected ${this.channelCount}`);
|
|
1711
|
+
}
|
|
1712
|
+
if (frames <= 0) return;
|
|
1713
|
+
for (let channelIndex = 0; channelIndex < this.channelCount; channelIndex++) {
|
|
1714
|
+
const channel = channels[channelIndex];
|
|
1715
|
+
if (channel === void 0 || channel.length < frames) {
|
|
1716
|
+
throw new Error(`TruePeakAccumulator: channel ${channelIndex} has ${channel?.length ?? 0} samples, fewer than the requested ${frames}`);
|
|
1717
|
+
}
|
|
1718
|
+
const upsampler = this.upsamplers[channelIndex];
|
|
1719
|
+
if (upsampler === void 0) {
|
|
1720
|
+
throw new Error(`TruePeakAccumulator: missing upsampler for channel ${channelIndex}`);
|
|
1721
|
+
}
|
|
1722
|
+
const slice = channel.length === frames ? channel : channel.subarray(0, frames);
|
|
1723
|
+
if (this.upsampleScratch.length < frames * upsampler.factor) {
|
|
1724
|
+
this.upsampleScratch = new Float32Array(frames * upsampler.factor);
|
|
1725
|
+
}
|
|
1726
|
+
const upsampled = upsampler.upsample(slice, this.upsampleScratch);
|
|
1727
|
+
for (let index = 0; index < upsampled.length; index++) {
|
|
1728
|
+
const sample = upsampled[index] ?? 0;
|
|
1729
|
+
const magnitude = sample < 0 ? -sample : sample;
|
|
1730
|
+
if (magnitude > this.runningMax) this.runningMax = magnitude;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
finalize() {
|
|
1735
|
+
return this.runningMax;
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
function complexFft(inRe, inIm, outRe, outIm, workspaceA, workspaceB) {
|
|
1739
|
+
const { re: reOfRe, im: imOfRe } = fft(inRe, workspaceA);
|
|
1740
|
+
const { re: reOfIm, im: imOfIm } = fft(inIm, workspaceB);
|
|
1741
|
+
const size = inRe.length;
|
|
1742
|
+
for (let ii = 0; ii < size; ii++) {
|
|
1743
|
+
outRe[ii] = reOfRe[ii] - imOfIm[ii];
|
|
1744
|
+
outIm[ii] = imOfRe[ii] + reOfIm[ii];
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function applyDfttSmoothing(nlmSmoothed, rawMask, numFrames, numBins, dfttOptions, output, fftBackend, fftAddonOptions, profileMs) {
|
|
1748
|
+
const addon = fftBackend ? getFftAddon(fftBackend, fftAddonOptions) : null;
|
|
1749
|
+
if (!addon || typeof addon.batchFft2D !== "function") {
|
|
1750
|
+
applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOptions, output);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
let profileMark = profileMs ? performance.now() : 0;
|
|
1754
|
+
const profileAdd = (key) => {
|
|
1755
|
+
if (!profileMs) return;
|
|
1756
|
+
const now = performance.now();
|
|
1757
|
+
profileMs[key] += now - profileMark;
|
|
1758
|
+
profileMark = now;
|
|
1759
|
+
};
|
|
1760
|
+
const { blockFreq, blockTime, hopFreq, hopTime, threshold } = dfttOptions;
|
|
1761
|
+
const winFreq = hanningWindow(blockFreq, false);
|
|
1762
|
+
const winTime = hanningWindow(blockTime, false);
|
|
1763
|
+
const win2d = new Float32Array(blockTime * blockFreq);
|
|
1764
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1765
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1766
|
+
win2d[tf * blockFreq + bf] = winTime[tf] * winFreq[bf];
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const blocksPerFrame = Math.ceil(numFrames / hopTime);
|
|
1770
|
+
const blocksPerBin = Math.ceil(numBins / hopFreq);
|
|
1771
|
+
const totalBlocks = blocksPerFrame * blocksPerBin;
|
|
1772
|
+
const blockSize = blockTime * blockFreq;
|
|
1773
|
+
const complexBinsPerRow = blockFreq / 2 + 1;
|
|
1774
|
+
const complexBlockSize = blockTime * complexBinsPerRow;
|
|
1775
|
+
const rawBatch = new Float32Array(totalBlocks * blockSize);
|
|
1776
|
+
const nlmBatch = new Float32Array(totalBlocks * blockSize);
|
|
1777
|
+
for (let frameIdx = 0; frameIdx < blocksPerFrame; frameIdx++) {
|
|
1778
|
+
const frameStart = frameIdx * hopTime;
|
|
1779
|
+
for (let binIdx = 0; binIdx < blocksPerBin; binIdx++) {
|
|
1780
|
+
const binStart = binIdx * hopFreq;
|
|
1781
|
+
const blockIdx = frameIdx * blocksPerBin + binIdx;
|
|
1782
|
+
const blockOffset = blockIdx * blockSize;
|
|
1783
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1784
|
+
const srcFrame = frameStart + tf < numFrames ? frameStart + tf : numFrames - 1;
|
|
1785
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1786
|
+
const srcBin = binStart + bf < numBins ? binStart + bf : numBins - 1;
|
|
1787
|
+
const winVal = win2d[tf * blockFreq + bf];
|
|
1788
|
+
const srcPos = srcFrame * numBins + srcBin;
|
|
1789
|
+
const dstPos = blockOffset + tf * blockFreq + bf;
|
|
1790
|
+
rawBatch[dstPos] = rawMask[srcPos] * winVal;
|
|
1791
|
+
nlmBatch[dstPos] = nlmSmoothed[srcPos] * winVal;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
profileAdd("fill");
|
|
1797
|
+
const rawFft = addon.batchFft2D(rawBatch, blockTime, blockFreq, totalBlocks);
|
|
1798
|
+
const nlmFft = addon.batchFft2D(nlmBatch, blockTime, blockFreq, totalBlocks);
|
|
1799
|
+
profileAdd("forward");
|
|
1800
|
+
const sigmaSq = threshold * threshold;
|
|
1801
|
+
const totalComplex = totalBlocks * complexBlockSize;
|
|
1802
|
+
const rawRe = rawFft.re;
|
|
1803
|
+
const rawIm = rawFft.im;
|
|
1804
|
+
const nlmRe = nlmFft.re;
|
|
1805
|
+
const nlmIm = nlmFft.im;
|
|
1806
|
+
for (let flatIdx = 0; flatIdx < totalComplex; flatIdx++) {
|
|
1807
|
+
const nRe = nlmRe[flatIdx];
|
|
1808
|
+
const nIm = nlmIm[flatIdx];
|
|
1809
|
+
const nMagSq = nRe * nRe + nIm * nIm;
|
|
1810
|
+
const gain = nMagSq / (nMagSq + sigmaSq);
|
|
1811
|
+
rawRe[flatIdx] = rawRe[flatIdx] * gain;
|
|
1812
|
+
rawIm[flatIdx] = rawIm[flatIdx] * gain;
|
|
1813
|
+
}
|
|
1814
|
+
profileAdd("gain");
|
|
1815
|
+
const synth = addon.batchIfft2D(rawRe, rawIm, blockTime, blockFreq, totalBlocks);
|
|
1816
|
+
profileAdd("inverse");
|
|
1817
|
+
const accumulator = new Float32Array(numFrames * numBins);
|
|
1818
|
+
const windowSumSq = new Float32Array(numFrames * numBins);
|
|
1819
|
+
for (let frameIdx = 0; frameIdx < blocksPerFrame; frameIdx++) {
|
|
1820
|
+
const frameStart = frameIdx * hopTime;
|
|
1821
|
+
for (let binIdx = 0; binIdx < blocksPerBin; binIdx++) {
|
|
1822
|
+
const binStart = binIdx * hopFreq;
|
|
1823
|
+
const blockIdx = frameIdx * blocksPerBin + binIdx;
|
|
1824
|
+
const blockOffset = blockIdx * blockSize;
|
|
1825
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1826
|
+
const destFrame = frameStart + tf;
|
|
1827
|
+
if (destFrame >= numFrames) break;
|
|
1828
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1829
|
+
const destBin = binStart + bf;
|
|
1830
|
+
if (destBin >= numBins) break;
|
|
1831
|
+
const winVal = win2d[tf * blockFreq + bf];
|
|
1832
|
+
const destPos = destFrame * numBins + destBin;
|
|
1833
|
+
const srcVal = synth[blockOffset + tf * blockFreq + bf];
|
|
1834
|
+
accumulator[destPos] = accumulator[destPos] + srcVal * winVal;
|
|
1835
|
+
windowSumSq[destPos] = windowSumSq[destPos] + winVal * winVal;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
profileAdd("ola");
|
|
1841
|
+
for (let flatIdx = 0; flatIdx < numFrames * numBins; flatIdx++) {
|
|
1842
|
+
const ws = windowSumSq[flatIdx];
|
|
1843
|
+
if (ws > 1e-8) {
|
|
1844
|
+
const normalisedVal = accumulator[flatIdx] / ws;
|
|
1845
|
+
output[flatIdx] = normalisedVal < 0 ? 0 : normalisedVal > 1 ? 1 : normalisedVal;
|
|
1846
|
+
} else {
|
|
1847
|
+
output[flatIdx] = rawMask[flatIdx];
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
profileAdd("normalize");
|
|
1851
|
+
}
|
|
1852
|
+
function applyDfttSmoothingJs(nlmSmoothed, rawMask, numFrames, numBins, dfttOptions, output) {
|
|
1853
|
+
const { blockFreq, blockTime, hopFreq, hopTime, threshold } = dfttOptions;
|
|
1854
|
+
const winFreq = hanningWindow(blockFreq, false);
|
|
1855
|
+
const winTime = hanningWindow(blockTime, false);
|
|
1856
|
+
const win2d = new Float32Array(blockTime * blockFreq);
|
|
1857
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1858
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1859
|
+
win2d[tf * blockFreq + bf] = winTime[tf] * winFreq[bf];
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
const accumulator = new Float32Array(numFrames * numBins);
|
|
1863
|
+
const windowSumSq = new Float32Array(numFrames * numBins);
|
|
1864
|
+
const blockRaw = new Float32Array(blockTime * blockFreq);
|
|
1865
|
+
const blockNlm = new Float32Array(blockTime * blockFreq);
|
|
1866
|
+
const rawRowRe = new Float32Array(blockTime * blockFreq);
|
|
1867
|
+
const rawRowIm = new Float32Array(blockTime * blockFreq);
|
|
1868
|
+
const nlmRowRe = new Float32Array(blockTime * blockFreq);
|
|
1869
|
+
const nlmRowIm = new Float32Array(blockTime * blockFreq);
|
|
1870
|
+
const colInRe = new Float32Array(blockTime * blockFreq);
|
|
1871
|
+
const colInIm = new Float32Array(blockTime * blockFreq);
|
|
1872
|
+
const rawColRe = new Float32Array(blockTime * blockFreq);
|
|
1873
|
+
const rawColIm = new Float32Array(blockTime * blockFreq);
|
|
1874
|
+
const nlmColRe = new Float32Array(blockTime * blockFreq);
|
|
1875
|
+
const nlmColIm = new Float32Array(blockTime * blockFreq);
|
|
1876
|
+
const gainColRe = new Float32Array(blockTime * blockFreq);
|
|
1877
|
+
const gainColIm = new Float32Array(blockTime * blockFreq);
|
|
1878
|
+
const scratchRe = new Float32Array(blockTime);
|
|
1879
|
+
const scratchIm = new Float32Array(blockTime);
|
|
1880
|
+
const scratchOutRe = new Float32Array(blockTime);
|
|
1881
|
+
const scratchOutIm = new Float32Array(blockTime);
|
|
1882
|
+
const rowScratch = new Float32Array(blockFreq);
|
|
1883
|
+
const rowScratchRe = new Float32Array(blockFreq);
|
|
1884
|
+
const rowScratchIm = new Float32Array(blockFreq);
|
|
1885
|
+
const synthBlock = new Float32Array(blockTime * blockFreq);
|
|
1886
|
+
const rowFwdWorkspace = createFftWorkspace(blockFreq);
|
|
1887
|
+
const colFwdWorkspaceA = createFftWorkspace(blockTime);
|
|
1888
|
+
const colFwdWorkspaceB = createFftWorkspace(blockTime);
|
|
1889
|
+
const colInvWorkspace = createFftWorkspace(blockTime);
|
|
1890
|
+
const rowInvWorkspace = createFftWorkspace(blockFreq);
|
|
1891
|
+
for (let frameStart = 0; frameStart < numFrames; frameStart += hopTime) {
|
|
1892
|
+
for (let binStart = 0; binStart < numBins; binStart += hopFreq) {
|
|
1893
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1894
|
+
const srcFrame = frameStart + tf < numFrames ? frameStart + tf : numFrames - 1;
|
|
1895
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1896
|
+
const srcBin = binStart + bf < numBins ? binStart + bf : numBins - 1;
|
|
1897
|
+
const winVal = win2d[tf * blockFreq + bf];
|
|
1898
|
+
const srcPos = srcFrame * numBins + srcBin;
|
|
1899
|
+
blockRaw[tf * blockFreq + bf] = rawMask[srcPos] * winVal;
|
|
1900
|
+
blockNlm[tf * blockFreq + bf] = nlmSmoothed[srcPos] * winVal;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1904
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1905
|
+
rowScratch[bf] = blockRaw[tf * blockFreq + bf];
|
|
1906
|
+
}
|
|
1907
|
+
const { re: rowRe, im: rowIm } = fft(rowScratch, rowFwdWorkspace);
|
|
1908
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1909
|
+
rawRowRe[tf * blockFreq + bf] = rowRe[bf];
|
|
1910
|
+
rawRowIm[tf * blockFreq + bf] = rowIm[bf];
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1914
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1915
|
+
rowScratch[bf] = blockNlm[tf * blockFreq + bf];
|
|
1916
|
+
}
|
|
1917
|
+
const { re: rowRe, im: rowIm } = fft(rowScratch, rowFwdWorkspace);
|
|
1918
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1919
|
+
nlmRowRe[tf * blockFreq + bf] = rowRe[bf];
|
|
1920
|
+
nlmRowIm[tf * blockFreq + bf] = rowIm[bf];
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1924
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1925
|
+
colInRe[bf * blockTime + tf] = rawRowRe[tf * blockFreq + bf];
|
|
1926
|
+
colInIm[bf * blockTime + tf] = rawRowIm[tf * blockFreq + bf];
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1930
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1931
|
+
scratchRe[tf] = colInRe[bf * blockTime + tf];
|
|
1932
|
+
scratchIm[tf] = colInIm[bf * blockTime + tf];
|
|
1933
|
+
}
|
|
1934
|
+
complexFft(scratchRe, scratchIm, scratchOutRe, scratchOutIm, colFwdWorkspaceA, colFwdWorkspaceB);
|
|
1935
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1936
|
+
rawColRe[bf * blockTime + tf] = scratchOutRe[tf];
|
|
1937
|
+
rawColIm[bf * blockTime + tf] = scratchOutIm[tf];
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1941
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1942
|
+
colInRe[bf * blockTime + tf] = nlmRowRe[tf * blockFreq + bf];
|
|
1943
|
+
colInIm[bf * blockTime + tf] = nlmRowIm[tf * blockFreq + bf];
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1947
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1948
|
+
scratchRe[tf] = colInRe[bf * blockTime + tf];
|
|
1949
|
+
scratchIm[tf] = colInIm[bf * blockTime + tf];
|
|
1950
|
+
}
|
|
1951
|
+
complexFft(scratchRe, scratchIm, scratchOutRe, scratchOutIm, colFwdWorkspaceA, colFwdWorkspaceB);
|
|
1952
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1953
|
+
nlmColRe[bf * blockTime + tf] = scratchOutRe[tf];
|
|
1954
|
+
nlmColIm[bf * blockTime + tf] = scratchOutIm[tf];
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
const sigmaSq = threshold * threshold;
|
|
1958
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1959
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1960
|
+
const flatIdx = bf * blockTime + tf;
|
|
1961
|
+
const nlmRe = nlmColRe[flatIdx];
|
|
1962
|
+
const nlmIm = nlmColIm[flatIdx];
|
|
1963
|
+
const nlmMagSq = nlmRe * nlmRe + nlmIm * nlmIm;
|
|
1964
|
+
const gain = nlmMagSq / (nlmMagSq + sigmaSq);
|
|
1965
|
+
gainColRe[flatIdx] = rawColRe[flatIdx] * gain;
|
|
1966
|
+
gainColIm[flatIdx] = rawColIm[flatIdx] * gain;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1970
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1971
|
+
scratchRe[tf] = gainColRe[bf * blockTime + tf];
|
|
1972
|
+
scratchIm[tf] = gainColIm[bf * blockTime + tf];
|
|
1973
|
+
}
|
|
1974
|
+
const icolResult = ifft(scratchRe, scratchIm, colInvWorkspace);
|
|
1975
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1976
|
+
colInRe[bf * blockTime + tf] = icolResult[tf];
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1980
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1981
|
+
rowScratchRe[bf] = colInRe[bf * blockTime + tf];
|
|
1982
|
+
rowScratchIm[bf] = 0;
|
|
1983
|
+
}
|
|
1984
|
+
const irowResult = ifft(rowScratchRe, rowScratchIm, rowInvWorkspace);
|
|
1985
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1986
|
+
synthBlock[tf * blockFreq + bf] = irowResult[bf];
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
for (let tf = 0; tf < blockTime; tf++) {
|
|
1990
|
+
const destFrame = frameStart + tf;
|
|
1991
|
+
if (destFrame >= numFrames) break;
|
|
1992
|
+
for (let bf = 0; bf < blockFreq; bf++) {
|
|
1993
|
+
const destBin = binStart + bf;
|
|
1994
|
+
if (destBin >= numBins) break;
|
|
1995
|
+
const winVal = win2d[tf * blockFreq + bf];
|
|
1996
|
+
const destPos = destFrame * numBins + destBin;
|
|
1997
|
+
accumulator[destPos] = accumulator[destPos] + synthBlock[tf * blockFreq + bf] * winVal;
|
|
1998
|
+
windowSumSq[destPos] = windowSumSq[destPos] + winVal * winVal;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
for (let flatIdx = 0; flatIdx < numFrames * numBins; flatIdx++) {
|
|
2004
|
+
const ws = windowSumSq[flatIdx];
|
|
2005
|
+
if (ws > 1e-8) {
|
|
2006
|
+
const normalisedVal = accumulator[flatIdx] / ws;
|
|
2007
|
+
output[flatIdx] = normalisedVal < 0 ? 0 : normalisedVal > 1 ? 1 : normalisedVal;
|
|
2008
|
+
} else {
|
|
2009
|
+
output[flatIdx] = rawMask[flatIdx];
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function applyNlmSmoothingRange(mask, numFrames, numBins, nlmOptions, output, blockFrameStart, blockFrameEnd) {
|
|
2014
|
+
const { patchSize, searchFreqRadius, searchTimePre, searchTimePost, pasteBlockSize, threshold } = nlmOptions;
|
|
2015
|
+
const hSq = threshold * threshold;
|
|
2016
|
+
const halfPatch = Math.floor(patchSize / 2);
|
|
2017
|
+
for (let blockFrame = blockFrameStart; blockFrame < blockFrameEnd; blockFrame += pasteBlockSize) {
|
|
2018
|
+
for (let blockBin = 0; blockBin < numBins; blockBin += pasteBlockSize) {
|
|
2019
|
+
const centreFrame = blockFrame + Math.floor(pasteBlockSize / 2);
|
|
2020
|
+
const centreBin = blockBin + Math.floor(pasteBlockSize / 2);
|
|
2021
|
+
let weightSum = 0;
|
|
2022
|
+
let valueSum = 0;
|
|
2023
|
+
const timeStart = centreFrame - searchTimePre;
|
|
2024
|
+
const timeEnd = centreFrame + searchTimePost;
|
|
2025
|
+
const freqStart = centreBin - searchFreqRadius;
|
|
2026
|
+
const freqEnd = centreBin + searchFreqRadius;
|
|
2027
|
+
for (let candFrame = timeStart; candFrame <= timeEnd; candFrame++) {
|
|
2028
|
+
const clampedCandFrame = candFrame < 0 ? 0 : candFrame >= numFrames ? numFrames - 1 : candFrame;
|
|
2029
|
+
for (let candBin = freqStart; candBin <= freqEnd; candBin++) {
|
|
2030
|
+
const clampedCandBin = candBin < 0 ? 0 : candBin >= numBins ? numBins - 1 : candBin;
|
|
2031
|
+
let patchDistSq = 0;
|
|
2032
|
+
for (let pf = -halfPatch; pf < halfPatch; pf++) {
|
|
2033
|
+
for (let pb = -halfPatch; pb < halfPatch; pb++) {
|
|
2034
|
+
const cf = centreFrame + pf;
|
|
2035
|
+
const cBin = centreBin + pb;
|
|
2036
|
+
const cf2 = cf < 0 ? 0 : cf >= numFrames ? numFrames - 1 : cf;
|
|
2037
|
+
const cBin2 = cBin < 0 ? 0 : cBin >= numBins ? numBins - 1 : cBin;
|
|
2038
|
+
const vCentre = mask[cf2 * numBins + cBin2];
|
|
2039
|
+
const df = clampedCandFrame + pf;
|
|
2040
|
+
const db = clampedCandBin + pb;
|
|
2041
|
+
const df2 = df < 0 ? 0 : df >= numFrames ? numFrames - 1 : df;
|
|
2042
|
+
const db2 = db < 0 ? 0 : db >= numBins ? numBins - 1 : db;
|
|
2043
|
+
const vCand = mask[df2 * numBins + db2];
|
|
2044
|
+
const diff = vCentre - vCand;
|
|
2045
|
+
patchDistSq += diff * diff;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const weight = hSq > 0 ? Math.exp(-patchDistSq / hSq) : patchDistSq === 0 ? 1 : 0;
|
|
2049
|
+
weightSum += weight;
|
|
2050
|
+
valueSum += weight * mask[clampedCandFrame * numBins + clampedCandBin];
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
const smoothed = weightSum > 0 ? valueSum / weightSum : mask[centreFrame * numBins + centreBin];
|
|
2054
|
+
for (let pf = 0; pf < pasteBlockSize; pf++) {
|
|
2055
|
+
const outFrame = blockFrame + pf;
|
|
2056
|
+
if (outFrame >= numFrames) break;
|
|
2057
|
+
for (let pb = 0; pb < pasteBlockSize; pb++) {
|
|
2058
|
+
const outBin = blockBin + pb;
|
|
2059
|
+
if (outBin >= numBins) break;
|
|
2060
|
+
output[outFrame * numBins + outBin] = smoothed;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
export { AmplitudeHistogramAccumulator, BidirectionalIir, IntegratedLufsAccumulator, LoudnessAccumulator, MixedRadixFft, ResampleStream, SlidingWindowMaxStream, SlidingWindowMinStream, TruePeakAccumulator, TruePeakUpsampler, __commonJS, __export, __toESM, applyDfttSmoothing, applyNlmSmoothingRange, bandpass, createFftWorkspace, dbToLinear, deinterleaveBuffer, detectFftBackend, fft, getFftAddon, getLraConsideredStats, hanningWindow, initFftBackend, interleave, istft, linearToDb, stft };
|