@libraz/libsonare 1.1.0 → 1.2.0

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