@libraz/libsonare 1.2.1 → 1.2.2

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
@@ -56,7 +56,10 @@ import type {
56
56
  PairAnalysis,
57
57
  PairProcessor,
58
58
  PanLaw,
59
+ PanMode,
59
60
  PitchResult,
61
+ RealtimeVoiceChangerConfigInput,
62
+ RealtimeVoiceChangerPodConfig,
60
63
  Section,
61
64
  SectionType,
62
65
  SendTiming,
@@ -68,15 +71,9 @@ import type {
68
71
  StreamingPlatform,
69
72
  StreamingRetuneConfig,
70
73
  TempogramMode,
74
+ VoicePresetId,
71
75
  } from './public_types';
72
76
  import { KeyProfile as KeyProfileValues, Mode, PitchClass } from './public_types';
73
- import type {
74
- AnalyzerStats,
75
- FrameBuffer,
76
- StreamConfig,
77
- StreamFramesI16,
78
- StreamFramesU8,
79
- } from './stream_types';
80
77
  import type {
81
78
  ProgressCallback,
82
79
  SonareModule,
@@ -84,6 +81,7 @@ import type {
84
81
  WasmAnalysisResult,
85
82
  WasmChordAnalysisResult,
86
83
  WasmCyclicTempogramResult,
84
+ WasmDecomposeResult,
87
85
  WasmEngineAutomationPoint,
88
86
  WasmEngineBounceOptions,
89
87
  WasmEngineBounceResult,
@@ -101,13 +99,23 @@ import type {
101
99
  WasmEngineTransportState,
102
100
  WasmFourierTempogramResult,
103
101
  WasmFrameResult,
102
+ WasmHpssWithResidualResult,
104
103
  WasmKeyCandidateResult,
104
+ WasmLufsResult,
105
+ WasmMatrix2dResult,
105
106
  WasmNnlsChromaResult,
106
107
  WasmRealtimeEngine,
107
108
  WasmStreamAnalyzer,
108
109
  WasmTempogramResult,
109
110
  WasmTrimResult,
110
- } from './wasm_types';
111
+ } from './sonare.js';
112
+ import type {
113
+ AnalyzerStats,
114
+ FrameBuffer,
115
+ StreamConfig,
116
+ StreamFramesI16,
117
+ StreamFramesU8,
118
+ } from './stream_types';
111
119
 
112
120
  export type {
113
121
  AcousticResult,
@@ -155,6 +163,7 @@ export type {
155
163
  PanLaw,
156
164
  PanMode,
157
165
  PitchResult,
166
+ RealtimeVoiceChangerConfigInput,
158
167
  RhythmFeatures,
159
168
  Section,
160
169
  SendTiming,
@@ -166,6 +175,7 @@ export type {
166
175
  StreamingRetuneConfig,
167
176
  Timbre,
168
177
  TimeSignature,
178
+ VoicePresetId,
169
179
  } from './public_types';
170
180
  export {
171
181
  ChordQuality,
@@ -174,6 +184,7 @@ export {
174
184
  PitchClass,
175
185
  SectionType,
176
186
  } from './public_types';
187
+ export type { ProgressCallback } from './sonare.js';
177
188
  export type {
178
189
  AnalyzerStats,
179
190
  BarChord,
@@ -185,7 +196,6 @@ export type {
185
196
  StreamFramesI16,
186
197
  StreamFramesU8,
187
198
  } from './stream_types';
188
- export type { ProgressCallback } from './wasm_types';
189
199
 
190
200
  export type EngineClip = WasmEngineClip;
191
201
  export type EngineParameterInfo = WasmEngineParameterInfo;
@@ -202,6 +212,13 @@ export type EngineTelemetry = WasmEngineTelemetry;
202
212
  export type EngineMeterTelemetry = WasmEngineMeterTelemetry;
203
213
  export type EngineTransportState = WasmEngineTransportState;
204
214
 
215
+ /** Row-major 2-D matrix as a flat buffer plus its dimensions. */
216
+ export type Matrix2dResult = WasmMatrix2dResult;
217
+ /** NMF factor matrices { w, h } from {@link decompose}. */
218
+ export type DecomposeResult = WasmDecomposeResult;
219
+ /** Harmonic / percussive / residual signals from {@link hpssWithResidual}. */
220
+ export type HpssWithResidualResult = WasmHpssWithResidualResult;
221
+
205
222
  export const EXPECTED_ENGINE_ABI_VERSION = 2;
206
223
 
207
224
  export interface EngineCapabilities {
@@ -222,6 +239,43 @@ export interface MixerRealtimeBuffer {
222
239
  process: (numSamples?: number) => void;
223
240
  }
224
241
 
242
+ /**
243
+ * Zero-copy realtime buffer pair for {@link RealtimeVoiceChanger} mono
244
+ * processing. The `input` / `output` `Float32Array`s are typed-memory views
245
+ * onto the WASM heap — write samples into `input`, call `process()`, then
246
+ * read from `output`. The views are owned by the {@link RealtimeVoiceChanger}
247
+ * and remain valid until `delete()` is called on it.
248
+ */
249
+ export interface RealtimeVoiceChangerMonoBuffer {
250
+ input: Float32Array;
251
+ output: Float32Array;
252
+ process: () => void;
253
+ }
254
+
255
+ /**
256
+ * Zero-copy realtime buffer pair for {@link RealtimeVoiceChanger} interleaved
257
+ * multi-channel processing. Layout is L0,R0,L1,R1,... for stereo. The views
258
+ * are owned by the {@link RealtimeVoiceChanger}.
259
+ */
260
+ export interface RealtimeVoiceChangerInterleavedBuffer {
261
+ input: Float32Array;
262
+ output: Float32Array;
263
+ channels: number;
264
+ process: () => void;
265
+ }
266
+
267
+ /**
268
+ * Zero-copy realtime buffer for {@link RealtimeVoiceChanger} planar stereo
269
+ * processing. Each entry in `channels` is a heap-backed `Float32Array` for one
270
+ * channel (matching AudioWorklet's native layout). Process happens in place:
271
+ * write samples into each channel view, call `process()`, then read back from
272
+ * the same views.
273
+ */
274
+ export interface RealtimeVoiceChangerPlanarBuffer {
275
+ channels: Float32Array[];
276
+ process: () => void;
277
+ }
278
+
225
279
  function automationCurveCode(curve: AutomationCurve): number {
226
280
  switch (curve) {
227
281
  case 'linear':
@@ -253,6 +307,22 @@ function panLawCode(panLaw: PanLaw | number): number {
253
307
  }
254
308
  }
255
309
 
310
+ function panModeCode(panMode: PanMode | number): number {
311
+ if (typeof panMode === 'number') {
312
+ return panMode;
313
+ }
314
+ switch (panMode) {
315
+ case 'stereoPan':
316
+ case 'stereo-pan':
317
+ return 1;
318
+ case 'dualPan':
319
+ case 'dual-pan':
320
+ return 2;
321
+ default:
322
+ return 0;
323
+ }
324
+ }
325
+
256
326
  function meterTapCode(tap: MeterTap | number): number {
257
327
  return tap === 'preFader' || tap === 0 ? 0 : 1;
258
328
  }
@@ -268,6 +338,62 @@ function sendTimingCode(timing: SendTiming | number): number {
268
338
  let module: SonareModule | null = null;
269
339
  let initPromise: Promise<void> | null = null;
270
340
 
341
+ // ============================================================================
342
+ // Input validation helpers (empty + NaN/Inf guards for sample buffers)
343
+ // ============================================================================
344
+
345
+ /**
346
+ * Per-call validation options accepted by guarded wrappers. Empty-buffer
347
+ * checks are always performed; pass `{ validate: false }` to opt out of the
348
+ * O(n) NaN/Inf scan on hot paths.
349
+ */
350
+ export interface ValidateOptions {
351
+ validate?: boolean;
352
+ }
353
+
354
+ function assertNonEmptySamples(
355
+ fnName: string,
356
+ samples: ArrayLike<number>,
357
+ argName = 'samples',
358
+ ): void {
359
+ if (samples.length === 0) {
360
+ throw new RangeError(`${fnName}: ${argName} must not be empty`);
361
+ }
362
+ }
363
+
364
+ function assertFiniteSamples(
365
+ fnName: string,
366
+ samples: ArrayLike<number>,
367
+ validate: boolean,
368
+ argName = 'samples',
369
+ ): void {
370
+ if (!validate) {
371
+ return;
372
+ }
373
+ for (let i = 0; i < samples.length; i++) {
374
+ const v = samples[i] as number;
375
+ if (!Number.isFinite(v)) {
376
+ throw new RangeError(`${fnName}: ${argName} contains NaN or Inf at index ${i}`);
377
+ }
378
+ }
379
+ }
380
+
381
+ function assertSamples(
382
+ fnName: string,
383
+ samples: ArrayLike<number>,
384
+ validate: boolean,
385
+ argName = 'samples',
386
+ ): void {
387
+ assertNonEmptySamples(fnName, samples, argName);
388
+ assertFiniteSamples(fnName, samples, validate, argName);
389
+ }
390
+
391
+ function assertFiniteScalar(fnName: string, value: number, argName: string): void {
392
+ if (!Number.isFinite(value)) {
393
+ throw new RangeError(`${fnName}: ${argName} must be a finite number`);
394
+ }
395
+ }
396
+
271
397
  // ============================================================================
272
398
  // Initialization
273
399
  // ============================================================================
@@ -327,6 +453,60 @@ export function engineAbiVersion(): number {
327
453
  return module.engineAbiVersion();
328
454
  }
329
455
 
456
+ export function voiceChangerAbiVersion(): number {
457
+ if (!module) {
458
+ throw new Error('Module not initialized. Call init() first.');
459
+ }
460
+ return module.voiceChangerAbiVersion();
461
+ }
462
+
463
+ // Canonical ordinal order of the built-in voice-character presets, matching the
464
+ // C ABI SonareVoiceCharacterPreset enum and SONARE_REALTIME_VOICE_CHANGER_PRESET_IDS.
465
+ const VOICE_PRESET_ORDINALS: readonly VoicePresetId[] = [
466
+ 'neutral-monitor',
467
+ 'bright-idol',
468
+ 'soft-whisper',
469
+ 'deep-narrator',
470
+ 'robot-mascot',
471
+ 'dark-villain',
472
+ ];
473
+
474
+ function resolveVoicePresetOrdinal(preset: VoicePresetId | number): number {
475
+ if (typeof preset === 'number') {
476
+ return preset;
477
+ }
478
+ const ordinal = VOICE_PRESET_ORDINALS.indexOf(preset);
479
+ if (ordinal < 0) {
480
+ throw new Error(`Unknown voice character preset: ${preset}`);
481
+ }
482
+ return ordinal;
483
+ }
484
+
485
+ /**
486
+ * Map a voice-character preset ordinal (or canonical id) to its canonical id
487
+ * string (e.g. `'bright-idol'`). Returns `null` for an out-of-range ordinal.
488
+ */
489
+ export function voiceCharacterPresetId(preset: VoicePresetId | number): string | null {
490
+ if (!module) {
491
+ throw new Error('Module not initialized. Call init() first.');
492
+ }
493
+ return module.voiceCharacterPresetId(resolveVoicePresetOrdinal(preset));
494
+ }
495
+
496
+ /**
497
+ * Return the canonical (normalized) flat POD config for a built-in voice
498
+ * preset, skipping the JSON round-trip. Accepts a canonical preset id or its
499
+ * integer ordinal. Returns `null` for an out-of-range ordinal.
500
+ */
501
+ export function realtimeVoiceChangerPresetConfig(
502
+ preset: VoicePresetId | number,
503
+ ): RealtimeVoiceChangerPodConfig | null {
504
+ if (!module) {
505
+ throw new Error('Module not initialized. Call init() first.');
506
+ }
507
+ return module.realtimeVoiceChangerPresetConfig(resolveVoicePresetOrdinal(preset));
508
+ }
509
+
330
510
  export function engineCapabilities(): EngineCapabilities {
331
511
  const abiVersion = engineAbiVersion();
332
512
  const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === 'function';
@@ -569,10 +749,10 @@ export class RealtimeEngine {
569
749
  * Detect BPM from audio samples.
570
750
  *
571
751
  * @param samples - Audio samples (mono, float32)
572
- * @param sampleRate - Sample rate in Hz
752
+ * @param sampleRate - Sample rate in Hz (default: 22050)
573
753
  * @returns Detected BPM
574
754
  */
575
- export function detectBpm(samples: Float32Array, sampleRate: number): number {
755
+ export function detectBpm(samples: Float32Array, sampleRate = 22050): number {
576
756
  if (!module) {
577
757
  throw new Error('Module not initialized. Call init() first.');
578
758
  }
@@ -583,12 +763,12 @@ export function detectBpm(samples: Float32Array, sampleRate: number): number {
583
763
  * Detect musical key from audio samples.
584
764
  *
585
765
  * @param samples - Audio samples (mono, float32)
586
- * @param sampleRate - Sample rate in Hz
766
+ * @param sampleRate - Sample rate in Hz (default: 22050)
587
767
  * @returns Detected key
588
768
  */
589
769
  export function detectKey(
590
770
  samples: Float32Array,
591
- sampleRate: number,
771
+ sampleRate = 22050,
592
772
  options: KeyDetectionOptions = {},
593
773
  ): Key {
594
774
  if (!module) {
@@ -685,7 +865,7 @@ function keyProfileValue(profile: KeyDetectionOptions['profile'] | undefined): n
685
865
 
686
866
  export function detectKeyCandidates(
687
867
  samples: Float32Array,
688
- sampleRate: number,
868
+ sampleRate = 22050,
689
869
  options: KeyDetectionOptions = {},
690
870
  ): KeyCandidate[] {
691
871
  if (!module) {
@@ -711,10 +891,10 @@ export function detectKeyCandidates(
711
891
  * Detect onset times from audio samples.
712
892
  *
713
893
  * @param samples - Audio samples (mono, float32)
714
- * @param sampleRate - Sample rate in Hz
894
+ * @param sampleRate - Sample rate in Hz (default: 22050)
715
895
  * @returns Array of onset times in seconds
716
896
  */
717
- export function detectOnsets(samples: Float32Array, sampleRate: number): Float32Array {
897
+ export function detectOnsets(samples: Float32Array, sampleRate = 22050): Float32Array {
718
898
  if (!module) {
719
899
  throw new Error('Module not initialized. Call init() first.');
720
900
  }
@@ -725,10 +905,10 @@ export function detectOnsets(samples: Float32Array, sampleRate: number): Float32
725
905
  * Detect beat times from audio samples.
726
906
  *
727
907
  * @param samples - Audio samples (mono, float32)
728
- * @param sampleRate - Sample rate in Hz
908
+ * @param sampleRate - Sample rate in Hz (default: 22050)
729
909
  * @returns Array of beat times in seconds
730
910
  */
731
- export function detectBeats(samples: Float32Array, sampleRate: number): Float32Array {
911
+ export function detectBeats(samples: Float32Array, sampleRate = 22050): Float32Array {
732
912
  if (!module) {
733
913
  throw new Error('Module not initialized. Call init() first.');
734
914
  }
@@ -739,10 +919,10 @@ export function detectBeats(samples: Float32Array, sampleRate: number): Float32A
739
919
  * Detect downbeat times from audio samples.
740
920
  *
741
921
  * @param samples - Audio samples (mono, float32)
742
- * @param sampleRate - Sample rate in Hz
922
+ * @param sampleRate - Sample rate in Hz (default: 22050)
743
923
  * @returns Array of downbeat times in seconds
744
924
  */
745
- export function detectDownbeats(samples: Float32Array, sampleRate: number): Float32Array {
925
+ export function detectDownbeats(samples: Float32Array, sampleRate = 22050): Float32Array {
746
926
  if (!module) {
747
927
  throw new Error('Module not initialized. Call init() first.');
748
928
  }
@@ -767,13 +947,13 @@ function convertChordAnalysisResult(wasm: WasmChordAnalysisResult): ChordAnalysi
767
947
  * Detect chords from audio samples.
768
948
  *
769
949
  * @param samples - Audio samples (mono, float32)
770
- * @param sampleRate - Sample rate in Hz
950
+ * @param sampleRate - Sample rate in Hz (default: 22050)
771
951
  * @param options - Optional chord detection settings
772
952
  * @returns Detected chord segments
773
953
  */
774
954
  export function detectChords(
775
955
  samples: Float32Array,
776
- sampleRate: number,
956
+ sampleRate = 22050,
777
957
  options: ChordDetectionOptions = {},
778
958
  ): ChordAnalysisResult {
779
959
  if (!module) {
@@ -857,10 +1037,10 @@ function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult {
857
1037
  * Perform complete music analysis.
858
1038
  *
859
1039
  * @param samples - Audio samples (mono, float32)
860
- * @param sampleRate - Sample rate in Hz
1040
+ * @param sampleRate - Sample rate in Hz (default: 22050)
861
1041
  * @returns Complete analysis result
862
1042
  */
863
- export function analyze(samples: Float32Array, sampleRate: number): AnalysisResult {
1043
+ export function analyze(samples: Float32Array, sampleRate = 22050): AnalysisResult {
864
1044
  if (!module) {
865
1045
  throw new Error('Module not initialized. Call init() first.');
866
1046
  }
@@ -870,7 +1050,7 @@ export function analyze(samples: Float32Array, sampleRate: number): AnalysisResu
870
1050
 
871
1051
  export function analyzeImpulseResponse(
872
1052
  samples: Float32Array,
873
- sampleRate: number,
1053
+ sampleRate = 48000,
874
1054
  nOctaveBands = 6,
875
1055
  ): AcousticResult {
876
1056
  if (!module) {
@@ -886,7 +1066,7 @@ export function analyzeImpulseResponse(
886
1066
 
887
1067
  export function detectAcoustic(
888
1068
  samples: Float32Array,
889
- sampleRate: number,
1069
+ sampleRate = 48000,
890
1070
  nOctaveBands = 6,
891
1071
  nThirdOctaveSubbands = 24,
892
1072
  minDecayDb = 30.0,
@@ -910,13 +1090,13 @@ export function detectAcoustic(
910
1090
  * Perform complete music analysis with progress reporting.
911
1091
  *
912
1092
  * @param samples - Audio samples (mono, float32)
913
- * @param sampleRate - Sample rate in Hz
1093
+ * @param sampleRate - Sample rate in Hz (default: 22050)
914
1094
  * @param onProgress - Progress callback (progress: 0-1, stage: string)
915
1095
  * @returns Complete analysis result
916
1096
  */
917
1097
  export function analyzeWithProgress(
918
1098
  samples: Float32Array,
919
- sampleRate: number,
1099
+ sampleRate = 22050,
920
1100
  onProgress: ProgressCallback,
921
1101
  ): AnalysisResult {
922
1102
  if (!module) {
@@ -926,6 +1106,154 @@ export function analyzeWithProgress(
926
1106
  return convertAnalysisResult(result);
927
1107
  }
928
1108
 
1109
+ export interface BpmCandidate {
1110
+ bpm: number;
1111
+ confidence: number;
1112
+ }
1113
+
1114
+ export interface BpmAnalysisResult {
1115
+ bpm: number;
1116
+ confidence: number;
1117
+ candidates: BpmCandidate[];
1118
+ autocorrelation: Float32Array;
1119
+ tempogram: Float32Array;
1120
+ }
1121
+
1122
+ export interface RhythmAnalysisResult {
1123
+ timeSignature: { numerator: number; denominator: number; confidence: number };
1124
+ syncopation: number;
1125
+ grooveType: string;
1126
+ patternRegularity: number;
1127
+ tempoStability: number;
1128
+ bpm: number;
1129
+ beatIntervals: Float32Array;
1130
+ }
1131
+
1132
+ export interface DynamicsAnalysisResult {
1133
+ dynamicRangeDb: number;
1134
+ peakDb: number;
1135
+ rmsDb: number;
1136
+ crestFactor: number;
1137
+ loudnessRangeDb: number;
1138
+ isCompressed: boolean;
1139
+ /** Loudness curve timestamps (seconds), parallel to {@link loudnessRmsDb}. */
1140
+ loudnessTimes: Float32Array;
1141
+ /** Loudness curve RMS values (dB), parallel to {@link loudnessTimes}. */
1142
+ loudnessRmsDb: Float32Array;
1143
+ }
1144
+
1145
+ /** Timbre metrics for one analysis window. Entries are ordered by time in `timbreOverTime`. */
1146
+ export interface TimbreFrame {
1147
+ brightness: number;
1148
+ warmth: number;
1149
+ density: number;
1150
+ roughness: number;
1151
+ complexity: number;
1152
+ }
1153
+
1154
+ export interface TimbreAnalysisResult extends TimbreFrame {
1155
+ spectralCentroid: Float32Array;
1156
+ spectralFlatness: Float32Array;
1157
+ spectralRolloff: Float32Array;
1158
+ /** Time-varying timbre metrics, one entry per analysis window. */
1159
+ timbreOverTime: TimbreFrame[];
1160
+ }
1161
+
1162
+ /**
1163
+ * Detailed BPM analysis (BPM, confidence, alternate candidates, autocorrelation,
1164
+ * tempogram). Matches the Node `analyzeBpm` / Python `analyze_bpm` surface.
1165
+ */
1166
+ export function analyzeBpm(
1167
+ samples: Float32Array,
1168
+ sampleRate = 22050,
1169
+ bpmMin = 30.0,
1170
+ bpmMax = 300.0,
1171
+ startBpm = 120.0,
1172
+ nFft = 2048,
1173
+ hopLength = 512,
1174
+ maxCandidates = 5,
1175
+ ): BpmAnalysisResult {
1176
+ if (!module) {
1177
+ throw new Error('Module not initialized. Call init() first.');
1178
+ }
1179
+ return module.analyzeBpm(
1180
+ samples,
1181
+ sampleRate,
1182
+ bpmMin,
1183
+ bpmMax,
1184
+ startBpm,
1185
+ nFft,
1186
+ hopLength,
1187
+ maxCandidates,
1188
+ );
1189
+ }
1190
+
1191
+ /**
1192
+ * Detailed rhythm analysis (time signature, groove, syncopation, beat intervals).
1193
+ */
1194
+ export function analyzeRhythm(
1195
+ samples: Float32Array,
1196
+ sampleRate = 22050,
1197
+ bpmMin = 60.0,
1198
+ bpmMax = 200.0,
1199
+ startBpm = 120.0,
1200
+ nFft = 2048,
1201
+ hopLength = 512,
1202
+ ): RhythmAnalysisResult {
1203
+ if (!module) {
1204
+ throw new Error('Module not initialized. Call init() first.');
1205
+ }
1206
+ return module.analyzeRhythm(samples, sampleRate, bpmMin, bpmMax, startBpm, nFft, hopLength);
1207
+ }
1208
+
1209
+ /**
1210
+ * Dynamics analysis (RMS, peak, crest factor, LRA, loudness curve).
1211
+ */
1212
+ export function analyzeDynamics(
1213
+ samples: Float32Array,
1214
+ sampleRate = 22050,
1215
+ windowSec = 0.4,
1216
+ hopLength = 512,
1217
+ compressionThreshold = 6.0,
1218
+ ): DynamicsAnalysisResult {
1219
+ if (!module) {
1220
+ throw new Error('Module not initialized. Call init() first.');
1221
+ }
1222
+ return module.analyzeDynamics(samples, sampleRate, windowSec, hopLength, compressionThreshold);
1223
+ }
1224
+
1225
+ /**
1226
+ * Timbre analysis (brightness/warmth/density/roughness/complexity plus spectral
1227
+ * features and per-window timbre frames).
1228
+ */
1229
+ export function analyzeTimbre(
1230
+ samples: Float32Array,
1231
+ sampleRate = 22050,
1232
+ nFft = 2048,
1233
+ hopLength = 512,
1234
+ nMels = 128,
1235
+ nMfcc = 13,
1236
+ windowSec = 0.5,
1237
+ ): TimbreAnalysisResult {
1238
+ if (!module) {
1239
+ throw new Error('Module not initialized. Call init() first.');
1240
+ }
1241
+ return module.analyzeTimbre(samples, sampleRate, nFft, hopLength, nMels, nMfcc, windowSec);
1242
+ }
1243
+
1244
+ /**
1245
+ * Whether this WASM build was compiled with FFmpeg support. Mirrors Node /
1246
+ * Python `hasFfmpegSupport`. In the published WASM binding this currently
1247
+ * always returns `false` (FFmpeg is not bundled into the .wasm), but the API
1248
+ * exists so caller code can branch on capabilities portably.
1249
+ */
1250
+ export function hasFfmpegSupport(): boolean {
1251
+ if (!module) {
1252
+ throw new Error('Module not initialized. Call init() first.');
1253
+ }
1254
+ return module.hasFfmpegSupport();
1255
+ }
1256
+
929
1257
  // ============================================================================
930
1258
  // Effects
931
1259
  // ============================================================================
@@ -934,14 +1262,14 @@ export function analyzeWithProgress(
934
1262
  * Perform Harmonic-Percussive Source Separation (HPSS).
935
1263
  *
936
1264
  * @param samples - Audio samples (mono, float32)
937
- * @param sampleRate - Sample rate in Hz
1265
+ * @param sampleRate - Sample rate in Hz (default: 22050)
938
1266
  * @param kernelHarmonic - Horizontal median filter size for harmonic (default: 31)
939
1267
  * @param kernelPercussive - Vertical median filter size for percussive (default: 31)
940
1268
  * @returns Separated harmonic and percussive components
941
1269
  */
942
1270
  export function hpss(
943
1271
  samples: Float32Array,
944
- sampleRate: number,
1272
+ sampleRate = 22050,
945
1273
  kernelHarmonic = 31,
946
1274
  kernelPercussive = 31,
947
1275
  ): HpssResult {
@@ -1024,9 +1352,9 @@ export function pitchShift(
1024
1352
  */
1025
1353
  export function pitchCorrectToMidi(
1026
1354
  samples: Float32Array,
1027
- sampleRate: number,
1028
- currentMidi: number,
1029
- targetMidi: number,
1355
+ sampleRate = 22050,
1356
+ currentMidi = 69.0,
1357
+ targetMidi = 69.0,
1030
1358
  ): Float32Array {
1031
1359
  if (!module) {
1032
1360
  throw new Error('Module not initialized. Call init() first.');
@@ -1046,10 +1374,10 @@ export function pitchCorrectToMidi(
1046
1374
  */
1047
1375
  export function noteStretch(
1048
1376
  samples: Float32Array,
1049
- sampleRate: number,
1050
- onsetSample: number,
1051
- offsetSample: number,
1052
- stretchRatio: number,
1377
+ sampleRate = 22050,
1378
+ onsetSample = 0,
1379
+ offsetSample = 0,
1380
+ stretchRatio = 1.0,
1053
1381
  ): Float32Array {
1054
1382
  if (!module) {
1055
1383
  throw new Error('Module not initialized. Call init() first.');
@@ -1068,13 +1396,15 @@ export function noteStretch(
1068
1396
  */
1069
1397
  export function voiceChange(
1070
1398
  samples: Float32Array,
1071
- sampleRate: number,
1072
- pitchSemitones: number,
1073
- formantFactor: number,
1399
+ sampleRate = 22050,
1400
+ pitchSemitones = 0.0,
1401
+ formantFactor = 1.0,
1402
+ options: ValidateOptions = {},
1074
1403
  ): Float32Array {
1075
1404
  if (!module) {
1076
1405
  throw new Error('Module not initialized. Call init() first.');
1077
1406
  }
1407
+ assertSamples('voiceChange', samples, options.validate !== false);
1078
1408
  return module.voiceChange(samples, sampleRate, pitchSemitones, formantFactor);
1079
1409
  }
1080
1410
 
@@ -1097,7 +1427,7 @@ export function normalize(samples: Float32Array, sampleRate: number, targetDb =
1097
1427
  * Apply mastering loudness normalization with a true-peak ceiling.
1098
1428
  *
1099
1429
  * @param samples - Audio samples (mono, float32)
1100
- * @param sampleRate - Sample rate in Hz
1430
+ * @param sampleRate - Sample rate in Hz (default: 22050)
1101
1431
  * @param targetLufs - Target integrated LUFS (default: -14)
1102
1432
  * @param ceilingDb - True/sample peak ceiling in dBFS (default: -1)
1103
1433
  * @param truePeakOversample - Oversampling factor used for peak estimation
@@ -1105,7 +1435,7 @@ export function normalize(samples: Float32Array, sampleRate: number, targetDb =
1105
1435
  */
1106
1436
  export function mastering(
1107
1437
  samples: Float32Array,
1108
- sampleRate: number,
1438
+ sampleRate = 22050,
1109
1439
  targetLufs = -14.0,
1110
1440
  ceilingDb = -1.0,
1111
1441
  truePeakOversample = 4,
@@ -1147,7 +1477,7 @@ export function masteringStereoAnalysisNames(): StereoAnalysis[] {
1147
1477
  export function masteringProcess(
1148
1478
  processorName: SoloProcessor,
1149
1479
  samples: Float32Array,
1150
- sampleRate: number,
1480
+ sampleRate = 22050,
1151
1481
  params: MasteringProcessorParams = {},
1152
1482
  ): MasteringResult {
1153
1483
  if (!module) {
@@ -1160,7 +1490,7 @@ export function masteringProcessStereo(
1160
1490
  processorName: SoloProcessor,
1161
1491
  left: Float32Array,
1162
1492
  right: Float32Array,
1163
- sampleRate: number,
1493
+ sampleRate = 22050,
1164
1494
  params: MasteringProcessorParams = {},
1165
1495
  ): MasteringStereoResult {
1166
1496
  if (!module) {
@@ -1176,7 +1506,7 @@ export function masteringPairProcess(
1176
1506
  processorName: PairProcessor,
1177
1507
  source: Float32Array,
1178
1508
  reference: Float32Array,
1179
- sampleRate: number,
1509
+ sampleRate = 22050,
1180
1510
  params: MasteringProcessorParams = {},
1181
1511
  ): MasteringResult {
1182
1512
  if (!module) {
@@ -1189,7 +1519,7 @@ export function masteringPairAnalyze(
1189
1519
  analysisName: PairAnalysis,
1190
1520
  source: Float32Array,
1191
1521
  reference: Float32Array,
1192
- sampleRate: number,
1522
+ sampleRate = 22050,
1193
1523
  params: MasteringProcessorParams = {},
1194
1524
  ): string {
1195
1525
  if (!module) {
@@ -1202,7 +1532,7 @@ export function masteringStereoAnalyze(
1202
1532
  analysisName: StereoAnalysis,
1203
1533
  left: Float32Array,
1204
1534
  right: Float32Array,
1205
- sampleRate: number,
1535
+ sampleRate = 22050,
1206
1536
  params: MasteringProcessorParams = {},
1207
1537
  ): string {
1208
1538
  if (!module) {
@@ -1213,7 +1543,7 @@ export function masteringStereoAnalyze(
1213
1543
 
1214
1544
  export function masteringAssistantSuggest(
1215
1545
  samples: Float32Array,
1216
- sampleRate: number,
1546
+ sampleRate = 22050,
1217
1547
  params: MasteringProcessorParams = {},
1218
1548
  ): string {
1219
1549
  if (!module) {
@@ -1224,7 +1554,7 @@ export function masteringAssistantSuggest(
1224
1554
 
1225
1555
  export function masteringAudioProfile(
1226
1556
  samples: Float32Array,
1227
- sampleRate: number,
1557
+ sampleRate = 22050,
1228
1558
  params: MasteringProcessorParams = {},
1229
1559
  ): string {
1230
1560
  if (!module) {
@@ -1235,7 +1565,7 @@ export function masteringAudioProfile(
1235
1565
 
1236
1566
  export function masteringStreamingPreview(
1237
1567
  samples: Float32Array,
1238
- sampleRate: number,
1568
+ sampleRate = 22050,
1239
1569
  platforms: StreamingPlatform[] = [],
1240
1570
  ): string {
1241
1571
  if (!module) {
@@ -1244,61 +1574,315 @@ export function masteringStreamingPreview(
1244
1574
  return module.masteringStreamingPreview(samples, sampleRate, platforms);
1245
1575
  }
1246
1576
 
1247
- /**
1248
- * Apply a configurable mastering chain in WASM.
1249
- *
1250
- * @param samples - Audio samples (mono, float32)
1251
- * @param sampleRate - Sample rate in Hz
1252
- * @param config - Chain stage configuration
1253
- * @returns Processed audio, loudness metadata, and applied stage names
1254
- */
1255
- export function masteringChain(
1577
+ // ============================================================================
1578
+ // Mastering repair (declick, denoise_classical, declip, decrackle, dehum,
1579
+ // dereverb_classical, trim_silence) — hand-written bindings.
1580
+ // ============================================================================
1581
+
1582
+ /** Options for `masteringRepairDeclick`. */
1583
+ export interface DeclickOptions {
1584
+ threshold?: number;
1585
+ neighborRatio?: number;
1586
+ maxClickSamples?: number;
1587
+ lpcOrder?: number;
1588
+ residualRatio?: number;
1589
+ }
1590
+
1591
+ /** Algorithms accepted by `masteringRepairDenoiseClassical`. */
1592
+ export type DenoiseClassicalMode = 'logMmse' | 'mmseStsa' | 'spectralSubtraction';
1593
+
1594
+ /** Noise PSD estimators accepted by `masteringRepairDenoiseClassical`. */
1595
+ export type DenoiseClassicalNoiseEstimator = 'quantile' | 'mcra' | 'imcra';
1596
+
1597
+ /** Options for `masteringRepairDenoiseClassical`. */
1598
+ export interface DenoiseClassicalOptions {
1599
+ mode?: DenoiseClassicalMode;
1600
+ noiseEstimator?: DenoiseClassicalNoiseEstimator;
1601
+ nFft?: number;
1602
+ hopLength?: number;
1603
+ ddAlpha?: number;
1604
+ gainFloor?: number;
1605
+ overSubtraction?: number;
1606
+ spectralFloor?: number;
1607
+ noiseEstimationQuantile?: number;
1608
+ speechPresenceGain?: boolean;
1609
+ gainSmoothing?: boolean;
1610
+ }
1611
+
1612
+ /** Offline LPC-based declicker. */
1613
+ export function masteringRepairDeclick(
1256
1614
  samples: Float32Array,
1257
1615
  sampleRate: number,
1258
- config: MasteringChainConfig,
1259
- ): MasteringChainResult {
1260
- if (!module) {
1261
- throw new Error('Module not initialized. Call init() first.');
1262
- }
1263
- return module.masteringChain(samples, sampleRate, config as Record<string, unknown>);
1616
+ options: DeclickOptions = {},
1617
+ ): Float32Array {
1618
+ return requireModule().masteringRepairDeclick(samples, sampleRate, options);
1264
1619
  }
1265
1620
 
1266
- /**
1267
- * Apply a configurable stereo mastering chain in WASM.
1268
- *
1269
- * @param left - Left channel samples
1270
- * @param right - Right channel samples
1271
- * @param sampleRate - Sample rate in Hz
1272
- * @param config - Chain stage configuration
1273
- * @returns Processed stereo audio, loudness metadata, and applied stage names
1274
- */
1275
- export function masteringChainStereo(
1276
- left: Float32Array,
1277
- right: Float32Array,
1621
+ /** Offline STFT-domain classical denoiser (LogMMSE / MMSE-STSA / SpectralSubtraction). */
1622
+ export function masteringRepairDenoiseClassical(
1623
+ samples: Float32Array,
1278
1624
  sampleRate: number,
1279
- config: MasteringChainConfig,
1280
- ): MasteringStereoChainResult {
1281
- if (!module) {
1282
- throw new Error('Module not initialized. Call init() first.');
1283
- }
1284
- if (left.length !== right.length) {
1285
- throw new Error('Stereo channel lengths must match.');
1286
- }
1287
- return module.masteringChainStereo(left, right, sampleRate, config as Record<string, unknown>);
1625
+ options: DenoiseClassicalOptions = {},
1626
+ ): Float32Array {
1627
+ return requireModule().masteringRepairDenoiseClassical(samples, sampleRate, options);
1288
1628
  }
1289
1629
 
1290
- /**
1291
- * Apply a configurable mastering chain in WASM with progress reporting.
1292
- *
1293
- * @param samples - Audio samples (mono, float32)
1630
+ /** Options for `masteringRepairDeclip`. */
1631
+ export interface DeclipOptions {
1632
+ clipThreshold?: number;
1633
+ lpcOrder?: number;
1634
+ iterations?: number;
1635
+ lpcBlend?: number;
1636
+ }
1637
+
1638
+ /** Algorithms accepted by `masteringRepairDecrackle`. */
1639
+ export type DecrackleMode = 'median' | 'waveletShrinkage';
1640
+
1641
+ /** Options for `masteringRepairDecrackle`. */
1642
+ export interface DecrackleOptions {
1643
+ threshold?: number;
1644
+ mode?: DecrackleMode;
1645
+ levels?: number;
1646
+ }
1647
+
1648
+ /** Options for `masteringRepairDehum`. */
1649
+ export interface DehumOptions {
1650
+ fundamentalHz?: number;
1651
+ harmonics?: number;
1652
+ q?: number;
1653
+ adaptive?: boolean;
1654
+ searchRangeHz?: number;
1655
+ adaptation?: number;
1656
+ frameSize?: number;
1657
+ pllBandwidth?: number;
1658
+ }
1659
+
1660
+ /** Options for `masteringRepairDereverbClassical`. */
1661
+ export interface DereverbClassicalOptions {
1662
+ threshold?: number;
1663
+ attenuation?: number;
1664
+ nFft?: number;
1665
+ hopLength?: number;
1666
+ t60Sec?: number;
1667
+ lateDelayMs?: number;
1668
+ overSubtraction?: number;
1669
+ spectralFloor?: number;
1670
+ wpeEnabled?: boolean;
1671
+ wpeIterations?: number;
1672
+ wpeTaps?: number;
1673
+ wpeStrength?: number;
1674
+ }
1675
+
1676
+ /** Trimming modes accepted by `masteringRepairTrimSilence`. */
1677
+ export type TrimSilenceMode = 'peak' | 'lufsGated';
1678
+
1679
+ /** Options for `masteringRepairTrimSilence`. */
1680
+ export interface TrimSilenceOptions {
1681
+ threshold?: number;
1682
+ paddingSamples?: number;
1683
+ mode?: TrimSilenceMode;
1684
+ gateLufs?: number;
1685
+ windowMs?: number;
1686
+ }
1687
+
1688
+ /** Offline LPC-based declipper. */
1689
+ export function masteringRepairDeclip(
1690
+ samples: Float32Array,
1691
+ sampleRate: number,
1692
+ options: DeclipOptions = {},
1693
+ ): Float32Array {
1694
+ return requireModule().masteringRepairDeclip(samples, sampleRate, options);
1695
+ }
1696
+
1697
+ /** Offline crackle suppressor (median or wavelet-shrinkage). */
1698
+ export function masteringRepairDecrackle(
1699
+ samples: Float32Array,
1700
+ sampleRate: number,
1701
+ options: DecrackleOptions = {},
1702
+ ): Float32Array {
1703
+ return requireModule().masteringRepairDecrackle(samples, sampleRate, options);
1704
+ }
1705
+
1706
+ /** Offline mains-hum remover. */
1707
+ export function masteringRepairDehum(
1708
+ samples: Float32Array,
1709
+ sampleRate: number,
1710
+ options: DehumOptions = {},
1711
+ ): Float32Array {
1712
+ return requireModule().masteringRepairDehum(samples, sampleRate, options);
1713
+ }
1714
+
1715
+ /** Offline classical dereverberator (spectral subtraction + optional WPE). */
1716
+ export function masteringRepairDereverbClassical(
1717
+ samples: Float32Array,
1718
+ sampleRate: number,
1719
+ options: DereverbClassicalOptions = {},
1720
+ ): Float32Array {
1721
+ return requireModule().masteringRepairDereverbClassical(samples, sampleRate, options);
1722
+ }
1723
+
1724
+ /** Offline silence trimmer (peak threshold or LUFS-gated). */
1725
+ export function masteringRepairTrimSilence(
1726
+ samples: Float32Array,
1727
+ sampleRate: number,
1728
+ options: TrimSilenceOptions = {},
1729
+ ): Float32Array {
1730
+ return requireModule().masteringRepairTrimSilence(samples, sampleRate, options);
1731
+ }
1732
+
1733
+ // ============================================================================
1734
+ // Mastering — offline dynamics processors (compressor / gate / transient_shaper)
1735
+ // ============================================================================
1736
+
1737
+ /** Compressor sidechain detector mode. */
1738
+ export type CompressorDetector = 'peak' | 'rms' | 'log_rms';
1739
+
1740
+ /** Options for `masteringDynamicsCompressor`. */
1741
+ export interface CompressorOptions extends ValidateOptions {
1742
+ thresholdDb?: number;
1743
+ ratio?: number;
1744
+ attackMs?: number;
1745
+ releaseMs?: number;
1746
+ kneeDb?: number;
1747
+ makeupGainDb?: number;
1748
+ autoMakeup?: boolean;
1749
+ detector?: CompressorDetector | number;
1750
+ sidechainHpfEnabled?: boolean;
1751
+ sidechainHpfHz?: number;
1752
+ pdrTimeMs?: number;
1753
+ pdrReleaseScale?: number;
1754
+ }
1755
+
1756
+ /** Options for `masteringDynamicsGate`. */
1757
+ export interface GateOptions extends ValidateOptions {
1758
+ thresholdDb?: number;
1759
+ attackMs?: number;
1760
+ releaseMs?: number;
1761
+ rangeDb?: number;
1762
+ holdMs?: number;
1763
+ closeThresholdDb?: number;
1764
+ keyHpfHz?: number;
1765
+ }
1766
+
1767
+ /** Options for `masteringDynamicsTransientShaper`. */
1768
+ export interface TransientShaperOptions extends ValidateOptions {
1769
+ attackGainDb?: number;
1770
+ sustainGainDb?: number;
1771
+ fastAttackMs?: number;
1772
+ fastReleaseMs?: number;
1773
+ slowAttackMs?: number;
1774
+ slowReleaseMs?: number;
1775
+ sensitivity?: number;
1776
+ maxGainDb?: number;
1777
+ gainSmoothingMs?: number;
1778
+ lookaheadMs?: number;
1779
+ }
1780
+
1781
+ /** Result envelope returned by offline mastering dynamics processors. */
1782
+ export interface DynamicsResult {
1783
+ samples: Float32Array;
1784
+ latencySamples: number;
1785
+ }
1786
+
1787
+ const COMPRESSOR_DETECTOR_MAP: Record<CompressorDetector, number> = {
1788
+ peak: 0,
1789
+ rms: 1,
1790
+ log_rms: 2,
1791
+ };
1792
+
1793
+ /** Offline feed-forward compressor (soft knee, optional auto-makeup / sidechain HPF). */
1794
+ export function masteringDynamicsCompressor(
1795
+ samples: Float32Array,
1796
+ sampleRate: number,
1797
+ options: CompressorOptions = {},
1798
+ ): DynamicsResult {
1799
+ assertSamples('masteringDynamicsCompressor', samples, options.validate !== false);
1800
+ const detector =
1801
+ typeof options.detector === 'string'
1802
+ ? COMPRESSOR_DETECTOR_MAP[options.detector]
1803
+ : options.detector;
1804
+ const opts: Record<string, unknown> = { ...options };
1805
+ if (detector !== undefined) {
1806
+ opts.detector = detector;
1807
+ }
1808
+ return requireModule().masteringDynamicsCompressor(samples, sampleRate, opts);
1809
+ }
1810
+
1811
+ /** Offline noise gate (hysteresis, hold, optional key HPF). */
1812
+ export function masteringDynamicsGate(
1813
+ samples: Float32Array,
1814
+ sampleRate: number,
1815
+ options: GateOptions = {},
1816
+ ): DynamicsResult {
1817
+ assertSamples('masteringDynamicsGate', samples, options.validate !== false);
1818
+ return requireModule().masteringDynamicsGate(samples, sampleRate, options);
1819
+ }
1820
+
1821
+ /** Offline transient shaper (envelope-difference attack/sustain control). */
1822
+ export function masteringDynamicsTransientShaper(
1823
+ samples: Float32Array,
1824
+ sampleRate: number,
1825
+ options: TransientShaperOptions = {},
1826
+ ): DynamicsResult {
1827
+ assertSamples('masteringDynamicsTransientShaper', samples, options.validate !== false);
1828
+ return requireModule().masteringDynamicsTransientShaper(samples, sampleRate, options);
1829
+ }
1830
+
1831
+ /**
1832
+ * Apply a configurable mastering chain in WASM.
1833
+ *
1834
+ * @param samples - Audio samples (mono, float32)
1835
+ * @param sampleRate - Sample rate in Hz (default: 22050)
1836
+ * @param config - Chain stage configuration
1837
+ * @returns Processed audio, loudness metadata, and applied stage names
1838
+ */
1839
+ export function masteringChain(
1840
+ samples: Float32Array,
1841
+ sampleRate = 22050,
1842
+ config: MasteringChainConfig,
1843
+ ): MasteringChainResult {
1844
+ if (!module) {
1845
+ throw new Error('Module not initialized. Call init() first.');
1846
+ }
1847
+ return module.masteringChain(samples, sampleRate, config as Record<string, unknown>);
1848
+ }
1849
+
1850
+ /**
1851
+ * Apply a configurable stereo mastering chain in WASM.
1852
+ *
1853
+ * @param left - Left channel samples
1854
+ * @param right - Right channel samples
1294
1855
  * @param sampleRate - Sample rate in Hz
1295
1856
  * @param config - Chain stage configuration
1857
+ * @returns Processed stereo audio, loudness metadata, and applied stage names
1858
+ */
1859
+ export function masteringChainStereo(
1860
+ left: Float32Array,
1861
+ right: Float32Array,
1862
+ sampleRate = 22050,
1863
+ config: MasteringChainConfig,
1864
+ ): MasteringStereoChainResult {
1865
+ if (!module) {
1866
+ throw new Error('Module not initialized. Call init() first.');
1867
+ }
1868
+ if (left.length !== right.length) {
1869
+ throw new Error('Stereo channel lengths must match.');
1870
+ }
1871
+ return module.masteringChainStereo(left, right, sampleRate, config as Record<string, unknown>);
1872
+ }
1873
+
1874
+ /**
1875
+ * Apply a configurable mastering chain in WASM with progress reporting.
1876
+ *
1877
+ * @param samples - Audio samples (mono, float32)
1878
+ * @param sampleRate - Sample rate in Hz (default: 22050)
1879
+ * @param config - Chain stage configuration
1296
1880
  * @param onProgress - Progress callback (progress: 0-1, stage: string)
1297
1881
  * @returns Processed audio, loudness metadata, and applied stage names
1298
1882
  */
1299
1883
  export function masteringChainWithProgress(
1300
1884
  samples: Float32Array,
1301
- sampleRate: number,
1885
+ sampleRate = 22050,
1302
1886
  config: MasteringChainConfig,
1303
1887
  onProgress: ProgressCallback,
1304
1888
  ): MasteringChainResult {
@@ -1326,7 +1910,7 @@ export function masteringChainWithProgress(
1326
1910
  export function masteringChainStereoWithProgress(
1327
1911
  left: Float32Array,
1328
1912
  right: Float32Array,
1329
- sampleRate: number,
1913
+ sampleRate = 22050,
1330
1914
  config: MasteringChainConfig,
1331
1915
  onProgress: ProgressCallback,
1332
1916
  ): MasteringStereoChainResult {
@@ -1361,14 +1945,14 @@ export function masteringPresetNames(): MasteringPreset[] {
1361
1945
  * Apply a named mastering preset chain to mono audio.
1362
1946
  *
1363
1947
  * @param samples - Audio samples (mono, float32)
1364
- * @param sampleRate - Sample rate in Hz
1948
+ * @param sampleRate - Sample rate in Hz (default: 22050)
1365
1949
  * @param presetName - Preset identifier from {@link masteringPresetNames}
1366
1950
  * @param overrides - Optional flat overrides (dot-notation, e.g. `'loudness.targetLufs'`) applied on top of the preset. Pass `null` for preset defaults.
1367
1951
  * @returns Processed audio, loudness metadata, and applied stage names
1368
1952
  */
1369
1953
  export function masterAudio(
1370
1954
  samples: Float32Array,
1371
- sampleRate: number,
1955
+ sampleRate = 22050,
1372
1956
  presetName: MasteringPreset,
1373
1957
  overrides: Record<string, number | boolean> | null = null,
1374
1958
  ): MasteringChainResult {
@@ -1391,7 +1975,7 @@ export function masterAudio(
1391
1975
  export function masterAudioStereo(
1392
1976
  left: Float32Array,
1393
1977
  right: Float32Array,
1394
- sampleRate: number,
1978
+ sampleRate = 22050,
1395
1979
  presetName: MasteringPreset,
1396
1980
  overrides: Record<string, number | boolean> | null = null,
1397
1981
  ): MasteringStereoChainResult {
@@ -1404,6 +1988,50 @@ export function masterAudioStereo(
1404
1988
  return module.masterAudioStereo(presetName, left, right, sampleRate, overrides);
1405
1989
  }
1406
1990
 
1991
+ /**
1992
+ * Mono `masterAudio` with per-stage progress reporting. `onProgress` is invoked
1993
+ * with `(progress, stage)` between each chain stage (progress is in [0,1]).
1994
+ */
1995
+ export function masterAudioWithProgress(
1996
+ samples: Float32Array,
1997
+ sampleRate = 22050,
1998
+ presetName: MasteringPreset,
1999
+ onProgress: ProgressCallback,
2000
+ overrides: Record<string, number | boolean> | null = null,
2001
+ ): MasteringChainResult {
2002
+ if (!module) {
2003
+ throw new Error('Module not initialized. Call init() first.');
2004
+ }
2005
+ return module.masterAudioWithProgress(presetName, samples, sampleRate, overrides, onProgress);
2006
+ }
2007
+
2008
+ /**
2009
+ * Stereo `masterAudio` with per-stage progress reporting.
2010
+ */
2011
+ export function masterAudioStereoWithProgress(
2012
+ left: Float32Array,
2013
+ right: Float32Array,
2014
+ sampleRate = 22050,
2015
+ presetName: MasteringPreset,
2016
+ onProgress: ProgressCallback,
2017
+ overrides: Record<string, number | boolean> | null = null,
2018
+ ): MasteringStereoChainResult {
2019
+ if (!module) {
2020
+ throw new Error('Module not initialized. Call init() first.');
2021
+ }
2022
+ if (left.length !== right.length) {
2023
+ throw new Error('Stereo channel lengths must match.');
2024
+ }
2025
+ return module.masterAudioStereoWithProgress(
2026
+ presetName,
2027
+ left,
2028
+ right,
2029
+ sampleRate,
2030
+ overrides,
2031
+ onProgress,
2032
+ );
2033
+ }
2034
+
1407
2035
  export function mixingScenePresetNames(): string[] {
1408
2036
  if (!module) {
1409
2037
  throw new Error('Module not initialized. Call init() first.');
@@ -1416,14 +2044,14 @@ export function mixingScenePresetNames(): string[] {
1416
2044
  * name shared with the Node and Python bindings; the returned JSON loads
1417
2045
  * directly into a {@link Mixer} via {@link Mixer.fromSceneJson}.
1418
2046
  *
1419
- * @param preset - Preset name (see {@link mixingScenePresetNames})
2047
+ * @param presetName - Preset name (see {@link mixingScenePresetNames})
1420
2048
  * @returns Scene JSON string
1421
2049
  */
1422
- export function mixingScenePresetJson(preset: string): string {
2050
+ export function mixingScenePresetJson(presetName: string): string {
1423
2051
  if (!module) {
1424
2052
  throw new Error('Module not initialized. Call init() first.');
1425
2053
  }
1426
- return module.mixingScenePresetJson(preset);
2054
+ return module.mixingScenePresetJson(presetName);
1427
2055
  }
1428
2056
 
1429
2057
  export function mixStereo(
@@ -1472,7 +2100,7 @@ export function mixStereo(
1472
2100
  * ```
1473
2101
  */
1474
2102
  export class StreamingMasteringChain {
1475
- private chain: import('./wasm_types').WasmStreamingMasteringChain;
2103
+ private chain: import('./sonare.js').WasmStreamingMasteringChain;
1476
2104
 
1477
2105
  constructor(config: MasteringChainConfig) {
1478
2106
  if (!module) {
@@ -1559,7 +2187,7 @@ export class StreamingMasteringChain {
1559
2187
  * ```
1560
2188
  */
1561
2189
  export class StreamingEqualizer {
1562
- private eq: import('./wasm_types').WasmStreamingEqualizer;
2190
+ private eq: import('./sonare.js').WasmStreamingEqualizer;
1563
2191
 
1564
2192
  constructor(config: StreamingEqualizerConfig = {}) {
1565
2193
  if (!module) {
@@ -1697,7 +2325,7 @@ export class StreamingEqualizer {
1697
2325
  * the underlying WASM object.
1698
2326
  */
1699
2327
  export class StreamingRetune {
1700
- private retune: import('./wasm_types').WasmStreamingRetune;
2328
+ private retune: import('./sonare.js').WasmStreamingRetune;
1701
2329
 
1702
2330
  constructor(config: StreamingRetuneConfig = {}) {
1703
2331
  if (!module) {
@@ -1749,32 +2377,207 @@ export class StreamingRetune {
1749
2377
  }
1750
2378
 
1751
2379
  // ============================================================================
1752
- // Mixer Class (scene-based persistent mixer)
2380
+ // RealtimeVoiceChanger Class
1753
2381
  // ============================================================================
1754
2382
 
1755
- /**
1756
- * Get a built-in mixing scene preset serialized as JSON, normalized through the
1757
- * C mixer API (the same path {@link Mixer.fromSceneJson} uses to load it).
1758
- *
1759
- * @deprecated Use {@link mixingScenePresetJson}, the canonical name shared with
1760
- * the Node and Python bindings. This alias is retained for backwards
1761
- * compatibility and may be removed in a future release. Both functions return a
1762
- * scene JSON string that loads cleanly into a {@link Mixer}.
1763
- *
1764
- * @param preset - Preset name (see {@link mixingScenePresetNames})
1765
- * @returns Scene JSON string
1766
- */
1767
- export function mixerScenePresetJson(preset: string): string {
2383
+ export class RealtimeVoiceChanger {
2384
+ private changer: import('./sonare.js').WasmRealtimeVoiceChanger;
2385
+
2386
+ constructor(config: RealtimeVoiceChangerConfigInput = 'neutral-monitor') {
2387
+ if (!module) {
2388
+ throw new Error('Module not initialized. Call init() first.');
2389
+ }
2390
+ this.changer = module.createRealtimeVoiceChanger(config as Record<string, unknown> | string);
2391
+ }
2392
+
2393
+ prepare(sampleRate: number, maxBlockSize = 128, channels = 1): void {
2394
+ this.changer.prepare(sampleRate, maxBlockSize, channels);
2395
+ }
2396
+
2397
+ reset(): void {
2398
+ this.changer.reset();
2399
+ }
2400
+
2401
+ setConfig(config: RealtimeVoiceChangerConfigInput): void {
2402
+ this.changer.setConfig(config as Record<string, unknown> | string);
2403
+ }
2404
+
2405
+ configJson(): string {
2406
+ return this.changer.configJson();
2407
+ }
2408
+
2409
+ latencySamples(): number {
2410
+ return this.changer.latencySamples();
2411
+ }
2412
+
2413
+ processMono(samples: Float32Array): Float32Array {
2414
+ return this.changer.processMono(samples);
2415
+ }
2416
+
2417
+ processMonoInto(samples: Float32Array, output: Float32Array): void {
2418
+ this.changer.processMonoInto(samples, output);
2419
+ }
2420
+
2421
+ processInterleaved(samples: Float32Array, channels: number): Float32Array {
2422
+ return this.changer.processInterleaved(samples, channels);
2423
+ }
2424
+
2425
+ processInterleavedInto(samples: Float32Array, channels: number, output: Float32Array): void {
2426
+ this.changer.processInterleavedInto(samples, channels, output);
2427
+ }
2428
+
2429
+ /**
2430
+ * Acquire a typed-memory view onto the WASM heap for mono input.
2431
+ *
2432
+ * Write your input samples into the returned `Float32Array` directly (e.g.
2433
+ * via `input.set(source)`); no copy crosses the JS↔C++ bridge until
2434
+ * {@link processPreparedMono} is called. The view is owned by this
2435
+ * RealtimeVoiceChanger and becomes invalid after {@link delete}; it may
2436
+ * also be invalidated if you later call this method with a larger
2437
+ * `numSamples` value (the underlying buffer may be reallocated).
2438
+ */
2439
+ getMonoInputBuffer(numSamples: number): Float32Array {
2440
+ return this.changer.getMonoInputBuffer(numSamples);
2441
+ }
2442
+
2443
+ /** Mono output view counterpart to {@link getMonoInputBuffer}. */
2444
+ getMonoOutputBuffer(numSamples: number): Float32Array {
2445
+ return this.changer.getMonoOutputBuffer(numSamples);
2446
+ }
2447
+
2448
+ /**
2449
+ * Process the previously-acquired mono input buffer in place. The output
2450
+ * appears in the buffer returned by {@link getMonoOutputBuffer}. No JS↔C++
2451
+ * sample-level crossings happen on this call — it just hands control to
2452
+ * the underlying DSP on already-on-heap data.
2453
+ */
2454
+ processPreparedMono(numSamples: number): void {
2455
+ this.changer.processPreparedMono(numSamples);
2456
+ }
2457
+
2458
+ /** Interleaved input view (layout L0,R0,L1,R1,...). */
2459
+ getInterleavedInputBuffer(numFrames: number, numChannels: number): Float32Array {
2460
+ return this.changer.getInterleavedInputBuffer(numFrames, numChannels);
2461
+ }
2462
+
2463
+ /** Interleaved output view counterpart. */
2464
+ getInterleavedOutputBuffer(numFrames: number, numChannels: number): Float32Array {
2465
+ return this.changer.getInterleavedOutputBuffer(numFrames, numChannels);
2466
+ }
2467
+
2468
+ /**
2469
+ * Process the previously-acquired interleaved buffer in place. Output
2470
+ * appears in the buffer returned by {@link getInterleavedOutputBuffer}.
2471
+ */
2472
+ processPreparedInterleaved(numFrames: number, numChannels: number): void {
2473
+ this.changer.processPreparedInterleaved(numFrames, numChannels);
2474
+ }
2475
+
2476
+ /**
2477
+ * Planar-channel input/output view (one Float32Array per channel). Matches
2478
+ * AudioWorklet's native layout; processing happens in place.
2479
+ */
2480
+ getPlanarChannelBuffer(channel: number, numFrames: number): Float32Array {
2481
+ return this.changer.getPlanarChannelBuffer(channel, numFrames);
2482
+ }
2483
+
2484
+ /**
2485
+ * Process the previously-acquired planar channel buffers in place. Each
2486
+ * channel must have been obtained from {@link getPlanarChannelBuffer}
2487
+ * with the same `numFrames`. Output replaces input in the same buffers.
2488
+ */
2489
+ processPreparedPlanar(numFrames: number): void {
2490
+ this.changer.processPreparedPlanar(numFrames);
2491
+ }
2492
+
2493
+ /**
2494
+ * Convenience factory for the mono zero-copy path: returns the input/output
2495
+ * heap views plus a `process()` thunk wired to the same `numSamples`. The
2496
+ * views are reused across calls and become invalid after {@link delete}.
2497
+ */
2498
+ createRealtimeMonoBuffer(numSamples: number): RealtimeVoiceChangerMonoBuffer {
2499
+ const input = this.getMonoInputBuffer(numSamples);
2500
+ const output = this.getMonoOutputBuffer(numSamples);
2501
+ return {
2502
+ input,
2503
+ output,
2504
+ process: () => this.processPreparedMono(numSamples),
2505
+ };
2506
+ }
2507
+
2508
+ /** Same as {@link createRealtimeMonoBuffer} but for interleaved I/O. */
2509
+ createRealtimeInterleavedBuffer(
2510
+ numFrames: number,
2511
+ numChannels: number,
2512
+ ): RealtimeVoiceChangerInterleavedBuffer {
2513
+ const input = this.getInterleavedInputBuffer(numFrames, numChannels);
2514
+ const output = this.getInterleavedOutputBuffer(numFrames, numChannels);
2515
+ return {
2516
+ input,
2517
+ output,
2518
+ channels: numChannels,
2519
+ process: () => this.processPreparedInterleaved(numFrames, numChannels),
2520
+ };
2521
+ }
2522
+
2523
+ /**
2524
+ * Convenience factory for the planar zero-copy path. Acquires one
2525
+ * heap-backed Float32Array per channel and returns a `process()` thunk
2526
+ * wired to the same `numFrames`. Buffers are reused across calls and
2527
+ * become invalid after {@link delete}.
2528
+ */
2529
+ createRealtimePlanarBuffer(
2530
+ numFrames: number,
2531
+ numChannels: number,
2532
+ ): RealtimeVoiceChangerPlanarBuffer {
2533
+ const channels: Float32Array[] = [];
2534
+ for (let ch = 0; ch < numChannels; ch++) {
2535
+ channels.push(this.getPlanarChannelBuffer(ch, numFrames));
2536
+ }
2537
+ return {
2538
+ channels,
2539
+ process: () => this.processPreparedPlanar(numFrames),
2540
+ };
2541
+ }
2542
+
2543
+ delete(): void {
2544
+ this.changer.delete();
2545
+ }
2546
+ }
2547
+
2548
+ export function realtimeVoiceChangerPresetNames(): VoicePresetId[] {
1768
2549
  if (!module) {
1769
2550
  throw new Error('Module not initialized. Call init() first.');
1770
2551
  }
1771
- return module.mixerPresetJson(preset);
2552
+ return module.realtimeVoiceChangerPresetNames() as VoicePresetId[];
1772
2553
  }
1773
2554
 
2555
+ export function realtimeVoiceChangerPresetJson(name: VoicePresetId): string {
2556
+ if (!module) {
2557
+ throw new Error('Module not initialized. Call init() first.');
2558
+ }
2559
+ return module.realtimeVoiceChangerPresetJson(name);
2560
+ }
2561
+
2562
+ export function validateRealtimeVoiceChangerPresetJson(json: string): {
2563
+ ok: boolean;
2564
+ normalizedJson?: string;
2565
+ error?: string;
2566
+ } {
2567
+ if (!module) {
2568
+ throw new Error('Module not initialized. Call init() first.');
2569
+ }
2570
+ return module.validateRealtimeVoiceChangerPresetJson(json);
2571
+ }
2572
+
2573
+ // ============================================================================
2574
+ // Mixer Class (scene-based persistent mixer)
2575
+ // ============================================================================
2576
+
1774
2577
  /**
1775
2578
  * Persistent, scene-based stereo mixer.
1776
2579
  *
1777
- * Build one from a scene JSON string (e.g. {@link mixerScenePresetJson} or a
2580
+ * Build one from a scene JSON string (e.g. {@link mixingScenePresetJson} or a
1778
2581
  * hand-authored scene), then feed per-strip stereo blocks through
1779
2582
  * {@link processStereo} to get the routed stereo master. Strips, sends, buses,
1780
2583
  * and inserts are described entirely by the scene; the routing graph is
@@ -1786,7 +2589,7 @@ export function mixerScenePresetJson(preset: string): string {
1786
2589
  *
1787
2590
  * @example
1788
2591
  * ```typescript
1789
- * const mixer = Mixer.fromSceneJson(mixerScenePresetJson('basicStereo'), 48000, 512);
2592
+ * const mixer = Mixer.fromSceneJson(mixingScenePresetJson('basicStereo'), 48000, 512);
1790
2593
  * try {
1791
2594
  * const out = mixer.processStereo([stripL], [stripR]);
1792
2595
  * } finally {
@@ -1795,9 +2598,9 @@ export function mixerScenePresetJson(preset: string): string {
1795
2598
  * ```
1796
2599
  */
1797
2600
  export class Mixer {
1798
- private mixer: import('./wasm_types').WasmMixer;
2601
+ private mixer: import('./sonare.js').WasmMixer;
1799
2602
 
1800
- private constructor(mixer: import('./wasm_types').WasmMixer) {
2603
+ private constructor(mixer: import('./sonare.js').WasmMixer) {
1801
2604
  this.mixer = mixer;
1802
2605
  }
1803
2606
 
@@ -1966,6 +2769,31 @@ export class Mixer {
1966
2769
  return this.mixer.vcaGroupCount();
1967
2770
  }
1968
2771
 
2772
+ /** Set the strip's input trim in dB. */
2773
+ setInputTrimDb(stripIndex: number, db: number): void {
2774
+ this.mixer.setInputTrimDb(stripIndex, db);
2775
+ }
2776
+
2777
+ /** Set the strip's fader level in dB. */
2778
+ setFaderDb(stripIndex: number, db: number): void {
2779
+ this.mixer.setFaderDb(stripIndex, db);
2780
+ }
2781
+
2782
+ /** Set the strip's pan position. */
2783
+ setPan(stripIndex: number, pan: number, panMode: PanMode | number = 0): void {
2784
+ this.mixer.setPan(stripIndex, pan, panModeCode(panMode));
2785
+ }
2786
+
2787
+ /** Set the strip's stereo width. */
2788
+ setWidth(stripIndex: number, width: number): void {
2789
+ this.mixer.setWidth(stripIndex, width);
2790
+ }
2791
+
2792
+ /** Set the strip's mute state. */
2793
+ setMuted(stripIndex: number, muted: boolean): void {
2794
+ this.mixer.setMuted(stripIndex, muted);
2795
+ }
2796
+
1969
2797
  /**
1970
2798
  * Set a strip's solo state. Takes effect on the next process without a
1971
2799
  * graph recompile.
@@ -2179,14 +3007,14 @@ export function trim(samples: Float32Array, sampleRate: number, thresholdDb = -6
2179
3007
  * Compute Short-Time Fourier Transform (STFT).
2180
3008
  *
2181
3009
  * @param samples - Audio samples (mono, float32)
2182
- * @param sampleRate - Sample rate in Hz
3010
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2183
3011
  * @param nFft - FFT size (default: 2048)
2184
3012
  * @param hopLength - Hop length (default: 512)
2185
3013
  * @returns STFT result with magnitude and power spectrograms
2186
3014
  */
2187
3015
  export function stft(
2188
3016
  samples: Float32Array,
2189
- sampleRate: number,
3017
+ sampleRate = 22050,
2190
3018
  nFft = 2048,
2191
3019
  hopLength = 512,
2192
3020
  ): StftResult {
@@ -2200,14 +3028,14 @@ export function stft(
2200
3028
  * Compute STFT and return magnitude in decibels.
2201
3029
  *
2202
3030
  * @param samples - Audio samples (mono, float32)
2203
- * @param sampleRate - Sample rate in Hz
3031
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2204
3032
  * @param nFft - FFT size (default: 2048)
2205
3033
  * @param hopLength - Hop length (default: 512)
2206
3034
  * @returns STFT result with dB values
2207
3035
  */
2208
3036
  export function stftDb(
2209
3037
  samples: Float32Array,
2210
- sampleRate: number,
3038
+ sampleRate = 22050,
2211
3039
  nFft = 2048,
2212
3040
  hopLength = 512,
2213
3041
  ): { nBins: number; nFrames: number; db: Float32Array } {
@@ -2225,7 +3053,7 @@ export function stftDb(
2225
3053
  * Compute Mel spectrogram.
2226
3054
  *
2227
3055
  * @param samples - Audio samples (mono, float32)
2228
- * @param sampleRate - Sample rate in Hz
3056
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2229
3057
  * @param nFft - FFT size (default: 2048)
2230
3058
  * @param hopLength - Hop length (default: 512)
2231
3059
  * @param nMels - Number of Mel bands (default: 128)
@@ -2233,7 +3061,7 @@ export function stftDb(
2233
3061
  */
2234
3062
  export function melSpectrogram(
2235
3063
  samples: Float32Array,
2236
- sampleRate: number,
3064
+ sampleRate = 22050,
2237
3065
  nFft = 2048,
2238
3066
  hopLength = 512,
2239
3067
  nMels = 128,
@@ -2248,20 +3076,20 @@ export function melSpectrogram(
2248
3076
  * Compute MFCC (Mel-Frequency Cepstral Coefficients).
2249
3077
  *
2250
3078
  * @param samples - Audio samples (mono, float32)
2251
- * @param sampleRate - Sample rate in Hz
3079
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2252
3080
  * @param nFft - FFT size (default: 2048)
2253
3081
  * @param hopLength - Hop length (default: 512)
2254
3082
  * @param nMels - Number of Mel bands (default: 128)
2255
- * @param nMfcc - Number of MFCC coefficients (default: 13)
3083
+ * @param nMfcc - Number of MFCC coefficients (default: 20)
2256
3084
  * @returns MFCC result
2257
3085
  */
2258
3086
  export function mfcc(
2259
3087
  samples: Float32Array,
2260
- sampleRate: number,
3088
+ sampleRate = 22050,
2261
3089
  nFft = 2048,
2262
3090
  hopLength = 512,
2263
3091
  nMels = 128,
2264
- nMfcc = 13,
3092
+ nMfcc = 20,
2265
3093
  ): MfccResult {
2266
3094
  if (!module) {
2267
3095
  throw new Error('Module not initialized. Call init() first.');
@@ -2282,23 +3110,23 @@ export function mfcc(
2282
3110
  * @param nFrames - Number of time frames
2283
3111
  * @param sampleRate - Sample rate in Hz
2284
3112
  * @param nFft - FFT size (default: 2048)
2285
- * @param hopLength - Hop length (default: 512)
3113
+ * @param fmin - Lower Mel band edge in Hz (default: 0)
3114
+ * @param fmax - Upper Mel band edge in Hz (default: sr/2 when 0)
2286
3115
  * @returns STFT power spectrogram result
2287
3116
  */
2288
3117
  export function melToStft(
2289
3118
  melPower: Float32Array,
2290
3119
  nMels: number,
2291
3120
  nFrames: number,
2292
- sampleRate: number,
3121
+ sampleRate = 22050,
2293
3122
  nFft = 2048,
2294
- hopLength = 512,
2295
3123
  fmin = 0,
2296
3124
  fmax = 0,
2297
3125
  ): StftPowerResult {
2298
3126
  if (!module) {
2299
3127
  throw new Error('Module not initialized. Call init() first.');
2300
3128
  }
2301
- return module.melToStft(melPower, nMels, nFrames, sampleRate, nFft, hopLength, fmin, fmax);
3129
+ return module.melToStft(melPower, nMels, nFrames, sampleRate, nFft, fmin, fmax);
2302
3130
  }
2303
3131
 
2304
3132
  /**
@@ -2311,6 +3139,8 @@ export function melToStft(
2311
3139
  * @param sampleRate - Sample rate in Hz
2312
3140
  * @param nFft - FFT size (default: 2048)
2313
3141
  * @param hopLength - Hop length (default: 512)
3142
+ * @param fmin - Minimum Mel frequency in Hz (default: 0)
3143
+ * @param fmax - Maximum Mel frequency in Hz (default: 0 = sr/2)
2314
3144
  * @param nIter - Griffin-Lim iterations (default: 32)
2315
3145
  * @returns Reconstructed audio samples (mono, float32)
2316
3146
  */
@@ -2318,12 +3148,12 @@ export function melToAudio(
2318
3148
  melPower: Float32Array,
2319
3149
  nMels: number,
2320
3150
  nFrames: number,
2321
- sampleRate: number,
3151
+ sampleRate = 22050,
2322
3152
  nFft = 2048,
2323
3153
  hopLength = 512,
2324
- nIter = 32,
2325
3154
  fmin = 0,
2326
3155
  fmax = 0,
3156
+ nIter = 32,
2327
3157
  ): Float32Array {
2328
3158
  if (!module) {
2329
3159
  throw new Error('Module not initialized. Call init() first.');
@@ -2335,9 +3165,9 @@ export function melToAudio(
2335
3165
  sampleRate,
2336
3166
  nFft,
2337
3167
  hopLength,
2338
- nIter,
2339
3168
  fmin,
2340
3169
  fmax,
3170
+ nIter,
2341
3171
  );
2342
3172
  }
2343
3173
 
@@ -2360,114 +3190,318 @@ export function mfccToMel(
2360
3190
  if (!module) {
2361
3191
  throw new Error('Module not initialized. Call init() first.');
2362
3192
  }
2363
- return module.mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels);
3193
+ return module.mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels);
3194
+ }
3195
+
3196
+ /**
3197
+ * Reconstruct audio directly from MFCC coefficients via Griffin-Lim. Mirrors
3198
+ * `feature::mfcc_to_audio`.
3199
+ *
3200
+ * @param mfccCoefficients - MFCC matrix [nMfcc x nFrames] row-major
3201
+ * @param nMfcc - Number of MFCC coefficients
3202
+ * @param nFrames - Number of time frames
3203
+ * @param nMels - Number of Mel bins (default: 128)
3204
+ * @param sampleRate - Sample rate in Hz (default: 22050)
3205
+ * @param nFft - FFT size (default: 2048)
3206
+ * @param hopLength - Hop length (default: 512)
3207
+ * @param fmin - Minimum Mel frequency in Hz (default: 0)
3208
+ * @param fmax - Maximum Mel frequency in Hz (default: 0 = sr/2)
3209
+ * @param nIter - Griffin-Lim iterations (default: 32)
3210
+ * @returns Reconstructed audio samples (mono, float32)
3211
+ */
3212
+ export function mfccToAudio(
3213
+ mfccCoefficients: Float32Array,
3214
+ nMfcc: number,
3215
+ nFrames: number,
3216
+ nMels = 128,
3217
+ sampleRate = 22050,
3218
+ nFft = 2048,
3219
+ hopLength = 512,
3220
+ fmin = 0,
3221
+ fmax = 0,
3222
+ nIter = 32,
3223
+ ): Float32Array {
3224
+ if (!module) {
3225
+ throw new Error('Module not initialized. Call init() first.');
3226
+ }
3227
+ return module.mfccToAudio(
3228
+ mfccCoefficients,
3229
+ nMfcc,
3230
+ nFrames,
3231
+ nMels,
3232
+ sampleRate,
3233
+ nFft,
3234
+ hopLength,
3235
+ fmin,
3236
+ fmax,
3237
+ nIter,
3238
+ );
3239
+ }
3240
+
3241
+ // ============================================================================
3242
+ // Features - Chroma
3243
+ // ============================================================================
3244
+
3245
+ /**
3246
+ * Compute chromagram (pitch class distribution).
3247
+ *
3248
+ * @param samples - Audio samples (mono, float32)
3249
+ * @param sampleRate - Sample rate in Hz (default: 22050)
3250
+ * @param nFft - FFT size (default: 2048)
3251
+ * @param hopLength - Hop length (default: 512)
3252
+ * @returns Chroma features result
3253
+ */
3254
+ export function chroma(
3255
+ samples: Float32Array,
3256
+ sampleRate = 22050,
3257
+ nFft = 2048,
3258
+ hopLength = 512,
3259
+ ): ChromaResult {
3260
+ if (!module) {
3261
+ throw new Error('Module not initialized. Call init() first.');
3262
+ }
3263
+ return module.chroma(samples, sampleRate, nFft, hopLength);
3264
+ }
3265
+
3266
+ // ============================================================================
3267
+ // Features - Spectral
3268
+ // ============================================================================
3269
+
3270
+ /**
3271
+ * Compute spectral centroid (center of mass of spectrum).
3272
+ *
3273
+ * @param samples - Audio samples (mono, float32)
3274
+ * @param sampleRate - Sample rate in Hz (default: 22050)
3275
+ * @param nFft - FFT size (default: 2048)
3276
+ * @param hopLength - Hop length (default: 512)
3277
+ * @returns Spectral centroid in Hz for each frame
3278
+ */
3279
+ export function spectralCentroid(
3280
+ samples: Float32Array,
3281
+ sampleRate = 22050,
3282
+ nFft = 2048,
3283
+ hopLength = 512,
3284
+ ): Float32Array {
3285
+ if (!module) {
3286
+ throw new Error('Module not initialized. Call init() first.');
3287
+ }
3288
+ return module.spectralCentroid(samples, sampleRate, nFft, hopLength);
3289
+ }
3290
+
3291
+ /**
3292
+ * Compute spectral contrast (librosa.feature.spectral_contrast).
3293
+ *
3294
+ * @returns Matrix2d of shape (nBands + 1) x nFrames.
3295
+ */
3296
+ export function spectralContrast(
3297
+ samples: Float32Array,
3298
+ sampleRate = 22050,
3299
+ nFft = 2048,
3300
+ hopLength = 512,
3301
+ nBands = 6,
3302
+ fmin = 200.0,
3303
+ quantile = 0.02,
3304
+ ): WasmMatrix2dResult {
3305
+ if (!module) {
3306
+ throw new Error('Module not initialized. Call init() first.');
3307
+ }
3308
+ return module.spectralContrast(samples, sampleRate, nFft, hopLength, nBands, fmin, quantile);
3309
+ }
3310
+
3311
+ /**
3312
+ * Fit per-frame polynomial coefficients (librosa.feature.poly_features).
3313
+ *
3314
+ * @returns Matrix2d of shape (order + 1) x nFrames.
3315
+ */
3316
+ export function polyFeatures(
3317
+ samples: Float32Array,
3318
+ sampleRate = 22050,
3319
+ nFft = 2048,
3320
+ hopLength = 512,
3321
+ order = 1,
3322
+ ): WasmMatrix2dResult {
3323
+ if (!module) {
3324
+ throw new Error('Module not initialized. Call init() first.');
3325
+ }
3326
+ return module.polyFeatures(samples, sampleRate, nFft, hopLength, order);
3327
+ }
3328
+
3329
+ /**
3330
+ * Locate zero-crossing indices of a signal (librosa.zero_crossings).
3331
+ */
3332
+ export function zeroCrossings(
3333
+ samples: Float32Array,
3334
+ threshold = 1e-10,
3335
+ refMagnitude = false,
3336
+ pad = true,
3337
+ zeroPos = true,
3338
+ ): Int32Array {
3339
+ if (!module) {
3340
+ throw new Error('Module not initialized. Call init() first.');
3341
+ }
3342
+ return module.zeroCrossings(samples, threshold, refMagnitude, pad, zeroPos);
3343
+ }
3344
+
3345
+ /**
3346
+ * Estimate the global tuning offset from a set of frequencies
3347
+ * (librosa.pitch_tuning). Returns a deviation in fractions of a bin.
3348
+ */
3349
+ export function pitchTuning(
3350
+ frequencies: Float32Array,
3351
+ resolution = 0.01,
3352
+ binsPerOctave = 12,
3353
+ ): number {
3354
+ if (!module) {
3355
+ throw new Error('Module not initialized. Call init() first.');
3356
+ }
3357
+ return module.pitchTuning(frequencies, resolution, binsPerOctave);
3358
+ }
3359
+
3360
+ /**
3361
+ * Estimate the tuning offset of an audio signal (librosa.estimate_tuning).
3362
+ */
3363
+ export function estimateTuning(
3364
+ samples: Float32Array,
3365
+ sampleRate = 22050,
3366
+ nFft = 2048,
3367
+ hopLength = 512,
3368
+ resolution = 0.01,
3369
+ binsPerOctave = 12,
3370
+ ): number {
3371
+ if (!module) {
3372
+ throw new Error('Module not initialized. Call init() first.');
3373
+ }
3374
+ return module.estimateTuning(samples, sampleRate, nFft, hopLength, resolution, binsPerOctave);
3375
+ }
3376
+
3377
+ /**
3378
+ * Non-negative matrix factorisation of a flattened [nFeatures x nFrames]
3379
+ * spectrogram (librosa.decompose.decompose). Returns the W and H factors.
3380
+ */
3381
+ export function decompose(
3382
+ s: Float32Array,
3383
+ nFeatures: number,
3384
+ nFrames: number,
3385
+ nComponents: number,
3386
+ nIter = 50,
3387
+ beta = 2.0,
3388
+ ): WasmDecomposeResult {
3389
+ if (!module) {
3390
+ throw new Error('Module not initialized. Call init() first.');
3391
+ }
3392
+ return module.decompose(s, nFeatures, nFrames, nComponents, nIter, beta);
3393
+ }
3394
+
3395
+ /**
3396
+ * Nearest-neighbour filtering of a flattened [nFeatures x nFrames] spectrogram
3397
+ * (librosa.decompose.nn_filter).
3398
+ */
3399
+ export function nnFilter(
3400
+ s: Float32Array,
3401
+ nFeatures: number,
3402
+ nFrames: number,
3403
+ aggregate = 'mean',
3404
+ k = 7,
3405
+ width = 1,
3406
+ ): WasmMatrix2dResult {
3407
+ if (!module) {
3408
+ throw new Error('Module not initialized. Call init() first.');
3409
+ }
3410
+ return module.nnFilter(s, nFeatures, nFrames, aggregate, k, width);
3411
+ }
3412
+
3413
+ /**
3414
+ * Reorder/concatenate a signal by interval slices (librosa.effects.remix).
3415
+ *
3416
+ * @param intervals - Flat (start, end) sample pairs (even length).
3417
+ */
3418
+ export function remix(
3419
+ samples: Float32Array,
3420
+ intervals: Int32Array | ArrayLike<number>,
3421
+ sampleRate = 22050,
3422
+ alignZeros = false,
3423
+ ): Float32Array {
3424
+ if (!module) {
3425
+ throw new Error('Module not initialized. Call init() first.');
3426
+ }
3427
+ // Sample indices must reach the native side as exact 32-bit integers. Passing
3428
+ // a Float32Array (or a number[] holding fractional/large values) would round
3429
+ // boundaries above 2^24 and misalign the slice. Coerce to an Int32Array,
3430
+ // truncating toward zero, so callers can hand us any numeric array safely.
3431
+ const intervalsI32 =
3432
+ intervals instanceof Int32Array ? intervals : Int32Array.from(intervals, (v) => Math.trunc(v));
3433
+ return module.remix(samples, intervalsI32, sampleRate, alignZeros);
2364
3434
  }
2365
3435
 
2366
3436
  /**
2367
- * Reconstruct audio directly from MFCC coefficients via Griffin-Lim. Mirrors
2368
- * `feature::mfcc_to_audio`.
2369
- *
2370
- * @param mfccCoefficients - MFCC matrix [nMfcc x nFrames] row-major
2371
- * @param nMfcc - Number of MFCC coefficients
2372
- * @param nFrames - Number of time frames
2373
- * @param nMels - Number of Mel bins (default: 128)
2374
- * @param sampleRate - Sample rate in Hz
2375
- * @param nFft - FFT size (default: 2048)
2376
- * @param hopLength - Hop length (default: 512)
2377
- * @param nIter - Griffin-Lim iterations (default: 32)
2378
- * @returns Reconstructed audio samples (mono, float32)
3437
+ * Phase-vocoder time-scale modification (rate > 1 faster, < 1 slower).
2379
3438
  */
2380
- export function mfccToAudio(
2381
- mfccCoefficients: Float32Array,
2382
- nMfcc: number,
2383
- nFrames: number,
2384
- nMels: number,
2385
- sampleRate: number,
3439
+ export function phaseVocoder(
3440
+ samples: Float32Array,
3441
+ rate: number,
3442
+ sampleRate = 22050,
2386
3443
  nFft = 2048,
2387
3444
  hopLength = 512,
2388
- nIter = 32,
2389
- fmin = 0,
2390
- fmax = 0,
2391
3445
  ): Float32Array {
2392
3446
  if (!module) {
2393
3447
  throw new Error('Module not initialized. Call init() first.');
2394
3448
  }
2395
- return module.mfccToAudio(
2396
- mfccCoefficients,
2397
- nMfcc,
2398
- nFrames,
2399
- nMels,
2400
- sampleRate,
2401
- nFft,
2402
- hopLength,
2403
- nIter,
2404
- fmin,
2405
- fmax,
2406
- );
3449
+ return module.phaseVocoder(samples, sampleRate, rate, nFft, hopLength);
2407
3450
  }
2408
3451
 
2409
- // ============================================================================
2410
- // Features - Chroma
2411
- // ============================================================================
2412
-
2413
3452
  /**
2414
- * Compute chromagram (pitch class distribution).
2415
- *
2416
- * @param samples - Audio samples (mono, float32)
2417
- * @param sampleRate - Sample rate in Hz
2418
- * @param nFft - FFT size (default: 2048)
2419
- * @param hopLength - Hop length (default: 512)
2420
- * @returns Chroma features result
3453
+ * HPSS into harmonic / percussive / residual signals.
2421
3454
  */
2422
- export function chroma(
3455
+ export function hpssWithResidual(
2423
3456
  samples: Float32Array,
2424
- sampleRate: number,
2425
- nFft = 2048,
2426
- hopLength = 512,
2427
- ): ChromaResult {
3457
+ sampleRate = 22050,
3458
+ kernelHarmonic = 31,
3459
+ kernelPercussive = 31,
3460
+ ): WasmHpssWithResidualResult {
2428
3461
  if (!module) {
2429
3462
  throw new Error('Module not initialized. Call init() first.');
2430
3463
  }
2431
- return module.chroma(samples, sampleRate, nFft, hopLength);
3464
+ return module.hpssWithResidual(samples, sampleRate, kernelHarmonic, kernelPercussive);
2432
3465
  }
2433
3466
 
2434
- // ============================================================================
2435
- // Features - Spectral
2436
- // ============================================================================
2437
-
2438
3467
  /**
2439
- * Compute spectral centroid (center of mass of spectrum).
2440
- *
2441
- * @param samples - Audio samples (mono, float32)
2442
- * @param sampleRate - Sample rate in Hz
2443
- * @param nFft - FFT size (default: 2048)
2444
- * @param hopLength - Hop length (default: 512)
2445
- * @returns Spectral centroid in Hz for each frame
3468
+ * Channel-weighted multichannel integrated loudness + LRA (ITU-R BS.1770 /
3469
+ * EBU R128) from an interleaved buffer of `frames * channels` samples. The
3470
+ * per-channel frame count is derived from the buffer length and `channels`.
2446
3471
  */
2447
- export function spectralCentroid(
3472
+ export function lufsInterleaved(
2448
3473
  samples: Float32Array,
2449
- sampleRate: number,
2450
- nFft = 2048,
2451
- hopLength = 512,
2452
- ): Float32Array {
3474
+ channels: number,
3475
+ sampleRate = 22050,
3476
+ ): WasmLufsResult {
2453
3477
  if (!module) {
2454
3478
  throw new Error('Module not initialized. Call init() first.');
2455
3479
  }
2456
- return module.spectralCentroid(samples, sampleRate, nFft, hopLength);
3480
+ return module.lufsInterleaved(samples, channels, sampleRate);
3481
+ }
3482
+
3483
+ /**
3484
+ * Standards-compliant EBU R128 loudness range (LRA) in LU.
3485
+ */
3486
+ export function ebur128LoudnessRange(samples: Float32Array, sampleRate = 22050): number {
3487
+ if (!module) {
3488
+ throw new Error('Module not initialized. Call init() first.');
3489
+ }
3490
+ return module.ebur128LoudnessRange(samples, sampleRate);
2457
3491
  }
2458
3492
 
2459
3493
  /**
2460
3494
  * Compute spectral bandwidth.
2461
3495
  *
2462
3496
  * @param samples - Audio samples (mono, float32)
2463
- * @param sampleRate - Sample rate in Hz
3497
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2464
3498
  * @param nFft - FFT size (default: 2048)
2465
3499
  * @param hopLength - Hop length (default: 512)
2466
3500
  * @returns Spectral bandwidth in Hz for each frame
2467
3501
  */
2468
3502
  export function spectralBandwidth(
2469
3503
  samples: Float32Array,
2470
- sampleRate: number,
3504
+ sampleRate = 22050,
2471
3505
  nFft = 2048,
2472
3506
  hopLength = 512,
2473
3507
  ): Float32Array {
@@ -2481,7 +3515,7 @@ export function spectralBandwidth(
2481
3515
  * Compute spectral rolloff frequency.
2482
3516
  *
2483
3517
  * @param samples - Audio samples (mono, float32)
2484
- * @param sampleRate - Sample rate in Hz
3518
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2485
3519
  * @param nFft - FFT size (default: 2048)
2486
3520
  * @param hopLength - Hop length (default: 512)
2487
3521
  * @param rollPercent - Percentage threshold (default: 0.85)
@@ -2489,7 +3523,7 @@ export function spectralBandwidth(
2489
3523
  */
2490
3524
  export function spectralRolloff(
2491
3525
  samples: Float32Array,
2492
- sampleRate: number,
3526
+ sampleRate = 22050,
2493
3527
  nFft = 2048,
2494
3528
  hopLength = 512,
2495
3529
  rollPercent = 0.85,
@@ -2504,14 +3538,14 @@ export function spectralRolloff(
2504
3538
  * Compute spectral flatness.
2505
3539
  *
2506
3540
  * @param samples - Audio samples (mono, float32)
2507
- * @param sampleRate - Sample rate in Hz
3541
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2508
3542
  * @param nFft - FFT size (default: 2048)
2509
3543
  * @param hopLength - Hop length (default: 512)
2510
3544
  * @returns Spectral flatness for each frame (0 = tonal, 1 = noise-like)
2511
3545
  */
2512
3546
  export function spectralFlatness(
2513
3547
  samples: Float32Array,
2514
- sampleRate: number,
3548
+ sampleRate = 22050,
2515
3549
  nFft = 2048,
2516
3550
  hopLength = 512,
2517
3551
  ): Float32Array {
@@ -2525,14 +3559,14 @@ export function spectralFlatness(
2525
3559
  * Compute zero crossing rate.
2526
3560
  *
2527
3561
  * @param samples - Audio samples (mono, float32)
2528
- * @param sampleRate - Sample rate in Hz
3562
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2529
3563
  * @param frameLength - Frame length (default: 2048)
2530
3564
  * @param hopLength - Hop length (default: 512)
2531
3565
  * @returns Zero crossing rate for each frame
2532
3566
  */
2533
3567
  export function zeroCrossingRate(
2534
3568
  samples: Float32Array,
2535
- sampleRate: number,
3569
+ sampleRate = 22050,
2536
3570
  frameLength = 2048,
2537
3571
  hopLength = 512,
2538
3572
  ): Float32Array {
@@ -2546,14 +3580,14 @@ export function zeroCrossingRate(
2546
3580
  * Compute RMS energy.
2547
3581
  *
2548
3582
  * @param samples - Audio samples (mono, float32)
2549
- * @param sampleRate - Sample rate in Hz
3583
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2550
3584
  * @param frameLength - Frame length (default: 2048)
2551
3585
  * @param hopLength - Hop length (default: 512)
2552
3586
  * @returns RMS energy for each frame
2553
3587
  */
2554
3588
  export function rmsEnergy(
2555
3589
  samples: Float32Array,
2556
- sampleRate: number,
3590
+ sampleRate = 22050,
2557
3591
  frameLength = 2048,
2558
3592
  hopLength = 512,
2559
3593
  ): Float32Array {
@@ -2571,54 +3605,76 @@ export function rmsEnergy(
2571
3605
  * Detect pitch using YIN algorithm.
2572
3606
  *
2573
3607
  * @param samples - Audio samples (mono, float32)
2574
- * @param sampleRate - Sample rate in Hz
3608
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2575
3609
  * @param frameLength - Frame length (default: 2048)
2576
3610
  * @param hopLength - Hop length (default: 512)
2577
3611
  * @param fmin - Minimum frequency in Hz (default: 65)
2578
3612
  * @param fmax - Maximum frequency in Hz (default: 2093)
2579
3613
  * @param threshold - YIN threshold (default: 0.3)
3614
+ * @param fillNa - If true, return 0 for unvoiced f0 frames; otherwise keep NaN (default: false)
2580
3615
  * @returns Pitch detection result
2581
3616
  */
2582
3617
  export function pitchYin(
2583
3618
  samples: Float32Array,
2584
- sampleRate: number,
3619
+ sampleRate = 22050,
2585
3620
  frameLength = 2048,
2586
3621
  hopLength = 512,
2587
3622
  fmin = 65.0,
2588
3623
  fmax = 2093.0,
2589
3624
  threshold = 0.3,
3625
+ fillNa = false,
2590
3626
  ): PitchResult {
2591
3627
  if (!module) {
2592
3628
  throw new Error('Module not initialized. Call init() first.');
2593
3629
  }
2594
- return module.pitchYin(samples, sampleRate, frameLength, hopLength, fmin, fmax, threshold);
3630
+ return module.pitchYin(
3631
+ samples,
3632
+ sampleRate,
3633
+ frameLength,
3634
+ hopLength,
3635
+ fmin,
3636
+ fmax,
3637
+ threshold,
3638
+ fillNa,
3639
+ );
2595
3640
  }
2596
3641
 
2597
3642
  /**
2598
3643
  * Detect pitch using pYIN algorithm (probabilistic YIN with HMM smoothing).
2599
3644
  *
2600
3645
  * @param samples - Audio samples (mono, float32)
2601
- * @param sampleRate - Sample rate in Hz
3646
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2602
3647
  * @param frameLength - Frame length (default: 2048)
2603
3648
  * @param hopLength - Hop length (default: 512)
2604
3649
  * @param fmin - Minimum frequency in Hz (default: 65)
2605
3650
  * @param fmax - Maximum frequency in Hz (default: 2093)
2606
3651
  * @param threshold - YIN threshold (default: 0.3)
3652
+ * @param fillNa - If true, return 0 for unvoiced f0 frames; otherwise keep NaN (default: false)
2607
3653
  * @returns Pitch detection result
2608
3654
  */
2609
3655
  export function pitchPyin(
2610
3656
  samples: Float32Array,
2611
- sampleRate: number,
3657
+ sampleRate = 22050,
2612
3658
  frameLength = 2048,
2613
3659
  hopLength = 512,
2614
3660
  fmin = 65.0,
2615
3661
  fmax = 2093.0,
2616
3662
  threshold = 0.3,
3663
+ fillNa = false,
2617
3664
  ): PitchResult {
2618
3665
  if (!module) {
2619
3666
  throw new Error('Module not initialized. Call init() first.');
2620
3667
  }
2621
- return module.pitchPyin(samples, sampleRate, frameLength, hopLength, fmin, fmax, threshold);
3668
+ return module.pitchPyin(
3669
+ samples,
3670
+ sampleRate,
3671
+ frameLength,
3672
+ hopLength,
3673
+ fmin,
3674
+ fmax,
3675
+ threshold,
3676
+ fillNa,
3677
+ );
2622
3678
  }
2623
3679
 
2624
3680
  // ============================================================================
@@ -2707,11 +3763,11 @@ export function noteToHz(note: string): number {
2707
3763
  * Convert frame index to time in seconds.
2708
3764
  *
2709
3765
  * @param frames - Frame index
2710
- * @param sr - Sample rate in Hz
2711
- * @param hopLength - Hop length in samples
3766
+ * @param sr - Sample rate in Hz (default: 22050)
3767
+ * @param hopLength - Hop length in samples (default: 512)
2712
3768
  * @returns Time in seconds
2713
3769
  */
2714
- export function framesToTime(frames: number, sr: number, hopLength: number): number {
3770
+ export function framesToTime(frames: number, sr = 22050, hopLength = 512): number {
2715
3771
  if (!module) {
2716
3772
  throw new Error('Module not initialized. Call init() first.');
2717
3773
  }
@@ -2722,11 +3778,11 @@ export function framesToTime(frames: number, sr: number, hopLength: number): num
2722
3778
  * Convert time in seconds to frame index.
2723
3779
  *
2724
3780
  * @param time - Time in seconds
2725
- * @param sr - Sample rate in Hz
2726
- * @param hopLength - Hop length in samples
3781
+ * @param sr - Sample rate in Hz (default: 22050)
3782
+ * @param hopLength - Hop length in samples (default: 512)
2727
3783
  * @returns Frame index
2728
3784
  */
2729
- export function timeToFrames(time: number, sr: number, hopLength: number): number {
3785
+ export function timeToFrames(time: number, sr = 22050, hopLength = 512): number {
2730
3786
  if (!module) {
2731
3787
  throw new Error('Module not initialized. Call init() first.');
2732
3788
  }
@@ -2834,18 +3890,18 @@ export function frameSignal(
2834
3890
  return module.frameSignal(samples, frameLength, hopLength);
2835
3891
  }
2836
3892
 
2837
- export function padCenter(values: Float32Array, size: number, padValue = 0.0): Float32Array {
3893
+ export function padCenter(values: Float32Array, targetSize: number, padValue = 0.0): Float32Array {
2838
3894
  if (!module) {
2839
3895
  throw new Error('Module not initialized. Call init() first.');
2840
3896
  }
2841
- return module.padCenter(values, size, padValue);
3897
+ return module.padCenter(values, targetSize, padValue);
2842
3898
  }
2843
3899
 
2844
- export function fixLength(values: Float32Array, size: number, padValue = 0.0): Float32Array {
3900
+ export function fixLength(values: Float32Array, targetSize: number, padValue = 0.0): Float32Array {
2845
3901
  if (!module) {
2846
3902
  throw new Error('Module not initialized. Call init() first.');
2847
3903
  }
2848
- return module.fixLength(values, size, padValue);
3904
+ return module.fixLength(values, targetSize, padValue);
2849
3905
  }
2850
3906
 
2851
3907
  export function fixFrames(frames: Int32Array, xMin = 0, xMax = -1, pad = true): Int32Array {
@@ -2870,11 +3926,7 @@ export function peakPick(
2870
3926
  return module.peakPick(values, preMax, postMax, preAvg, postAvg, delta, wait);
2871
3927
  }
2872
3928
 
2873
- export function vectorNormalize(
2874
- values: Float32Array,
2875
- normType = 0,
2876
- threshold = 1e-12,
2877
- ): Float32Array {
3929
+ export function vectorNormalize(values: Float32Array, normType = 0, threshold = 0.0): Float32Array {
2878
3930
  if (!module) {
2879
3931
  throw new Error('Module not initialized. Call init() first.');
2880
3932
  }
@@ -2902,7 +3954,7 @@ export function tonnetz(chromagram: Float32Array, nChroma: number, nFrames: numb
2902
3954
 
2903
3955
  export function tempogram(
2904
3956
  onsetEnvelope: Float32Array,
2905
- sampleRate: number,
3957
+ sampleRate = 22050,
2906
3958
  hopLength = 512,
2907
3959
  winLength = 384,
2908
3960
  mode: TempogramMode = 'autocorrelation',
@@ -2915,7 +3967,7 @@ export function tempogram(
2915
3967
 
2916
3968
  export function cyclicTempogram(
2917
3969
  onsetEnvelope: Float32Array,
2918
- sampleRate: number,
3970
+ sampleRate = 22050,
2919
3971
  hopLength = 512,
2920
3972
  winLength = 384,
2921
3973
  bpmMin = 60.0,
@@ -2929,7 +3981,7 @@ export function cyclicTempogram(
2929
3981
 
2930
3982
  export function plp(
2931
3983
  onsetEnvelope: Float32Array,
2932
- sampleRate: number,
3984
+ sampleRate = 22050,
2933
3985
  hopLength = 512,
2934
3986
  tempoMin = 30.0,
2935
3987
  tempoMax = 300.0,
@@ -3014,7 +4066,7 @@ export function vqt(
3014
4066
  * @param sampleRate - Sample rate in Hz (default: 22050)
3015
4067
  * @param nFft - FFT size (default: 2048)
3016
4068
  * @param hopLength - Hop length (default: 512)
3017
- * @param minSectionSec - Minimum section duration in seconds (default: 8.0)
4069
+ * @param minSectionSec - Minimum section duration in seconds (default: 4.0)
3018
4070
  * @returns Array of detected sections
3019
4071
  */
3020
4072
  export function analyzeSections(
@@ -3022,7 +4074,7 @@ export function analyzeSections(
3022
4074
  sampleRate = 22050,
3023
4075
  nFft = 2048,
3024
4076
  hopLength = 512,
3025
- minSectionSec = 8.0,
4077
+ minSectionSec = 4.0,
3026
4078
  ): Section[] {
3027
4079
  if (!module) {
3028
4080
  throw new Error('Module not initialized. Call init() first.');
@@ -3050,7 +4102,7 @@ export function analyzeMelody(
3050
4102
  fmin = 65.0,
3051
4103
  fmax = 2093.0,
3052
4104
  frameLength = 2048,
3053
- hopLength = 512,
4105
+ hopLength = 256,
3054
4106
  threshold = 0.1,
3055
4107
  ): MelodyResult {
3056
4108
  if (!module) {
@@ -3131,10 +4183,15 @@ export function tempogramRatio(
3131
4183
  * @param sampleRate - Sample rate in Hz (default: 22050)
3132
4184
  * @returns Loudness measurement result
3133
4185
  */
3134
- export function lufs(samples: Float32Array, sampleRate = 22050): LufsResult {
4186
+ export function lufs(
4187
+ samples: Float32Array,
4188
+ sampleRate = 22050,
4189
+ options: ValidateOptions = {},
4190
+ ): LufsResult {
3135
4191
  if (!module) {
3136
4192
  throw new Error('Module not initialized. Call init() first.');
3137
4193
  }
4194
+ assertSamples('lufs', samples, options.validate !== false);
3138
4195
  return module.lufs(samples, sampleRate);
3139
4196
  }
3140
4197
 
@@ -3145,10 +4202,15 @@ export function lufs(samples: Float32Array, sampleRate = 22050): LufsResult {
3145
4202
  * @param sampleRate - Sample rate in Hz (default: 22050)
3146
4203
  * @returns Momentary LUFS values over time
3147
4204
  */
3148
- export function momentaryLufs(samples: Float32Array, sampleRate = 22050): Float32Array {
4205
+ export function momentaryLufs(
4206
+ samples: Float32Array,
4207
+ sampleRate = 22050,
4208
+ options: ValidateOptions = {},
4209
+ ): Float32Array {
3149
4210
  if (!module) {
3150
4211
  throw new Error('Module not initialized. Call init() first.');
3151
4212
  }
4213
+ assertSamples('momentaryLufs', samples, options.validate !== false);
3152
4214
  return module.momentaryLufs(samples, sampleRate);
3153
4215
  }
3154
4216
 
@@ -3159,13 +4221,292 @@ export function momentaryLufs(samples: Float32Array, sampleRate = 22050): Float3
3159
4221
  * @param sampleRate - Sample rate in Hz (default: 22050)
3160
4222
  * @returns Short-term LUFS values over time
3161
4223
  */
3162
- export function shortTermLufs(samples: Float32Array, sampleRate = 22050): Float32Array {
4224
+ export function shortTermLufs(
4225
+ samples: Float32Array,
4226
+ sampleRate = 22050,
4227
+ options: ValidateOptions = {},
4228
+ ): Float32Array {
3163
4229
  if (!module) {
3164
4230
  throw new Error('Module not initialized. Call init() first.');
3165
4231
  }
4232
+ assertSamples('shortTermLufs', samples, options.validate !== false);
3166
4233
  return module.shortTermLufs(samples, sampleRate);
3167
4234
  }
3168
4235
 
4236
+ // ============================================================================
4237
+ // Metering — basic / true-peak / clipping / dynamic range
4238
+ // ============================================================================
4239
+
4240
+ /** One contiguous run of clipped samples reported by `meteringDetectClipping`. */
4241
+ export interface ClippingRegion {
4242
+ startSample: number;
4243
+ endSample: number;
4244
+ length: number;
4245
+ peak: number;
4246
+ }
4247
+
4248
+ /** Aggregated clipping report. */
4249
+ export interface ClippingReport {
4250
+ clippedSamples: number;
4251
+ clippingRatio: number;
4252
+ maxClippedPeak: number;
4253
+ regions: ClippingRegion[];
4254
+ }
4255
+
4256
+ /** Sliding-window dynamic range report. */
4257
+ export interface DynamicRangeReport {
4258
+ dynamicRangeDb: number;
4259
+ lowPercentileDb: number;
4260
+ highPercentileDb: number;
4261
+ windowRmsDb: Float32Array;
4262
+ }
4263
+
4264
+ function requireModule() {
4265
+ if (!module) {
4266
+ throw new Error('Module not initialized. Call init() first.');
4267
+ }
4268
+ return module;
4269
+ }
4270
+
4271
+ export function meteringPeakDb(
4272
+ samples: Float32Array,
4273
+ sampleRate = 22050,
4274
+ options: ValidateOptions = {},
4275
+ ): number {
4276
+ assertSamples('meteringPeakDb', samples, options.validate !== false);
4277
+ return requireModule().meteringPeakDb(samples, sampleRate);
4278
+ }
4279
+
4280
+ export function meteringRmsDb(
4281
+ samples: Float32Array,
4282
+ sampleRate = 22050,
4283
+ options: ValidateOptions = {},
4284
+ ): number {
4285
+ assertSamples('meteringRmsDb', samples, options.validate !== false);
4286
+ return requireModule().meteringRmsDb(samples, sampleRate);
4287
+ }
4288
+
4289
+ export function meteringCrestFactorDb(
4290
+ samples: Float32Array,
4291
+ sampleRate = 22050,
4292
+ options: ValidateOptions = {},
4293
+ ): number {
4294
+ assertSamples('meteringCrestFactorDb', samples, options.validate !== false);
4295
+ return requireModule().meteringCrestFactorDb(samples, sampleRate);
4296
+ }
4297
+
4298
+ export function meteringDcOffset(
4299
+ samples: Float32Array,
4300
+ sampleRate = 22050,
4301
+ options: ValidateOptions = {},
4302
+ ): number {
4303
+ assertSamples('meteringDcOffset', samples, options.validate !== false);
4304
+ return requireModule().meteringDcOffset(samples, sampleRate);
4305
+ }
4306
+
4307
+ /**
4308
+ * Inter-sample (true) peak in dBFS. `oversampleFactor` must be a power of two
4309
+ * in [1, 16]; pass 0 to use the library default (4).
4310
+ */
4311
+ export function meteringTruePeakDb(
4312
+ samples: Float32Array,
4313
+ sampleRate = 22050,
4314
+ oversampleFactor = 4,
4315
+ options: ValidateOptions = {},
4316
+ ): number {
4317
+ assertSamples('meteringTruePeakDb', samples, options.validate !== false);
4318
+ return requireModule().meteringTruePeakDb(samples, sampleRate, oversampleFactor);
4319
+ }
4320
+
4321
+ /**
4322
+ * Detect contiguous runs of clipped samples.
4323
+ *
4324
+ * @param threshold Linear absolute threshold (default 0.999).
4325
+ * @param minRegionSamples Minimum run length to report (default 1).
4326
+ */
4327
+ export function meteringDetectClipping(
4328
+ samples: Float32Array,
4329
+ sampleRate = 22050,
4330
+ threshold = 0.999,
4331
+ minRegionSamples = 1,
4332
+ options: ValidateOptions = {},
4333
+ ): ClippingReport {
4334
+ assertSamples('meteringDetectClipping', samples, options.validate !== false);
4335
+ return requireModule().meteringDetectClipping(samples, sampleRate, threshold, minRegionSamples);
4336
+ }
4337
+
4338
+ /**
4339
+ * Sliding-window dynamic range. Pass 0 for any parameter to use the library
4340
+ * default (window=3 s, hop=1 s, low=0.10, high=0.95).
4341
+ */
4342
+ export function meteringDynamicRange(
4343
+ samples: Float32Array,
4344
+ sampleRate = 22050,
4345
+ windowSec = 0,
4346
+ hopSec = 0,
4347
+ lowPercentile = 0,
4348
+ highPercentile = 0,
4349
+ options: ValidateOptions = {},
4350
+ ): DynamicRangeReport {
4351
+ assertSamples('meteringDynamicRange', samples, options.validate !== false);
4352
+ return requireModule().meteringDynamicRange(
4353
+ samples,
4354
+ sampleRate,
4355
+ windowSec,
4356
+ hopSec,
4357
+ lowPercentile,
4358
+ highPercentile,
4359
+ );
4360
+ }
4361
+
4362
+ // ============================================================================
4363
+ // Metering — stereo / phase-scope / spectrum
4364
+ // ============================================================================
4365
+
4366
+ /** Mid/side vectorscope point series for a (left, right) stereo pair. */
4367
+ export interface VectorscopeReport {
4368
+ mid: Float32Array;
4369
+ side: Float32Array;
4370
+ }
4371
+
4372
+ /** Phase-scope (Lissajous) point series plus summary stats. */
4373
+ export interface PhaseScopeReport {
4374
+ mid: Float32Array;
4375
+ side: Float32Array;
4376
+ radius: Float32Array;
4377
+ angleRad: Float32Array;
4378
+ correlation: number;
4379
+ averageAbsAngleRad: number;
4380
+ maxRadius: number;
4381
+ }
4382
+
4383
+ /** Options for `meteringSpectrum`. */
4384
+ export interface SpectrumOptions {
4385
+ /** FFT size. Pass 0 / omit for the library default (2048). */
4386
+ nFft?: number;
4387
+ /** Apply fractional-octave smoothing to magnitude. */
4388
+ applyOctaveSmoothing?: boolean;
4389
+ /** Smoothing fraction (e.g. 3 = 1/3-octave). 0 / omit = library default (3). */
4390
+ octaveFraction?: number;
4391
+ /** Linear reference for the dB conversion. 0 / omit = 1.0. */
4392
+ dbRef?: number;
4393
+ /** Linear floor used to avoid log(0). 0 / omit = library default. */
4394
+ dbAmin?: number;
4395
+ }
4396
+
4397
+ /** Single-frame magnitude / power / dB spectrum returned by `meteringSpectrum`. */
4398
+ export interface SpectrumReport {
4399
+ frequencies: Float32Array;
4400
+ magnitude: Float32Array;
4401
+ power: Float32Array;
4402
+ db: Float32Array;
4403
+ nFft: number;
4404
+ sampleRate: number;
4405
+ }
4406
+
4407
+ /** Pearson correlation in [-1, 1] between two equal-length channels. */
4408
+ export function meteringStereoCorrelation(
4409
+ left: Float32Array,
4410
+ right: Float32Array,
4411
+ sampleRate = 22050,
4412
+ options: ValidateOptions = {},
4413
+ ): number {
4414
+ const validate = options.validate !== false;
4415
+ assertSamples('meteringStereoCorrelation', left, validate, 'left');
4416
+ assertSamples('meteringStereoCorrelation', right, validate, 'right');
4417
+ return requireModule().meteringStereoCorrelation(left, right, sampleRate);
4418
+ }
4419
+
4420
+ /** Side / mid energy ratio: 0 = pure mono, ~1 = wide stereo. */
4421
+ export function meteringStereoWidth(
4422
+ left: Float32Array,
4423
+ right: Float32Array,
4424
+ sampleRate = 22050,
4425
+ options: ValidateOptions = {},
4426
+ ): number {
4427
+ const validate = options.validate !== false;
4428
+ assertSamples('meteringStereoWidth', left, validate, 'left');
4429
+ assertSamples('meteringStereoWidth', right, validate, 'right');
4430
+ return requireModule().meteringStereoWidth(left, right, sampleRate);
4431
+ }
4432
+
4433
+ /** Per-sample mid/side point series (one entry per input frame). */
4434
+ export function meteringVectorscope(
4435
+ left: Float32Array,
4436
+ right: Float32Array,
4437
+ sampleRate = 22050,
4438
+ options: ValidateOptions = {},
4439
+ ): VectorscopeReport {
4440
+ const validate = options.validate !== false;
4441
+ assertSamples('meteringVectorscope', left, validate, 'left');
4442
+ assertSamples('meteringVectorscope', right, validate, 'right');
4443
+ return requireModule().meteringVectorscope(left, right, sampleRate);
4444
+ }
4445
+
4446
+ /** Phase-scope point series plus summary stats. */
4447
+ export function meteringPhaseScope(
4448
+ left: Float32Array,
4449
+ right: Float32Array,
4450
+ sampleRate = 22050,
4451
+ options: ValidateOptions = {},
4452
+ ): PhaseScopeReport {
4453
+ const validate = options.validate !== false;
4454
+ assertSamples('meteringPhaseScope', left, validate, 'left');
4455
+ assertSamples('meteringPhaseScope', right, validate, 'right');
4456
+ return requireModule().meteringPhaseScope(left, right, sampleRate);
4457
+ }
4458
+
4459
+ /** Single-frame spectrum view (uses the first `nFft` samples of `samples`). */
4460
+ export function meteringSpectrum(
4461
+ samples: Float32Array,
4462
+ sampleRate = 22050,
4463
+ options?: SpectrumOptions & ValidateOptions,
4464
+ ): SpectrumReport {
4465
+ const validate = options?.validate !== false;
4466
+ assertSamples('meteringSpectrum', samples, validate);
4467
+ return requireModule().meteringSpectrum(samples, sampleRate, options ?? {});
4468
+ }
4469
+
4470
+ // ============================================================================
4471
+ // Editing — 12-TET scale quantizer
4472
+ // ============================================================================
4473
+
4474
+ /**
4475
+ * Snap a MIDI value to the nearest pitch class enabled by `modeMask`.
4476
+ *
4477
+ * `modeMask` is a 12-bit mask. For natural C major use `0b101010110101`.
4478
+ * `referenceMidi` defaults to A4 (69) when passed as 0.
4479
+ */
4480
+ export function scaleQuantizeMidi(
4481
+ root: number,
4482
+ modeMask: number,
4483
+ midi: number,
4484
+ referenceMidi = 0,
4485
+ ): number {
4486
+ assertFiniteScalar('scaleQuantizeMidi', midi, 'midi');
4487
+ assertFiniteScalar('scaleQuantizeMidi', referenceMidi, 'referenceMidi');
4488
+ return requireModule().scaleQuantizeMidi(root, modeMask, midi, referenceMidi);
4489
+ }
4490
+
4491
+ export function scaleCorrectionSemitones(
4492
+ root: number,
4493
+ modeMask: number,
4494
+ midi: number,
4495
+ referenceMidi = 0,
4496
+ ): number {
4497
+ assertFiniteScalar('scaleCorrectionSemitones', midi, 'midi');
4498
+ assertFiniteScalar('scaleCorrectionSemitones', referenceMidi, 'referenceMidi');
4499
+ return requireModule().scaleCorrectionSemitones(root, modeMask, midi, referenceMidi);
4500
+ }
4501
+
4502
+ export function scalePitchClassEnabled(
4503
+ root: number,
4504
+ modeMask: number,
4505
+ pitchClass: number,
4506
+ ): boolean {
4507
+ return requireModule().scalePitchClassEnabled(root, modeMask, pitchClass);
4508
+ }
4509
+
3169
4510
  // ============================================================================
3170
4511
  // Core - Resample
3171
4512
  // ============================================================================
@@ -3299,15 +4640,15 @@ export class Audio {
3299
4640
  return pitchShift(this._samples, this._sampleRate, semitones);
3300
4641
  }
3301
4642
 
3302
- pitchCorrectToMidi(currentMidi: number, targetMidi: number): Float32Array {
4643
+ pitchCorrectToMidi(currentMidi = 69.0, targetMidi = 69.0): Float32Array {
3303
4644
  return pitchCorrectToMidi(this._samples, this._sampleRate, currentMidi, targetMidi);
3304
4645
  }
3305
4646
 
3306
- noteStretch(onsetSample: number, offsetSample: number, stretchRatio: number): Float32Array {
4647
+ noteStretch(onsetSample = 0, offsetSample = 0, stretchRatio = 1.0): Float32Array {
3307
4648
  return noteStretch(this._samples, this._sampleRate, onsetSample, offsetSample, stretchRatio);
3308
4649
  }
3309
4650
 
3310
- voiceChange(pitchSemitones: number, formantFactor: number): Float32Array {
4651
+ voiceChange(pitchSemitones = 0.0, formantFactor = 1.0): Float32Array {
3311
4652
  return voiceChange(this._samples, this._sampleRate, pitchSemitones, formantFactor);
3312
4653
  }
3313
4654
 
@@ -3355,7 +4696,7 @@ export class Audio {
3355
4696
  return melSpectrogram(this._samples, this._sampleRate, nFft, hopLength, nMels);
3356
4697
  }
3357
4698
 
3358
- mfcc(nFft = 2048, hopLength = 512, nMels = 128, nMfcc = 13): MfccResult {
4699
+ mfcc(nFft = 2048, hopLength = 512, nMels = 128, nMfcc = 20): MfccResult {
3359
4700
  return mfcc(this._samples, this._sampleRate, nFft, hopLength, nMels, nMfcc);
3360
4701
  }
3361
4702
 
@@ -3413,8 +4754,18 @@ export class Audio {
3413
4754
  fmin = 65.0,
3414
4755
  fmax = 2093.0,
3415
4756
  threshold = 0.3,
4757
+ fillNa = false,
3416
4758
  ): PitchResult {
3417
- return pitchYin(this._samples, this._sampleRate, frameLength, hopLength, fmin, fmax, threshold);
4759
+ return pitchYin(
4760
+ this._samples,
4761
+ this._sampleRate,
4762
+ frameLength,
4763
+ hopLength,
4764
+ fmin,
4765
+ fmax,
4766
+ threshold,
4767
+ fillNa,
4768
+ );
3418
4769
  }
3419
4770
 
3420
4771
  pitchPyin(
@@ -3423,6 +4774,7 @@ export class Audio {
3423
4774
  fmin = 65.0,
3424
4775
  fmax = 2093.0,
3425
4776
  threshold = 0.3,
4777
+ fillNa = false,
3426
4778
  ): PitchResult {
3427
4779
  return pitchPyin(
3428
4780
  this._samples,
@@ -3432,6 +4784,7 @@ export class Audio {
3432
4784
  fmin,
3433
4785
  fmax,
3434
4786
  threshold,
4787
+ fillNa,
3435
4788
  );
3436
4789
  }
3437
4790
 
@@ -3477,8 +4830,7 @@ export class StreamAnalyzer {
3477
4830
  if (!module) {
3478
4831
  throw new Error('Module not initialized. Call init() first.');
3479
4832
  }
3480
- const wasmModule = module;
3481
- const args = [
4833
+ this.analyzer = new module.StreamAnalyzer(
3482
4834
  config.sampleRate,
3483
4835
  config.nFft ?? 2048,
3484
4836
  config.hopLength ?? 512,
@@ -3497,63 +4849,7 @@ export class StreamAnalyzer {
3497
4849
  config.bpmUpdateIntervalSec ?? 10,
3498
4850
  config.window ?? 0,
3499
4851
  config.outputFormat ?? 0,
3500
- ] as const;
3501
- const isArityError = (error: unknown): boolean => {
3502
- const message = String((error as { message?: unknown } | null)?.message ?? error);
3503
- return message.includes('invalid number of parameters');
3504
- };
3505
- const createLegacy = (): WasmStreamAnalyzer => {
3506
- const LegacyStreamAnalyzer = wasmModule.StreamAnalyzer as unknown as new (
3507
- sampleRate: number,
3508
- nFft: number,
3509
- hopLength: number,
3510
- nMels: number,
3511
- computeMel: boolean,
3512
- computeChroma: boolean,
3513
- computeOnset: boolean,
3514
- emitEveryNFrames: number,
3515
- ) => WasmStreamAnalyzer;
3516
- return new LegacyStreamAnalyzer(
3517
- args[0],
3518
- args[1],
3519
- args[2],
3520
- args[3],
3521
- args[8],
3522
- args[9],
3523
- args[10],
3524
- args[12],
3525
- );
3526
- };
3527
- const hasExtendedConfig =
3528
- config.fmin !== undefined ||
3529
- config.fmax !== undefined ||
3530
- config.tuningRefHz !== undefined ||
3531
- config.computeMagnitude !== undefined ||
3532
- config.computeSpectral !== undefined ||
3533
- config.magnitudeDownsample !== undefined ||
3534
- config.keyUpdateIntervalSec !== undefined ||
3535
- config.bpmUpdateIntervalSec !== undefined ||
3536
- config.window !== undefined ||
3537
- config.outputFormat !== undefined;
3538
- if (hasExtendedConfig) {
3539
- try {
3540
- this.analyzer = new wasmModule.StreamAnalyzer(...args);
3541
- } catch (error) {
3542
- if (!isArityError(error)) {
3543
- throw error;
3544
- }
3545
- this.analyzer = createLegacy();
3546
- }
3547
- } else {
3548
- try {
3549
- this.analyzer = createLegacy();
3550
- } catch (error) {
3551
- if (!isArityError(error)) {
3552
- throw error;
3553
- }
3554
- this.analyzer = new wasmModule.StreamAnalyzer(...args);
3555
- }
3556
- }
4852
+ );
3557
4853
  }
3558
4854
 
3559
4855
  /**