@libraz/libsonare 1.0.3 → 1.1.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 ADDED
@@ -0,0 +1,1838 @@
1
+ /**
2
+ * sonare - Audio Analysis Library
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { init, detectBpm, detectKey, analyze } from '@libraz/sonare';
7
+ *
8
+ * await init();
9
+ *
10
+ * // Detect BPM from audio samples
11
+ * const bpm = detectBpm(samples, sampleRate);
12
+ *
13
+ * // Detect musical key
14
+ * const key = detectKey(samples, sampleRate);
15
+ *
16
+ * // Full analysis
17
+ * const result = analyze(samples, sampleRate);
18
+ * ```
19
+ */
20
+
21
+ import type {
22
+ AnalysisResult,
23
+ ChordQuality,
24
+ ChromaResult,
25
+ HpssResult,
26
+ Key,
27
+ MasteringChainConfig,
28
+ MasteringChainResult,
29
+ MasteringProcessorParams,
30
+ MasteringResult,
31
+ MasteringStereoChainResult,
32
+ MasteringStereoResult,
33
+ MelSpectrogramResult,
34
+ MfccResult,
35
+ Mode,
36
+ PitchClass,
37
+ PitchResult,
38
+ SectionType,
39
+ StftResult,
40
+ } from './public_types';
41
+ import type { AnalyzerStats, FrameBuffer, StreamConfig } from './stream_types';
42
+ import type {
43
+ ProgressCallback,
44
+ SonareModule,
45
+ WasmAnalysisResult,
46
+ WasmFrameResult,
47
+ WasmStreamAnalyzer,
48
+ WasmTempogramResult,
49
+ WasmTrimResult,
50
+ } from './wasm_types';
51
+
52
+ export type {
53
+ AnalysisResult,
54
+ Beat,
55
+ Chord,
56
+ ChromaResult,
57
+ Dynamics,
58
+ HpssResult,
59
+ Key,
60
+ MasteringChainConfig,
61
+ MasteringChainResult,
62
+ MasteringProcessorParams,
63
+ MasteringResult,
64
+ MasteringStereoChainResult,
65
+ MasteringStereoResult,
66
+ MelSpectrogramResult,
67
+ MfccResult,
68
+ PitchResult,
69
+ RhythmFeatures,
70
+ Section,
71
+ StftResult,
72
+ Timbre,
73
+ TimeSignature,
74
+ } from './public_types';
75
+ export {
76
+ ChordQuality,
77
+ Mode,
78
+ PitchClass,
79
+ SectionType,
80
+ } from './public_types';
81
+ export type {
82
+ AnalyzerStats,
83
+ BarChord,
84
+ ChordChange,
85
+ FrameBuffer,
86
+ PatternScore,
87
+ ProgressiveEstimate,
88
+ StreamConfig,
89
+ } from './stream_types';
90
+ export type { ProgressCallback } from './wasm_types';
91
+
92
+ // ============================================================================
93
+ // Module State
94
+ // ============================================================================
95
+
96
+ let module: SonareModule | null = null;
97
+ let initPromise: Promise<void> | null = null;
98
+
99
+ // ============================================================================
100
+ // Initialization
101
+ // ============================================================================
102
+
103
+ /**
104
+ * Initialize the WASM module.
105
+ * Must be called before using any analysis functions.
106
+ *
107
+ * @param options - Optional module configuration
108
+ * @returns Promise that resolves when initialization is complete
109
+ */
110
+ export async function init(options?: {
111
+ locateFile?: (path: string, prefix: string) => string;
112
+ }): Promise<void> {
113
+ if (module) {
114
+ return;
115
+ }
116
+
117
+ if (initPromise) {
118
+ return initPromise;
119
+ }
120
+
121
+ initPromise = (async () => {
122
+ try {
123
+ const createModule = (await import('./sonare.js')).default;
124
+ module = await createModule(options);
125
+ } catch (error) {
126
+ initPromise = null;
127
+ throw error;
128
+ }
129
+ })();
130
+
131
+ return initPromise;
132
+ }
133
+
134
+ /**
135
+ * Check if the module is initialized.
136
+ */
137
+ export function isInitialized(): boolean {
138
+ return module !== null;
139
+ }
140
+
141
+ /**
142
+ * Get the library version.
143
+ */
144
+ export function version(): string {
145
+ if (!module) {
146
+ throw new Error('Module not initialized. Call init() first.');
147
+ }
148
+ return module.version();
149
+ }
150
+
151
+ // ============================================================================
152
+ // Quick API (High-level Analysis)
153
+ // ============================================================================
154
+
155
+ /**
156
+ * Detect BPM from audio samples.
157
+ *
158
+ * @param samples - Audio samples (mono, float32)
159
+ * @param sampleRate - Sample rate in Hz
160
+ * @returns Detected BPM
161
+ */
162
+ export function detectBpm(samples: Float32Array, sampleRate: number): number {
163
+ if (!module) {
164
+ throw new Error('Module not initialized. Call init() first.');
165
+ }
166
+ return module.detectBpm(samples, sampleRate);
167
+ }
168
+
169
+ /**
170
+ * Detect musical key from audio samples.
171
+ *
172
+ * @param samples - Audio samples (mono, float32)
173
+ * @param sampleRate - Sample rate in Hz
174
+ * @returns Detected key
175
+ */
176
+ export function detectKey(samples: Float32Array, sampleRate: number): Key {
177
+ if (!module) {
178
+ throw new Error('Module not initialized. Call init() first.');
179
+ }
180
+ const result = module.detectKey(samples, sampleRate);
181
+ return {
182
+ root: result.root as PitchClass,
183
+ mode: result.mode as Mode,
184
+ confidence: result.confidence,
185
+ name: result.name,
186
+ shortName: result.shortName,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Detect onset times from audio samples.
192
+ *
193
+ * @param samples - Audio samples (mono, float32)
194
+ * @param sampleRate - Sample rate in Hz
195
+ * @returns Array of onset times in seconds
196
+ */
197
+ export function detectOnsets(samples: Float32Array, sampleRate: number): Float32Array {
198
+ if (!module) {
199
+ throw new Error('Module not initialized. Call init() first.');
200
+ }
201
+ return module.detectOnsets(samples, sampleRate);
202
+ }
203
+
204
+ /**
205
+ * Detect beat times from audio samples.
206
+ *
207
+ * @param samples - Audio samples (mono, float32)
208
+ * @param sampleRate - Sample rate in Hz
209
+ * @returns Array of beat times in seconds
210
+ */
211
+ export function detectBeats(samples: Float32Array, sampleRate: number): Float32Array {
212
+ if (!module) {
213
+ throw new Error('Module not initialized. Call init() first.');
214
+ }
215
+ return module.detectBeats(samples, sampleRate);
216
+ }
217
+
218
+ // Helper to convert WASM result to typed result
219
+ function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult {
220
+ const beatTimes = new Float32Array(wasm.beats.length);
221
+ for (let i = 0; i < wasm.beats.length; i++) {
222
+ beatTimes[i] = wasm.beats[i].time;
223
+ }
224
+ return {
225
+ bpm: wasm.bpm,
226
+ bpmConfidence: wasm.bpmConfidence,
227
+ key: {
228
+ root: wasm.key.root as PitchClass,
229
+ mode: wasm.key.mode as Mode,
230
+ confidence: wasm.key.confidence,
231
+ name: wasm.key.name,
232
+ shortName: wasm.key.shortName,
233
+ },
234
+ timeSignature: wasm.timeSignature,
235
+ beatTimes,
236
+ beats: wasm.beats,
237
+ chords: wasm.chords.map((c) => ({
238
+ root: c.root as PitchClass,
239
+ quality: c.quality as ChordQuality,
240
+ start: c.start,
241
+ end: c.end,
242
+ confidence: c.confidence,
243
+ name: c.name,
244
+ })),
245
+ sections: wasm.sections.map((s) => ({
246
+ type: s.type as SectionType,
247
+ start: s.start,
248
+ end: s.end,
249
+ energyLevel: s.energyLevel,
250
+ confidence: s.confidence,
251
+ name: s.name,
252
+ })),
253
+ timbre: wasm.timbre,
254
+ dynamics: wasm.dynamics,
255
+ rhythm: wasm.rhythm,
256
+ form: wasm.form,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Perform complete music analysis.
262
+ *
263
+ * @param samples - Audio samples (mono, float32)
264
+ * @param sampleRate - Sample rate in Hz
265
+ * @returns Complete analysis result
266
+ */
267
+ export function analyze(samples: Float32Array, sampleRate: number): AnalysisResult {
268
+ if (!module) {
269
+ throw new Error('Module not initialized. Call init() first.');
270
+ }
271
+ const result = module.analyze(samples, sampleRate);
272
+ return convertAnalysisResult(result);
273
+ }
274
+
275
+ /**
276
+ * Perform complete music analysis with progress reporting.
277
+ *
278
+ * @param samples - Audio samples (mono, float32)
279
+ * @param sampleRate - Sample rate in Hz
280
+ * @param onProgress - Progress callback (progress: 0-1, stage: string)
281
+ * @returns Complete analysis result
282
+ */
283
+ export function analyzeWithProgress(
284
+ samples: Float32Array,
285
+ sampleRate: number,
286
+ onProgress: ProgressCallback,
287
+ ): AnalysisResult {
288
+ if (!module) {
289
+ throw new Error('Module not initialized. Call init() first.');
290
+ }
291
+ const result = module.analyzeWithProgress(samples, sampleRate, onProgress);
292
+ return convertAnalysisResult(result);
293
+ }
294
+
295
+ // ============================================================================
296
+ // Effects
297
+ // ============================================================================
298
+
299
+ /**
300
+ * Perform Harmonic-Percussive Source Separation (HPSS).
301
+ *
302
+ * @param samples - Audio samples (mono, float32)
303
+ * @param sampleRate - Sample rate in Hz
304
+ * @param kernelHarmonic - Horizontal median filter size for harmonic (default: 31)
305
+ * @param kernelPercussive - Vertical median filter size for percussive (default: 31)
306
+ * @returns Separated harmonic and percussive components
307
+ */
308
+ export function hpss(
309
+ samples: Float32Array,
310
+ sampleRate: number,
311
+ kernelHarmonic = 31,
312
+ kernelPercussive = 31,
313
+ ): HpssResult {
314
+ if (!module) {
315
+ throw new Error('Module not initialized. Call init() first.');
316
+ }
317
+ return module.hpss(samples, sampleRate, kernelHarmonic, kernelPercussive);
318
+ }
319
+
320
+ /**
321
+ * Extract harmonic component from audio.
322
+ *
323
+ * @param samples - Audio samples (mono, float32)
324
+ * @param sampleRate - Sample rate in Hz
325
+ * @returns Harmonic component
326
+ */
327
+ export function harmonic(samples: Float32Array, sampleRate: number): Float32Array {
328
+ if (!module) {
329
+ throw new Error('Module not initialized. Call init() first.');
330
+ }
331
+ return module.harmonic(samples, sampleRate);
332
+ }
333
+
334
+ /**
335
+ * Extract percussive component from audio.
336
+ *
337
+ * @param samples - Audio samples (mono, float32)
338
+ * @param sampleRate - Sample rate in Hz
339
+ * @returns Percussive component
340
+ */
341
+ export function percussive(samples: Float32Array, sampleRate: number): Float32Array {
342
+ if (!module) {
343
+ throw new Error('Module not initialized. Call init() first.');
344
+ }
345
+ return module.percussive(samples, sampleRate);
346
+ }
347
+
348
+ /**
349
+ * Time-stretch audio without changing pitch.
350
+ *
351
+ * @param samples - Audio samples (mono, float32)
352
+ * @param sampleRate - Sample rate in Hz
353
+ * @param rate - Time stretch rate (0.5 = double duration, 2.0 = half duration)
354
+ * @returns Time-stretched audio
355
+ */
356
+ export function timeStretch(samples: Float32Array, sampleRate: number, rate: number): Float32Array {
357
+ if (!module) {
358
+ throw new Error('Module not initialized. Call init() first.');
359
+ }
360
+ return module.timeStretch(samples, sampleRate, rate);
361
+ }
362
+
363
+ /**
364
+ * Pitch-shift audio without changing duration.
365
+ *
366
+ * @param samples - Audio samples (mono, float32)
367
+ * @param sampleRate - Sample rate in Hz
368
+ * @param semitones - Pitch shift in semitones (+12 = one octave up, -12 = one octave down)
369
+ * @returns Pitch-shifted audio
370
+ */
371
+ export function pitchShift(
372
+ samples: Float32Array,
373
+ sampleRate: number,
374
+ semitones: number,
375
+ ): Float32Array {
376
+ if (!module) {
377
+ throw new Error('Module not initialized. Call init() first.');
378
+ }
379
+ return module.pitchShift(samples, sampleRate, semitones);
380
+ }
381
+
382
+ /**
383
+ * Normalize audio to target peak level.
384
+ *
385
+ * @param samples - Audio samples (mono, float32)
386
+ * @param sampleRate - Sample rate in Hz
387
+ * @param targetDb - Target peak level in dB (default: 0 dB = full scale)
388
+ * @returns Normalized audio
389
+ */
390
+ export function normalize(samples: Float32Array, sampleRate: number, targetDb = 0.0): Float32Array {
391
+ if (!module) {
392
+ throw new Error('Module not initialized. Call init() first.');
393
+ }
394
+ return module.normalize(samples, sampleRate, targetDb);
395
+ }
396
+
397
+ /**
398
+ * Apply mastering loudness normalization with a true-peak ceiling.
399
+ *
400
+ * @param samples - Audio samples (mono, float32)
401
+ * @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
406
+ */
407
+ export function mastering(
408
+ samples: Float32Array,
409
+ 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[] {
421
+ if (!module) {
422
+ throw new Error('Module not initialized. Call init() first.');
423
+ }
424
+ return module.masteringProcessorNames();
425
+ }
426
+
427
+ export function masteringPairProcessorNames(): string[] {
428
+ if (!module) {
429
+ throw new Error('Module not initialized. Call init() first.');
430
+ }
431
+ return module.masteringPairProcessorNames();
432
+ }
433
+
434
+ export function masteringPairAnalysisNames(): string[] {
435
+ if (!module) {
436
+ throw new Error('Module not initialized. Call init() first.');
437
+ }
438
+ return module.masteringPairAnalysisNames();
439
+ }
440
+
441
+ export function masteringStereoAnalysisNames(): string[] {
442
+ if (!module) {
443
+ throw new Error('Module not initialized. Call init() first.');
444
+ }
445
+ return module.masteringStereoAnalysisNames();
446
+ }
447
+
448
+ export function masteringProcess(
449
+ processorName: string,
450
+ samples: Float32Array,
451
+ sampleRate: number,
452
+ params: MasteringProcessorParams = {},
453
+ ): MasteringResult {
454
+ if (!module) {
455
+ throw new Error('Module not initialized. Call init() first.');
456
+ }
457
+ return module.masteringProcess(processorName, samples, sampleRate, params);
458
+ }
459
+
460
+ export function masteringProcessStereo(
461
+ processorName: string,
462
+ left: Float32Array,
463
+ right: Float32Array,
464
+ sampleRate: number,
465
+ params: MasteringProcessorParams = {},
466
+ ): MasteringStereoResult {
467
+ if (!module) {
468
+ throw new Error('Module not initialized. Call init() first.');
469
+ }
470
+ if (left.length !== right.length) {
471
+ throw new Error('Stereo channel lengths must match.');
472
+ }
473
+ return module.masteringProcessStereo(processorName, left, right, sampleRate, params);
474
+ }
475
+
476
+ export function masteringPairProcess(
477
+ processorName: string,
478
+ source: Float32Array,
479
+ reference: Float32Array,
480
+ sampleRate: number,
481
+ params: MasteringProcessorParams = {},
482
+ ): MasteringResult {
483
+ if (!module) {
484
+ throw new Error('Module not initialized. Call init() first.');
485
+ }
486
+ return module.masteringPairProcess(processorName, source, reference, sampleRate, params);
487
+ }
488
+
489
+ export function masteringPairAnalyze(
490
+ analysisName: string,
491
+ source: Float32Array,
492
+ reference: Float32Array,
493
+ sampleRate: number,
494
+ params: MasteringProcessorParams = {},
495
+ ): string {
496
+ if (!module) {
497
+ throw new Error('Module not initialized. Call init() first.');
498
+ }
499
+ return module.masteringPairAnalyze(analysisName, source, reference, sampleRate, params);
500
+ }
501
+
502
+ export function masteringStereoAnalyze(
503
+ analysisName: string,
504
+ left: Float32Array,
505
+ right: Float32Array,
506
+ sampleRate: number,
507
+ params: MasteringProcessorParams = {},
508
+ ): string {
509
+ if (!module) {
510
+ throw new Error('Module not initialized. Call init() first.');
511
+ }
512
+ return module.masteringStereoAnalyze(analysisName, left, right, sampleRate, params);
513
+ }
514
+
515
+ /**
516
+ * Apply a configurable mastering chain in WASM.
517
+ *
518
+ * @param samples - Audio samples (mono, float32)
519
+ * @param sampleRate - Sample rate in Hz
520
+ * @param config - Chain stage configuration
521
+ * @returns Processed audio, loudness metadata, and applied stage names
522
+ */
523
+ export function masteringChain(
524
+ samples: Float32Array,
525
+ sampleRate: number,
526
+ config: MasteringChainConfig,
527
+ ): MasteringChainResult {
528
+ if (!module) {
529
+ throw new Error('Module not initialized. Call init() first.');
530
+ }
531
+ return module.masteringChain(samples, sampleRate, config as Record<string, unknown>);
532
+ }
533
+
534
+ /**
535
+ * Apply a configurable stereo mastering chain in WASM.
536
+ *
537
+ * @param left - Left channel samples
538
+ * @param right - Right channel samples
539
+ * @param sampleRate - Sample rate in Hz
540
+ * @param config - Chain stage configuration
541
+ * @returns Processed stereo audio, loudness metadata, and applied stage names
542
+ */
543
+ export function masteringChainStereo(
544
+ left: Float32Array,
545
+ right: Float32Array,
546
+ sampleRate: number,
547
+ config: MasteringChainConfig,
548
+ ): MasteringStereoChainResult {
549
+ if (!module) {
550
+ throw new Error('Module not initialized. Call init() first.');
551
+ }
552
+ if (left.length !== right.length) {
553
+ throw new Error('Stereo channel lengths must match.');
554
+ }
555
+ return module.masteringChainStereo(left, right, sampleRate, config as Record<string, unknown>);
556
+ }
557
+
558
+ /**
559
+ * Apply a configurable mastering chain in WASM with progress reporting.
560
+ *
561
+ * @param samples - Audio samples (mono, float32)
562
+ * @param sampleRate - Sample rate in Hz
563
+ * @param config - Chain stage configuration
564
+ * @param onProgress - Progress callback (progress: 0-1, stage: string)
565
+ * @returns Processed audio, loudness metadata, and applied stage names
566
+ */
567
+ export function masteringChainWithProgress(
568
+ samples: Float32Array,
569
+ sampleRate: number,
570
+ config: MasteringChainConfig,
571
+ onProgress: ProgressCallback,
572
+ ): MasteringChainResult {
573
+ if (!module) {
574
+ throw new Error('Module not initialized. Call init() first.');
575
+ }
576
+ return module.masteringChainWithProgress(
577
+ samples,
578
+ sampleRate,
579
+ config as Record<string, unknown>,
580
+ onProgress,
581
+ );
582
+ }
583
+
584
+ /**
585
+ * Apply a configurable stereo mastering chain in WASM with progress reporting.
586
+ *
587
+ * @param left - Left channel samples
588
+ * @param right - Right channel samples
589
+ * @param sampleRate - Sample rate in Hz
590
+ * @param config - Chain stage configuration
591
+ * @param onProgress - Progress callback (progress: 0-1, stage: string)
592
+ * @returns Processed stereo audio, loudness metadata, and applied stage names
593
+ */
594
+ export function masteringChainStereoWithProgress(
595
+ left: Float32Array,
596
+ right: Float32Array,
597
+ sampleRate: number,
598
+ config: MasteringChainConfig,
599
+ onProgress: ProgressCallback,
600
+ ): MasteringStereoChainResult {
601
+ if (!module) {
602
+ throw new Error('Module not initialized. Call init() first.');
603
+ }
604
+ if (left.length !== right.length) {
605
+ throw new Error('Stereo channel lengths must match.');
606
+ }
607
+ return module.masteringChainStereoWithProgress(
608
+ left,
609
+ right,
610
+ sampleRate,
611
+ config as Record<string, unknown>,
612
+ onProgress,
613
+ );
614
+ }
615
+
616
+ /**
617
+ * List built-in mastering preset identifiers.
618
+ *
619
+ * @returns Preset names in display order (e.g. "pop", "edm", "aiMusic")
620
+ */
621
+ export function masteringPresetNames(): string[] {
622
+ if (!module) {
623
+ throw new Error('Module not initialized. Call init() first.');
624
+ }
625
+ return module.masteringPresetNames();
626
+ }
627
+
628
+ /**
629
+ * Apply a named mastering preset chain to mono audio.
630
+ *
631
+ * @param samples - Audio samples (mono, float32)
632
+ * @param sampleRate - Sample rate in Hz
633
+ * @param presetName - Preset identifier from {@link masteringPresetNames}
634
+ * @param overrides - Optional flat overrides (dot-notation, e.g. `'loudness.targetLufs'`) applied on top of the preset. Pass `null` for preset defaults.
635
+ * @returns Processed audio, loudness metadata, and applied stage names
636
+ */
637
+ export function masterAudio(
638
+ samples: Float32Array,
639
+ sampleRate: number,
640
+ presetName: string,
641
+ overrides: Record<string, number | boolean> | null = null,
642
+ ): MasteringChainResult {
643
+ if (!module) {
644
+ throw new Error('Module not initialized. Call init() first.');
645
+ }
646
+ return module.masterAudio(presetName, samples, sampleRate, overrides);
647
+ }
648
+
649
+ /**
650
+ * Apply a named mastering preset chain to stereo audio.
651
+ *
652
+ * @param left - Left channel samples
653
+ * @param right - Right channel samples
654
+ * @param sampleRate - Sample rate in Hz
655
+ * @param presetName - Preset identifier from {@link masteringPresetNames}
656
+ * @param overrides - Optional flat overrides (dot-notation, e.g. `'loudness.targetLufs'`) applied on top of the preset. Pass `null` for preset defaults.
657
+ * @returns Processed stereo audio, loudness metadata, and applied stage names
658
+ */
659
+ export function masterAudioStereo(
660
+ left: Float32Array,
661
+ right: Float32Array,
662
+ sampleRate: number,
663
+ presetName: string,
664
+ overrides: Record<string, number | boolean> | null = null,
665
+ ): MasteringStereoChainResult {
666
+ if (!module) {
667
+ throw new Error('Module not initialized. Call init() first.');
668
+ }
669
+ if (left.length !== right.length) {
670
+ throw new Error('Stereo channel lengths must match.');
671
+ }
672
+ return module.masterAudioStereo(presetName, left, right, sampleRate, overrides);
673
+ }
674
+
675
+ // ============================================================================
676
+ // StreamingMasteringChain Class
677
+ // ============================================================================
678
+
679
+ /**
680
+ * Block-by-block streaming variant of {@link masteringChain}.
681
+ *
682
+ * Maintains processor state across {@link processMono}/{@link processStereo}
683
+ * calls. Only ProcessorBase-backed stages are supported. Configurations that
684
+ * enable `repair.denoise` or `loudness` throw at construction.
685
+ *
686
+ * Call {@link delete} (or use a `try/finally`) to release the underlying WASM
687
+ * object — the embind handle is not garbage-collected automatically.
688
+ *
689
+ * @example
690
+ * ```typescript
691
+ * const chain = new StreamingMasteringChain({ eq: { tiltDb: 1.0 } });
692
+ * try {
693
+ * chain.prepare(44100, 512, 1);
694
+ * const out = chain.processMono(blockSamples);
695
+ * } finally {
696
+ * chain.delete();
697
+ * }
698
+ * ```
699
+ */
700
+ export class StreamingMasteringChain {
701
+ private chain: import('./wasm_types').WasmStreamingMasteringChain;
702
+
703
+ constructor(config: MasteringChainConfig) {
704
+ if (!module) {
705
+ throw new Error('Module not initialized. Call init() first.');
706
+ }
707
+ this.chain = module.createStreamingMasteringChain(config as Record<string, unknown>);
708
+ }
709
+
710
+ /**
711
+ * Initialize processors for the given sample rate and block layout.
712
+ *
713
+ * @param sampleRate - Sample rate in Hz
714
+ * @param maxBlockSize - Maximum block size per process call
715
+ * @param numChannels - 1 (mono) or 2 (stereo)
716
+ */
717
+ prepare(sampleRate: number, maxBlockSize: number, numChannels: number): void {
718
+ this.chain.prepare(sampleRate, maxBlockSize, numChannels);
719
+ }
720
+
721
+ /**
722
+ * Process one mono block, returning the processed samples (same length).
723
+ */
724
+ processMono(samples: Float32Array): Float32Array {
725
+ return this.chain.processMono(samples);
726
+ }
727
+
728
+ /**
729
+ * Process one stereo block, returning the processed channels.
730
+ */
731
+ processStereo(
732
+ left: Float32Array,
733
+ right: Float32Array,
734
+ ): { left: Float32Array; right: Float32Array } {
735
+ if (left.length !== right.length) {
736
+ throw new Error('Stereo channel lengths must match.');
737
+ }
738
+ return this.chain.processStereo(left, right);
739
+ }
740
+
741
+ /** Reset all processor state without rebuilding. */
742
+ reset(): void {
743
+ this.chain.reset();
744
+ }
745
+
746
+ /** Total reported latency in samples across all active processors. */
747
+ latencySamples(): number {
748
+ return this.chain.latencySamples();
749
+ }
750
+
751
+ /** Ordered stage names that will run (e.g. `"eq.tilt"`). */
752
+ stageNames(): string[] {
753
+ return this.chain.stageNames();
754
+ }
755
+
756
+ /** Release the underlying WASM object. Safe to call only once. */
757
+ delete(): void {
758
+ this.chain.delete();
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Trim silence from beginning and end of audio.
764
+ *
765
+ * @param samples - Audio samples (mono, float32)
766
+ * @param sampleRate - Sample rate in Hz
767
+ * @param thresholdDb - Silence threshold in dB (default: -60 dB)
768
+ * @returns Trimmed audio
769
+ */
770
+ export function trim(samples: Float32Array, sampleRate: number, thresholdDb = -60.0): Float32Array {
771
+ if (!module) {
772
+ throw new Error('Module not initialized. Call init() first.');
773
+ }
774
+ return module.trim(samples, sampleRate, thresholdDb);
775
+ }
776
+
777
+ // ============================================================================
778
+ // Features - Spectrogram
779
+ // ============================================================================
780
+
781
+ /**
782
+ * Compute Short-Time Fourier Transform (STFT).
783
+ *
784
+ * @param samples - Audio samples (mono, float32)
785
+ * @param sampleRate - Sample rate in Hz
786
+ * @param nFft - FFT size (default: 2048)
787
+ * @param hopLength - Hop length (default: 512)
788
+ * @returns STFT result with magnitude and power spectrograms
789
+ */
790
+ export function stft(
791
+ samples: Float32Array,
792
+ sampleRate: number,
793
+ nFft = 2048,
794
+ hopLength = 512,
795
+ ): StftResult {
796
+ if (!module) {
797
+ throw new Error('Module not initialized. Call init() first.');
798
+ }
799
+ return module.stft(samples, sampleRate, nFft, hopLength);
800
+ }
801
+
802
+ /**
803
+ * Compute STFT and return magnitude in decibels.
804
+ *
805
+ * @param samples - Audio samples (mono, float32)
806
+ * @param sampleRate - Sample rate in Hz
807
+ * @param nFft - FFT size (default: 2048)
808
+ * @param hopLength - Hop length (default: 512)
809
+ * @returns STFT result with dB values
810
+ */
811
+ export function stftDb(
812
+ samples: Float32Array,
813
+ sampleRate: number,
814
+ nFft = 2048,
815
+ hopLength = 512,
816
+ ): { nBins: number; nFrames: number; db: Float32Array } {
817
+ if (!module) {
818
+ throw new Error('Module not initialized. Call init() first.');
819
+ }
820
+ return module.stftDb(samples, sampleRate, nFft, hopLength);
821
+ }
822
+
823
+ // ============================================================================
824
+ // Features - Mel Spectrogram
825
+ // ============================================================================
826
+
827
+ /**
828
+ * Compute Mel spectrogram.
829
+ *
830
+ * @param samples - Audio samples (mono, float32)
831
+ * @param sampleRate - Sample rate in Hz
832
+ * @param nFft - FFT size (default: 2048)
833
+ * @param hopLength - Hop length (default: 512)
834
+ * @param nMels - Number of Mel bands (default: 128)
835
+ * @returns Mel spectrogram result
836
+ */
837
+ export function melSpectrogram(
838
+ samples: Float32Array,
839
+ sampleRate: number,
840
+ nFft = 2048,
841
+ hopLength = 512,
842
+ nMels = 128,
843
+ ): MelSpectrogramResult {
844
+ if (!module) {
845
+ throw new Error('Module not initialized. Call init() first.');
846
+ }
847
+ return module.melSpectrogram(samples, sampleRate, nFft, hopLength, nMels);
848
+ }
849
+
850
+ /**
851
+ * Compute MFCC (Mel-Frequency Cepstral Coefficients).
852
+ *
853
+ * @param samples - Audio samples (mono, float32)
854
+ * @param sampleRate - Sample rate in Hz
855
+ * @param nFft - FFT size (default: 2048)
856
+ * @param hopLength - Hop length (default: 512)
857
+ * @param nMels - Number of Mel bands (default: 128)
858
+ * @param nMfcc - Number of MFCC coefficients (default: 13)
859
+ * @returns MFCC result
860
+ */
861
+ export function mfcc(
862
+ samples: Float32Array,
863
+ sampleRate: number,
864
+ nFft = 2048,
865
+ hopLength = 512,
866
+ nMels = 128,
867
+ nMfcc = 13,
868
+ ): MfccResult {
869
+ if (!module) {
870
+ throw new Error('Module not initialized. Call init() first.');
871
+ }
872
+ return module.mfcc(samples, sampleRate, nFft, hopLength, nMels, nMfcc);
873
+ }
874
+
875
+ // ============================================================================
876
+ // Features - Chroma
877
+ // ============================================================================
878
+
879
+ /**
880
+ * Compute chromagram (pitch class distribution).
881
+ *
882
+ * @param samples - Audio samples (mono, float32)
883
+ * @param sampleRate - Sample rate in Hz
884
+ * @param nFft - FFT size (default: 2048)
885
+ * @param hopLength - Hop length (default: 512)
886
+ * @returns Chroma features result
887
+ */
888
+ export function chroma(
889
+ samples: Float32Array,
890
+ sampleRate: number,
891
+ nFft = 2048,
892
+ hopLength = 512,
893
+ ): ChromaResult {
894
+ if (!module) {
895
+ throw new Error('Module not initialized. Call init() first.');
896
+ }
897
+ return module.chroma(samples, sampleRate, nFft, hopLength);
898
+ }
899
+
900
+ // ============================================================================
901
+ // Features - Spectral
902
+ // ============================================================================
903
+
904
+ /**
905
+ * Compute spectral centroid (center of mass of spectrum).
906
+ *
907
+ * @param samples - Audio samples (mono, float32)
908
+ * @param sampleRate - Sample rate in Hz
909
+ * @param nFft - FFT size (default: 2048)
910
+ * @param hopLength - Hop length (default: 512)
911
+ * @returns Spectral centroid in Hz for each frame
912
+ */
913
+ export function spectralCentroid(
914
+ samples: Float32Array,
915
+ sampleRate: number,
916
+ nFft = 2048,
917
+ hopLength = 512,
918
+ ): Float32Array {
919
+ if (!module) {
920
+ throw new Error('Module not initialized. Call init() first.');
921
+ }
922
+ return module.spectralCentroid(samples, sampleRate, nFft, hopLength);
923
+ }
924
+
925
+ /**
926
+ * Compute spectral bandwidth.
927
+ *
928
+ * @param samples - Audio samples (mono, float32)
929
+ * @param sampleRate - Sample rate in Hz
930
+ * @param nFft - FFT size (default: 2048)
931
+ * @param hopLength - Hop length (default: 512)
932
+ * @returns Spectral bandwidth in Hz for each frame
933
+ */
934
+ export function spectralBandwidth(
935
+ samples: Float32Array,
936
+ sampleRate: number,
937
+ nFft = 2048,
938
+ hopLength = 512,
939
+ ): Float32Array {
940
+ if (!module) {
941
+ throw new Error('Module not initialized. Call init() first.');
942
+ }
943
+ return module.spectralBandwidth(samples, sampleRate, nFft, hopLength);
944
+ }
945
+
946
+ /**
947
+ * Compute spectral rolloff frequency.
948
+ *
949
+ * @param samples - Audio samples (mono, float32)
950
+ * @param sampleRate - Sample rate in Hz
951
+ * @param nFft - FFT size (default: 2048)
952
+ * @param hopLength - Hop length (default: 512)
953
+ * @param rollPercent - Percentage threshold (default: 0.85)
954
+ * @returns Rolloff frequency in Hz for each frame
955
+ */
956
+ export function spectralRolloff(
957
+ samples: Float32Array,
958
+ sampleRate: number,
959
+ nFft = 2048,
960
+ hopLength = 512,
961
+ rollPercent = 0.85,
962
+ ): Float32Array {
963
+ if (!module) {
964
+ throw new Error('Module not initialized. Call init() first.');
965
+ }
966
+ return module.spectralRolloff(samples, sampleRate, nFft, hopLength, rollPercent);
967
+ }
968
+
969
+ /**
970
+ * Compute spectral flatness.
971
+ *
972
+ * @param samples - Audio samples (mono, float32)
973
+ * @param sampleRate - Sample rate in Hz
974
+ * @param nFft - FFT size (default: 2048)
975
+ * @param hopLength - Hop length (default: 512)
976
+ * @returns Spectral flatness for each frame (0 = tonal, 1 = noise-like)
977
+ */
978
+ export function spectralFlatness(
979
+ samples: Float32Array,
980
+ sampleRate: number,
981
+ nFft = 2048,
982
+ hopLength = 512,
983
+ ): Float32Array {
984
+ if (!module) {
985
+ throw new Error('Module not initialized. Call init() first.');
986
+ }
987
+ return module.spectralFlatness(samples, sampleRate, nFft, hopLength);
988
+ }
989
+
990
+ /**
991
+ * Compute zero crossing rate.
992
+ *
993
+ * @param samples - Audio samples (mono, float32)
994
+ * @param sampleRate - Sample rate in Hz
995
+ * @param frameLength - Frame length (default: 2048)
996
+ * @param hopLength - Hop length (default: 512)
997
+ * @returns Zero crossing rate for each frame
998
+ */
999
+ export function zeroCrossingRate(
1000
+ samples: Float32Array,
1001
+ sampleRate: number,
1002
+ frameLength = 2048,
1003
+ hopLength = 512,
1004
+ ): Float32Array {
1005
+ if (!module) {
1006
+ throw new Error('Module not initialized. Call init() first.');
1007
+ }
1008
+ return module.zeroCrossingRate(samples, sampleRate, frameLength, hopLength);
1009
+ }
1010
+
1011
+ /**
1012
+ * Compute RMS energy.
1013
+ *
1014
+ * @param samples - Audio samples (mono, float32)
1015
+ * @param sampleRate - Sample rate in Hz
1016
+ * @param frameLength - Frame length (default: 2048)
1017
+ * @param hopLength - Hop length (default: 512)
1018
+ * @returns RMS energy for each frame
1019
+ */
1020
+ export function rmsEnergy(
1021
+ samples: Float32Array,
1022
+ sampleRate: number,
1023
+ frameLength = 2048,
1024
+ hopLength = 512,
1025
+ ): Float32Array {
1026
+ if (!module) {
1027
+ throw new Error('Module not initialized. Call init() first.');
1028
+ }
1029
+ return module.rmsEnergy(samples, sampleRate, frameLength, hopLength);
1030
+ }
1031
+
1032
+ // ============================================================================
1033
+ // Features - Pitch
1034
+ // ============================================================================
1035
+
1036
+ /**
1037
+ * Detect pitch using YIN algorithm.
1038
+ *
1039
+ * @param samples - Audio samples (mono, float32)
1040
+ * @param sampleRate - Sample rate in Hz
1041
+ * @param frameLength - Frame length (default: 2048)
1042
+ * @param hopLength - Hop length (default: 512)
1043
+ * @param fmin - Minimum frequency in Hz (default: 65)
1044
+ * @param fmax - Maximum frequency in Hz (default: 2093)
1045
+ * @param threshold - YIN threshold (default: 0.3)
1046
+ * @returns Pitch detection result
1047
+ */
1048
+ export function pitchYin(
1049
+ samples: Float32Array,
1050
+ sampleRate: number,
1051
+ frameLength = 2048,
1052
+ hopLength = 512,
1053
+ fmin = 65.0,
1054
+ fmax = 2093.0,
1055
+ threshold = 0.3,
1056
+ ): PitchResult {
1057
+ if (!module) {
1058
+ throw new Error('Module not initialized. Call init() first.');
1059
+ }
1060
+ return module.pitchYin(samples, sampleRate, frameLength, hopLength, fmin, fmax, threshold);
1061
+ }
1062
+
1063
+ /**
1064
+ * Detect pitch using pYIN algorithm (probabilistic YIN with HMM smoothing).
1065
+ *
1066
+ * @param samples - Audio samples (mono, float32)
1067
+ * @param sampleRate - Sample rate in Hz
1068
+ * @param frameLength - Frame length (default: 2048)
1069
+ * @param hopLength - Hop length (default: 512)
1070
+ * @param fmin - Minimum frequency in Hz (default: 65)
1071
+ * @param fmax - Maximum frequency in Hz (default: 2093)
1072
+ * @param threshold - YIN threshold (default: 0.3)
1073
+ * @returns Pitch detection result
1074
+ */
1075
+ export function pitchPyin(
1076
+ samples: Float32Array,
1077
+ sampleRate: number,
1078
+ frameLength = 2048,
1079
+ hopLength = 512,
1080
+ fmin = 65.0,
1081
+ fmax = 2093.0,
1082
+ threshold = 0.3,
1083
+ ): PitchResult {
1084
+ if (!module) {
1085
+ throw new Error('Module not initialized. Call init() first.');
1086
+ }
1087
+ return module.pitchPyin(samples, sampleRate, frameLength, hopLength, fmin, fmax, threshold);
1088
+ }
1089
+
1090
+ // ============================================================================
1091
+ // Core - Unit Conversion
1092
+ // ============================================================================
1093
+
1094
+ /**
1095
+ * Convert frequency in Hz to Mel scale.
1096
+ *
1097
+ * @param hz - Frequency in Hz
1098
+ * @returns Mel frequency
1099
+ */
1100
+ export function hzToMel(hz: number): number {
1101
+ if (!module) {
1102
+ throw new Error('Module not initialized. Call init() first.');
1103
+ }
1104
+ return module.hzToMel(hz);
1105
+ }
1106
+
1107
+ /**
1108
+ * Convert Mel scale to frequency in Hz.
1109
+ *
1110
+ * @param mel - Mel frequency
1111
+ * @returns Frequency in Hz
1112
+ */
1113
+ export function melToHz(mel: number): number {
1114
+ if (!module) {
1115
+ throw new Error('Module not initialized. Call init() first.');
1116
+ }
1117
+ return module.melToHz(mel);
1118
+ }
1119
+
1120
+ /**
1121
+ * Convert frequency in Hz to MIDI note number.
1122
+ *
1123
+ * @param hz - Frequency in Hz
1124
+ * @returns MIDI note number (A4 = 440 Hz = 69)
1125
+ */
1126
+ export function hzToMidi(hz: number): number {
1127
+ if (!module) {
1128
+ throw new Error('Module not initialized. Call init() first.');
1129
+ }
1130
+ return module.hzToMidi(hz);
1131
+ }
1132
+
1133
+ /**
1134
+ * Convert MIDI note number to frequency in Hz.
1135
+ *
1136
+ * @param midi - MIDI note number
1137
+ * @returns Frequency in Hz
1138
+ */
1139
+ export function midiToHz(midi: number): number {
1140
+ if (!module) {
1141
+ throw new Error('Module not initialized. Call init() first.');
1142
+ }
1143
+ return module.midiToHz(midi);
1144
+ }
1145
+
1146
+ /**
1147
+ * Convert frequency in Hz to note name.
1148
+ *
1149
+ * @param hz - Frequency in Hz
1150
+ * @returns Note name (e.g., "A4", "C#5")
1151
+ */
1152
+ export function hzToNote(hz: number): string {
1153
+ if (!module) {
1154
+ throw new Error('Module not initialized. Call init() first.');
1155
+ }
1156
+ return module.hzToNote(hz);
1157
+ }
1158
+
1159
+ /**
1160
+ * Convert note name to frequency in Hz.
1161
+ *
1162
+ * @param note - Note name (e.g., "A4", "C#5")
1163
+ * @returns Frequency in Hz
1164
+ */
1165
+ export function noteToHz(note: string): number {
1166
+ if (!module) {
1167
+ throw new Error('Module not initialized. Call init() first.');
1168
+ }
1169
+ return module.noteToHz(note);
1170
+ }
1171
+
1172
+ /**
1173
+ * Convert frame index to time in seconds.
1174
+ *
1175
+ * @param frames - Frame index
1176
+ * @param sr - Sample rate in Hz
1177
+ * @param hopLength - Hop length in samples
1178
+ * @returns Time in seconds
1179
+ */
1180
+ export function framesToTime(frames: number, sr: number, hopLength: number): number {
1181
+ if (!module) {
1182
+ throw new Error('Module not initialized. Call init() first.');
1183
+ }
1184
+ return module.framesToTime(frames, sr, hopLength);
1185
+ }
1186
+
1187
+ /**
1188
+ * Convert time in seconds to frame index.
1189
+ *
1190
+ * @param time - Time in seconds
1191
+ * @param sr - Sample rate in Hz
1192
+ * @param hopLength - Hop length in samples
1193
+ * @returns Frame index
1194
+ */
1195
+ export function timeToFrames(time: number, sr: number, hopLength: number): number {
1196
+ if (!module) {
1197
+ throw new Error('Module not initialized. Call init() first.');
1198
+ }
1199
+ return module.timeToFrames(time, sr, hopLength);
1200
+ }
1201
+
1202
+ export function framesToSamples(frames: number, hopLength = 512, nFft = 0): number {
1203
+ if (!module) {
1204
+ throw new Error('Module not initialized. Call init() first.');
1205
+ }
1206
+ return module.framesToSamples(frames, hopLength, nFft);
1207
+ }
1208
+
1209
+ export function samplesToFrames(samples: number, hopLength = 512, nFft = 0): number {
1210
+ if (!module) {
1211
+ throw new Error('Module not initialized. Call init() first.');
1212
+ }
1213
+ return module.samplesToFrames(samples, hopLength, nFft);
1214
+ }
1215
+
1216
+ export function powerToDb(
1217
+ values: Float32Array,
1218
+ ref = 1.0,
1219
+ amin = 1e-10,
1220
+ topDb = 80.0,
1221
+ ): Float32Array {
1222
+ if (!module) {
1223
+ throw new Error('Module not initialized. Call init() first.');
1224
+ }
1225
+ return module.powerToDb(values, ref, amin, topDb);
1226
+ }
1227
+
1228
+ export function amplitudeToDb(
1229
+ values: Float32Array,
1230
+ ref = 1.0,
1231
+ amin = 1e-5,
1232
+ topDb = 80.0,
1233
+ ): Float32Array {
1234
+ if (!module) {
1235
+ throw new Error('Module not initialized. Call init() first.');
1236
+ }
1237
+ return module.amplitudeToDb(values, ref, amin, topDb);
1238
+ }
1239
+
1240
+ export function dbToPower(values: Float32Array, ref = 1.0): Float32Array {
1241
+ if (!module) {
1242
+ throw new Error('Module not initialized. Call init() first.');
1243
+ }
1244
+ return module.dbToPower(values, ref);
1245
+ }
1246
+
1247
+ export function dbToAmplitude(values: Float32Array, ref = 1.0): Float32Array {
1248
+ if (!module) {
1249
+ throw new Error('Module not initialized. Call init() first.');
1250
+ }
1251
+ return module.dbToAmplitude(values, ref);
1252
+ }
1253
+
1254
+ export function preemphasis(samples: Float32Array, coef = 0.97, zi?: number): Float32Array {
1255
+ if (!module) {
1256
+ throw new Error('Module not initialized. Call init() first.');
1257
+ }
1258
+ return module.preemphasis(samples, coef, zi ?? null);
1259
+ }
1260
+
1261
+ export function deemphasis(samples: Float32Array, coef = 0.97, zi?: number): Float32Array {
1262
+ if (!module) {
1263
+ throw new Error('Module not initialized. Call init() first.');
1264
+ }
1265
+ return module.deemphasis(samples, coef, zi ?? null);
1266
+ }
1267
+
1268
+ export function trimSilence(
1269
+ samples: Float32Array,
1270
+ topDb = 60.0,
1271
+ frameLength = 2048,
1272
+ hopLength = 512,
1273
+ ): WasmTrimResult {
1274
+ if (!module) {
1275
+ throw new Error('Module not initialized. Call init() first.');
1276
+ }
1277
+ return module.trimSilence(samples, topDb, frameLength, hopLength);
1278
+ }
1279
+
1280
+ export function splitSilence(
1281
+ samples: Float32Array,
1282
+ topDb = 60.0,
1283
+ frameLength = 2048,
1284
+ hopLength = 512,
1285
+ ): Int32Array {
1286
+ if (!module) {
1287
+ throw new Error('Module not initialized. Call init() first.');
1288
+ }
1289
+ return module.splitSilence(samples, topDb, frameLength, hopLength);
1290
+ }
1291
+
1292
+ export function frameSignal(
1293
+ samples: Float32Array,
1294
+ frameLength: number,
1295
+ hopLength: number,
1296
+ ): WasmFrameResult {
1297
+ if (!module) {
1298
+ throw new Error('Module not initialized. Call init() first.');
1299
+ }
1300
+ return module.frameSignal(samples, frameLength, hopLength);
1301
+ }
1302
+
1303
+ export function padCenter(values: Float32Array, size: number, padValue = 0.0): Float32Array {
1304
+ if (!module) {
1305
+ throw new Error('Module not initialized. Call init() first.');
1306
+ }
1307
+ return module.padCenter(values, size, padValue);
1308
+ }
1309
+
1310
+ export function fixLength(values: Float32Array, size: number, padValue = 0.0): Float32Array {
1311
+ if (!module) {
1312
+ throw new Error('Module not initialized. Call init() first.');
1313
+ }
1314
+ return module.fixLength(values, size, padValue);
1315
+ }
1316
+
1317
+ export function fixFrames(frames: Int32Array, xMin = 0, xMax = -1, pad = true): Int32Array {
1318
+ if (!module) {
1319
+ throw new Error('Module not initialized. Call init() first.');
1320
+ }
1321
+ return module.fixFrames(frames, xMin, xMax, pad);
1322
+ }
1323
+
1324
+ export function peakPick(
1325
+ values: Float32Array,
1326
+ preMax: number,
1327
+ postMax: number,
1328
+ preAvg: number,
1329
+ postAvg: number,
1330
+ delta: number,
1331
+ wait: number,
1332
+ ): Int32Array {
1333
+ if (!module) {
1334
+ throw new Error('Module not initialized. Call init() first.');
1335
+ }
1336
+ return module.peakPick(values, preMax, postMax, preAvg, postAvg, delta, wait);
1337
+ }
1338
+
1339
+ export function vectorNormalize(
1340
+ values: Float32Array,
1341
+ normType = 0,
1342
+ threshold = 1e-12,
1343
+ ): Float32Array {
1344
+ if (!module) {
1345
+ throw new Error('Module not initialized. Call init() first.');
1346
+ }
1347
+ return module.vectorNormalize(values, normType, threshold);
1348
+ }
1349
+
1350
+ export function pcen(
1351
+ values: Float32Array,
1352
+ nBins: number,
1353
+ nFrames: number,
1354
+ options: Record<string, number> = {},
1355
+ ): Float32Array {
1356
+ if (!module) {
1357
+ throw new Error('Module not initialized. Call init() first.');
1358
+ }
1359
+ return module.pcen(values, nBins, nFrames, options);
1360
+ }
1361
+
1362
+ export function tonnetz(chromagram: Float32Array, nChroma: number, nFrames: number): Float32Array {
1363
+ if (!module) {
1364
+ throw new Error('Module not initialized. Call init() first.');
1365
+ }
1366
+ return module.tonnetz(chromagram, nChroma, nFrames);
1367
+ }
1368
+
1369
+ export function tempogram(
1370
+ onsetEnvelope: Float32Array,
1371
+ sampleRate: number,
1372
+ hopLength = 512,
1373
+ winLength = 384,
1374
+ ): WasmTempogramResult {
1375
+ if (!module) {
1376
+ throw new Error('Module not initialized. Call init() first.');
1377
+ }
1378
+ return module.tempogram(onsetEnvelope, sampleRate, hopLength, winLength);
1379
+ }
1380
+
1381
+ export function plp(
1382
+ onsetEnvelope: Float32Array,
1383
+ sampleRate: number,
1384
+ hopLength = 512,
1385
+ tempoMin = 30.0,
1386
+ tempoMax = 300.0,
1387
+ winLength = 384,
1388
+ ): Float32Array {
1389
+ if (!module) {
1390
+ throw new Error('Module not initialized. Call init() first.');
1391
+ }
1392
+ return module.plp(onsetEnvelope, sampleRate, hopLength, tempoMin, tempoMax, winLength);
1393
+ }
1394
+
1395
+ // ============================================================================
1396
+ // Core - Resample
1397
+ // ============================================================================
1398
+
1399
+ /**
1400
+ * Resample audio to a different sample rate.
1401
+ *
1402
+ * @param samples - Audio samples (mono, float32)
1403
+ * @param srcSr - Source sample rate in Hz
1404
+ * @param targetSr - Target sample rate in Hz
1405
+ * @returns Resampled audio
1406
+ */
1407
+ export function resample(samples: Float32Array, srcSr: number, targetSr: number): Float32Array {
1408
+ if (!module) {
1409
+ throw new Error('Module not initialized. Call init() first.');
1410
+ }
1411
+ return module.resample(samples, srcSr, targetSr);
1412
+ }
1413
+
1414
+ // ============================================================================
1415
+ // Audio Class
1416
+ // ============================================================================
1417
+
1418
+ /**
1419
+ * Wrapper around audio data that exposes all analysis and feature functions as instance methods.
1420
+ *
1421
+ * @example
1422
+ * ```typescript
1423
+ * import { init, Audio } from '@libraz/sonare';
1424
+ *
1425
+ * await init();
1426
+ *
1427
+ * const audio = Audio.fromBuffer(samples, 44100);
1428
+ * console.log('BPM:', audio.detectBpm());
1429
+ * console.log('Key:', audio.detectKey().name);
1430
+ *
1431
+ * const mel = audio.melSpectrogram();
1432
+ * ```
1433
+ */
1434
+ export class Audio {
1435
+ private _samples: Float32Array;
1436
+ private _sampleRate: number;
1437
+
1438
+ private constructor(samples: Float32Array, sampleRate: number) {
1439
+ this._samples = samples;
1440
+ this._sampleRate = sampleRate;
1441
+ }
1442
+
1443
+ /** Create an Audio instance from raw sample data. */
1444
+ static fromBuffer(samples: Float32Array, sampleRate: number): Audio {
1445
+ return new Audio(samples, sampleRate);
1446
+ }
1447
+
1448
+ /** The raw audio samples. */
1449
+ get data(): Float32Array {
1450
+ return this._samples;
1451
+ }
1452
+
1453
+ /** Number of samples. */
1454
+ get length(): number {
1455
+ return this._samples.length;
1456
+ }
1457
+
1458
+ /** Sample rate in Hz. */
1459
+ get sampleRate(): number {
1460
+ return this._sampleRate;
1461
+ }
1462
+
1463
+ /** Duration in seconds. */
1464
+ get duration(): number {
1465
+ return this._samples.length / this._sampleRate;
1466
+ }
1467
+
1468
+ // -- Analysis --
1469
+
1470
+ detectBpm(): number {
1471
+ return detectBpm(this._samples, this._sampleRate);
1472
+ }
1473
+
1474
+ detectKey(): Key {
1475
+ return detectKey(this._samples, this._sampleRate);
1476
+ }
1477
+
1478
+ detectOnsets(): Float32Array {
1479
+ return detectOnsets(this._samples, this._sampleRate);
1480
+ }
1481
+
1482
+ detectBeats(): Float32Array {
1483
+ return detectBeats(this._samples, this._sampleRate);
1484
+ }
1485
+
1486
+ analyze(): AnalysisResult {
1487
+ return analyze(this._samples, this._sampleRate);
1488
+ }
1489
+
1490
+ analyzeWithProgress(onProgress: ProgressCallback): AnalysisResult {
1491
+ return analyzeWithProgress(this._samples, this._sampleRate, onProgress);
1492
+ }
1493
+
1494
+ // -- Effects --
1495
+
1496
+ hpss(kernelHarmonic = 31, kernelPercussive = 31): HpssResult {
1497
+ return hpss(this._samples, this._sampleRate, kernelHarmonic, kernelPercussive);
1498
+ }
1499
+
1500
+ harmonic(): Float32Array {
1501
+ return harmonic(this._samples, this._sampleRate);
1502
+ }
1503
+
1504
+ percussive(): Float32Array {
1505
+ return percussive(this._samples, this._sampleRate);
1506
+ }
1507
+
1508
+ timeStretch(rate: number): Float32Array {
1509
+ return timeStretch(this._samples, this._sampleRate, rate);
1510
+ }
1511
+
1512
+ pitchShift(semitones: number): Float32Array {
1513
+ return pitchShift(this._samples, this._sampleRate, semitones);
1514
+ }
1515
+
1516
+ normalize(targetDb = 0.0): Float32Array {
1517
+ return normalize(this._samples, this._sampleRate, targetDb);
1518
+ }
1519
+
1520
+ mastering(targetLufs = -14.0, ceilingDb = -1.0, truePeakOversample = 4): MasteringResult {
1521
+ return mastering(this._samples, this._sampleRate, targetLufs, ceilingDb, truePeakOversample);
1522
+ }
1523
+
1524
+ masteringChain(config: MasteringChainConfig): MasteringChainResult {
1525
+ return masteringChain(this._samples, this._sampleRate, config);
1526
+ }
1527
+
1528
+ masterAudio(
1529
+ presetName: string,
1530
+ overrides: Record<string, number | boolean> | null = null,
1531
+ ): MasteringChainResult {
1532
+ return masterAudio(this._samples, this._sampleRate, presetName, overrides);
1533
+ }
1534
+
1535
+ masteringProcess(processorName: string, params: MasteringProcessorParams = {}): MasteringResult {
1536
+ return masteringProcess(processorName, this._samples, this._sampleRate, params);
1537
+ }
1538
+
1539
+ trim(thresholdDb = -60.0): Float32Array {
1540
+ return trim(this._samples, this._sampleRate, thresholdDb);
1541
+ }
1542
+
1543
+ // -- Features --
1544
+
1545
+ stft(nFft = 2048, hopLength = 512): StftResult {
1546
+ return stft(this._samples, this._sampleRate, nFft, hopLength);
1547
+ }
1548
+
1549
+ stftDb(nFft = 2048, hopLength = 512): { nBins: number; nFrames: number; db: Float32Array } {
1550
+ return stftDb(this._samples, this._sampleRate, nFft, hopLength);
1551
+ }
1552
+
1553
+ melSpectrogram(nFft = 2048, hopLength = 512, nMels = 128): MelSpectrogramResult {
1554
+ return melSpectrogram(this._samples, this._sampleRate, nFft, hopLength, nMels);
1555
+ }
1556
+
1557
+ mfcc(nFft = 2048, hopLength = 512, nMels = 128, nMfcc = 13): MfccResult {
1558
+ return mfcc(this._samples, this._sampleRate, nFft, hopLength, nMels, nMfcc);
1559
+ }
1560
+
1561
+ chroma(nFft = 2048, hopLength = 512): ChromaResult {
1562
+ return chroma(this._samples, this._sampleRate, nFft, hopLength);
1563
+ }
1564
+
1565
+ spectralCentroid(nFft = 2048, hopLength = 512): Float32Array {
1566
+ return spectralCentroid(this._samples, this._sampleRate, nFft, hopLength);
1567
+ }
1568
+
1569
+ spectralBandwidth(nFft = 2048, hopLength = 512): Float32Array {
1570
+ return spectralBandwidth(this._samples, this._sampleRate, nFft, hopLength);
1571
+ }
1572
+
1573
+ spectralRolloff(nFft = 2048, hopLength = 512, rollPercent = 0.85): Float32Array {
1574
+ return spectralRolloff(this._samples, this._sampleRate, nFft, hopLength, rollPercent);
1575
+ }
1576
+
1577
+ spectralFlatness(nFft = 2048, hopLength = 512): Float32Array {
1578
+ return spectralFlatness(this._samples, this._sampleRate, nFft, hopLength);
1579
+ }
1580
+
1581
+ zeroCrossingRate(frameLength = 2048, hopLength = 512): Float32Array {
1582
+ return zeroCrossingRate(this._samples, this._sampleRate, frameLength, hopLength);
1583
+ }
1584
+
1585
+ rmsEnergy(frameLength = 2048, hopLength = 512): Float32Array {
1586
+ return rmsEnergy(this._samples, this._sampleRate, frameLength, hopLength);
1587
+ }
1588
+
1589
+ pitchYin(
1590
+ frameLength = 2048,
1591
+ hopLength = 512,
1592
+ fmin = 65.0,
1593
+ fmax = 2093.0,
1594
+ threshold = 0.3,
1595
+ ): PitchResult {
1596
+ return pitchYin(this._samples, this._sampleRate, frameLength, hopLength, fmin, fmax, threshold);
1597
+ }
1598
+
1599
+ pitchPyin(
1600
+ frameLength = 2048,
1601
+ hopLength = 512,
1602
+ fmin = 65.0,
1603
+ fmax = 2093.0,
1604
+ threshold = 0.3,
1605
+ ): PitchResult {
1606
+ return pitchPyin(
1607
+ this._samples,
1608
+ this._sampleRate,
1609
+ frameLength,
1610
+ hopLength,
1611
+ fmin,
1612
+ fmax,
1613
+ threshold,
1614
+ );
1615
+ }
1616
+
1617
+ resample(targetSr: number): Float32Array {
1618
+ return resample(this._samples, this._sampleRate, targetSr);
1619
+ }
1620
+ }
1621
+
1622
+ // ============================================================================
1623
+ // StreamAnalyzer Class
1624
+ // ============================================================================
1625
+
1626
+ /**
1627
+ * Real-time streaming audio analyzer.
1628
+ *
1629
+ * @example
1630
+ * ```typescript
1631
+ * import { init, StreamAnalyzer } from '@libraz/sonare';
1632
+ *
1633
+ * await init();
1634
+ *
1635
+ * const analyzer = new StreamAnalyzer({ sampleRate: 44100 });
1636
+ *
1637
+ * // In audio processing callback
1638
+ * analyzer.process(samples);
1639
+ *
1640
+ * // Get current analysis state
1641
+ * const stats = analyzer.stats();
1642
+ * console.log('BPM:', stats.estimate.bpm);
1643
+ * console.log('Key:', stats.estimate.key);
1644
+ * console.log('Chord progression:', stats.estimate.chordProgression);
1645
+ * ```
1646
+ */
1647
+ export class StreamAnalyzer {
1648
+ private analyzer: WasmStreamAnalyzer;
1649
+
1650
+ /**
1651
+ * Create a new StreamAnalyzer.
1652
+ *
1653
+ * @param config - Configuration options
1654
+ */
1655
+ constructor(config: StreamConfig) {
1656
+ if (!module) {
1657
+ throw new Error('Module not initialized. Call init() first.');
1658
+ }
1659
+ this.analyzer = new module.StreamAnalyzer(
1660
+ config.sampleRate,
1661
+ config.nFft ?? 2048,
1662
+ config.hopLength ?? 512,
1663
+ config.nMels ?? 128,
1664
+ config.computeMel ?? true,
1665
+ config.computeChroma ?? true,
1666
+ config.computeOnset ?? true,
1667
+ config.emitEveryNFrames ?? 1,
1668
+ );
1669
+ }
1670
+
1671
+ /**
1672
+ * Process audio samples.
1673
+ *
1674
+ * @param samples - Audio samples (mono, float32)
1675
+ */
1676
+ process(samples: Float32Array): void {
1677
+ this.analyzer.process(samples);
1678
+ }
1679
+
1680
+ /**
1681
+ * Process audio samples with explicit sample offset.
1682
+ *
1683
+ * @param samples - Audio samples (mono, float32)
1684
+ * @param sampleOffset - Cumulative sample count at start of this chunk
1685
+ */
1686
+ processWithOffset(samples: Float32Array, sampleOffset: number): void {
1687
+ this.analyzer.processWithOffset(samples, sampleOffset);
1688
+ }
1689
+
1690
+ /**
1691
+ * Get the number of frames available to read.
1692
+ */
1693
+ availableFrames(): number {
1694
+ return this.analyzer.availableFrames();
1695
+ }
1696
+
1697
+ /**
1698
+ * Read processed frames as Structure of Arrays.
1699
+ *
1700
+ * @param maxFrames - Maximum number of frames to read
1701
+ * @returns Frame buffer with analysis results
1702
+ */
1703
+ readFrames(maxFrames: number): FrameBuffer {
1704
+ return this.analyzer.readFramesSoa(maxFrames);
1705
+ }
1706
+
1707
+ /**
1708
+ * Reset the analyzer state.
1709
+ *
1710
+ * @param baseSampleOffset - Starting sample offset (default 0)
1711
+ */
1712
+ reset(baseSampleOffset = 0): void {
1713
+ this.analyzer.reset(baseSampleOffset);
1714
+ }
1715
+
1716
+ /**
1717
+ * Get current statistics and progressive estimates.
1718
+ *
1719
+ * @returns Analyzer statistics including BPM, key, and chord progression
1720
+ */
1721
+ stats(): AnalyzerStats {
1722
+ const s = this.analyzer.stats();
1723
+ return {
1724
+ totalFrames: s.totalFrames,
1725
+ totalSamples: s.totalSamples,
1726
+ durationSeconds: s.durationSeconds,
1727
+ estimate: {
1728
+ bpm: s.estimate.bpm,
1729
+ bpmConfidence: s.estimate.bpmConfidence,
1730
+ bpmCandidateCount: s.estimate.bpmCandidateCount,
1731
+ key: s.estimate.key as PitchClass,
1732
+ keyMinor: s.estimate.keyMinor,
1733
+ keyConfidence: s.estimate.keyConfidence,
1734
+ chordRoot: s.estimate.chordRoot as PitchClass,
1735
+ chordQuality: s.estimate.chordQuality as ChordQuality,
1736
+ chordConfidence: s.estimate.chordConfidence,
1737
+ chordProgression: s.estimate.chordProgression.map((c) => ({
1738
+ root: c.root as PitchClass,
1739
+ quality: c.quality as ChordQuality,
1740
+ startTime: c.startTime,
1741
+ confidence: c.confidence,
1742
+ })),
1743
+ barChordProgression: s.estimate.barChordProgression.map((c) => ({
1744
+ barIndex: c.barIndex,
1745
+ root: c.root as PitchClass,
1746
+ quality: c.quality as ChordQuality,
1747
+ startTime: c.startTime,
1748
+ confidence: c.confidence,
1749
+ })),
1750
+ currentBar: s.estimate.currentBar,
1751
+ barDuration: s.estimate.barDuration,
1752
+ votedPattern: (s.estimate.votedPattern || []).map((c) => ({
1753
+ barIndex: c.barIndex,
1754
+ root: c.root as PitchClass,
1755
+ quality: c.quality as ChordQuality,
1756
+ startTime: c.startTime,
1757
+ confidence: c.confidence,
1758
+ })),
1759
+ patternLength: s.estimate.patternLength,
1760
+ detectedPatternName: s.estimate.detectedPatternName || '',
1761
+ detectedPatternScore: s.estimate.detectedPatternScore || 0,
1762
+ allPatternScores: (s.estimate.allPatternScores || []).map((p) => ({
1763
+ name: p.name,
1764
+ score: p.score,
1765
+ })),
1766
+ accumulatedSeconds: s.estimate.accumulatedSeconds,
1767
+ usedFrames: s.estimate.usedFrames,
1768
+ updated: s.estimate.updated,
1769
+ },
1770
+ };
1771
+ }
1772
+
1773
+ /**
1774
+ * Get total frames processed.
1775
+ */
1776
+ frameCount(): number {
1777
+ return this.analyzer.frameCount();
1778
+ }
1779
+
1780
+ /**
1781
+ * Get current time position in seconds.
1782
+ */
1783
+ currentTime(): number {
1784
+ return this.analyzer.currentTime();
1785
+ }
1786
+
1787
+ /**
1788
+ * Get the sample rate.
1789
+ */
1790
+ sampleRate(): number {
1791
+ return this.analyzer.sampleRate();
1792
+ }
1793
+
1794
+ /**
1795
+ * Set the expected total duration for pattern lock timing.
1796
+ *
1797
+ * @param durationSeconds - Total duration in seconds
1798
+ */
1799
+ setExpectedDuration(durationSeconds: number): void {
1800
+ this.analyzer.setExpectedDuration(durationSeconds);
1801
+ }
1802
+
1803
+ /**
1804
+ * Set normalization gain for loud/compressed audio.
1805
+ *
1806
+ * @param gain - Gain factor to apply (e.g., 0.5 for -6dB reduction)
1807
+ */
1808
+ setNormalizationGain(gain: number): void {
1809
+ this.analyzer.setNormalizationGain(gain);
1810
+ }
1811
+
1812
+ /**
1813
+ * Set tuning reference frequency for non-standard tuning.
1814
+ *
1815
+ * @param refHz - Reference frequency for A4 (default 440 Hz)
1816
+ * @example
1817
+ * // If audio is 1 semitone sharp (A4 = 466.16 Hz)
1818
+ * analyzer.setTuningRefHz(466.16);
1819
+ * // If audio is 1 semitone flat (A4 = 415.30 Hz)
1820
+ * analyzer.setTuningRefHz(415.30);
1821
+ */
1822
+ setTuningRefHz(refHz: number): void {
1823
+ this.analyzer.setTuningRefHz(refHz);
1824
+ }
1825
+
1826
+ /**
1827
+ * Release resources. Call when done using the analyzer.
1828
+ */
1829
+ dispose(): void {
1830
+ this.analyzer.delete();
1831
+ }
1832
+ }
1833
+
1834
+ // ============================================================================
1835
+ // Re-exports
1836
+ // ============================================================================
1837
+
1838
+ export { PitchClass as Pitch } from './public_types';