@libraz/libsonare 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -2
- package/dist/index.d.ts +1162 -18
- package/dist/index.js +1167 -14
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +2 -0
- package/dist/sonare-rt.js +2 -0
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +447 -0
- package/dist/worklet.js +2078 -0
- package/dist/worklet.js.map +1 -0
- package/package.json +14 -4
- package/src/index.ts +1960 -63
- package/src/public_types.ts +461 -0
- package/src/sonare-rt.d.ts +93 -0
- package/src/sonare.js.d.ts +723 -2
- package/src/stream_types.ts +35 -0
- package/src/wasm_types.ts +706 -2
- package/src/worklet.ts +2140 -0
package/src/index.ts
CHANGED
|
@@ -19,61 +19,157 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type {
|
|
22
|
+
AcousticResult,
|
|
22
23
|
AnalysisResult,
|
|
24
|
+
AutomationCurve,
|
|
25
|
+
ChordAnalysisResult,
|
|
26
|
+
ChordDetectionOptions,
|
|
23
27
|
ChordQuality,
|
|
24
28
|
ChromaResult,
|
|
29
|
+
CqtResult,
|
|
30
|
+
EqBand,
|
|
31
|
+
EqMatchOptions,
|
|
32
|
+
EqSpectrumSnapshot,
|
|
33
|
+
GoniometerPoint,
|
|
25
34
|
HpssResult,
|
|
26
35
|
Key,
|
|
36
|
+
KeyCandidate,
|
|
37
|
+
KeyDetectionOptions,
|
|
38
|
+
KeyProfileName,
|
|
39
|
+
LufsResult,
|
|
27
40
|
MasteringChainConfig,
|
|
28
41
|
MasteringChainResult,
|
|
42
|
+
MasteringPreset,
|
|
29
43
|
MasteringProcessorParams,
|
|
30
44
|
MasteringResult,
|
|
31
45
|
MasteringStereoChainResult,
|
|
32
46
|
MasteringStereoResult,
|
|
47
|
+
MelodyResult,
|
|
48
|
+
MelPowerResult,
|
|
33
49
|
MelSpectrogramResult,
|
|
50
|
+
MeterTap,
|
|
34
51
|
MfccResult,
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
MixerProcessResult,
|
|
53
|
+
MixMeterSnapshot,
|
|
54
|
+
MixOptions,
|
|
55
|
+
MixResult,
|
|
56
|
+
PairAnalysis,
|
|
57
|
+
PairProcessor,
|
|
58
|
+
PanLaw,
|
|
37
59
|
PitchResult,
|
|
60
|
+
Section,
|
|
38
61
|
SectionType,
|
|
62
|
+
SendTiming,
|
|
63
|
+
SoloProcessor,
|
|
64
|
+
StereoAnalysis,
|
|
65
|
+
StftPowerResult,
|
|
39
66
|
StftResult,
|
|
67
|
+
StreamingEqualizerConfig,
|
|
68
|
+
StreamingPlatform,
|
|
69
|
+
StreamingRetuneConfig,
|
|
70
|
+
TempogramMode,
|
|
40
71
|
} from './public_types';
|
|
41
|
-
import
|
|
72
|
+
import { KeyProfile as KeyProfileValues, Mode, PitchClass } from './public_types';
|
|
73
|
+
import type {
|
|
74
|
+
AnalyzerStats,
|
|
75
|
+
FrameBuffer,
|
|
76
|
+
StreamConfig,
|
|
77
|
+
StreamFramesI16,
|
|
78
|
+
StreamFramesU8,
|
|
79
|
+
} from './stream_types';
|
|
42
80
|
import type {
|
|
43
81
|
ProgressCallback,
|
|
44
82
|
SonareModule,
|
|
83
|
+
WasmAcousticResult,
|
|
45
84
|
WasmAnalysisResult,
|
|
85
|
+
WasmChordAnalysisResult,
|
|
86
|
+
WasmCyclicTempogramResult,
|
|
87
|
+
WasmEngineAutomationPoint,
|
|
88
|
+
WasmEngineBounceOptions,
|
|
89
|
+
WasmEngineBounceResult,
|
|
90
|
+
WasmEngineCaptureStatus,
|
|
91
|
+
WasmEngineClip,
|
|
92
|
+
WasmEngineFreezeOptions,
|
|
93
|
+
WasmEngineFreezeResult,
|
|
94
|
+
WasmEngineGraphSpec,
|
|
95
|
+
WasmEngineMarker,
|
|
96
|
+
WasmEngineMeterTelemetry,
|
|
97
|
+
WasmEngineMetronomeConfig,
|
|
98
|
+
WasmEngineParameterInfo,
|
|
99
|
+
WasmEngineProcessWithMonitorResult,
|
|
100
|
+
WasmEngineTelemetry,
|
|
101
|
+
WasmEngineTransportState,
|
|
102
|
+
WasmFourierTempogramResult,
|
|
46
103
|
WasmFrameResult,
|
|
104
|
+
WasmKeyCandidateResult,
|
|
105
|
+
WasmNnlsChromaResult,
|
|
106
|
+
WasmRealtimeEngine,
|
|
47
107
|
WasmStreamAnalyzer,
|
|
48
108
|
WasmTempogramResult,
|
|
49
109
|
WasmTrimResult,
|
|
50
110
|
} from './wasm_types';
|
|
51
111
|
|
|
52
112
|
export type {
|
|
113
|
+
AcousticResult,
|
|
53
114
|
AnalysisResult,
|
|
115
|
+
AutomationCurve,
|
|
54
116
|
Beat,
|
|
55
117
|
Chord,
|
|
118
|
+
ChordAnalysisResult,
|
|
119
|
+
ChordDetectionOptions,
|
|
56
120
|
ChromaResult,
|
|
121
|
+
CqtResult,
|
|
57
122
|
Dynamics,
|
|
123
|
+
EqBand,
|
|
124
|
+
EqBandPhase,
|
|
125
|
+
EqBandType,
|
|
126
|
+
EqCoeffMode,
|
|
127
|
+
EqMatchOptions,
|
|
128
|
+
EqSpectrumSnapshot,
|
|
129
|
+
EqStereoPlacement,
|
|
130
|
+
GoniometerPoint,
|
|
58
131
|
HpssResult,
|
|
59
132
|
Key,
|
|
133
|
+
KeyCandidate,
|
|
134
|
+
KeyDetectionOptions,
|
|
135
|
+
KeyProfileName,
|
|
136
|
+
LufsResult,
|
|
60
137
|
MasteringChainConfig,
|
|
61
138
|
MasteringChainResult,
|
|
139
|
+
MasteringPreset,
|
|
62
140
|
MasteringProcessorParams,
|
|
63
141
|
MasteringResult,
|
|
64
142
|
MasteringStereoChainResult,
|
|
65
143
|
MasteringStereoResult,
|
|
144
|
+
MelodyPoint,
|
|
145
|
+
MelodyResult,
|
|
66
146
|
MelSpectrogramResult,
|
|
147
|
+
MeterTap,
|
|
67
148
|
MfccResult,
|
|
149
|
+
MixerProcessResult,
|
|
150
|
+
MixMeterSnapshot,
|
|
151
|
+
MixOptions,
|
|
152
|
+
MixResult,
|
|
153
|
+
PairAnalysis,
|
|
154
|
+
PairProcessor,
|
|
155
|
+
PanLaw,
|
|
156
|
+
PanMode,
|
|
68
157
|
PitchResult,
|
|
69
158
|
RhythmFeatures,
|
|
70
159
|
Section,
|
|
160
|
+
SendTiming,
|
|
161
|
+
SoloProcessor,
|
|
162
|
+
StereoAnalysis,
|
|
71
163
|
StftResult,
|
|
164
|
+
StreamingEqualizerConfig,
|
|
165
|
+
StreamingPlatform,
|
|
166
|
+
StreamingRetuneConfig,
|
|
72
167
|
Timbre,
|
|
73
168
|
TimeSignature,
|
|
74
169
|
} from './public_types';
|
|
75
170
|
export {
|
|
76
171
|
ChordQuality,
|
|
172
|
+
KeyProfile,
|
|
77
173
|
Mode,
|
|
78
174
|
PitchClass,
|
|
79
175
|
SectionType,
|
|
@@ -86,9 +182,85 @@ export type {
|
|
|
86
182
|
PatternScore,
|
|
87
183
|
ProgressiveEstimate,
|
|
88
184
|
StreamConfig,
|
|
185
|
+
StreamFramesI16,
|
|
186
|
+
StreamFramesU8,
|
|
89
187
|
} from './stream_types';
|
|
90
188
|
export type { ProgressCallback } from './wasm_types';
|
|
91
189
|
|
|
190
|
+
export type EngineClip = WasmEngineClip;
|
|
191
|
+
export type EngineParameterInfo = WasmEngineParameterInfo;
|
|
192
|
+
export type EngineAutomationPoint = WasmEngineAutomationPoint;
|
|
193
|
+
export type EngineMarker = WasmEngineMarker;
|
|
194
|
+
export type EngineMetronomeConfig = WasmEngineMetronomeConfig;
|
|
195
|
+
export type EngineGraphSpec = WasmEngineGraphSpec;
|
|
196
|
+
export type EngineCaptureStatus = WasmEngineCaptureStatus;
|
|
197
|
+
export type EngineBounceOptions = WasmEngineBounceOptions;
|
|
198
|
+
export type EngineBounceResult = WasmEngineBounceResult;
|
|
199
|
+
export type EngineFreezeOptions = WasmEngineFreezeOptions;
|
|
200
|
+
export type EngineFreezeResult = WasmEngineFreezeResult;
|
|
201
|
+
export type EngineTelemetry = WasmEngineTelemetry;
|
|
202
|
+
export type EngineMeterTelemetry = WasmEngineMeterTelemetry;
|
|
203
|
+
export type EngineTransportState = WasmEngineTransportState;
|
|
204
|
+
|
|
205
|
+
export const EXPECTED_ENGINE_ABI_VERSION = 2;
|
|
206
|
+
|
|
207
|
+
export interface EngineCapabilities {
|
|
208
|
+
engineAbiVersion: number;
|
|
209
|
+
expectedEngineAbiVersion: number;
|
|
210
|
+
abiCompatible: boolean;
|
|
211
|
+
sharedArrayBuffer: boolean;
|
|
212
|
+
atomics: boolean;
|
|
213
|
+
audioWorklet: boolean;
|
|
214
|
+
mode: 'sab' | 'postMessage';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface MixerRealtimeBuffer {
|
|
218
|
+
leftInputs: Float32Array[];
|
|
219
|
+
rightInputs: Float32Array[];
|
|
220
|
+
outLeft: Float32Array;
|
|
221
|
+
outRight: Float32Array;
|
|
222
|
+
process: (numSamples?: number) => void;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function automationCurveCode(curve: AutomationCurve): number {
|
|
226
|
+
switch (curve) {
|
|
227
|
+
case 'linear':
|
|
228
|
+
return 0;
|
|
229
|
+
case 'exponential':
|
|
230
|
+
return 1;
|
|
231
|
+
case 'hold':
|
|
232
|
+
return 2;
|
|
233
|
+
case 's-curve':
|
|
234
|
+
return 3;
|
|
235
|
+
default:
|
|
236
|
+
throw new Error(`Invalid automation curve: ${curve}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function panLawCode(panLaw: PanLaw | number): number {
|
|
241
|
+
if (typeof panLaw === 'number') {
|
|
242
|
+
return panLaw;
|
|
243
|
+
}
|
|
244
|
+
switch (panLaw) {
|
|
245
|
+
case 'const4.5dB':
|
|
246
|
+
return 1;
|
|
247
|
+
case 'const6dB':
|
|
248
|
+
return 2;
|
|
249
|
+
case 'linear0dB':
|
|
250
|
+
return 3;
|
|
251
|
+
default:
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function meterTapCode(tap: MeterTap | number): number {
|
|
257
|
+
return tap === 'preFader' || tap === 0 ? 0 : 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function sendTimingCode(timing: SendTiming | number): number {
|
|
261
|
+
return timing === 'preFader' || timing === 0 ? 0 : 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
92
264
|
// ============================================================================
|
|
93
265
|
// Module State
|
|
94
266
|
// ============================================================================
|
|
@@ -148,6 +320,247 @@ export function version(): string {
|
|
|
148
320
|
return module.version();
|
|
149
321
|
}
|
|
150
322
|
|
|
323
|
+
export function engineAbiVersion(): number {
|
|
324
|
+
if (!module) {
|
|
325
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
326
|
+
}
|
|
327
|
+
return module.engineAbiVersion();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function engineCapabilities(): EngineCapabilities {
|
|
331
|
+
const abiVersion = engineAbiVersion();
|
|
332
|
+
const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === 'function';
|
|
333
|
+
const atomics = typeof globalThis.Atomics === 'object';
|
|
334
|
+
const audioWorklet =
|
|
335
|
+
typeof AudioWorkletNode !== 'undefined' ||
|
|
336
|
+
typeof (globalThis as typeof globalThis & { AudioWorkletProcessor?: unknown })
|
|
337
|
+
.AudioWorkletProcessor !== 'undefined';
|
|
338
|
+
return {
|
|
339
|
+
engineAbiVersion: abiVersion,
|
|
340
|
+
expectedEngineAbiVersion: EXPECTED_ENGINE_ABI_VERSION,
|
|
341
|
+
abiCompatible: abiVersion === EXPECTED_ENGINE_ABI_VERSION,
|
|
342
|
+
sharedArrayBuffer,
|
|
343
|
+
atomics,
|
|
344
|
+
audioWorklet,
|
|
345
|
+
mode: sharedArrayBuffer && atomics ? 'sab' : 'postMessage',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export class RealtimeEngine {
|
|
350
|
+
private native: WasmRealtimeEngine;
|
|
351
|
+
|
|
352
|
+
constructor(
|
|
353
|
+
sampleRate = 48000,
|
|
354
|
+
maxBlockSize = 128,
|
|
355
|
+
commandCapacity = 1024,
|
|
356
|
+
telemetryCapacity = 1024,
|
|
357
|
+
) {
|
|
358
|
+
if (!module) {
|
|
359
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
360
|
+
}
|
|
361
|
+
const capabilities = engineCapabilities();
|
|
362
|
+
if (!capabilities.abiCompatible) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Engine ABI mismatch: wasm=${capabilities.engineAbiVersion}, expected=${capabilities.expectedEngineAbiVersion}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
this.native = new module.RealtimeEngine(
|
|
368
|
+
sampleRate,
|
|
369
|
+
maxBlockSize,
|
|
370
|
+
commandCapacity,
|
|
371
|
+
telemetryCapacity,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
prepare(
|
|
376
|
+
sampleRate: number,
|
|
377
|
+
maxBlockSize: number,
|
|
378
|
+
commandCapacity = 1024,
|
|
379
|
+
telemetryCapacity = 1024,
|
|
380
|
+
): void {
|
|
381
|
+
this.native.prepare(sampleRate, maxBlockSize, commandCapacity, telemetryCapacity);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Queue a sample-accurate parameter change (engine kSetParam). */
|
|
385
|
+
setParameter(paramId: number, value: number, renderFrame = -1): void {
|
|
386
|
+
this.native.setParameter(paramId, value, renderFrame);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Queue a smoothed parameter change (engine kSetParamSmoothed). */
|
|
390
|
+
setParameterSmoothed(paramId: number, value: number, renderFrame = -1): void {
|
|
391
|
+
this.native.setParameterSmoothed(paramId, value, renderFrame);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Read back the current transport state snapshot. */
|
|
395
|
+
getTransportState(): EngineTransportState {
|
|
396
|
+
return this.native.getTransportState();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
play(renderFrame = -1): void {
|
|
400
|
+
this.native.play(renderFrame);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
stop(renderFrame = -1): void {
|
|
404
|
+
this.native.stop(renderFrame);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
seekSample(timelineSample: number, renderFrame = -1): void {
|
|
408
|
+
this.native.seekSample(timelineSample, renderFrame);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
seekPpq(ppq: number, renderFrame = -1): void {
|
|
412
|
+
this.native.seekPpq(ppq, renderFrame);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
setTempo(bpm: number): void {
|
|
416
|
+
this.native.setTempo(bpm);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
setTimeSignature(numerator: number, denominator: number): void {
|
|
420
|
+
this.native.setTimeSignature(numerator, denominator);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
setLoop(startPpq: number, endPpq: number, enabled = true): void {
|
|
424
|
+
this.native.setLoop(startPpq, endPpq, enabled);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
addParameter(info: EngineParameterInfo): void {
|
|
428
|
+
this.native.addParameter(info);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
parameterCount(): number {
|
|
432
|
+
return this.native.parameterCount();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
parameterInfoByIndex(index: number): EngineParameterInfo {
|
|
436
|
+
return this.native.parameterInfoByIndex(index);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
parameterInfo(id: number): EngineParameterInfo {
|
|
440
|
+
return this.native.parameterInfo(id);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
setAutomationLane(paramId: number, points: EngineAutomationPoint[]): void {
|
|
444
|
+
this.native.setAutomationLane(paramId, points);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
automationLaneCount(): number {
|
|
448
|
+
return this.native.automationLaneCount();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
setMarkers(markers: EngineMarker[]): void {
|
|
452
|
+
this.native.setMarkers(markers);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
markerCount(): number {
|
|
456
|
+
return this.native.markerCount();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
markerByIndex(index: number): EngineMarker {
|
|
460
|
+
return this.native.markerByIndex(index);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
marker(id: number): EngineMarker {
|
|
464
|
+
return this.native.marker(id);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
seekMarker(markerId: number, renderFrame = -1): void {
|
|
468
|
+
this.native.seekMarker(markerId, renderFrame);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
setLoopFromMarkers(startMarkerId: number, endMarkerId: number): void {
|
|
472
|
+
this.native.setLoopFromMarkers(startMarkerId, endMarkerId);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
setMetronome(config: EngineMetronomeConfig): void {
|
|
476
|
+
this.native.setMetronome(config);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
metronome(): Required<EngineMetronomeConfig> {
|
|
480
|
+
return this.native.metronome();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
countInEndSample(startSample: number, bars: number): number {
|
|
484
|
+
return Number(this.native.countInEndSample(startSample, bars));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
setGraph(spec: EngineGraphSpec): void {
|
|
488
|
+
this.native.setGraph(spec);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
graphNodeCount(): number {
|
|
492
|
+
return this.native.graphNodeCount();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
graphConnectionCount(): number {
|
|
496
|
+
return this.native.graphConnectionCount();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
setClips(clips: EngineClip[]): void {
|
|
500
|
+
this.native.setClips(clips);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
clipCount(): number {
|
|
504
|
+
return this.native.clipCount();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
setCaptureBuffer(numChannels: number, capacityFrames: number): void {
|
|
508
|
+
this.native.setCaptureBuffer(numChannels, capacityFrames);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
armCapture(armed = true): void {
|
|
512
|
+
this.native.armCapture(armed);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
setCapturePunch(startSample: number, endSample: number, enabled = true): void {
|
|
516
|
+
this.native.setCapturePunch(startSample, endSample, enabled);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
resetCapture(): void {
|
|
520
|
+
this.native.resetCapture();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
captureStatus(): EngineCaptureStatus {
|
|
524
|
+
return this.native.captureStatus();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
capturedAudio(): Float32Array[] {
|
|
528
|
+
return this.native.capturedAudio();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
process(channels: Float32Array[]): Float32Array[] {
|
|
532
|
+
return this.native.process(channels);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
processWithMonitor(channels: Float32Array[]): WasmEngineProcessWithMonitorResult {
|
|
536
|
+
return this.native.processWithMonitor(channels);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
renderOffline(channels: Float32Array[], blockSize = 128): Float32Array[] {
|
|
540
|
+
return this.native.renderOffline(channels, blockSize);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
bounceOffline(options: EngineBounceOptions): EngineBounceResult {
|
|
544
|
+
return this.native.bounceOffline(options);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
freezeOffline(options: EngineFreezeOptions): EngineFreezeResult {
|
|
548
|
+
return this.native.freezeOffline(options);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
drainTelemetry(maxRecords = 1024): EngineTelemetry[] {
|
|
552
|
+
return this.native.drainTelemetry(maxRecords);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
drainMeterTelemetry(maxRecords = 1024): EngineMeterTelemetry[] {
|
|
556
|
+
return this.native.drainMeterTelemetry(maxRecords);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
destroy(): void {
|
|
560
|
+
this.native.delete();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
151
564
|
// ============================================================================
|
|
152
565
|
// Quick API (High-level Analysis)
|
|
153
566
|
// ============================================================================
|
|
@@ -173,11 +586,26 @@ export function detectBpm(samples: Float32Array, sampleRate: number): number {
|
|
|
173
586
|
* @param sampleRate - Sample rate in Hz
|
|
174
587
|
* @returns Detected key
|
|
175
588
|
*/
|
|
176
|
-
export function detectKey(
|
|
589
|
+
export function detectKey(
|
|
590
|
+
samples: Float32Array,
|
|
591
|
+
sampleRate: number,
|
|
592
|
+
options: KeyDetectionOptions = {},
|
|
593
|
+
): Key {
|
|
177
594
|
if (!module) {
|
|
178
595
|
throw new Error('Module not initialized. Call init() first.');
|
|
179
596
|
}
|
|
180
|
-
const result = module.
|
|
597
|
+
const result = module._detectKeyWithOptions(
|
|
598
|
+
samples,
|
|
599
|
+
sampleRate,
|
|
600
|
+
options.nFft ?? 4096,
|
|
601
|
+
options.hopLength ?? 512,
|
|
602
|
+
options.useHpss ?? false,
|
|
603
|
+
options.loudnessWeighted ?? false,
|
|
604
|
+
options.highPassHz ?? 0,
|
|
605
|
+
keyModeValues(options.modes),
|
|
606
|
+
keyProfileValue(options.profile),
|
|
607
|
+
options.genreHint ?? '',
|
|
608
|
+
);
|
|
181
609
|
return {
|
|
182
610
|
root: result.root as PitchClass,
|
|
183
611
|
mode: result.mode as Mode,
|
|
@@ -187,6 +615,98 @@ export function detectKey(samples: Float32Array, sampleRate: number): Key {
|
|
|
187
615
|
};
|
|
188
616
|
}
|
|
189
617
|
|
|
618
|
+
function convertKeyCandidate(wasm: WasmKeyCandidateResult): KeyCandidate {
|
|
619
|
+
return {
|
|
620
|
+
key: {
|
|
621
|
+
root: wasm.key.root as PitchClass,
|
|
622
|
+
mode: wasm.key.mode as Mode,
|
|
623
|
+
confidence: wasm.key.confidence,
|
|
624
|
+
name: wasm.key.name,
|
|
625
|
+
shortName: wasm.key.shortName,
|
|
626
|
+
},
|
|
627
|
+
correlation: wasm.correlation,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function keyModeValues(modes: KeyDetectionOptions['modes'] | undefined): number[] {
|
|
632
|
+
if (!modes) {
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
if (modes === 'major-minor') {
|
|
636
|
+
return [Mode.Major, Mode.Minor];
|
|
637
|
+
}
|
|
638
|
+
if (modes === 'all' || modes === 'modal') {
|
|
639
|
+
return [
|
|
640
|
+
Mode.Major,
|
|
641
|
+
Mode.Minor,
|
|
642
|
+
Mode.Dorian,
|
|
643
|
+
Mode.Phrygian,
|
|
644
|
+
Mode.Lydian,
|
|
645
|
+
Mode.Mixolydian,
|
|
646
|
+
Mode.Locrian,
|
|
647
|
+
];
|
|
648
|
+
}
|
|
649
|
+
const names = {
|
|
650
|
+
major: Mode.Major,
|
|
651
|
+
minor: Mode.Minor,
|
|
652
|
+
dorian: Mode.Dorian,
|
|
653
|
+
phrygian: Mode.Phrygian,
|
|
654
|
+
lydian: Mode.Lydian,
|
|
655
|
+
mixolydian: Mode.Mixolydian,
|
|
656
|
+
locrian: Mode.Locrian,
|
|
657
|
+
} as const;
|
|
658
|
+
return modes.map((mode) => (typeof mode === 'number' ? mode : names[mode]));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function keyProfileValue(profile: KeyDetectionOptions['profile'] | undefined): number {
|
|
662
|
+
if (profile === undefined) {
|
|
663
|
+
return -1;
|
|
664
|
+
}
|
|
665
|
+
if (typeof profile === 'number') {
|
|
666
|
+
return profile;
|
|
667
|
+
}
|
|
668
|
+
const names: Record<KeyProfileName, number> = {
|
|
669
|
+
ks: KeyProfileValues.KrumhanslSchmuckler,
|
|
670
|
+
krumhansl: KeyProfileValues.KrumhanslSchmuckler,
|
|
671
|
+
temperley: KeyProfileValues.Temperley,
|
|
672
|
+
shaath: KeyProfileValues.Shaath,
|
|
673
|
+
keyfinder: KeyProfileValues.Shaath,
|
|
674
|
+
'faraldo-edmt': KeyProfileValues.FaraldoEDMT,
|
|
675
|
+
edmt: KeyProfileValues.FaraldoEDMT,
|
|
676
|
+
'faraldo-edma': KeyProfileValues.FaraldoEDMA,
|
|
677
|
+
edma: KeyProfileValues.FaraldoEDMA,
|
|
678
|
+
'faraldo-edmm': KeyProfileValues.FaraldoEDMM,
|
|
679
|
+
edmm: KeyProfileValues.FaraldoEDMM,
|
|
680
|
+
'bellman-budge': KeyProfileValues.BellmanBudge,
|
|
681
|
+
bellman: KeyProfileValues.BellmanBudge,
|
|
682
|
+
};
|
|
683
|
+
return names[profile];
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export function detectKeyCandidates(
|
|
687
|
+
samples: Float32Array,
|
|
688
|
+
sampleRate: number,
|
|
689
|
+
options: KeyDetectionOptions = {},
|
|
690
|
+
): KeyCandidate[] {
|
|
691
|
+
if (!module) {
|
|
692
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
693
|
+
}
|
|
694
|
+
return module
|
|
695
|
+
._detectKeyCandidates(
|
|
696
|
+
samples,
|
|
697
|
+
sampleRate,
|
|
698
|
+
options.nFft ?? 4096,
|
|
699
|
+
options.hopLength ?? 512,
|
|
700
|
+
options.useHpss ?? false,
|
|
701
|
+
options.loudnessWeighted ?? false,
|
|
702
|
+
options.highPassHz ?? 0,
|
|
703
|
+
keyModeValues(options.modes),
|
|
704
|
+
keyProfileValue(options.profile),
|
|
705
|
+
options.genreHint ?? '',
|
|
706
|
+
)
|
|
707
|
+
.map(convertKeyCandidate);
|
|
708
|
+
}
|
|
709
|
+
|
|
190
710
|
/**
|
|
191
711
|
* Detect onset times from audio samples.
|
|
192
712
|
*
|
|
@@ -215,6 +735,81 @@ export function detectBeats(samples: Float32Array, sampleRate: number): Float32A
|
|
|
215
735
|
return module.detectBeats(samples, sampleRate);
|
|
216
736
|
}
|
|
217
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Detect downbeat times from audio samples.
|
|
740
|
+
*
|
|
741
|
+
* @param samples - Audio samples (mono, float32)
|
|
742
|
+
* @param sampleRate - Sample rate in Hz
|
|
743
|
+
* @returns Array of downbeat times in seconds
|
|
744
|
+
*/
|
|
745
|
+
export function detectDownbeats(samples: Float32Array, sampleRate: number): Float32Array {
|
|
746
|
+
if (!module) {
|
|
747
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
748
|
+
}
|
|
749
|
+
return module.detectDownbeats(samples, sampleRate);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function convertChordAnalysisResult(wasm: WasmChordAnalysisResult): ChordAnalysisResult {
|
|
753
|
+
return {
|
|
754
|
+
chords: wasm.chords.map((c) => ({
|
|
755
|
+
root: c.root as PitchClass,
|
|
756
|
+
bass: c.bass as PitchClass,
|
|
757
|
+
quality: c.quality as ChordQuality,
|
|
758
|
+
start: c.start,
|
|
759
|
+
end: c.end,
|
|
760
|
+
confidence: c.confidence,
|
|
761
|
+
name: c.name,
|
|
762
|
+
})),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Detect chords from audio samples.
|
|
768
|
+
*
|
|
769
|
+
* @param samples - Audio samples (mono, float32)
|
|
770
|
+
* @param sampleRate - Sample rate in Hz
|
|
771
|
+
* @param options - Optional chord detection settings
|
|
772
|
+
* @returns Detected chord segments
|
|
773
|
+
*/
|
|
774
|
+
export function detectChords(
|
|
775
|
+
samples: Float32Array,
|
|
776
|
+
sampleRate: number,
|
|
777
|
+
options: ChordDetectionOptions = {},
|
|
778
|
+
): ChordAnalysisResult {
|
|
779
|
+
if (!module) {
|
|
780
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
781
|
+
}
|
|
782
|
+
const result = module.detectChords(
|
|
783
|
+
samples,
|
|
784
|
+
sampleRate,
|
|
785
|
+
options.minDuration ?? 0.3,
|
|
786
|
+
options.smoothingWindow ?? 2.0,
|
|
787
|
+
options.threshold ?? 0.5,
|
|
788
|
+
options.useTriadsOnly ?? false,
|
|
789
|
+
options.nFft ?? 2048,
|
|
790
|
+
options.hopLength ?? 512,
|
|
791
|
+
options.useBeatSync ?? true,
|
|
792
|
+
options.useHmm ?? false,
|
|
793
|
+
options.hmmBeamWidth ?? 24,
|
|
794
|
+
options.useKeyContext ?? false,
|
|
795
|
+
options.keyRoot ?? PitchClass.C,
|
|
796
|
+
options.keyMode ?? Mode.Major,
|
|
797
|
+
options.detectInversions ?? false,
|
|
798
|
+
chordChromaMethodValue(options.chromaMethod ?? 'stft'),
|
|
799
|
+
);
|
|
800
|
+
return convertChordAnalysisResult(result);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function chordChromaMethodValue(method: 'stft' | 'nnls'): number {
|
|
804
|
+
if (method === 'stft') {
|
|
805
|
+
return 0;
|
|
806
|
+
}
|
|
807
|
+
if (method === 'nnls') {
|
|
808
|
+
return 1;
|
|
809
|
+
}
|
|
810
|
+
throw new Error(`Invalid chord chroma method: ${method}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
218
813
|
// Helper to convert WASM result to typed result
|
|
219
814
|
function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult {
|
|
220
815
|
const beatTimes = new Float32Array(wasm.beats.length);
|
|
@@ -236,6 +831,7 @@ function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult {
|
|
|
236
831
|
beats: wasm.beats,
|
|
237
832
|
chords: wasm.chords.map((c) => ({
|
|
238
833
|
root: c.root as PitchClass,
|
|
834
|
+
bass: c.bass as PitchClass,
|
|
239
835
|
quality: c.quality as ChordQuality,
|
|
240
836
|
start: c.start,
|
|
241
837
|
end: c.end,
|
|
@@ -272,6 +868,44 @@ export function analyze(samples: Float32Array, sampleRate: number): AnalysisResu
|
|
|
272
868
|
return convertAnalysisResult(result);
|
|
273
869
|
}
|
|
274
870
|
|
|
871
|
+
export function analyzeImpulseResponse(
|
|
872
|
+
samples: Float32Array,
|
|
873
|
+
sampleRate: number,
|
|
874
|
+
nOctaveBands = 6,
|
|
875
|
+
): AcousticResult {
|
|
876
|
+
if (!module) {
|
|
877
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
878
|
+
}
|
|
879
|
+
const result: WasmAcousticResult = module.analyzeImpulseResponse(
|
|
880
|
+
samples,
|
|
881
|
+
sampleRate,
|
|
882
|
+
nOctaveBands,
|
|
883
|
+
);
|
|
884
|
+
return result;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
export function detectAcoustic(
|
|
888
|
+
samples: Float32Array,
|
|
889
|
+
sampleRate: number,
|
|
890
|
+
nOctaveBands = 6,
|
|
891
|
+
nThirdOctaveSubbands = 24,
|
|
892
|
+
minDecayDb = 30.0,
|
|
893
|
+
noiseFloorMarginDb = 10.0,
|
|
894
|
+
): AcousticResult {
|
|
895
|
+
if (!module) {
|
|
896
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
897
|
+
}
|
|
898
|
+
const result: WasmAcousticResult = module.detectAcoustic(
|
|
899
|
+
samples,
|
|
900
|
+
sampleRate,
|
|
901
|
+
nOctaveBands,
|
|
902
|
+
nThirdOctaveSubbands,
|
|
903
|
+
minDecayDb,
|
|
904
|
+
noiseFloorMarginDb,
|
|
905
|
+
);
|
|
906
|
+
return result;
|
|
907
|
+
}
|
|
908
|
+
|
|
275
909
|
/**
|
|
276
910
|
* Perform complete music analysis with progress reporting.
|
|
277
911
|
*
|
|
@@ -380,73 +1014,138 @@ export function pitchShift(
|
|
|
380
1014
|
}
|
|
381
1015
|
|
|
382
1016
|
/**
|
|
383
|
-
*
|
|
1017
|
+
* Pitch-correct audio from a current MIDI note to a target MIDI note.
|
|
384
1018
|
*
|
|
385
1019
|
* @param samples - Audio samples (mono, float32)
|
|
386
1020
|
* @param sampleRate - Sample rate in Hz
|
|
387
|
-
* @param
|
|
388
|
-
* @
|
|
1021
|
+
* @param currentMidi - Detected/current MIDI note number
|
|
1022
|
+
* @param targetMidi - Desired MIDI note number
|
|
1023
|
+
* @returns Pitch-corrected audio
|
|
389
1024
|
*/
|
|
390
|
-
export function
|
|
1025
|
+
export function pitchCorrectToMidi(
|
|
1026
|
+
samples: Float32Array,
|
|
1027
|
+
sampleRate: number,
|
|
1028
|
+
currentMidi: number,
|
|
1029
|
+
targetMidi: number,
|
|
1030
|
+
): Float32Array {
|
|
391
1031
|
if (!module) {
|
|
392
1032
|
throw new Error('Module not initialized. Call init() first.');
|
|
393
1033
|
}
|
|
394
|
-
return module.
|
|
1034
|
+
return module.pitchCorrectToMidi(samples, sampleRate, currentMidi, targetMidi);
|
|
395
1035
|
}
|
|
396
1036
|
|
|
397
1037
|
/**
|
|
398
|
-
*
|
|
1038
|
+
* Time-stretch a note region between two sample offsets without changing pitch.
|
|
399
1039
|
*
|
|
400
1040
|
* @param samples - Audio samples (mono, float32)
|
|
401
1041
|
* @param sampleRate - Sample rate in Hz
|
|
402
|
-
* @param
|
|
403
|
-
* @param
|
|
404
|
-
* @param
|
|
405
|
-
* @returns
|
|
1042
|
+
* @param onsetSample - Note onset position in samples
|
|
1043
|
+
* @param offsetSample - Note offset position in samples
|
|
1044
|
+
* @param stretchRatio - Stretch ratio (0.5 = double duration, 2.0 = half duration)
|
|
1045
|
+
* @returns Audio with the note region stretched
|
|
406
1046
|
*/
|
|
407
|
-
export function
|
|
1047
|
+
export function noteStretch(
|
|
408
1048
|
samples: Float32Array,
|
|
409
1049
|
sampleRate: number,
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
):
|
|
414
|
-
if (!module) {
|
|
415
|
-
throw new Error('Module not initialized. Call init() first.');
|
|
416
|
-
}
|
|
417
|
-
return module.mastering(samples, sampleRate, targetLufs, ceilingDb, truePeakOversample);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export function masteringProcessorNames(): string[] {
|
|
1050
|
+
onsetSample: number,
|
|
1051
|
+
offsetSample: number,
|
|
1052
|
+
stretchRatio: number,
|
|
1053
|
+
): Float32Array {
|
|
421
1054
|
if (!module) {
|
|
422
1055
|
throw new Error('Module not initialized. Call init() first.');
|
|
423
1056
|
}
|
|
424
|
-
return module.
|
|
1057
|
+
return module.noteStretch(samples, sampleRate, onsetSample, offsetSample, stretchRatio);
|
|
425
1058
|
}
|
|
426
1059
|
|
|
427
|
-
|
|
1060
|
+
/**
|
|
1061
|
+
* Apply a voice change by shifting pitch and formants independently.
|
|
1062
|
+
*
|
|
1063
|
+
* @param samples - Audio samples (mono, float32)
|
|
1064
|
+
* @param sampleRate - Sample rate in Hz
|
|
1065
|
+
* @param pitchSemitones - Pitch shift in semitones
|
|
1066
|
+
* @param formantFactor - Formant scaling factor (1.0 = unchanged)
|
|
1067
|
+
* @returns Voice-changed audio
|
|
1068
|
+
*/
|
|
1069
|
+
export function voiceChange(
|
|
1070
|
+
samples: Float32Array,
|
|
1071
|
+
sampleRate: number,
|
|
1072
|
+
pitchSemitones: number,
|
|
1073
|
+
formantFactor: number,
|
|
1074
|
+
): Float32Array {
|
|
428
1075
|
if (!module) {
|
|
429
1076
|
throw new Error('Module not initialized. Call init() first.');
|
|
430
1077
|
}
|
|
431
|
-
return module.
|
|
1078
|
+
return module.voiceChange(samples, sampleRate, pitchSemitones, formantFactor);
|
|
432
1079
|
}
|
|
433
1080
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1081
|
+
/**
|
|
1082
|
+
* Normalize audio to target peak level.
|
|
1083
|
+
*
|
|
1084
|
+
* @param samples - Audio samples (mono, float32)
|
|
1085
|
+
* @param sampleRate - Sample rate in Hz
|
|
1086
|
+
* @param targetDb - Target peak level in dB (default: 0 dB = full scale)
|
|
1087
|
+
* @returns Normalized audio
|
|
1088
|
+
*/
|
|
1089
|
+
export function normalize(samples: Float32Array, sampleRate: number, targetDb = 0.0): Float32Array {
|
|
1090
|
+
if (!module) {
|
|
1091
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1092
|
+
}
|
|
1093
|
+
return module.normalize(samples, sampleRate, targetDb);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Apply mastering loudness normalization with a true-peak ceiling.
|
|
1098
|
+
*
|
|
1099
|
+
* @param samples - Audio samples (mono, float32)
|
|
1100
|
+
* @param sampleRate - Sample rate in Hz
|
|
1101
|
+
* @param targetLufs - Target integrated LUFS (default: -14)
|
|
1102
|
+
* @param ceilingDb - True/sample peak ceiling in dBFS (default: -1)
|
|
1103
|
+
* @param truePeakOversample - Oversampling factor used for peak estimation
|
|
1104
|
+
* @returns Processed audio and loudness metadata
|
|
1105
|
+
*/
|
|
1106
|
+
export function mastering(
|
|
1107
|
+
samples: Float32Array,
|
|
1108
|
+
sampleRate: number,
|
|
1109
|
+
targetLufs = -14.0,
|
|
1110
|
+
ceilingDb = -1.0,
|
|
1111
|
+
truePeakOversample = 4,
|
|
1112
|
+
): MasteringResult {
|
|
1113
|
+
if (!module) {
|
|
1114
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1115
|
+
}
|
|
1116
|
+
return module.mastering(samples, sampleRate, targetLufs, ceilingDb, truePeakOversample);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
export function masteringProcessorNames(): SoloProcessor[] {
|
|
1120
|
+
if (!module) {
|
|
1121
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1122
|
+
}
|
|
1123
|
+
return module.masteringProcessorNames() as SoloProcessor[];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
export function masteringPairProcessorNames(): PairProcessor[] {
|
|
1127
|
+
if (!module) {
|
|
1128
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1129
|
+
}
|
|
1130
|
+
return module.masteringPairProcessorNames() as PairProcessor[];
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
export function masteringPairAnalysisNames(): PairAnalysis[] {
|
|
1134
|
+
if (!module) {
|
|
1135
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1136
|
+
}
|
|
1137
|
+
return module.masteringPairAnalysisNames() as PairAnalysis[];
|
|
439
1138
|
}
|
|
440
1139
|
|
|
441
|
-
export function masteringStereoAnalysisNames():
|
|
1140
|
+
export function masteringStereoAnalysisNames(): StereoAnalysis[] {
|
|
442
1141
|
if (!module) {
|
|
443
1142
|
throw new Error('Module not initialized. Call init() first.');
|
|
444
1143
|
}
|
|
445
|
-
return module.masteringStereoAnalysisNames();
|
|
1144
|
+
return module.masteringStereoAnalysisNames() as StereoAnalysis[];
|
|
446
1145
|
}
|
|
447
1146
|
|
|
448
1147
|
export function masteringProcess(
|
|
449
|
-
processorName:
|
|
1148
|
+
processorName: SoloProcessor,
|
|
450
1149
|
samples: Float32Array,
|
|
451
1150
|
sampleRate: number,
|
|
452
1151
|
params: MasteringProcessorParams = {},
|
|
@@ -458,7 +1157,7 @@ export function masteringProcess(
|
|
|
458
1157
|
}
|
|
459
1158
|
|
|
460
1159
|
export function masteringProcessStereo(
|
|
461
|
-
processorName:
|
|
1160
|
+
processorName: SoloProcessor,
|
|
462
1161
|
left: Float32Array,
|
|
463
1162
|
right: Float32Array,
|
|
464
1163
|
sampleRate: number,
|
|
@@ -474,7 +1173,7 @@ export function masteringProcessStereo(
|
|
|
474
1173
|
}
|
|
475
1174
|
|
|
476
1175
|
export function masteringPairProcess(
|
|
477
|
-
processorName:
|
|
1176
|
+
processorName: PairProcessor,
|
|
478
1177
|
source: Float32Array,
|
|
479
1178
|
reference: Float32Array,
|
|
480
1179
|
sampleRate: number,
|
|
@@ -487,7 +1186,7 @@ export function masteringPairProcess(
|
|
|
487
1186
|
}
|
|
488
1187
|
|
|
489
1188
|
export function masteringPairAnalyze(
|
|
490
|
-
analysisName:
|
|
1189
|
+
analysisName: PairAnalysis,
|
|
491
1190
|
source: Float32Array,
|
|
492
1191
|
reference: Float32Array,
|
|
493
1192
|
sampleRate: number,
|
|
@@ -500,7 +1199,7 @@ export function masteringPairAnalyze(
|
|
|
500
1199
|
}
|
|
501
1200
|
|
|
502
1201
|
export function masteringStereoAnalyze(
|
|
503
|
-
analysisName:
|
|
1202
|
+
analysisName: StereoAnalysis,
|
|
504
1203
|
left: Float32Array,
|
|
505
1204
|
right: Float32Array,
|
|
506
1205
|
sampleRate: number,
|
|
@@ -512,6 +1211,39 @@ export function masteringStereoAnalyze(
|
|
|
512
1211
|
return module.masteringStereoAnalyze(analysisName, left, right, sampleRate, params);
|
|
513
1212
|
}
|
|
514
1213
|
|
|
1214
|
+
export function masteringAssistantSuggest(
|
|
1215
|
+
samples: Float32Array,
|
|
1216
|
+
sampleRate: number,
|
|
1217
|
+
params: MasteringProcessorParams = {},
|
|
1218
|
+
): string {
|
|
1219
|
+
if (!module) {
|
|
1220
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1221
|
+
}
|
|
1222
|
+
return module.masteringAssistantSuggest(samples, sampleRate, params);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
export function masteringAudioProfile(
|
|
1226
|
+
samples: Float32Array,
|
|
1227
|
+
sampleRate: number,
|
|
1228
|
+
params: MasteringProcessorParams = {},
|
|
1229
|
+
): string {
|
|
1230
|
+
if (!module) {
|
|
1231
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1232
|
+
}
|
|
1233
|
+
return module.masteringAudioProfile(samples, sampleRate, params);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
export function masteringStreamingPreview(
|
|
1237
|
+
samples: Float32Array,
|
|
1238
|
+
sampleRate: number,
|
|
1239
|
+
platforms: StreamingPlatform[] = [],
|
|
1240
|
+
): string {
|
|
1241
|
+
if (!module) {
|
|
1242
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1243
|
+
}
|
|
1244
|
+
return module.masteringStreamingPreview(samples, sampleRate, platforms);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
515
1247
|
/**
|
|
516
1248
|
* Apply a configurable mastering chain in WASM.
|
|
517
1249
|
*
|
|
@@ -618,11 +1350,11 @@ export function masteringChainStereoWithProgress(
|
|
|
618
1350
|
*
|
|
619
1351
|
* @returns Preset names in display order (e.g. "pop", "edm", "aiMusic")
|
|
620
1352
|
*/
|
|
621
|
-
export function masteringPresetNames():
|
|
1353
|
+
export function masteringPresetNames(): MasteringPreset[] {
|
|
622
1354
|
if (!module) {
|
|
623
1355
|
throw new Error('Module not initialized. Call init() first.');
|
|
624
1356
|
}
|
|
625
|
-
return module.masteringPresetNames();
|
|
1357
|
+
return module.masteringPresetNames() as MasteringPreset[];
|
|
626
1358
|
}
|
|
627
1359
|
|
|
628
1360
|
/**
|
|
@@ -637,7 +1369,7 @@ export function masteringPresetNames(): string[] {
|
|
|
637
1369
|
export function masterAudio(
|
|
638
1370
|
samples: Float32Array,
|
|
639
1371
|
sampleRate: number,
|
|
640
|
-
presetName:
|
|
1372
|
+
presetName: MasteringPreset,
|
|
641
1373
|
overrides: Record<string, number | boolean> | null = null,
|
|
642
1374
|
): MasteringChainResult {
|
|
643
1375
|
if (!module) {
|
|
@@ -660,7 +1392,7 @@ export function masterAudioStereo(
|
|
|
660
1392
|
left: Float32Array,
|
|
661
1393
|
right: Float32Array,
|
|
662
1394
|
sampleRate: number,
|
|
663
|
-
presetName:
|
|
1395
|
+
presetName: MasteringPreset,
|
|
664
1396
|
overrides: Record<string, number | boolean> | null = null,
|
|
665
1397
|
): MasteringStereoChainResult {
|
|
666
1398
|
if (!module) {
|
|
@@ -672,6 +1404,48 @@ export function masterAudioStereo(
|
|
|
672
1404
|
return module.masterAudioStereo(presetName, left, right, sampleRate, overrides);
|
|
673
1405
|
}
|
|
674
1406
|
|
|
1407
|
+
export function mixingScenePresetNames(): string[] {
|
|
1408
|
+
if (!module) {
|
|
1409
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1410
|
+
}
|
|
1411
|
+
return module.mixingScenePresetNames();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Get a built-in mixing scene preset serialized as JSON. This is the canonical
|
|
1416
|
+
* name shared with the Node and Python bindings; the returned JSON loads
|
|
1417
|
+
* directly into a {@link Mixer} via {@link Mixer.fromSceneJson}.
|
|
1418
|
+
*
|
|
1419
|
+
* @param preset - Preset name (see {@link mixingScenePresetNames})
|
|
1420
|
+
* @returns Scene JSON string
|
|
1421
|
+
*/
|
|
1422
|
+
export function mixingScenePresetJson(preset: string): string {
|
|
1423
|
+
if (!module) {
|
|
1424
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1425
|
+
}
|
|
1426
|
+
return module.mixingScenePresetJson(preset);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
export function mixStereo(
|
|
1430
|
+
leftChannels: Float32Array[],
|
|
1431
|
+
rightChannels: Float32Array[],
|
|
1432
|
+
sampleRate = 48000,
|
|
1433
|
+
options: MixOptions = {},
|
|
1434
|
+
): MixResult {
|
|
1435
|
+
if (!module) {
|
|
1436
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1437
|
+
}
|
|
1438
|
+
if (leftChannels.length === 0 || leftChannels.length !== rightChannels.length) {
|
|
1439
|
+
throw new Error('leftChannels and rightChannels must have the same non-zero length.');
|
|
1440
|
+
}
|
|
1441
|
+
return module.mixStereo(
|
|
1442
|
+
leftChannels,
|
|
1443
|
+
rightChannels,
|
|
1444
|
+
sampleRate,
|
|
1445
|
+
options as Record<string, unknown>,
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
675
1449
|
// ============================================================================
|
|
676
1450
|
// StreamingMasteringChain Class
|
|
677
1451
|
// ============================================================================
|
|
@@ -738,24 +1512,647 @@ export class StreamingMasteringChain {
|
|
|
738
1512
|
return this.chain.processStereo(left, right);
|
|
739
1513
|
}
|
|
740
1514
|
|
|
741
|
-
/** Reset all processor state without rebuilding. */
|
|
742
|
-
reset(): void {
|
|
743
|
-
this.chain.reset();
|
|
1515
|
+
/** Reset all processor state without rebuilding. */
|
|
1516
|
+
reset(): void {
|
|
1517
|
+
this.chain.reset();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** Total reported latency in samples across all active processors. */
|
|
1521
|
+
latencySamples(): number {
|
|
1522
|
+
return this.chain.latencySamples();
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/** Ordered stage names that will run (e.g. `"eq.tilt"`). */
|
|
1526
|
+
stageNames(): string[] {
|
|
1527
|
+
return this.chain.stageNames();
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/** Release the underlying WASM object. Safe to call only once. */
|
|
1531
|
+
delete(): void {
|
|
1532
|
+
this.chain.delete();
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// ============================================================================
|
|
1537
|
+
// StreamingEqualizer Class
|
|
1538
|
+
// ============================================================================
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Block-by-block streaming equalizer wrapping the unified C++
|
|
1542
|
+
* `EqualizerProcessor` (up to 24 bands, RBJ/Vicanek biquads, dynamic EQ,
|
|
1543
|
+
* linear-phase FIR, mid/side processing, and auto-gain).
|
|
1544
|
+
*
|
|
1545
|
+
* State is maintained across {@link processMono}/{@link processStereo} calls.
|
|
1546
|
+
* Call {@link delete} (or use `try/finally`) to release the underlying WASM
|
|
1547
|
+
* object — the embind handle is not garbage-collected automatically.
|
|
1548
|
+
*
|
|
1549
|
+
* @example
|
|
1550
|
+
* ```typescript
|
|
1551
|
+
* const eq = new StreamingEqualizer({ sampleRate: 48000, maxBlockSize: 512 });
|
|
1552
|
+
* try {
|
|
1553
|
+
* eq.setBand(0, { type: 'HighShelf', frequencyHz: 8000, gainDb: 6, enabled: true });
|
|
1554
|
+
* const out = eq.processStereo(left, right);
|
|
1555
|
+
* const snapshot = eq.spectrum();
|
|
1556
|
+
* } finally {
|
|
1557
|
+
* eq.delete();
|
|
1558
|
+
* }
|
|
1559
|
+
* ```
|
|
1560
|
+
*/
|
|
1561
|
+
export class StreamingEqualizer {
|
|
1562
|
+
private eq: import('./wasm_types').WasmStreamingEqualizer;
|
|
1563
|
+
|
|
1564
|
+
constructor(config: StreamingEqualizerConfig = {}) {
|
|
1565
|
+
if (!module) {
|
|
1566
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1567
|
+
}
|
|
1568
|
+
this.eq = module.createEqualizer(config as Record<string, unknown>);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* Configure the band at `index` (0..23). Omitted fields use C++ defaults.
|
|
1573
|
+
*/
|
|
1574
|
+
setBand(index: number, band: EqBand): void {
|
|
1575
|
+
this.eq.setBand(index, band as Record<string, unknown>);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/** Disable and reset every band. */
|
|
1579
|
+
clear(): void {
|
|
1580
|
+
this.eq.clear();
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Set the global phase mode: 1=ZeroLatency, 2=NaturalPhase, 3=LinearPhase.
|
|
1585
|
+
*/
|
|
1586
|
+
setPhaseMode(mode: number): void {
|
|
1587
|
+
this.eq.setPhaseMode(mode);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/** Enable or disable output auto-gain compensation. */
|
|
1591
|
+
setAutoGain(enabled: boolean): void {
|
|
1592
|
+
this.eq.setAutoGain(enabled);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/** Set all-band EQ gain scale as a 0.0..2.0 multiplier. */
|
|
1596
|
+
setGainScale(scale: number): void {
|
|
1597
|
+
this.eq.setGainScale(scale);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/** Set post-EQ output gain in dB. */
|
|
1601
|
+
setOutputGainDb(gainDb: number): void {
|
|
1602
|
+
this.eq.setOutputGainDb(gainDb);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/** Set post-EQ stereo balance in -1.0..1.0; mono input ignores pan. */
|
|
1606
|
+
setOutputPan(pan: number): void {
|
|
1607
|
+
this.eq.setOutputPan(pan);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Provide a mono external sidechain key for dynamic bands that opt into
|
|
1612
|
+
* `external_sidechain`. The samples are copied into an owned buffer.
|
|
1613
|
+
*/
|
|
1614
|
+
setSidechainMono(samples: Float32Array): void {
|
|
1615
|
+
this.eq.setSidechainMono(samples);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Provide a stereo external sidechain key. Both channels must match length.
|
|
1620
|
+
*/
|
|
1621
|
+
setSidechainStereo(left: Float32Array, right: Float32Array): void {
|
|
1622
|
+
if (left.length !== right.length) {
|
|
1623
|
+
throw new Error('Sidechain channel lengths must match.');
|
|
1624
|
+
}
|
|
1625
|
+
this.eq.setSidechainStereo(left, right);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/** Release any borrowed external sidechain buffers. */
|
|
1629
|
+
clearSidechain(): void {
|
|
1630
|
+
this.eq.clearSidechain();
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/** Auto-gain applied on the most recent block, in dB. */
|
|
1634
|
+
lastAutoGainDb(): number {
|
|
1635
|
+
return this.eq.lastAutoGainDb();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/** Reported processing latency in samples (non-zero for linear-phase bands). */
|
|
1639
|
+
latencySamples(): number {
|
|
1640
|
+
return this.eq.latencySamples();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Process one mono block, returning the equalized samples (same length).
|
|
1645
|
+
*/
|
|
1646
|
+
processMono(samples: Float32Array): Float32Array {
|
|
1647
|
+
return this.eq.processMono(samples);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Process one stereo block, returning the equalized channels.
|
|
1652
|
+
*/
|
|
1653
|
+
processStereo(
|
|
1654
|
+
left: Float32Array,
|
|
1655
|
+
right: Float32Array,
|
|
1656
|
+
): { left: Float32Array; right: Float32Array } {
|
|
1657
|
+
if (left.length !== right.length) {
|
|
1658
|
+
throw new Error('Stereo channel lengths must match.');
|
|
1659
|
+
}
|
|
1660
|
+
return this.eq.processStereo(left, right);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Read the latest pre/post spectrum snapshot for metering. `seq` increments
|
|
1665
|
+
* each time a new snapshot is published.
|
|
1666
|
+
*/
|
|
1667
|
+
spectrum(): EqSpectrumSnapshot {
|
|
1668
|
+
return this.eq.spectrum();
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Configure bands so the source spectrum matches the reference spectrum.
|
|
1673
|
+
*
|
|
1674
|
+
* @param source - Source audio (mono samples)
|
|
1675
|
+
* @param reference - Reference audio (mono samples)
|
|
1676
|
+
* @param options - `sampleRate` (default 48000) and `maxBands` (default 8)
|
|
1677
|
+
*/
|
|
1678
|
+
match(source: Float32Array, reference: Float32Array, options: EqMatchOptions = {}): void {
|
|
1679
|
+
this.eq.match(source, reference, options as Record<string, unknown>);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/** Release the underlying WASM object. Safe to call only once. */
|
|
1683
|
+
delete(): void {
|
|
1684
|
+
this.eq.delete();
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// ============================================================================
|
|
1689
|
+
// StreamingRetune Class
|
|
1690
|
+
// ============================================================================
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Block-by-block mono voice retune / pitch shifter.
|
|
1694
|
+
*
|
|
1695
|
+
* State is maintained across {@link processMono} calls. Call {@link prepare}
|
|
1696
|
+
* before processing, and call {@link delete} (or use `try/finally`) to release
|
|
1697
|
+
* the underlying WASM object.
|
|
1698
|
+
*/
|
|
1699
|
+
export class StreamingRetune {
|
|
1700
|
+
private retune: import('./wasm_types').WasmStreamingRetune;
|
|
1701
|
+
|
|
1702
|
+
constructor(config: StreamingRetuneConfig = {}) {
|
|
1703
|
+
if (!module) {
|
|
1704
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1705
|
+
}
|
|
1706
|
+
this.retune = module.createStreamingRetune(config as Record<string, unknown>);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Allocate and initialize native state for the given sample rate and maximum
|
|
1711
|
+
* process block size.
|
|
1712
|
+
*/
|
|
1713
|
+
prepare(sampleRate: number, maxBlockSize: number): void {
|
|
1714
|
+
this.retune.prepare(sampleRate, maxBlockSize);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/** Reset delay, grain, and overlap-add state without changing config. */
|
|
1718
|
+
reset(): void {
|
|
1719
|
+
this.retune.reset();
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Update retune settings. Changing `grainSize` takes effect after the next
|
|
1724
|
+
* {@link prepare} call.
|
|
1725
|
+
*/
|
|
1726
|
+
setConfig(config: StreamingRetuneConfig): void {
|
|
1727
|
+
this.retune.setConfig(config as Record<string, unknown>);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** Current native config. */
|
|
1731
|
+
config(): Required<StreamingRetuneConfig> {
|
|
1732
|
+
return this.retune.config();
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/** Resolved grain size in samples after {@link prepare}. */
|
|
1736
|
+
grainSize(): number {
|
|
1737
|
+
return this.retune.grainSize();
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/** Process one mono block, returning the shifted samples (same length). */
|
|
1741
|
+
processMono(samples: Float32Array): Float32Array {
|
|
1742
|
+
return this.retune.processMono(samples);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/** Release the underlying WASM object. Safe to call only once. */
|
|
1746
|
+
delete(): void {
|
|
1747
|
+
this.retune.delete();
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// ============================================================================
|
|
1752
|
+
// Mixer Class (scene-based persistent mixer)
|
|
1753
|
+
// ============================================================================
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Get a built-in mixing scene preset serialized as JSON, normalized through the
|
|
1757
|
+
* C mixer API (the same path {@link Mixer.fromSceneJson} uses to load it).
|
|
1758
|
+
*
|
|
1759
|
+
* @deprecated Use {@link mixingScenePresetJson}, the canonical name shared with
|
|
1760
|
+
* the Node and Python bindings. This alias is retained for backwards
|
|
1761
|
+
* compatibility and may be removed in a future release. Both functions return a
|
|
1762
|
+
* scene JSON string that loads cleanly into a {@link Mixer}.
|
|
1763
|
+
*
|
|
1764
|
+
* @param preset - Preset name (see {@link mixingScenePresetNames})
|
|
1765
|
+
* @returns Scene JSON string
|
|
1766
|
+
*/
|
|
1767
|
+
export function mixerScenePresetJson(preset: string): string {
|
|
1768
|
+
if (!module) {
|
|
1769
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1770
|
+
}
|
|
1771
|
+
return module.mixerPresetJson(preset);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Persistent, scene-based stereo mixer.
|
|
1776
|
+
*
|
|
1777
|
+
* Build one from a scene JSON string (e.g. {@link mixerScenePresetJson} or a
|
|
1778
|
+
* hand-authored scene), then feed per-strip stereo blocks through
|
|
1779
|
+
* {@link processStereo} to get the routed stereo master. Strips, sends, buses,
|
|
1780
|
+
* and inserts are described entirely by the scene; the routing graph is
|
|
1781
|
+
* compiled lazily on the first {@link processStereo} call (or eagerly via
|
|
1782
|
+
* {@link compile}).
|
|
1783
|
+
*
|
|
1784
|
+
* Call {@link delete} (or use a `try/finally`) to release the underlying WASM
|
|
1785
|
+
* object — the embind handle is not garbage-collected automatically.
|
|
1786
|
+
*
|
|
1787
|
+
* @example
|
|
1788
|
+
* ```typescript
|
|
1789
|
+
* const mixer = Mixer.fromSceneJson(mixerScenePresetJson('basicStereo'), 48000, 512);
|
|
1790
|
+
* try {
|
|
1791
|
+
* const out = mixer.processStereo([stripL], [stripR]);
|
|
1792
|
+
* } finally {
|
|
1793
|
+
* mixer.delete();
|
|
1794
|
+
* }
|
|
1795
|
+
* ```
|
|
1796
|
+
*/
|
|
1797
|
+
export class Mixer {
|
|
1798
|
+
private mixer: import('./wasm_types').WasmMixer;
|
|
1799
|
+
|
|
1800
|
+
private constructor(mixer: import('./wasm_types').WasmMixer) {
|
|
1801
|
+
this.mixer = mixer;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Build a mixer from a scene JSON string.
|
|
1806
|
+
*
|
|
1807
|
+
* @param json - Scene JSON (strips, buses, sends, connections, inserts)
|
|
1808
|
+
* @param sampleRate - Sample rate in Hz (default: 48000)
|
|
1809
|
+
* @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
|
|
1810
|
+
*/
|
|
1811
|
+
static fromSceneJson(json: string, sampleRate = 48000, blockSize = 512): Mixer {
|
|
1812
|
+
if (!module) {
|
|
1813
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
1814
|
+
}
|
|
1815
|
+
return new Mixer(module.createMixerFromSceneJson(json, sampleRate, blockSize));
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/** Rebuild and compile the routing graph from the current scene topology. */
|
|
1819
|
+
compile(): void {
|
|
1820
|
+
this.mixer.compile();
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* Mix one block of per-strip stereo audio into the stereo master.
|
|
1825
|
+
*
|
|
1826
|
+
* @param leftChannels - `leftChannels[i]` is the left channel of strip `i`
|
|
1827
|
+
* @param rightChannels - `rightChannels[i]` is the right channel of strip `i`
|
|
1828
|
+
* @returns Mixed stereo master (`left`, `right`, `sampleRate`)
|
|
1829
|
+
*/
|
|
1830
|
+
processStereo(leftChannels: Float32Array[], rightChannels: Float32Array[]): MixerProcessResult {
|
|
1831
|
+
if (leftChannels.length !== rightChannels.length) {
|
|
1832
|
+
throw new Error('leftChannels and rightChannels must have the same length.');
|
|
1833
|
+
}
|
|
1834
|
+
return this.mixer.processStereo(leftChannels, rightChannels);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* Mix one block into caller-owned output arrays.
|
|
1839
|
+
*
|
|
1840
|
+
* This avoids allocating the result object and result `Float32Array`s. It is
|
|
1841
|
+
* intended for realtime bridges such as AudioWorklet; the input channel count
|
|
1842
|
+
* must match the scene strip count and all arrays must have the same length.
|
|
1843
|
+
*/
|
|
1844
|
+
processStereoInto(
|
|
1845
|
+
leftChannels: Float32Array[],
|
|
1846
|
+
rightChannels: Float32Array[],
|
|
1847
|
+
outLeft: Float32Array,
|
|
1848
|
+
outRight: Float32Array,
|
|
1849
|
+
): void {
|
|
1850
|
+
if (leftChannels.length !== rightChannels.length) {
|
|
1851
|
+
throw new Error('leftChannels and rightChannels must have the same length.');
|
|
1852
|
+
}
|
|
1853
|
+
if (outLeft.length !== outRight.length) {
|
|
1854
|
+
throw new Error('outLeft and outRight must have the same length.');
|
|
1855
|
+
}
|
|
1856
|
+
this.mixer.processStereoInto(leftChannels, rightChannels, outLeft, outRight);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* Create reusable WASM-heap input/output views for realtime-style processing.
|
|
1861
|
+
*
|
|
1862
|
+
* Fill `leftInputs[i]` / `rightInputs[i]`, call `process()`, then read
|
|
1863
|
+
* `outLeft` / `outRight`. The views are owned by this mixer and become invalid
|
|
1864
|
+
* after {@link delete}.
|
|
1865
|
+
*/
|
|
1866
|
+
createRealtimeBuffer(): MixerRealtimeBuffer {
|
|
1867
|
+
const stripCount = this.stripCount();
|
|
1868
|
+
const leftInputs: Float32Array[] = [];
|
|
1869
|
+
const rightInputs: Float32Array[] = [];
|
|
1870
|
+
for (let index = 0; index < stripCount; index++) {
|
|
1871
|
+
leftInputs.push(this.mixer.inputLeftView(index));
|
|
1872
|
+
rightInputs.push(this.mixer.inputRightView(index));
|
|
1873
|
+
}
|
|
1874
|
+
const outLeft = this.mixer.outputLeftView();
|
|
1875
|
+
const outRight = this.mixer.outputRightView();
|
|
1876
|
+
return {
|
|
1877
|
+
leftInputs,
|
|
1878
|
+
rightInputs,
|
|
1879
|
+
outLeft,
|
|
1880
|
+
outRight,
|
|
1881
|
+
process: (numSamples = outLeft.length) => this.mixer.processPreparedStereo(numSamples),
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/** Number of strips in the mixer (e.g. strips loaded from the scene). */
|
|
1886
|
+
stripCount(): number {
|
|
1887
|
+
return this.mixer.stripCount();
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* Schedule sample-accurate insert-parameter automation on a strip's insert.
|
|
1892
|
+
*
|
|
1893
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1894
|
+
* @param insertIndex - Index into the strip's combined insert sequence
|
|
1895
|
+
* (`[pre-inserts... post-inserts...]`)
|
|
1896
|
+
* @param paramId - Processor-specific parameter id
|
|
1897
|
+
* @param samplePos - Absolute samples from the start of processing (the mixer
|
|
1898
|
+
* advances an internal position from 0 on the first {@link processStereo}
|
|
1899
|
+
* call; recompiling resets it to 0)
|
|
1900
|
+
* @param value - Target parameter value
|
|
1901
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
1902
|
+
* @throws If the strip index is out of range or the schedule call fails
|
|
1903
|
+
* (unknown curve, out-of-range insert index, or full event lane)
|
|
1904
|
+
*/
|
|
1905
|
+
scheduleInsertAutomation(
|
|
1906
|
+
stripIndex: number,
|
|
1907
|
+
insertIndex: number,
|
|
1908
|
+
paramId: number,
|
|
1909
|
+
samplePos: number,
|
|
1910
|
+
value: number,
|
|
1911
|
+
curve: AutomationCurve = 'linear',
|
|
1912
|
+
): void {
|
|
1913
|
+
this.mixer.scheduleInsertAutomation(
|
|
1914
|
+
stripIndex,
|
|
1915
|
+
insertIndex,
|
|
1916
|
+
paramId,
|
|
1917
|
+
samplePos,
|
|
1918
|
+
value,
|
|
1919
|
+
automationCurveCode(curve),
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Resolve a strip's index in `[0, stripCount())` from its scene id, or `null`
|
|
1925
|
+
* when no strip with that id exists (matches the Node binding's `number | null`).
|
|
1926
|
+
*/
|
|
1927
|
+
stripById(id: string): number | null {
|
|
1928
|
+
const index = this.mixer.stripById(id);
|
|
1929
|
+
return index < 0 ? null : index;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
|
|
1934
|
+
* `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
|
|
1935
|
+
* {@link compile} (or {@link processStereo}) to rebuild.
|
|
1936
|
+
*/
|
|
1937
|
+
addBus(id: string, role = 'aux'): void {
|
|
1938
|
+
this.mixer.addBus(id, role);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/** Remove a bus by id. Marks the routing graph dirty. */
|
|
1942
|
+
removeBus(id: string): void {
|
|
1943
|
+
this.mixer.removeBus(id);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/** Number of buses in the mixer topology. */
|
|
1947
|
+
busCount(): number {
|
|
1948
|
+
return this.mixer.busCount();
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* Add a VCA group with the given gain offset (dB). `members` is a list of
|
|
1953
|
+
* strip ids governed by the group (may be empty).
|
|
1954
|
+
*/
|
|
1955
|
+
addVcaGroup(id: string, gainDb = 0.0, members: string[] = []): void {
|
|
1956
|
+
this.mixer.addVcaGroup(id, gainDb, members);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/** Remove a VCA group by id. */
|
|
1960
|
+
removeVcaGroup(id: string): void {
|
|
1961
|
+
this.mixer.removeVcaGroup(id);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/** Number of VCA groups in the mixer topology. */
|
|
1965
|
+
vcaGroupCount(): number {
|
|
1966
|
+
return this.mixer.vcaGroupCount();
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Set a strip's solo state. Takes effect on the next process without a
|
|
1971
|
+
* graph recompile.
|
|
1972
|
+
*/
|
|
1973
|
+
setSoloed(stripIndex: number, soloed: boolean): void {
|
|
1974
|
+
this.mixer.setSoloed(stripIndex, soloed);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* Mark a strip solo-safe so it is never implied-muted by another strip's
|
|
1979
|
+
* solo. Takes effect on the next process without a graph recompile.
|
|
1980
|
+
*/
|
|
1981
|
+
setSoloSafe(stripIndex: number, soloSafe: boolean): void {
|
|
1982
|
+
this.mixer.setSoloSafe(stripIndex, soloSafe);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
/** Invert the polarity of the left and/or right channel of a strip. */
|
|
1986
|
+
setPolarityInvert(stripIndex: number, invertLeft: boolean, invertRight: boolean): void {
|
|
1987
|
+
this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
/** Set the strip's pan law. */
|
|
1991
|
+
setPanLaw(stripIndex: number, panLaw: PanLaw | number): void {
|
|
1992
|
+
this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Set a per-strip channel delay in samples. This changes the strip's reported
|
|
1997
|
+
* latency; recompile to re-run latency compensation.
|
|
1998
|
+
*/
|
|
1999
|
+
setChannelDelaySamples(stripIndex: number, delaySamples: number): void {
|
|
2000
|
+
this.mixer.setChannelDelaySamples(stripIndex, delaySamples);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
/** Set the strip's live VCA gain offset in dB (not persisted to the scene). */
|
|
2004
|
+
setVcaOffsetDb(stripIndex: number, offsetDb: number): void {
|
|
2005
|
+
this.mixer.setVcaOffsetDb(stripIndex, offsetDb);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/** Set independent left/right pan positions (dual-pan mode). */
|
|
2009
|
+
setDualPan(stripIndex: number, leftPan: number, rightPan: number): void {
|
|
2010
|
+
this.mixer.setDualPan(stripIndex, leftPan, rightPan);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Add a send to a strip after construction.
|
|
2015
|
+
*
|
|
2016
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
2017
|
+
* @param id - Send id
|
|
2018
|
+
* @param destinationBusId - Destination bus id
|
|
2019
|
+
* @param sendDb - Initial send level in dB
|
|
2020
|
+
* @param timing - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
2021
|
+
* @returns The new send's index
|
|
2022
|
+
*/
|
|
2023
|
+
addSend(
|
|
2024
|
+
stripIndex: number,
|
|
2025
|
+
id: string,
|
|
2026
|
+
destinationBusId: string,
|
|
2027
|
+
sendDb: number,
|
|
2028
|
+
timing: SendTiming | number = 'postFader',
|
|
2029
|
+
): number {
|
|
2030
|
+
return this.mixer.addSend(stripIndex, id, destinationBusId, sendDb, sendTimingCode(timing));
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/** Set the send level (in dB) for an existing send by index. */
|
|
2034
|
+
setSendDb(stripIndex: number, sendIndex: number, sendDb: number): void {
|
|
2035
|
+
this.mixer.setSendDb(stripIndex, sendIndex, sendDb);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Read a strip's meter snapshot at the given tap point.
|
|
2040
|
+
*
|
|
2041
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
2042
|
+
* @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
2043
|
+
*/
|
|
2044
|
+
meterTap(stripIndex: number, tap: MeterTap = 'postFader'): MixMeterSnapshot {
|
|
2045
|
+
return this.mixer.meterTap(stripIndex, meterTapCode(tap));
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* Read a strip's meter snapshot. Alias of {@link meterTap}, provided for
|
|
2050
|
+
* cross-binding (Node/Python) parity.
|
|
2051
|
+
*
|
|
2052
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
2053
|
+
* @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
2054
|
+
*/
|
|
2055
|
+
stripMeter(stripIndex: number, tap: MeterTap = 'postFader'): MixMeterSnapshot {
|
|
2056
|
+
return this.mixer.stripMeter(stripIndex, meterTapCode(tap));
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
/**
|
|
2060
|
+
* Schedule sample-accurate fader automation on a strip.
|
|
2061
|
+
*
|
|
2062
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
2063
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
2064
|
+
* @param faderDb - Target fader level in dB
|
|
2065
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
2066
|
+
*/
|
|
2067
|
+
scheduleFaderAutomation(
|
|
2068
|
+
stripIndex: number,
|
|
2069
|
+
samplePos: number,
|
|
2070
|
+
faderDb: number,
|
|
2071
|
+
curve: AutomationCurve = 'linear',
|
|
2072
|
+
): void {
|
|
2073
|
+
this.mixer.scheduleFaderAutomation(stripIndex, samplePos, faderDb, automationCurveCode(curve));
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Schedule sample-accurate pan automation on a strip.
|
|
2078
|
+
*
|
|
2079
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
2080
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
2081
|
+
* @param pan - Target pan position
|
|
2082
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
2083
|
+
*/
|
|
2084
|
+
schedulePanAutomation(
|
|
2085
|
+
stripIndex: number,
|
|
2086
|
+
samplePos: number,
|
|
2087
|
+
pan: number,
|
|
2088
|
+
curve: AutomationCurve = 'linear',
|
|
2089
|
+
): void {
|
|
2090
|
+
this.mixer.schedulePanAutomation(stripIndex, samplePos, pan, automationCurveCode(curve));
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
/**
|
|
2094
|
+
* Schedule sample-accurate width automation on a strip.
|
|
2095
|
+
*
|
|
2096
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
2097
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
2098
|
+
* @param width - Target stereo width
|
|
2099
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
2100
|
+
*/
|
|
2101
|
+
scheduleWidthAutomation(
|
|
2102
|
+
stripIndex: number,
|
|
2103
|
+
samplePos: number,
|
|
2104
|
+
width: number,
|
|
2105
|
+
curve: AutomationCurve = 'linear',
|
|
2106
|
+
): void {
|
|
2107
|
+
this.mixer.scheduleWidthAutomation(stripIndex, samplePos, width, automationCurveCode(curve));
|
|
744
2108
|
}
|
|
745
2109
|
|
|
746
|
-
/**
|
|
747
|
-
|
|
748
|
-
|
|
2110
|
+
/**
|
|
2111
|
+
* Schedule sample-accurate send-level automation on a strip's send.
|
|
2112
|
+
*
|
|
2113
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
2114
|
+
* @param sendIndex - Send index in the strip's add order
|
|
2115
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
2116
|
+
* @param db - Target send level in dB
|
|
2117
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
2118
|
+
*/
|
|
2119
|
+
scheduleSendAutomation(
|
|
2120
|
+
stripIndex: number,
|
|
2121
|
+
sendIndex: number,
|
|
2122
|
+
samplePos: number,
|
|
2123
|
+
db: number,
|
|
2124
|
+
curve: AutomationCurve = 'linear',
|
|
2125
|
+
): void {
|
|
2126
|
+
this.mixer.scheduleSendAutomation(
|
|
2127
|
+
stripIndex,
|
|
2128
|
+
sendIndex,
|
|
2129
|
+
samplePos,
|
|
2130
|
+
db,
|
|
2131
|
+
automationCurveCode(curve),
|
|
2132
|
+
);
|
|
749
2133
|
}
|
|
750
2134
|
|
|
751
|
-
/**
|
|
752
|
-
|
|
753
|
-
|
|
2135
|
+
/**
|
|
2136
|
+
* Read up to `maxPoints` of a strip's most recent goniometer samples
|
|
2137
|
+
* (oldest to newest).
|
|
2138
|
+
*/
|
|
2139
|
+
readGoniometerLatest(stripIndex: number, maxPoints: number): GoniometerPoint[] {
|
|
2140
|
+
return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/** Serialize the current scene (strips, buses, sends, connections) to JSON. */
|
|
2144
|
+
toSceneJson(): string {
|
|
2145
|
+
return this.mixer.toSceneJson();
|
|
754
2146
|
}
|
|
755
2147
|
|
|
756
2148
|
/** Release the underlying WASM object. Safe to call only once. */
|
|
757
2149
|
delete(): void {
|
|
758
|
-
this.
|
|
2150
|
+
this.mixer.delete();
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
|
|
2154
|
+
destroy(): void {
|
|
2155
|
+
this.delete();
|
|
759
2156
|
}
|
|
760
2157
|
}
|
|
761
2158
|
|
|
@@ -872,6 +2269,143 @@ export function mfcc(
|
|
|
872
2269
|
return module.mfcc(samples, sampleRate, nFft, hopLength, nMels, nMfcc);
|
|
873
2270
|
}
|
|
874
2271
|
|
|
2272
|
+
// ============================================================================
|
|
2273
|
+
// Features - Inverse reconstruction
|
|
2274
|
+
// ============================================================================
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Approximate inverse of a Mel filterbank: Mel power spectrogram -> STFT power
|
|
2278
|
+
* spectrogram. Mirrors `feature::mel_to_stft`.
|
|
2279
|
+
*
|
|
2280
|
+
* @param melPower - Mel power spectrogram [nMels x nFrames] row-major
|
|
2281
|
+
* @param nMels - Number of Mel bands
|
|
2282
|
+
* @param nFrames - Number of time frames
|
|
2283
|
+
* @param sampleRate - Sample rate in Hz
|
|
2284
|
+
* @param nFft - FFT size (default: 2048)
|
|
2285
|
+
* @param hopLength - Hop length (default: 512)
|
|
2286
|
+
* @returns STFT power spectrogram result
|
|
2287
|
+
*/
|
|
2288
|
+
export function melToStft(
|
|
2289
|
+
melPower: Float32Array,
|
|
2290
|
+
nMels: number,
|
|
2291
|
+
nFrames: number,
|
|
2292
|
+
sampleRate: number,
|
|
2293
|
+
nFft = 2048,
|
|
2294
|
+
hopLength = 512,
|
|
2295
|
+
fmin = 0,
|
|
2296
|
+
fmax = 0,
|
|
2297
|
+
): StftPowerResult {
|
|
2298
|
+
if (!module) {
|
|
2299
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
2300
|
+
}
|
|
2301
|
+
return module.melToStft(melPower, nMels, nFrames, sampleRate, nFft, hopLength, fmin, fmax);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
/**
|
|
2305
|
+
* Reconstruct audio from a Mel power spectrogram via Griffin-Lim. Mirrors
|
|
2306
|
+
* `feature::mel_to_audio`.
|
|
2307
|
+
*
|
|
2308
|
+
* @param melPower - Mel power spectrogram [nMels x nFrames] row-major
|
|
2309
|
+
* @param nMels - Number of Mel bands
|
|
2310
|
+
* @param nFrames - Number of time frames
|
|
2311
|
+
* @param sampleRate - Sample rate in Hz
|
|
2312
|
+
* @param nFft - FFT size (default: 2048)
|
|
2313
|
+
* @param hopLength - Hop length (default: 512)
|
|
2314
|
+
* @param nIter - Griffin-Lim iterations (default: 32)
|
|
2315
|
+
* @returns Reconstructed audio samples (mono, float32)
|
|
2316
|
+
*/
|
|
2317
|
+
export function melToAudio(
|
|
2318
|
+
melPower: Float32Array,
|
|
2319
|
+
nMels: number,
|
|
2320
|
+
nFrames: number,
|
|
2321
|
+
sampleRate: number,
|
|
2322
|
+
nFft = 2048,
|
|
2323
|
+
hopLength = 512,
|
|
2324
|
+
nIter = 32,
|
|
2325
|
+
fmin = 0,
|
|
2326
|
+
fmax = 0,
|
|
2327
|
+
): Float32Array {
|
|
2328
|
+
if (!module) {
|
|
2329
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
2330
|
+
}
|
|
2331
|
+
return module.melToAudio(
|
|
2332
|
+
melPower,
|
|
2333
|
+
nMels,
|
|
2334
|
+
nFrames,
|
|
2335
|
+
sampleRate,
|
|
2336
|
+
nFft,
|
|
2337
|
+
hopLength,
|
|
2338
|
+
nIter,
|
|
2339
|
+
fmin,
|
|
2340
|
+
fmax,
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
/**
|
|
2345
|
+
* Invert MFCC coefficients back to a Mel power spectrogram. Mirrors
|
|
2346
|
+
* `feature::mfcc_to_mel`.
|
|
2347
|
+
*
|
|
2348
|
+
* @param mfccCoefficients - MFCC matrix [nMfcc x nFrames] row-major
|
|
2349
|
+
* @param nMfcc - Number of MFCC coefficients
|
|
2350
|
+
* @param nFrames - Number of time frames
|
|
2351
|
+
* @param nMels - Number of Mel bins to reconstruct (default: 128)
|
|
2352
|
+
* @returns Mel power spectrogram result
|
|
2353
|
+
*/
|
|
2354
|
+
export function mfccToMel(
|
|
2355
|
+
mfccCoefficients: Float32Array,
|
|
2356
|
+
nMfcc: number,
|
|
2357
|
+
nFrames: number,
|
|
2358
|
+
nMels = 128,
|
|
2359
|
+
): MelPowerResult {
|
|
2360
|
+
if (!module) {
|
|
2361
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
2362
|
+
}
|
|
2363
|
+
return module.mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
/**
|
|
2367
|
+
* Reconstruct audio directly from MFCC coefficients via Griffin-Lim. Mirrors
|
|
2368
|
+
* `feature::mfcc_to_audio`.
|
|
2369
|
+
*
|
|
2370
|
+
* @param mfccCoefficients - MFCC matrix [nMfcc x nFrames] row-major
|
|
2371
|
+
* @param nMfcc - Number of MFCC coefficients
|
|
2372
|
+
* @param nFrames - Number of time frames
|
|
2373
|
+
* @param nMels - Number of Mel bins (default: 128)
|
|
2374
|
+
* @param sampleRate - Sample rate in Hz
|
|
2375
|
+
* @param nFft - FFT size (default: 2048)
|
|
2376
|
+
* @param hopLength - Hop length (default: 512)
|
|
2377
|
+
* @param nIter - Griffin-Lim iterations (default: 32)
|
|
2378
|
+
* @returns Reconstructed audio samples (mono, float32)
|
|
2379
|
+
*/
|
|
2380
|
+
export function mfccToAudio(
|
|
2381
|
+
mfccCoefficients: Float32Array,
|
|
2382
|
+
nMfcc: number,
|
|
2383
|
+
nFrames: number,
|
|
2384
|
+
nMels: number,
|
|
2385
|
+
sampleRate: number,
|
|
2386
|
+
nFft = 2048,
|
|
2387
|
+
hopLength = 512,
|
|
2388
|
+
nIter = 32,
|
|
2389
|
+
fmin = 0,
|
|
2390
|
+
fmax = 0,
|
|
2391
|
+
): Float32Array {
|
|
2392
|
+
if (!module) {
|
|
2393
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
2394
|
+
}
|
|
2395
|
+
return module.mfccToAudio(
|
|
2396
|
+
mfccCoefficients,
|
|
2397
|
+
nMfcc,
|
|
2398
|
+
nFrames,
|
|
2399
|
+
nMels,
|
|
2400
|
+
sampleRate,
|
|
2401
|
+
nFft,
|
|
2402
|
+
hopLength,
|
|
2403
|
+
nIter,
|
|
2404
|
+
fmin,
|
|
2405
|
+
fmax,
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
875
2409
|
// ============================================================================
|
|
876
2410
|
// Features - Chroma
|
|
877
2411
|
// ============================================================================
|
|
@@ -1371,11 +2905,26 @@ export function tempogram(
|
|
|
1371
2905
|
sampleRate: number,
|
|
1372
2906
|
hopLength = 512,
|
|
1373
2907
|
winLength = 384,
|
|
2908
|
+
mode: TempogramMode = 'autocorrelation',
|
|
1374
2909
|
): WasmTempogramResult {
|
|
1375
2910
|
if (!module) {
|
|
1376
2911
|
throw new Error('Module not initialized. Call init() first.');
|
|
1377
2912
|
}
|
|
1378
|
-
return module.tempogram(onsetEnvelope, sampleRate, hopLength, winLength);
|
|
2913
|
+
return module.tempogram(onsetEnvelope, sampleRate, hopLength, winLength, mode);
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
export function cyclicTempogram(
|
|
2917
|
+
onsetEnvelope: Float32Array,
|
|
2918
|
+
sampleRate: number,
|
|
2919
|
+
hopLength = 512,
|
|
2920
|
+
winLength = 384,
|
|
2921
|
+
bpmMin = 60.0,
|
|
2922
|
+
nBins = 60,
|
|
2923
|
+
): WasmCyclicTempogramResult {
|
|
2924
|
+
if (!module) {
|
|
2925
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
2926
|
+
}
|
|
2927
|
+
return module.cyclicTempogram(onsetEnvelope, sampleRate, hopLength, winLength, bpmMin, nBins);
|
|
1379
2928
|
}
|
|
1380
2929
|
|
|
1381
2930
|
export function plp(
|
|
@@ -1392,6 +2941,231 @@ export function plp(
|
|
|
1392
2941
|
return module.plp(onsetEnvelope, sampleRate, hopLength, tempoMin, tempoMax, winLength);
|
|
1393
2942
|
}
|
|
1394
2943
|
|
|
2944
|
+
/**
|
|
2945
|
+
* Compute NNLS (non-negative least squares) chromagram.
|
|
2946
|
+
*
|
|
2947
|
+
* @param samples - Audio samples (mono, float32)
|
|
2948
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
2949
|
+
* @returns NNLS chroma result
|
|
2950
|
+
*/
|
|
2951
|
+
export function nnlsChroma(samples: Float32Array, sampleRate = 22050): WasmNnlsChromaResult {
|
|
2952
|
+
if (!module) {
|
|
2953
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
2954
|
+
}
|
|
2955
|
+
return module.nnlsChroma(samples, sampleRate);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
/**
|
|
2959
|
+
* Compute the Constant-Q Transform magnitude.
|
|
2960
|
+
*
|
|
2961
|
+
* @param samples - Audio samples (mono, float32)
|
|
2962
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
2963
|
+
* @param hopLength - Hop length (default: 512)
|
|
2964
|
+
* @param fmin - Minimum frequency in Hz (default: 32.70319566257483, C1)
|
|
2965
|
+
* @param nBins - Number of frequency bins (default: 84)
|
|
2966
|
+
* @param binsPerOctave - Bins per octave (default: 12)
|
|
2967
|
+
* @returns CQT magnitude result
|
|
2968
|
+
*/
|
|
2969
|
+
export function cqt(
|
|
2970
|
+
samples: Float32Array,
|
|
2971
|
+
sampleRate = 22050,
|
|
2972
|
+
hopLength = 512,
|
|
2973
|
+
fmin = 32.70319566257483,
|
|
2974
|
+
nBins = 84,
|
|
2975
|
+
binsPerOctave = 12,
|
|
2976
|
+
): CqtResult {
|
|
2977
|
+
if (!module) {
|
|
2978
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
2979
|
+
}
|
|
2980
|
+
return module.cqt(samples, sampleRate, hopLength, fmin, nBins, binsPerOctave);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
/**
|
|
2984
|
+
* Compute the Variable-Q Transform magnitude (gamma controls Q).
|
|
2985
|
+
*
|
|
2986
|
+
* @param samples - Audio samples (mono, float32)
|
|
2987
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
2988
|
+
* @param hopLength - Hop length (default: 512)
|
|
2989
|
+
* @param fmin - Minimum frequency in Hz (default: 32.70319566257483, C1)
|
|
2990
|
+
* @param nBins - Number of frequency bins (default: 84)
|
|
2991
|
+
* @param binsPerOctave - Bins per octave (default: 12)
|
|
2992
|
+
* @param gamma - Bandwidth offset; 0 is equivalent to CQT (default: 0)
|
|
2993
|
+
* @returns VQT magnitude result (same shape as CQT)
|
|
2994
|
+
*/
|
|
2995
|
+
export function vqt(
|
|
2996
|
+
samples: Float32Array,
|
|
2997
|
+
sampleRate = 22050,
|
|
2998
|
+
hopLength = 512,
|
|
2999
|
+
fmin = 32.70319566257483,
|
|
3000
|
+
nBins = 84,
|
|
3001
|
+
binsPerOctave = 12,
|
|
3002
|
+
gamma = 0,
|
|
3003
|
+
): CqtResult {
|
|
3004
|
+
if (!module) {
|
|
3005
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3006
|
+
}
|
|
3007
|
+
return module.vqt(samples, sampleRate, hopLength, fmin, nBins, binsPerOctave, gamma);
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
/**
|
|
3011
|
+
* Detect song-structure sections (intro/verse/chorus/...).
|
|
3012
|
+
*
|
|
3013
|
+
* @param samples - Audio samples (mono, float32)
|
|
3014
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
3015
|
+
* @param nFft - FFT size (default: 2048)
|
|
3016
|
+
* @param hopLength - Hop length (default: 512)
|
|
3017
|
+
* @param minSectionSec - Minimum section duration in seconds (default: 8.0)
|
|
3018
|
+
* @returns Array of detected sections
|
|
3019
|
+
*/
|
|
3020
|
+
export function analyzeSections(
|
|
3021
|
+
samples: Float32Array,
|
|
3022
|
+
sampleRate = 22050,
|
|
3023
|
+
nFft = 2048,
|
|
3024
|
+
hopLength = 512,
|
|
3025
|
+
minSectionSec = 8.0,
|
|
3026
|
+
): Section[] {
|
|
3027
|
+
if (!module) {
|
|
3028
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3029
|
+
}
|
|
3030
|
+
return module
|
|
3031
|
+
.analyzeSections(samples, sampleRate, nFft, hopLength, minSectionSec)
|
|
3032
|
+
.map((s) => ({ ...s, type: s.type as SectionType }));
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
/**
|
|
3036
|
+
* Extract the melody contour from monophonic audio via YIN.
|
|
3037
|
+
*
|
|
3038
|
+
* @param samples - Audio samples (mono, float32)
|
|
3039
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
3040
|
+
* @param fmin - Minimum frequency in Hz (default: 65.0)
|
|
3041
|
+
* @param fmax - Maximum frequency in Hz (default: 2093.0)
|
|
3042
|
+
* @param frameLength - Frame length in samples (default: 2048)
|
|
3043
|
+
* @param hopLength - Hop length (default: 512)
|
|
3044
|
+
* @param threshold - YIN threshold; lower is stricter (default: 0.1)
|
|
3045
|
+
* @returns Melody contour with per-frame pitch points and summary stats
|
|
3046
|
+
*/
|
|
3047
|
+
export function analyzeMelody(
|
|
3048
|
+
samples: Float32Array,
|
|
3049
|
+
sampleRate = 22050,
|
|
3050
|
+
fmin = 65.0,
|
|
3051
|
+
fmax = 2093.0,
|
|
3052
|
+
frameLength = 2048,
|
|
3053
|
+
hopLength = 512,
|
|
3054
|
+
threshold = 0.1,
|
|
3055
|
+
): MelodyResult {
|
|
3056
|
+
if (!module) {
|
|
3057
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3058
|
+
}
|
|
3059
|
+
return module.analyzeMelody(samples, sampleRate, fmin, fmax, frameLength, hopLength, threshold);
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
/**
|
|
3063
|
+
* Compute the onset strength envelope.
|
|
3064
|
+
*
|
|
3065
|
+
* @param samples - Audio samples (mono, float32)
|
|
3066
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
3067
|
+
* @param nFft - FFT size (default: 2048)
|
|
3068
|
+
* @param hopLength - Hop length (default: 512)
|
|
3069
|
+
* @param nMels - Number of Mel bands (default: 128)
|
|
3070
|
+
* @returns Onset envelope for each frame
|
|
3071
|
+
*/
|
|
3072
|
+
export function onsetEnvelope(
|
|
3073
|
+
samples: Float32Array,
|
|
3074
|
+
sampleRate = 22050,
|
|
3075
|
+
nFft = 2048,
|
|
3076
|
+
hopLength = 512,
|
|
3077
|
+
nMels = 128,
|
|
3078
|
+
): Float32Array {
|
|
3079
|
+
if (!module) {
|
|
3080
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3081
|
+
}
|
|
3082
|
+
return module.onsetEnvelope(samples, sampleRate, nFft, hopLength, nMels);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
/**
|
|
3086
|
+
* Compute the Fourier tempogram from an onset envelope.
|
|
3087
|
+
*
|
|
3088
|
+
* @param onsetEnvelope - Onset strength envelope (float32)
|
|
3089
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
3090
|
+
* @param hopLength - Hop length (default: 512)
|
|
3091
|
+
* @param winLength - Window length in frames (default: 384)
|
|
3092
|
+
* @returns Fourier tempogram result
|
|
3093
|
+
*/
|
|
3094
|
+
export function fourierTempogram(
|
|
3095
|
+
onsetEnvelope: Float32Array,
|
|
3096
|
+
sampleRate = 22050,
|
|
3097
|
+
hopLength = 512,
|
|
3098
|
+
winLength = 384,
|
|
3099
|
+
): WasmFourierTempogramResult {
|
|
3100
|
+
if (!module) {
|
|
3101
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3102
|
+
}
|
|
3103
|
+
return module.fourierTempogram(onsetEnvelope, sampleRate, hopLength, winLength);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
/**
|
|
3107
|
+
* Compute tempogram ratio features.
|
|
3108
|
+
*
|
|
3109
|
+
* @param tempogramData - Tempogram data (float32)
|
|
3110
|
+
* @param winLength - Window length in frames (default: 384)
|
|
3111
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
3112
|
+
* @param hopLength - Hop length (default: 512)
|
|
3113
|
+
* @returns Tempogram ratio features
|
|
3114
|
+
*/
|
|
3115
|
+
export function tempogramRatio(
|
|
3116
|
+
tempogramData: Float32Array,
|
|
3117
|
+
winLength = 384,
|
|
3118
|
+
sampleRate = 22050,
|
|
3119
|
+
hopLength = 512,
|
|
3120
|
+
): Float32Array {
|
|
3121
|
+
if (!module) {
|
|
3122
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3123
|
+
}
|
|
3124
|
+
return module.tempogramRatio(tempogramData, winLength, sampleRate, hopLength);
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
/**
|
|
3128
|
+
* Measure loudness (EBU R128 / ITU-R BS.1770).
|
|
3129
|
+
*
|
|
3130
|
+
* @param samples - Audio samples (mono, float32)
|
|
3131
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
3132
|
+
* @returns Loudness measurement result
|
|
3133
|
+
*/
|
|
3134
|
+
export function lufs(samples: Float32Array, sampleRate = 22050): LufsResult {
|
|
3135
|
+
if (!module) {
|
|
3136
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3137
|
+
}
|
|
3138
|
+
return module.lufs(samples, sampleRate);
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
/**
|
|
3142
|
+
* Compute the momentary loudness (LUFS) over time.
|
|
3143
|
+
*
|
|
3144
|
+
* @param samples - Audio samples (mono, float32)
|
|
3145
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
3146
|
+
* @returns Momentary LUFS values over time
|
|
3147
|
+
*/
|
|
3148
|
+
export function momentaryLufs(samples: Float32Array, sampleRate = 22050): Float32Array {
|
|
3149
|
+
if (!module) {
|
|
3150
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3151
|
+
}
|
|
3152
|
+
return module.momentaryLufs(samples, sampleRate);
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
/**
|
|
3156
|
+
* Compute the short-term loudness (LUFS) over time.
|
|
3157
|
+
*
|
|
3158
|
+
* @param samples - Audio samples (mono, float32)
|
|
3159
|
+
* @param sampleRate - Sample rate in Hz (default: 22050)
|
|
3160
|
+
* @returns Short-term LUFS values over time
|
|
3161
|
+
*/
|
|
3162
|
+
export function shortTermLufs(samples: Float32Array, sampleRate = 22050): Float32Array {
|
|
3163
|
+
if (!module) {
|
|
3164
|
+
throw new Error('Module not initialized. Call init() first.');
|
|
3165
|
+
}
|
|
3166
|
+
return module.shortTermLufs(samples, sampleRate);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
1395
3169
|
// ============================================================================
|
|
1396
3170
|
// Core - Resample
|
|
1397
3171
|
// ============================================================================
|
|
@@ -1471,8 +3245,12 @@ export class Audio {
|
|
|
1471
3245
|
return detectBpm(this._samples, this._sampleRate);
|
|
1472
3246
|
}
|
|
1473
3247
|
|
|
1474
|
-
detectKey(): Key {
|
|
1475
|
-
return detectKey(this._samples, this._sampleRate);
|
|
3248
|
+
detectKey(options: KeyDetectionOptions = {}): Key {
|
|
3249
|
+
return detectKey(this._samples, this._sampleRate, options);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
detectKeyCandidates(options: KeyDetectionOptions = {}): KeyCandidate[] {
|
|
3253
|
+
return detectKeyCandidates(this._samples, this._sampleRate, options);
|
|
1476
3254
|
}
|
|
1477
3255
|
|
|
1478
3256
|
detectOnsets(): Float32Array {
|
|
@@ -1483,6 +3261,14 @@ export class Audio {
|
|
|
1483
3261
|
return detectBeats(this._samples, this._sampleRate);
|
|
1484
3262
|
}
|
|
1485
3263
|
|
|
3264
|
+
detectDownbeats(): Float32Array {
|
|
3265
|
+
return detectDownbeats(this._samples, this._sampleRate);
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
detectChords(options: ChordDetectionOptions = {}): ChordAnalysisResult {
|
|
3269
|
+
return detectChords(this._samples, this._sampleRate, options);
|
|
3270
|
+
}
|
|
3271
|
+
|
|
1486
3272
|
analyze(): AnalysisResult {
|
|
1487
3273
|
return analyze(this._samples, this._sampleRate);
|
|
1488
3274
|
}
|
|
@@ -1513,6 +3299,18 @@ export class Audio {
|
|
|
1513
3299
|
return pitchShift(this._samples, this._sampleRate, semitones);
|
|
1514
3300
|
}
|
|
1515
3301
|
|
|
3302
|
+
pitchCorrectToMidi(currentMidi: number, targetMidi: number): Float32Array {
|
|
3303
|
+
return pitchCorrectToMidi(this._samples, this._sampleRate, currentMidi, targetMidi);
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
noteStretch(onsetSample: number, offsetSample: number, stretchRatio: number): Float32Array {
|
|
3307
|
+
return noteStretch(this._samples, this._sampleRate, onsetSample, offsetSample, stretchRatio);
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
voiceChange(pitchSemitones: number, formantFactor: number): Float32Array {
|
|
3311
|
+
return voiceChange(this._samples, this._sampleRate, pitchSemitones, formantFactor);
|
|
3312
|
+
}
|
|
3313
|
+
|
|
1516
3314
|
normalize(targetDb = 0.0): Float32Array {
|
|
1517
3315
|
return normalize(this._samples, this._sampleRate, targetDb);
|
|
1518
3316
|
}
|
|
@@ -1526,13 +3324,16 @@ export class Audio {
|
|
|
1526
3324
|
}
|
|
1527
3325
|
|
|
1528
3326
|
masterAudio(
|
|
1529
|
-
presetName:
|
|
3327
|
+
presetName: MasteringPreset,
|
|
1530
3328
|
overrides: Record<string, number | boolean> | null = null,
|
|
1531
3329
|
): MasteringChainResult {
|
|
1532
3330
|
return masterAudio(this._samples, this._sampleRate, presetName, overrides);
|
|
1533
3331
|
}
|
|
1534
3332
|
|
|
1535
|
-
masteringProcess(
|
|
3333
|
+
masteringProcess(
|
|
3334
|
+
processorName: SoloProcessor,
|
|
3335
|
+
params: MasteringProcessorParams = {},
|
|
3336
|
+
): MasteringResult {
|
|
1536
3337
|
return masteringProcess(processorName, this._samples, this._sampleRate, params);
|
|
1537
3338
|
}
|
|
1538
3339
|
|
|
@@ -1562,6 +3363,26 @@ export class Audio {
|
|
|
1562
3363
|
return chroma(this._samples, this._sampleRate, nFft, hopLength);
|
|
1563
3364
|
}
|
|
1564
3365
|
|
|
3366
|
+
nnlsChroma(): WasmNnlsChromaResult {
|
|
3367
|
+
return nnlsChroma(this._samples, this._sampleRate);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
onsetEnvelope(nFft = 2048, hopLength = 512, nMels = 128): Float32Array {
|
|
3371
|
+
return onsetEnvelope(this._samples, this._sampleRate, nFft, hopLength, nMels);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
lufs(): LufsResult {
|
|
3375
|
+
return lufs(this._samples, this._sampleRate);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
momentaryLufs(): Float32Array {
|
|
3379
|
+
return momentaryLufs(this._samples, this._sampleRate);
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
shortTermLufs(): Float32Array {
|
|
3383
|
+
return shortTermLufs(this._samples, this._sampleRate);
|
|
3384
|
+
}
|
|
3385
|
+
|
|
1565
3386
|
spectralCentroid(nFft = 2048, hopLength = 512): Float32Array {
|
|
1566
3387
|
return spectralCentroid(this._samples, this._sampleRate, nFft, hopLength);
|
|
1567
3388
|
}
|
|
@@ -1656,16 +3477,83 @@ export class StreamAnalyzer {
|
|
|
1656
3477
|
if (!module) {
|
|
1657
3478
|
throw new Error('Module not initialized. Call init() first.');
|
|
1658
3479
|
}
|
|
1659
|
-
|
|
3480
|
+
const wasmModule = module;
|
|
3481
|
+
const args = [
|
|
1660
3482
|
config.sampleRate,
|
|
1661
3483
|
config.nFft ?? 2048,
|
|
1662
3484
|
config.hopLength ?? 512,
|
|
1663
3485
|
config.nMels ?? 128,
|
|
3486
|
+
config.fmin ?? 0,
|
|
3487
|
+
config.fmax ?? 0,
|
|
3488
|
+
config.tuningRefHz ?? 440,
|
|
3489
|
+
config.computeMagnitude ?? true,
|
|
1664
3490
|
config.computeMel ?? true,
|
|
1665
3491
|
config.computeChroma ?? true,
|
|
1666
3492
|
config.computeOnset ?? true,
|
|
3493
|
+
config.computeSpectral ?? true,
|
|
1667
3494
|
config.emitEveryNFrames ?? 1,
|
|
1668
|
-
|
|
3495
|
+
config.magnitudeDownsample ?? 1,
|
|
3496
|
+
config.keyUpdateIntervalSec ?? 5,
|
|
3497
|
+
config.bpmUpdateIntervalSec ?? 10,
|
|
3498
|
+
config.window ?? 0,
|
|
3499
|
+
config.outputFormat ?? 0,
|
|
3500
|
+
] as const;
|
|
3501
|
+
const isArityError = (error: unknown): boolean => {
|
|
3502
|
+
const message = String((error as { message?: unknown } | null)?.message ?? error);
|
|
3503
|
+
return message.includes('invalid number of parameters');
|
|
3504
|
+
};
|
|
3505
|
+
const createLegacy = (): WasmStreamAnalyzer => {
|
|
3506
|
+
const LegacyStreamAnalyzer = wasmModule.StreamAnalyzer as unknown as new (
|
|
3507
|
+
sampleRate: number,
|
|
3508
|
+
nFft: number,
|
|
3509
|
+
hopLength: number,
|
|
3510
|
+
nMels: number,
|
|
3511
|
+
computeMel: boolean,
|
|
3512
|
+
computeChroma: boolean,
|
|
3513
|
+
computeOnset: boolean,
|
|
3514
|
+
emitEveryNFrames: number,
|
|
3515
|
+
) => WasmStreamAnalyzer;
|
|
3516
|
+
return new LegacyStreamAnalyzer(
|
|
3517
|
+
args[0],
|
|
3518
|
+
args[1],
|
|
3519
|
+
args[2],
|
|
3520
|
+
args[3],
|
|
3521
|
+
args[8],
|
|
3522
|
+
args[9],
|
|
3523
|
+
args[10],
|
|
3524
|
+
args[12],
|
|
3525
|
+
);
|
|
3526
|
+
};
|
|
3527
|
+
const hasExtendedConfig =
|
|
3528
|
+
config.fmin !== undefined ||
|
|
3529
|
+
config.fmax !== undefined ||
|
|
3530
|
+
config.tuningRefHz !== undefined ||
|
|
3531
|
+
config.computeMagnitude !== undefined ||
|
|
3532
|
+
config.computeSpectral !== undefined ||
|
|
3533
|
+
config.magnitudeDownsample !== undefined ||
|
|
3534
|
+
config.keyUpdateIntervalSec !== undefined ||
|
|
3535
|
+
config.bpmUpdateIntervalSec !== undefined ||
|
|
3536
|
+
config.window !== undefined ||
|
|
3537
|
+
config.outputFormat !== undefined;
|
|
3538
|
+
if (hasExtendedConfig) {
|
|
3539
|
+
try {
|
|
3540
|
+
this.analyzer = new wasmModule.StreamAnalyzer(...args);
|
|
3541
|
+
} catch (error) {
|
|
3542
|
+
if (!isArityError(error)) {
|
|
3543
|
+
throw error;
|
|
3544
|
+
}
|
|
3545
|
+
this.analyzer = createLegacy();
|
|
3546
|
+
}
|
|
3547
|
+
} else {
|
|
3548
|
+
try {
|
|
3549
|
+
this.analyzer = createLegacy();
|
|
3550
|
+
} catch (error) {
|
|
3551
|
+
if (!isArityError(error)) {
|
|
3552
|
+
throw error;
|
|
3553
|
+
}
|
|
3554
|
+
this.analyzer = new wasmModule.StreamAnalyzer(...args);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
1669
3557
|
}
|
|
1670
3558
|
|
|
1671
3559
|
/**
|
|
@@ -1704,6 +3592,14 @@ export class StreamAnalyzer {
|
|
|
1704
3592
|
return this.analyzer.readFramesSoa(maxFrames);
|
|
1705
3593
|
}
|
|
1706
3594
|
|
|
3595
|
+
readFramesU8(maxFrames: number): StreamFramesU8 {
|
|
3596
|
+
return this.analyzer.readFramesU8(maxFrames) as StreamFramesU8;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
readFramesI16(maxFrames: number): StreamFramesI16 {
|
|
3600
|
+
return this.analyzer.readFramesI16(maxFrames) as StreamFramesI16;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
1707
3603
|
/**
|
|
1708
3604
|
* Reset the analyzer state.
|
|
1709
3605
|
*
|
|
@@ -1734,6 +3630,7 @@ export class StreamAnalyzer {
|
|
|
1734
3630
|
chordRoot: s.estimate.chordRoot as PitchClass,
|
|
1735
3631
|
chordQuality: s.estimate.chordQuality as ChordQuality,
|
|
1736
3632
|
chordConfidence: s.estimate.chordConfidence,
|
|
3633
|
+
chordStartTime: s.estimate.chordStartTime,
|
|
1737
3634
|
chordProgression: s.estimate.chordProgression.map((c) => ({
|
|
1738
3635
|
root: c.root as PitchClass,
|
|
1739
3636
|
quality: c.quality as ChordQuality,
|