@octoseq/mir 0.1.0-main.4ecb074 → 0.1.0-main.5fdb072
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-HF3QHCRK.js +3234 -0
- package/dist/chunk-HF3QHCRK.js.map +1 -0
- package/dist/index.d.ts +2122 -47
- package/dist/index.js +3524 -39
- package/dist/index.js.map +1 -1
- package/dist/runMir-CnJQbBr8.d.ts +187 -0
- package/dist/runner/runMir.d.ts +2 -2
- package/dist/runner/runMir.js +1 -1
- package/dist/runner/workerProtocol.d.ts +8 -1
- package/dist/runner/workerProtocol.js.map +1 -1
- package/dist/types-DqH4umN8.d.ts +761 -0
- package/package.json +2 -2
- package/src/dsp/activity.ts +544 -0
- package/src/dsp/bandCqt.ts +662 -0
- package/src/dsp/bandEvents.ts +351 -0
- package/src/dsp/bandMask.ts +225 -0
- package/src/dsp/bandMir.ts +524 -0
- package/src/dsp/bandProposal.ts +552 -0
- package/src/dsp/beatCandidates.ts +299 -0
- package/src/dsp/cqt.ts +386 -0
- package/src/dsp/cqtSignals.ts +462 -0
- package/src/dsp/customSignalReduction.ts +841 -0
- package/src/dsp/eventToSignal.ts +531 -0
- package/src/dsp/frequencyBand.ts +956 -0
- package/src/dsp/mel.ts +56 -3
- package/src/dsp/musicalTime.ts +240 -0
- package/src/dsp/onset.ts +296 -30
- package/src/dsp/peakPicking.ts +519 -0
- package/src/dsp/phaseAlignment.ts +153 -0
- package/src/dsp/pitch.ts +289 -0
- package/src/dsp/resample.ts +44 -0
- package/src/dsp/signalTransforms.ts +660 -0
- package/src/dsp/silenceGating.ts +511 -0
- package/src/dsp/spectral.ts +124 -4
- package/src/dsp/tempoHypotheses.ts +395 -0
- package/src/gpu/bufferPool.ts +266 -0
- package/src/gpu/context.ts +30 -6
- package/src/gpu/helpers.ts +85 -0
- package/src/gpu/melProject.ts +83 -0
- package/src/index.ts +366 -5
- package/src/runner/runMir.ts +234 -2
- package/src/runner/workerProtocol.ts +9 -1
- package/src/types.ts +768 -3
- package/dist/chunk-DUWYCAVG.js +0 -1525
- package/dist/chunk-DUWYCAVG.js.map +0 -1
- package/dist/runMir-CSIBwNZ3.d.ts +0 -84
- package/dist/types-BE3py4fZ.d.ts +0 -83
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal Transforms
|
|
3
|
+
*
|
|
4
|
+
* Transform chain implementation for derived signals.
|
|
5
|
+
* All transforms are pure functions that operate on Float32Array values.
|
|
6
|
+
*
|
|
7
|
+
* Transform categories:
|
|
8
|
+
* - Smooth: movingAverage, exponential, gaussian
|
|
9
|
+
* - Normalize: minMax, robust, zScore
|
|
10
|
+
* - Scale: linear scaling with offset
|
|
11
|
+
* - Polarity: signed, magnitude
|
|
12
|
+
* - Clamp: min/max bounds
|
|
13
|
+
* - Remap: input range to output range with curve
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// TYPES
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Base transform type.
|
|
22
|
+
*/
|
|
23
|
+
interface TransformBase {
|
|
24
|
+
kind: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Smooth with moving average.
|
|
29
|
+
*/
|
|
30
|
+
export interface TransformSmoothMovingAverage extends TransformBase {
|
|
31
|
+
kind: "smooth";
|
|
32
|
+
method: "movingAverage";
|
|
33
|
+
windowMs: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Smooth with exponential filter.
|
|
38
|
+
*/
|
|
39
|
+
export interface TransformSmoothExponential extends TransformBase {
|
|
40
|
+
kind: "smooth";
|
|
41
|
+
method: "exponential";
|
|
42
|
+
timeConstantMs: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Smooth with Gaussian kernel.
|
|
47
|
+
*/
|
|
48
|
+
export interface TransformSmoothGaussian extends TransformBase {
|
|
49
|
+
kind: "smooth";
|
|
50
|
+
method: "gaussian";
|
|
51
|
+
windowMs: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* All smoothing transform types.
|
|
56
|
+
*/
|
|
57
|
+
export type TransformSmooth =
|
|
58
|
+
| TransformSmoothMovingAverage
|
|
59
|
+
| TransformSmoothExponential
|
|
60
|
+
| TransformSmoothGaussian;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalize to 0-1 range using min/max.
|
|
64
|
+
*/
|
|
65
|
+
export interface TransformNormalizeMinMax extends TransformBase {
|
|
66
|
+
kind: "normalize";
|
|
67
|
+
method: "minMax";
|
|
68
|
+
targetMin?: number; // Default: 0
|
|
69
|
+
targetMax?: number; // Default: 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Normalize using robust percentile range.
|
|
74
|
+
*/
|
|
75
|
+
export interface TransformNormalizeRobust extends TransformBase {
|
|
76
|
+
kind: "normalize";
|
|
77
|
+
method: "robust";
|
|
78
|
+
percentileLow?: number; // Default: 5
|
|
79
|
+
percentileHigh?: number; // Default: 95
|
|
80
|
+
targetMin?: number; // Default: 0
|
|
81
|
+
targetMax?: number; // Default: 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalize using z-score (mean and std dev).
|
|
86
|
+
*/
|
|
87
|
+
export interface TransformNormalizeZScore extends TransformBase {
|
|
88
|
+
kind: "normalize";
|
|
89
|
+
method: "zScore";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* All normalization transform types.
|
|
94
|
+
*/
|
|
95
|
+
export type TransformNormalize =
|
|
96
|
+
| TransformNormalizeMinMax
|
|
97
|
+
| TransformNormalizeRobust
|
|
98
|
+
| TransformNormalizeZScore;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Linear scale and offset.
|
|
102
|
+
*/
|
|
103
|
+
export interface TransformScale extends TransformBase {
|
|
104
|
+
kind: "scale";
|
|
105
|
+
scale: number;
|
|
106
|
+
offset: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Polarity mode.
|
|
111
|
+
*/
|
|
112
|
+
export interface TransformPolarity extends TransformBase {
|
|
113
|
+
kind: "polarity";
|
|
114
|
+
mode: "signed" | "magnitude";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clamp to bounds.
|
|
119
|
+
*/
|
|
120
|
+
export interface TransformClamp extends TransformBase {
|
|
121
|
+
kind: "clamp";
|
|
122
|
+
min?: number;
|
|
123
|
+
max?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Remap from input range to output range.
|
|
128
|
+
*/
|
|
129
|
+
export interface TransformRemap extends TransformBase {
|
|
130
|
+
kind: "remap";
|
|
131
|
+
inputMin: number;
|
|
132
|
+
inputMax: number;
|
|
133
|
+
outputMin: number;
|
|
134
|
+
outputMax: number;
|
|
135
|
+
curve?: "linear" | "ease" | "easeIn" | "easeOut";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Union of all transform step types.
|
|
140
|
+
*/
|
|
141
|
+
export type TransformStep =
|
|
142
|
+
| TransformSmooth
|
|
143
|
+
| TransformNormalize
|
|
144
|
+
| TransformScale
|
|
145
|
+
| TransformPolarity
|
|
146
|
+
| TransformClamp
|
|
147
|
+
| TransformRemap;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A chain of transforms to apply in sequence.
|
|
151
|
+
*/
|
|
152
|
+
export type TransformChain = TransformStep[];
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Context for transform chain execution.
|
|
156
|
+
*/
|
|
157
|
+
export interface TransformContext {
|
|
158
|
+
/** Sample rate of the signal (samples per second). */
|
|
159
|
+
sampleRate: number;
|
|
160
|
+
/** Time points in seconds (optional, for time-aware transforms). */
|
|
161
|
+
times?: Float32Array;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// SMOOTHING IMPLEMENTATIONS
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Apply moving average smoothing.
|
|
170
|
+
*/
|
|
171
|
+
function smoothMovingAverage(
|
|
172
|
+
values: Float32Array,
|
|
173
|
+
windowMs: number,
|
|
174
|
+
sampleRate: number
|
|
175
|
+
): Float32Array {
|
|
176
|
+
const windowSamples = Math.max(1, Math.round((windowMs / 1000) * sampleRate)) | 1;
|
|
177
|
+
if (windowSamples <= 1) return values;
|
|
178
|
+
|
|
179
|
+
const n = values.length;
|
|
180
|
+
const result = new Float32Array(n);
|
|
181
|
+
const halfWindow = Math.floor(windowSamples / 2);
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < n; i++) {
|
|
184
|
+
const start = Math.max(0, i - halfWindow);
|
|
185
|
+
const end = Math.min(n, i + halfWindow + 1);
|
|
186
|
+
let sum = 0;
|
|
187
|
+
for (let j = start; j < end; j++) {
|
|
188
|
+
sum += values[j]!;
|
|
189
|
+
}
|
|
190
|
+
result[i] = sum / (end - start);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Apply exponential smoothing (first-order IIR filter).
|
|
198
|
+
*/
|
|
199
|
+
function smoothExponential(
|
|
200
|
+
values: Float32Array,
|
|
201
|
+
timeConstantMs: number,
|
|
202
|
+
sampleRate: number
|
|
203
|
+
): Float32Array {
|
|
204
|
+
const n = values.length;
|
|
205
|
+
if (n === 0) return values;
|
|
206
|
+
|
|
207
|
+
const result = new Float32Array(n);
|
|
208
|
+
const dt = 1 / sampleRate;
|
|
209
|
+
const alpha = 1 - Math.exp(-dt / (timeConstantMs / 1000));
|
|
210
|
+
|
|
211
|
+
result[0] = values[0]!;
|
|
212
|
+
for (let i = 1; i < n; i++) {
|
|
213
|
+
result[i] = result[i - 1]! + alpha * (values[i]! - result[i - 1]!);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Apply Gaussian smoothing.
|
|
221
|
+
*/
|
|
222
|
+
function smoothGaussian(
|
|
223
|
+
values: Float32Array,
|
|
224
|
+
windowMs: number,
|
|
225
|
+
sampleRate: number
|
|
226
|
+
): Float32Array {
|
|
227
|
+
const windowSamples = Math.max(1, Math.round((windowMs / 1000) * sampleRate));
|
|
228
|
+
if (windowSamples <= 1) return values;
|
|
229
|
+
|
|
230
|
+
const n = values.length;
|
|
231
|
+
const result = new Float32Array(n);
|
|
232
|
+
const sigma = windowSamples / 4;
|
|
233
|
+
const halfWindow = Math.floor(windowSamples / 2);
|
|
234
|
+
|
|
235
|
+
// Precompute Gaussian kernel
|
|
236
|
+
const kernel = new Float32Array(windowSamples);
|
|
237
|
+
let kernelSum = 0;
|
|
238
|
+
for (let i = 0; i < windowSamples; i++) {
|
|
239
|
+
const x = i - halfWindow;
|
|
240
|
+
kernel[i] = Math.exp(-(x * x) / (2 * sigma * sigma));
|
|
241
|
+
kernelSum += kernel[i]!;
|
|
242
|
+
}
|
|
243
|
+
// Normalize kernel
|
|
244
|
+
for (let i = 0; i < windowSamples; i++) {
|
|
245
|
+
kernel[i] = kernel[i]! / kernelSum;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Apply convolution
|
|
249
|
+
for (let i = 0; i < n; i++) {
|
|
250
|
+
let sum = 0;
|
|
251
|
+
let weightSum = 0;
|
|
252
|
+
for (let j = 0; j < windowSamples; j++) {
|
|
253
|
+
const idx = i - halfWindow + j;
|
|
254
|
+
if (idx >= 0 && idx < n) {
|
|
255
|
+
sum += values[idx]! * kernel[j]!;
|
|
256
|
+
weightSum += kernel[j]!;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
result[i] = weightSum > 0 ? sum / weightSum : values[i]!;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// NORMALIZATION IMPLEMENTATIONS
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Normalize using min/max.
|
|
271
|
+
*/
|
|
272
|
+
function normalizeMinMax(
|
|
273
|
+
values: Float32Array,
|
|
274
|
+
targetMin: number = 0,
|
|
275
|
+
targetMax: number = 1
|
|
276
|
+
): Float32Array {
|
|
277
|
+
const n = values.length;
|
|
278
|
+
if (n === 0) return values;
|
|
279
|
+
|
|
280
|
+
// Find min and max
|
|
281
|
+
let min = values[0]!;
|
|
282
|
+
let max = values[0]!;
|
|
283
|
+
for (let i = 1; i < n; i++) {
|
|
284
|
+
const v = values[i]!;
|
|
285
|
+
if (v < min) min = v;
|
|
286
|
+
if (v > max) max = v;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Avoid division by zero
|
|
290
|
+
if (max === min) {
|
|
291
|
+
const result = new Float32Array(n);
|
|
292
|
+
result.fill((targetMin + targetMax) / 2);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Normalize
|
|
297
|
+
const result = new Float32Array(n);
|
|
298
|
+
const sourceRange = max - min;
|
|
299
|
+
const targetRange = targetMax - targetMin;
|
|
300
|
+
for (let i = 0; i < n; i++) {
|
|
301
|
+
result[i] = targetMin + ((values[i]! - min) / sourceRange) * targetRange;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Normalize using robust percentile range.
|
|
309
|
+
*/
|
|
310
|
+
function normalizeRobust(
|
|
311
|
+
values: Float32Array,
|
|
312
|
+
percentileLow: number = 5,
|
|
313
|
+
percentileHigh: number = 95,
|
|
314
|
+
targetMin: number = 0,
|
|
315
|
+
targetMax: number = 1
|
|
316
|
+
): Float32Array {
|
|
317
|
+
const n = values.length;
|
|
318
|
+
if (n === 0) return values;
|
|
319
|
+
|
|
320
|
+
// Sort for percentile computation
|
|
321
|
+
const sorted = Array.from(values).sort((a, b) => a - b);
|
|
322
|
+
const lowIdx = Math.floor((percentileLow / 100) * (n - 1));
|
|
323
|
+
const highIdx = Math.floor((percentileHigh / 100) * (n - 1));
|
|
324
|
+
const pLow = sorted[lowIdx]!;
|
|
325
|
+
const pHigh = sorted[highIdx]!;
|
|
326
|
+
|
|
327
|
+
// Avoid division by zero
|
|
328
|
+
if (pHigh === pLow) {
|
|
329
|
+
const result = new Float32Array(n);
|
|
330
|
+
result.fill((targetMin + targetMax) / 2);
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Normalize and clamp
|
|
335
|
+
const result = new Float32Array(n);
|
|
336
|
+
const sourceRange = pHigh - pLow;
|
|
337
|
+
const targetRange = targetMax - targetMin;
|
|
338
|
+
for (let i = 0; i < n; i++) {
|
|
339
|
+
const normalized = (values[i]! - pLow) / sourceRange;
|
|
340
|
+
const clamped = Math.max(0, Math.min(1, normalized));
|
|
341
|
+
result[i] = targetMin + clamped * targetRange;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Normalize using z-score.
|
|
349
|
+
*/
|
|
350
|
+
function normalizeZScore(values: Float32Array): Float32Array {
|
|
351
|
+
const n = values.length;
|
|
352
|
+
if (n === 0) return values;
|
|
353
|
+
|
|
354
|
+
// Compute mean
|
|
355
|
+
let sum = 0;
|
|
356
|
+
for (let i = 0; i < n; i++) {
|
|
357
|
+
sum += values[i]!;
|
|
358
|
+
}
|
|
359
|
+
const mean = sum / n;
|
|
360
|
+
|
|
361
|
+
// Compute standard deviation
|
|
362
|
+
let variance = 0;
|
|
363
|
+
for (let i = 0; i < n; i++) {
|
|
364
|
+
const diff = values[i]! - mean;
|
|
365
|
+
variance += diff * diff;
|
|
366
|
+
}
|
|
367
|
+
const stdDev = Math.sqrt(variance / n);
|
|
368
|
+
|
|
369
|
+
// Avoid division by zero
|
|
370
|
+
if (stdDev === 0) {
|
|
371
|
+
const result = new Float32Array(n);
|
|
372
|
+
result.fill(0);
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Normalize
|
|
377
|
+
const result = new Float32Array(n);
|
|
378
|
+
for (let i = 0; i < n; i++) {
|
|
379
|
+
result[i] = (values[i]! - mean) / stdDev;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ============================================================================
|
|
386
|
+
// OTHER TRANSFORM IMPLEMENTATIONS
|
|
387
|
+
// ============================================================================
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Apply linear scale and offset.
|
|
391
|
+
*/
|
|
392
|
+
function applyScale(
|
|
393
|
+
values: Float32Array,
|
|
394
|
+
scale: number,
|
|
395
|
+
offset: number
|
|
396
|
+
): Float32Array {
|
|
397
|
+
const n = values.length;
|
|
398
|
+
const result = new Float32Array(n);
|
|
399
|
+
for (let i = 0; i < n; i++) {
|
|
400
|
+
result[i] = values[i]! * scale + offset;
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Apply polarity transformation.
|
|
407
|
+
*/
|
|
408
|
+
function applyPolarityTransform(
|
|
409
|
+
values: Float32Array,
|
|
410
|
+
mode: "signed" | "magnitude"
|
|
411
|
+
): Float32Array {
|
|
412
|
+
if (mode === "signed") {
|
|
413
|
+
return values;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const n = values.length;
|
|
417
|
+
const result = new Float32Array(n);
|
|
418
|
+
for (let i = 0; i < n; i++) {
|
|
419
|
+
result[i] = Math.abs(values[i]!);
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Apply clamping to bounds.
|
|
426
|
+
*/
|
|
427
|
+
function applyClamp(
|
|
428
|
+
values: Float32Array,
|
|
429
|
+
min?: number,
|
|
430
|
+
max?: number
|
|
431
|
+
): Float32Array {
|
|
432
|
+
const n = values.length;
|
|
433
|
+
const result = new Float32Array(n);
|
|
434
|
+
|
|
435
|
+
for (let i = 0; i < n; i++) {
|
|
436
|
+
let v = values[i]!;
|
|
437
|
+
if (min !== undefined && v < min) v = min;
|
|
438
|
+
if (max !== undefined && v > max) v = max;
|
|
439
|
+
result[i] = v;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Apply remap with optional curve.
|
|
447
|
+
*/
|
|
448
|
+
function applyRemap(
|
|
449
|
+
values: Float32Array,
|
|
450
|
+
inputMin: number,
|
|
451
|
+
inputMax: number,
|
|
452
|
+
outputMin: number,
|
|
453
|
+
outputMax: number,
|
|
454
|
+
curve: "linear" | "ease" | "easeIn" | "easeOut" = "linear"
|
|
455
|
+
): Float32Array {
|
|
456
|
+
const n = values.length;
|
|
457
|
+
const result = new Float32Array(n);
|
|
458
|
+
const inputRange = inputMax - inputMin;
|
|
459
|
+
const outputRange = outputMax - outputMin;
|
|
460
|
+
|
|
461
|
+
// Avoid division by zero
|
|
462
|
+
if (inputRange === 0) {
|
|
463
|
+
result.fill((outputMin + outputMax) / 2);
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
for (let i = 0; i < n; i++) {
|
|
468
|
+
// Normalize to 0-1
|
|
469
|
+
let t = (values[i]! - inputMin) / inputRange;
|
|
470
|
+
t = Math.max(0, Math.min(1, t)); // Clamp
|
|
471
|
+
|
|
472
|
+
// Apply curve
|
|
473
|
+
switch (curve) {
|
|
474
|
+
case "easeIn":
|
|
475
|
+
t = t * t;
|
|
476
|
+
break;
|
|
477
|
+
case "easeOut":
|
|
478
|
+
t = 1 - (1 - t) * (1 - t);
|
|
479
|
+
break;
|
|
480
|
+
case "ease":
|
|
481
|
+
t = t < 0.5
|
|
482
|
+
? 2 * t * t
|
|
483
|
+
: 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
484
|
+
break;
|
|
485
|
+
// linear: no change
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Map to output range
|
|
489
|
+
result[i] = outputMin + t * outputRange;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ============================================================================
|
|
496
|
+
// TRANSFORM CHAIN EXECUTION
|
|
497
|
+
// ============================================================================
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Apply a single transform step.
|
|
501
|
+
*/
|
|
502
|
+
export function applyTransformStep(
|
|
503
|
+
values: Float32Array,
|
|
504
|
+
step: TransformStep,
|
|
505
|
+
context: TransformContext
|
|
506
|
+
): Float32Array {
|
|
507
|
+
switch (step.kind) {
|
|
508
|
+
case "smooth":
|
|
509
|
+
switch (step.method) {
|
|
510
|
+
case "movingAverage":
|
|
511
|
+
return smoothMovingAverage(values, step.windowMs, context.sampleRate);
|
|
512
|
+
case "exponential":
|
|
513
|
+
return smoothExponential(values, step.timeConstantMs, context.sampleRate);
|
|
514
|
+
case "gaussian":
|
|
515
|
+
return smoothGaussian(values, step.windowMs, context.sampleRate);
|
|
516
|
+
default:
|
|
517
|
+
return values;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
case "normalize":
|
|
521
|
+
switch (step.method) {
|
|
522
|
+
case "minMax":
|
|
523
|
+
return normalizeMinMax(values, step.targetMin, step.targetMax);
|
|
524
|
+
case "robust":
|
|
525
|
+
return normalizeRobust(
|
|
526
|
+
values,
|
|
527
|
+
step.percentileLow,
|
|
528
|
+
step.percentileHigh,
|
|
529
|
+
step.targetMin,
|
|
530
|
+
step.targetMax
|
|
531
|
+
);
|
|
532
|
+
case "zScore":
|
|
533
|
+
return normalizeZScore(values);
|
|
534
|
+
default:
|
|
535
|
+
return values;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
case "scale":
|
|
539
|
+
return applyScale(values, step.scale, step.offset);
|
|
540
|
+
|
|
541
|
+
case "polarity":
|
|
542
|
+
return applyPolarityTransform(values, step.mode);
|
|
543
|
+
|
|
544
|
+
case "clamp":
|
|
545
|
+
return applyClamp(values, step.min, step.max);
|
|
546
|
+
|
|
547
|
+
case "remap":
|
|
548
|
+
return applyRemap(
|
|
549
|
+
values,
|
|
550
|
+
step.inputMin,
|
|
551
|
+
step.inputMax,
|
|
552
|
+
step.outputMin,
|
|
553
|
+
step.outputMax,
|
|
554
|
+
step.curve
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
default:
|
|
558
|
+
return values;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Apply a chain of transforms to a signal.
|
|
564
|
+
*/
|
|
565
|
+
export function applyTransformChain(
|
|
566
|
+
values: Float32Array,
|
|
567
|
+
chain: TransformChain,
|
|
568
|
+
context: TransformContext
|
|
569
|
+
): Float32Array {
|
|
570
|
+
let result = values;
|
|
571
|
+
|
|
572
|
+
for (const step of chain) {
|
|
573
|
+
result = applyTransformStep(result, step, context);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ============================================================================
|
|
580
|
+
// TRANSFORM UTILITIES
|
|
581
|
+
// ============================================================================
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Get human-readable label for a transform step.
|
|
585
|
+
*/
|
|
586
|
+
export function getTransformLabel(step: TransformStep): string {
|
|
587
|
+
switch (step.kind) {
|
|
588
|
+
case "smooth":
|
|
589
|
+
switch (step.method) {
|
|
590
|
+
case "movingAverage":
|
|
591
|
+
return `Smooth (${step.windowMs}ms avg)`;
|
|
592
|
+
case "exponential":
|
|
593
|
+
return `Smooth (${step.timeConstantMs}ms exp)`;
|
|
594
|
+
case "gaussian":
|
|
595
|
+
return `Smooth (${step.windowMs}ms gauss)`;
|
|
596
|
+
default:
|
|
597
|
+
return "Smooth";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
case "normalize":
|
|
601
|
+
switch (step.method) {
|
|
602
|
+
case "minMax":
|
|
603
|
+
return `Normalize (${step.targetMin ?? 0}-${step.targetMax ?? 1})`;
|
|
604
|
+
case "robust":
|
|
605
|
+
return `Normalize (robust ${step.percentileLow ?? 5}-${step.percentileHigh ?? 95}%)`;
|
|
606
|
+
case "zScore":
|
|
607
|
+
return "Normalize (z-score)";
|
|
608
|
+
default:
|
|
609
|
+
return "Normalize";
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
case "scale":
|
|
613
|
+
return step.offset !== 0
|
|
614
|
+
? `Scale (×${step.scale} + ${step.offset})`
|
|
615
|
+
: `Scale (×${step.scale})`;
|
|
616
|
+
|
|
617
|
+
case "polarity":
|
|
618
|
+
return step.mode === "magnitude" ? "Magnitude" : "Signed";
|
|
619
|
+
|
|
620
|
+
case "clamp":
|
|
621
|
+
if (step.min !== undefined && step.max !== undefined) {
|
|
622
|
+
return `Clamp (${step.min}-${step.max})`;
|
|
623
|
+
} else if (step.min !== undefined) {
|
|
624
|
+
return `Clamp (≥${step.min})`;
|
|
625
|
+
} else if (step.max !== undefined) {
|
|
626
|
+
return `Clamp (≤${step.max})`;
|
|
627
|
+
}
|
|
628
|
+
return "Clamp";
|
|
629
|
+
|
|
630
|
+
case "remap":
|
|
631
|
+
return `Remap (${step.inputMin}-${step.inputMax} → ${step.outputMin}-${step.outputMax})`;
|
|
632
|
+
|
|
633
|
+
default:
|
|
634
|
+
return "Unknown";
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Create a default transform step for a kind.
|
|
640
|
+
*/
|
|
641
|
+
export function createDefaultTransform(
|
|
642
|
+
kind: TransformStep["kind"]
|
|
643
|
+
): TransformStep | null {
|
|
644
|
+
switch (kind) {
|
|
645
|
+
case "smooth":
|
|
646
|
+
return { kind: "smooth", method: "movingAverage", windowMs: 10 };
|
|
647
|
+
case "normalize":
|
|
648
|
+
return { kind: "normalize", method: "minMax", targetMin: 0, targetMax: 1 };
|
|
649
|
+
case "scale":
|
|
650
|
+
return { kind: "scale", scale: 1, offset: 0 };
|
|
651
|
+
case "polarity":
|
|
652
|
+
return { kind: "polarity", mode: "signed" };
|
|
653
|
+
case "clamp":
|
|
654
|
+
return { kind: "clamp", min: 0, max: 1 };
|
|
655
|
+
case "remap":
|
|
656
|
+
return { kind: "remap", inputMin: 0, inputMax: 1, outputMin: 0, outputMax: 1 };
|
|
657
|
+
default:
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|