@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/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
- Mode,
36
- PitchClass,
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 type { AnalyzerStats, FrameBuffer, StreamConfig } from './stream_types';
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(samples: Float32Array, sampleRate: number): Key {
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.detectKey(samples, sampleRate);
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
- * Normalize audio to target peak level.
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 targetDb - Target peak level in dB (default: 0 dB = full scale)
388
- * @returns Normalized audio
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 normalize(samples: Float32Array, sampleRate: number, targetDb = 0.0): Float32Array {
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.normalize(samples, sampleRate, targetDb);
1034
+ return module.pitchCorrectToMidi(samples, sampleRate, currentMidi, targetMidi);
395
1035
  }
396
1036
 
397
1037
  /**
398
- * Apply mastering loudness normalization with a true-peak ceiling.
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 targetLufs - Target integrated LUFS (default: -14)
403
- * @param ceilingDb - True/sample peak ceiling in dBFS (default: -1)
404
- * @param truePeakOversample - Oversampling factor used for peak estimation
405
- * @returns Processed audio and loudness metadata
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 mastering(
1047
+ export function noteStretch(
408
1048
  samples: Float32Array,
409
1049
  sampleRate: number,
410
- targetLufs = -14.0,
411
- ceilingDb = -1.0,
412
- truePeakOversample = 4,
413
- ): MasteringResult {
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.masteringProcessorNames();
1057
+ return module.noteStretch(samples, sampleRate, onsetSample, offsetSample, stretchRatio);
425
1058
  }
426
1059
 
427
- export function masteringPairProcessorNames(): string[] {
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.masteringPairProcessorNames();
1078
+ return module.voiceChange(samples, sampleRate, pitchSemitones, formantFactor);
432
1079
  }
433
1080
 
434
- export function masteringPairAnalysisNames(): string[] {
435
- if (!module) {
436
- throw new Error('Module not initialized. Call init() first.');
437
- }
438
- return module.masteringPairAnalysisNames();
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(): string[] {
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: string,
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: string,
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: string,
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: string,
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: string,
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(): string[] {
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: string,
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: string,
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
- /** Total reported latency in samples across all active processors. */
747
- latencySamples(): number {
748
- return this.chain.latencySamples();
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
- /** Ordered stage names that will run (e.g. `"eq.tilt"`). */
752
- stageNames(): string[] {
753
- return this.chain.stageNames();
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.chain.delete();
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: string,
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(processorName: string, params: MasteringProcessorParams = {}): MasteringResult {
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
- this.analyzer = new module.StreamAnalyzer(
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,