@libraz/libsonare 1.2.1 → 1.2.3

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
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @example
5
5
  * ```typescript
6
- * import { init, detectBpm, detectKey, analyze } from '@libraz/sonare';
6
+ * import { init, detectBpm, detectKey, analyze } from '@libraz/libsonare';
7
7
  *
8
8
  * await init();
9
9
  *
@@ -56,7 +56,15 @@ import type {
56
56
  PairAnalysis,
57
57
  PairProcessor,
58
58
  PanLaw,
59
+ PanMode,
59
60
  PitchResult,
61
+ RealtimeVoiceChangerConfigInput,
62
+ RealtimeVoiceChangerPodConfig,
63
+ RirResult,
64
+ RirSynthOptions,
65
+ RoomEstimateOptions,
66
+ RoomEstimateResult,
67
+ RoomMorphOptions,
60
68
  Section,
61
69
  SectionType,
62
70
  SendTiming,
@@ -68,15 +76,9 @@ import type {
68
76
  StreamingPlatform,
69
77
  StreamingRetuneConfig,
70
78
  TempogramMode,
79
+ VoicePresetId,
71
80
  } from './public_types';
72
81
  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
82
  import type {
81
83
  ProgressCallback,
82
84
  SonareModule,
@@ -84,6 +86,7 @@ import type {
84
86
  WasmAnalysisResult,
85
87
  WasmChordAnalysisResult,
86
88
  WasmCyclicTempogramResult,
89
+ WasmDecomposeResult,
87
90
  WasmEngineAutomationPoint,
88
91
  WasmEngineBounceOptions,
89
92
  WasmEngineBounceResult,
@@ -101,13 +104,23 @@ import type {
101
104
  WasmEngineTransportState,
102
105
  WasmFourierTempogramResult,
103
106
  WasmFrameResult,
107
+ WasmHpssWithResidualResult,
104
108
  WasmKeyCandidateResult,
109
+ WasmLufsResult,
110
+ WasmMatrix2dResult,
105
111
  WasmNnlsChromaResult,
106
112
  WasmRealtimeEngine,
107
113
  WasmStreamAnalyzer,
108
114
  WasmTempogramResult,
109
115
  WasmTrimResult,
110
- } from './wasm_types';
116
+ } from './sonare.js';
117
+ import type {
118
+ AnalyzerStats,
119
+ FrameBuffer,
120
+ StreamConfig,
121
+ StreamFramesI16,
122
+ StreamFramesU8,
123
+ } from './stream_types';
111
124
 
112
125
  export type {
113
126
  AcousticResult,
@@ -143,6 +156,7 @@ export type {
143
156
  MasteringStereoResult,
144
157
  MelodyPoint,
145
158
  MelodyResult,
159
+ MelPowerResult,
146
160
  MelSpectrogramResult,
147
161
  MeterTap,
148
162
  MfccResult,
@@ -155,17 +169,28 @@ export type {
155
169
  PanLaw,
156
170
  PanMode,
157
171
  PitchResult,
172
+ RealtimeVoiceChangerConfigInput,
173
+ RealtimeVoiceChangerPodConfig,
158
174
  RhythmFeatures,
175
+ RirResult,
176
+ RirSynthOptions,
177
+ RoomEstimateOptions,
178
+ RoomEstimateResult,
179
+ RoomGeometryOptions,
180
+ RoomMorphOptions,
159
181
  Section,
160
182
  SendTiming,
161
183
  SoloProcessor,
162
184
  StereoAnalysis,
185
+ StftPowerResult,
163
186
  StftResult,
164
187
  StreamingEqualizerConfig,
165
188
  StreamingPlatform,
166
189
  StreamingRetuneConfig,
190
+ TempogramMode,
167
191
  Timbre,
168
192
  TimeSignature,
193
+ VoicePresetId,
169
194
  } from './public_types';
170
195
  export {
171
196
  ChordQuality,
@@ -174,6 +199,7 @@ export {
174
199
  PitchClass,
175
200
  SectionType,
176
201
  } from './public_types';
202
+ export type { ProgressCallback } from './sonare.js';
177
203
  export type {
178
204
  AnalyzerStats,
179
205
  BarChord,
@@ -185,7 +211,6 @@ export type {
185
211
  StreamFramesI16,
186
212
  StreamFramesU8,
187
213
  } from './stream_types';
188
- export type { ProgressCallback } from './wasm_types';
189
214
 
190
215
  export type EngineClip = WasmEngineClip;
191
216
  export type EngineParameterInfo = WasmEngineParameterInfo;
@@ -202,6 +227,13 @@ export type EngineTelemetry = WasmEngineTelemetry;
202
227
  export type EngineMeterTelemetry = WasmEngineMeterTelemetry;
203
228
  export type EngineTransportState = WasmEngineTransportState;
204
229
 
230
+ /** Row-major 2-D matrix as a flat buffer plus its dimensions. */
231
+ export type Matrix2dResult = WasmMatrix2dResult;
232
+ /** NMF factor matrices { w, h } from {@link decompose}. */
233
+ export type DecomposeResult = WasmDecomposeResult;
234
+ /** Harmonic / percussive / residual signals from {@link hpssWithResidual}. */
235
+ export type HpssWithResidualResult = WasmHpssWithResidualResult;
236
+
205
237
  export const EXPECTED_ENGINE_ABI_VERSION = 2;
206
238
 
207
239
  export interface EngineCapabilities {
@@ -222,6 +254,43 @@ export interface MixerRealtimeBuffer {
222
254
  process: (numSamples?: number) => void;
223
255
  }
224
256
 
257
+ /**
258
+ * Zero-copy realtime buffer pair for {@link RealtimeVoiceChanger} mono
259
+ * processing. The `input` / `output` `Float32Array`s are typed-memory views
260
+ * onto the WASM heap — write samples into `input`, call `process()`, then
261
+ * read from `output`. The views are owned by the {@link RealtimeVoiceChanger}
262
+ * and remain valid until `delete()` is called on it.
263
+ */
264
+ export interface RealtimeVoiceChangerMonoBuffer {
265
+ input: Float32Array;
266
+ output: Float32Array;
267
+ process: () => void;
268
+ }
269
+
270
+ /**
271
+ * Zero-copy realtime buffer pair for {@link RealtimeVoiceChanger} interleaved
272
+ * multi-channel processing. Layout is L0,R0,L1,R1,... for stereo. The views
273
+ * are owned by the {@link RealtimeVoiceChanger}.
274
+ */
275
+ export interface RealtimeVoiceChangerInterleavedBuffer {
276
+ input: Float32Array;
277
+ output: Float32Array;
278
+ channels: number;
279
+ process: () => void;
280
+ }
281
+
282
+ /**
283
+ * Zero-copy realtime buffer for {@link RealtimeVoiceChanger} planar stereo
284
+ * processing. Each entry in `channels` is a heap-backed `Float32Array` for one
285
+ * channel (matching AudioWorklet's native layout). Process happens in place:
286
+ * write samples into each channel view, call `process()`, then read back from
287
+ * the same views.
288
+ */
289
+ export interface RealtimeVoiceChangerPlanarBuffer {
290
+ channels: Float32Array[];
291
+ process: () => void;
292
+ }
293
+
225
294
  function automationCurveCode(curve: AutomationCurve): number {
226
295
  switch (curve) {
227
296
  case 'linear':
@@ -253,6 +322,22 @@ function panLawCode(panLaw: PanLaw | number): number {
253
322
  }
254
323
  }
255
324
 
325
+ function panModeCode(panMode: PanMode | number): number {
326
+ if (typeof panMode === 'number') {
327
+ return panMode;
328
+ }
329
+ switch (panMode) {
330
+ case 'stereoPan':
331
+ case 'stereo-pan':
332
+ return 1;
333
+ case 'dualPan':
334
+ case 'dual-pan':
335
+ return 2;
336
+ default:
337
+ return 0;
338
+ }
339
+ }
340
+
256
341
  function meterTapCode(tap: MeterTap | number): number {
257
342
  return tap === 'preFader' || tap === 0 ? 0 : 1;
258
343
  }
@@ -268,6 +353,62 @@ function sendTimingCode(timing: SendTiming | number): number {
268
353
  let module: SonareModule | null = null;
269
354
  let initPromise: Promise<void> | null = null;
270
355
 
356
+ // ============================================================================
357
+ // Input validation helpers (empty + NaN/Inf guards for sample buffers)
358
+ // ============================================================================
359
+
360
+ /**
361
+ * Per-call validation options accepted by guarded wrappers. Empty-buffer
362
+ * checks are always performed; pass `{ validate: false }` to opt out of the
363
+ * O(n) NaN/Inf scan on hot paths.
364
+ */
365
+ export interface ValidateOptions {
366
+ validate?: boolean;
367
+ }
368
+
369
+ function assertNonEmptySamples(
370
+ fnName: string,
371
+ samples: ArrayLike<number>,
372
+ argName = 'samples',
373
+ ): void {
374
+ if (samples.length === 0) {
375
+ throw new RangeError(`${fnName}: ${argName} must not be empty`);
376
+ }
377
+ }
378
+
379
+ function assertFiniteSamples(
380
+ fnName: string,
381
+ samples: ArrayLike<number>,
382
+ validate: boolean,
383
+ argName = 'samples',
384
+ ): void {
385
+ if (!validate) {
386
+ return;
387
+ }
388
+ for (let i = 0; i < samples.length; i++) {
389
+ const v = samples[i] as number;
390
+ if (!Number.isFinite(v)) {
391
+ throw new RangeError(`${fnName}: ${argName} contains NaN or Inf at index ${i}`);
392
+ }
393
+ }
394
+ }
395
+
396
+ function assertSamples(
397
+ fnName: string,
398
+ samples: ArrayLike<number>,
399
+ validate: boolean,
400
+ argName = 'samples',
401
+ ): void {
402
+ assertNonEmptySamples(fnName, samples, argName);
403
+ assertFiniteSamples(fnName, samples, validate, argName);
404
+ }
405
+
406
+ function assertFiniteScalar(fnName: string, value: number, argName: string): void {
407
+ if (!Number.isFinite(value)) {
408
+ throw new RangeError(`${fnName}: ${argName} must be a finite number`);
409
+ }
410
+ }
411
+
271
412
  // ============================================================================
272
413
  // Initialization
273
414
  // ============================================================================
@@ -327,6 +468,60 @@ export function engineAbiVersion(): number {
327
468
  return module.engineAbiVersion();
328
469
  }
329
470
 
471
+ export function voiceChangerAbiVersion(): number {
472
+ if (!module) {
473
+ throw new Error('Module not initialized. Call init() first.');
474
+ }
475
+ return module.voiceChangerAbiVersion();
476
+ }
477
+
478
+ // Canonical ordinal order of the built-in voice-character presets, matching the
479
+ // C ABI SonareVoiceCharacterPreset enum and SONARE_REALTIME_VOICE_CHANGER_PRESET_IDS.
480
+ const VOICE_PRESET_ORDINALS: readonly VoicePresetId[] = [
481
+ 'neutral-monitor',
482
+ 'bright-idol',
483
+ 'soft-whisper',
484
+ 'deep-narrator',
485
+ 'robot-mascot',
486
+ 'dark-villain',
487
+ ];
488
+
489
+ function resolveVoicePresetOrdinal(preset: VoicePresetId | number): number {
490
+ if (typeof preset === 'number') {
491
+ return preset;
492
+ }
493
+ const ordinal = VOICE_PRESET_ORDINALS.indexOf(preset);
494
+ if (ordinal < 0) {
495
+ throw new Error(`Unknown voice character preset: ${preset}`);
496
+ }
497
+ return ordinal;
498
+ }
499
+
500
+ /**
501
+ * Map a voice-character preset ordinal (or canonical id) to its canonical id
502
+ * string (e.g. `'bright-idol'`). Returns `null` for an out-of-range ordinal.
503
+ */
504
+ export function voiceCharacterPresetId(preset: VoicePresetId | number): string | null {
505
+ if (!module) {
506
+ throw new Error('Module not initialized. Call init() first.');
507
+ }
508
+ return module.voiceCharacterPresetId(resolveVoicePresetOrdinal(preset));
509
+ }
510
+
511
+ /**
512
+ * Return the canonical (normalized) flat POD config for a built-in voice
513
+ * preset, skipping the JSON round-trip. Accepts a canonical preset id or its
514
+ * integer ordinal. Returns `null` for an out-of-range ordinal.
515
+ */
516
+ export function realtimeVoiceChangerPresetConfig(
517
+ preset: VoicePresetId | number,
518
+ ): RealtimeVoiceChangerPodConfig | null {
519
+ if (!module) {
520
+ throw new Error('Module not initialized. Call init() first.');
521
+ }
522
+ return module.realtimeVoiceChangerPresetConfig(resolveVoicePresetOrdinal(preset));
523
+ }
524
+
330
525
  export function engineCapabilities(): EngineCapabilities {
331
526
  const abiVersion = engineAbiVersion();
332
527
  const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === 'function';
@@ -532,6 +727,34 @@ export class RealtimeEngine {
532
727
  return this.native.process(channels);
533
728
  }
534
729
 
730
+ /**
731
+ * Allocates persistent per-channel WASM-heap scratch for the zero-copy
732
+ * `getChannelBuffer` / `processPrepared` realtime path. Call once (off the
733
+ * audio thread) before driving `processPrepared` from an AudioWorklet so the
734
+ * render callback never allocates on the C++/JS heap.
735
+ */
736
+ prepareChannels(numChannels: number, maxFrames: number): void {
737
+ this.native.prepareChannels(numChannels, maxFrames);
738
+ }
739
+
740
+ /**
741
+ * Returns a Float32Array view onto the persistent WASM-heap scratch for one
742
+ * channel (valid for up to `numFrames`). Fill it, call `processPrepared`, then
743
+ * read the same view back. Re-acquire after WASM memory growth.
744
+ */
745
+ getChannelBuffer(channel: number, numFrames: number): Float32Array {
746
+ return this.native.getChannelBuffer(channel, numFrames);
747
+ }
748
+
749
+ /**
750
+ * Runs the engine in place over the prepared per-channel scratch buffers.
751
+ * Allocation-free: safe to call on the AudioWorklet render thread after
752
+ * `prepareChannels`.
753
+ */
754
+ processPrepared(numFrames: number): void {
755
+ this.native.processPrepared(numFrames);
756
+ }
757
+
535
758
  processWithMonitor(channels: Float32Array[]): WasmEngineProcessWithMonitorResult {
536
759
  return this.native.processWithMonitor(channels);
537
760
  }
@@ -569,10 +792,10 @@ export class RealtimeEngine {
569
792
  * Detect BPM from audio samples.
570
793
  *
571
794
  * @param samples - Audio samples (mono, float32)
572
- * @param sampleRate - Sample rate in Hz
795
+ * @param sampleRate - Sample rate in Hz (default: 22050)
573
796
  * @returns Detected BPM
574
797
  */
575
- export function detectBpm(samples: Float32Array, sampleRate: number): number {
798
+ export function detectBpm(samples: Float32Array, sampleRate = 22050): number {
576
799
  if (!module) {
577
800
  throw new Error('Module not initialized. Call init() first.');
578
801
  }
@@ -583,12 +806,12 @@ export function detectBpm(samples: Float32Array, sampleRate: number): number {
583
806
  * Detect musical key from audio samples.
584
807
  *
585
808
  * @param samples - Audio samples (mono, float32)
586
- * @param sampleRate - Sample rate in Hz
809
+ * @param sampleRate - Sample rate in Hz (default: 22050)
587
810
  * @returns Detected key
588
811
  */
589
812
  export function detectKey(
590
813
  samples: Float32Array,
591
- sampleRate: number,
814
+ sampleRate = 22050,
592
815
  options: KeyDetectionOptions = {},
593
816
  ): Key {
594
817
  if (!module) {
@@ -685,7 +908,7 @@ function keyProfileValue(profile: KeyDetectionOptions['profile'] | undefined): n
685
908
 
686
909
  export function detectKeyCandidates(
687
910
  samples: Float32Array,
688
- sampleRate: number,
911
+ sampleRate = 22050,
689
912
  options: KeyDetectionOptions = {},
690
913
  ): KeyCandidate[] {
691
914
  if (!module) {
@@ -711,10 +934,10 @@ export function detectKeyCandidates(
711
934
  * Detect onset times from audio samples.
712
935
  *
713
936
  * @param samples - Audio samples (mono, float32)
714
- * @param sampleRate - Sample rate in Hz
937
+ * @param sampleRate - Sample rate in Hz (default: 22050)
715
938
  * @returns Array of onset times in seconds
716
939
  */
717
- export function detectOnsets(samples: Float32Array, sampleRate: number): Float32Array {
940
+ export function detectOnsets(samples: Float32Array, sampleRate = 22050): Float32Array {
718
941
  if (!module) {
719
942
  throw new Error('Module not initialized. Call init() first.');
720
943
  }
@@ -725,10 +948,10 @@ export function detectOnsets(samples: Float32Array, sampleRate: number): Float32
725
948
  * Detect beat times from audio samples.
726
949
  *
727
950
  * @param samples - Audio samples (mono, float32)
728
- * @param sampleRate - Sample rate in Hz
951
+ * @param sampleRate - Sample rate in Hz (default: 22050)
729
952
  * @returns Array of beat times in seconds
730
953
  */
731
- export function detectBeats(samples: Float32Array, sampleRate: number): Float32Array {
954
+ export function detectBeats(samples: Float32Array, sampleRate = 22050): Float32Array {
732
955
  if (!module) {
733
956
  throw new Error('Module not initialized. Call init() first.');
734
957
  }
@@ -739,10 +962,10 @@ export function detectBeats(samples: Float32Array, sampleRate: number): Float32A
739
962
  * Detect downbeat times from audio samples.
740
963
  *
741
964
  * @param samples - Audio samples (mono, float32)
742
- * @param sampleRate - Sample rate in Hz
965
+ * @param sampleRate - Sample rate in Hz (default: 22050)
743
966
  * @returns Array of downbeat times in seconds
744
967
  */
745
- export function detectDownbeats(samples: Float32Array, sampleRate: number): Float32Array {
968
+ export function detectDownbeats(samples: Float32Array, sampleRate = 22050): Float32Array {
746
969
  if (!module) {
747
970
  throw new Error('Module not initialized. Call init() first.');
748
971
  }
@@ -767,13 +990,13 @@ function convertChordAnalysisResult(wasm: WasmChordAnalysisResult): ChordAnalysi
767
990
  * Detect chords from audio samples.
768
991
  *
769
992
  * @param samples - Audio samples (mono, float32)
770
- * @param sampleRate - Sample rate in Hz
993
+ * @param sampleRate - Sample rate in Hz (default: 22050)
771
994
  * @param options - Optional chord detection settings
772
995
  * @returns Detected chord segments
773
996
  */
774
997
  export function detectChords(
775
998
  samples: Float32Array,
776
- sampleRate: number,
999
+ sampleRate = 22050,
777
1000
  options: ChordDetectionOptions = {},
778
1001
  ): ChordAnalysisResult {
779
1002
  if (!module) {
@@ -857,10 +1080,10 @@ function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult {
857
1080
  * Perform complete music analysis.
858
1081
  *
859
1082
  * @param samples - Audio samples (mono, float32)
860
- * @param sampleRate - Sample rate in Hz
1083
+ * @param sampleRate - Sample rate in Hz (default: 22050)
861
1084
  * @returns Complete analysis result
862
1085
  */
863
- export function analyze(samples: Float32Array, sampleRate: number): AnalysisResult {
1086
+ export function analyze(samples: Float32Array, sampleRate = 22050): AnalysisResult {
864
1087
  if (!module) {
865
1088
  throw new Error('Module not initialized. Call init() first.');
866
1089
  }
@@ -870,7 +1093,7 @@ export function analyze(samples: Float32Array, sampleRate: number): AnalysisResu
870
1093
 
871
1094
  export function analyzeImpulseResponse(
872
1095
  samples: Float32Array,
873
- sampleRate: number,
1096
+ sampleRate = 48000,
874
1097
  nOctaveBands = 6,
875
1098
  ): AcousticResult {
876
1099
  if (!module) {
@@ -886,7 +1109,7 @@ export function analyzeImpulseResponse(
886
1109
 
887
1110
  export function detectAcoustic(
888
1111
  samples: Float32Array,
889
- sampleRate: number,
1112
+ sampleRate = 48000,
890
1113
  nOctaveBands = 6,
891
1114
  nThirdOctaveSubbands = 24,
892
1115
  minDecayDb = 30.0,
@@ -906,17 +1129,68 @@ export function detectAcoustic(
906
1129
  return result;
907
1130
  }
908
1131
 
1132
+ /**
1133
+ * Synthesize a room impulse response from shoebox geometry. `hasError` is true
1134
+ * when the source/listener falls outside the room (the RIR is then empty).
1135
+ */
1136
+ export function synthesizeRir(options: RirSynthOptions = {}): RirResult {
1137
+ if (!module) {
1138
+ throw new Error('Module not initialized. Call init() first.');
1139
+ }
1140
+ if (typeof module.synthesizeRir !== 'function') {
1141
+ throw new Error('libsonare was built without acoustic-simulation support');
1142
+ }
1143
+ return module.synthesizeRir(options);
1144
+ }
1145
+
1146
+ /**
1147
+ * Estimate an equivalent room (volume/dimensions/absorption/DRR) from a
1148
+ * recording or impulse response.
1149
+ */
1150
+ export function estimateRoom(
1151
+ samples: Float32Array,
1152
+ sampleRate = 48000,
1153
+ options: RoomEstimateOptions = {},
1154
+ ): RoomEstimateResult {
1155
+ if (!module) {
1156
+ throw new Error('Module not initialized. Call init() first.');
1157
+ }
1158
+ if (typeof module.estimateRoom !== 'function') {
1159
+ throw new Error('libsonare was built without acoustic-simulation support');
1160
+ }
1161
+ return module.estimateRoom(samples, sampleRate, options);
1162
+ }
1163
+
1164
+ /**
1165
+ * Morph a recording's reverberation toward a target room (creative FX, not
1166
+ * dereverberation). Returns the morphed samples (input length plus the target
1167
+ * room's reverb tail).
1168
+ */
1169
+ export function roomMorph(
1170
+ samples: Float32Array,
1171
+ sampleRate: number,
1172
+ options: RoomMorphOptions = {},
1173
+ ): Float32Array {
1174
+ if (!module) {
1175
+ throw new Error('Module not initialized. Call init() first.');
1176
+ }
1177
+ if (typeof module.roomMorph !== 'function') {
1178
+ throw new Error('libsonare was built without acoustic-simulation support');
1179
+ }
1180
+ return module.roomMorph(samples, sampleRate, options);
1181
+ }
1182
+
909
1183
  /**
910
1184
  * Perform complete music analysis with progress reporting.
911
1185
  *
912
1186
  * @param samples - Audio samples (mono, float32)
913
- * @param sampleRate - Sample rate in Hz
1187
+ * @param sampleRate - Sample rate in Hz (default: 22050)
914
1188
  * @param onProgress - Progress callback (progress: 0-1, stage: string)
915
1189
  * @returns Complete analysis result
916
1190
  */
917
1191
  export function analyzeWithProgress(
918
1192
  samples: Float32Array,
919
- sampleRate: number,
1193
+ sampleRate = 22050,
920
1194
  onProgress: ProgressCallback,
921
1195
  ): AnalysisResult {
922
1196
  if (!module) {
@@ -926,6 +1200,154 @@ export function analyzeWithProgress(
926
1200
  return convertAnalysisResult(result);
927
1201
  }
928
1202
 
1203
+ export interface BpmCandidate {
1204
+ bpm: number;
1205
+ confidence: number;
1206
+ }
1207
+
1208
+ export interface BpmAnalysisResult {
1209
+ bpm: number;
1210
+ confidence: number;
1211
+ candidates: BpmCandidate[];
1212
+ autocorrelation: Float32Array;
1213
+ tempogram: Float32Array;
1214
+ }
1215
+
1216
+ export interface RhythmAnalysisResult {
1217
+ timeSignature: { numerator: number; denominator: number; confidence: number };
1218
+ syncopation: number;
1219
+ grooveType: string;
1220
+ patternRegularity: number;
1221
+ tempoStability: number;
1222
+ bpm: number;
1223
+ beatIntervals: Float32Array;
1224
+ }
1225
+
1226
+ export interface DynamicsAnalysisResult {
1227
+ dynamicRangeDb: number;
1228
+ peakDb: number;
1229
+ rmsDb: number;
1230
+ crestFactor: number;
1231
+ loudnessRangeDb: number;
1232
+ isCompressed: boolean;
1233
+ /** Loudness curve timestamps (seconds), parallel to {@link loudnessRmsDb}. */
1234
+ loudnessTimes: Float32Array;
1235
+ /** Loudness curve RMS values (dB), parallel to {@link loudnessTimes}. */
1236
+ loudnessRmsDb: Float32Array;
1237
+ }
1238
+
1239
+ /** Timbre metrics for one analysis window. Entries are ordered by time in `timbreOverTime`. */
1240
+ export interface TimbreFrame {
1241
+ brightness: number;
1242
+ warmth: number;
1243
+ density: number;
1244
+ roughness: number;
1245
+ complexity: number;
1246
+ }
1247
+
1248
+ export interface TimbreAnalysisResult extends TimbreFrame {
1249
+ spectralCentroid: Float32Array;
1250
+ spectralFlatness: Float32Array;
1251
+ spectralRolloff: Float32Array;
1252
+ /** Time-varying timbre metrics, one entry per analysis window. */
1253
+ timbreOverTime: TimbreFrame[];
1254
+ }
1255
+
1256
+ /**
1257
+ * Detailed BPM analysis (BPM, confidence, alternate candidates, autocorrelation,
1258
+ * tempogram). Matches the Node `analyzeBpm` / Python `analyze_bpm` surface.
1259
+ */
1260
+ export function analyzeBpm(
1261
+ samples: Float32Array,
1262
+ sampleRate = 22050,
1263
+ bpmMin = 30.0,
1264
+ bpmMax = 300.0,
1265
+ startBpm = 120.0,
1266
+ nFft = 2048,
1267
+ hopLength = 512,
1268
+ maxCandidates = 5,
1269
+ ): BpmAnalysisResult {
1270
+ if (!module) {
1271
+ throw new Error('Module not initialized. Call init() first.');
1272
+ }
1273
+ return module.analyzeBpm(
1274
+ samples,
1275
+ sampleRate,
1276
+ bpmMin,
1277
+ bpmMax,
1278
+ startBpm,
1279
+ nFft,
1280
+ hopLength,
1281
+ maxCandidates,
1282
+ );
1283
+ }
1284
+
1285
+ /**
1286
+ * Detailed rhythm analysis (time signature, groove, syncopation, beat intervals).
1287
+ */
1288
+ export function analyzeRhythm(
1289
+ samples: Float32Array,
1290
+ sampleRate = 22050,
1291
+ bpmMin = 60.0,
1292
+ bpmMax = 200.0,
1293
+ startBpm = 120.0,
1294
+ nFft = 2048,
1295
+ hopLength = 512,
1296
+ ): RhythmAnalysisResult {
1297
+ if (!module) {
1298
+ throw new Error('Module not initialized. Call init() first.');
1299
+ }
1300
+ return module.analyzeRhythm(samples, sampleRate, bpmMin, bpmMax, startBpm, nFft, hopLength);
1301
+ }
1302
+
1303
+ /**
1304
+ * Dynamics analysis (RMS, peak, crest factor, LRA, loudness curve).
1305
+ */
1306
+ export function analyzeDynamics(
1307
+ samples: Float32Array,
1308
+ sampleRate = 22050,
1309
+ windowSec = 0.4,
1310
+ hopLength = 512,
1311
+ compressionThreshold = 6.0,
1312
+ ): DynamicsAnalysisResult {
1313
+ if (!module) {
1314
+ throw new Error('Module not initialized. Call init() first.');
1315
+ }
1316
+ return module.analyzeDynamics(samples, sampleRate, windowSec, hopLength, compressionThreshold);
1317
+ }
1318
+
1319
+ /**
1320
+ * Timbre analysis (brightness/warmth/density/roughness/complexity plus spectral
1321
+ * features and per-window timbre frames).
1322
+ */
1323
+ export function analyzeTimbre(
1324
+ samples: Float32Array,
1325
+ sampleRate = 22050,
1326
+ nFft = 2048,
1327
+ hopLength = 512,
1328
+ nMels = 128,
1329
+ nMfcc = 13,
1330
+ windowSec = 0.5,
1331
+ ): TimbreAnalysisResult {
1332
+ if (!module) {
1333
+ throw new Error('Module not initialized. Call init() first.');
1334
+ }
1335
+ return module.analyzeTimbre(samples, sampleRate, nFft, hopLength, nMels, nMfcc, windowSec);
1336
+ }
1337
+
1338
+ /**
1339
+ * Whether this WASM build was compiled with FFmpeg support. Mirrors Node /
1340
+ * Python `hasFfmpegSupport`. In the published WASM binding this currently
1341
+ * always returns `false` (FFmpeg is not bundled into the .wasm), but the API
1342
+ * exists so caller code can branch on capabilities portably.
1343
+ */
1344
+ export function hasFfmpegSupport(): boolean {
1345
+ if (!module) {
1346
+ throw new Error('Module not initialized. Call init() first.');
1347
+ }
1348
+ return module.hasFfmpegSupport();
1349
+ }
1350
+
929
1351
  // ============================================================================
930
1352
  // Effects
931
1353
  // ============================================================================
@@ -934,14 +1356,14 @@ export function analyzeWithProgress(
934
1356
  * Perform Harmonic-Percussive Source Separation (HPSS).
935
1357
  *
936
1358
  * @param samples - Audio samples (mono, float32)
937
- * @param sampleRate - Sample rate in Hz
1359
+ * @param sampleRate - Sample rate in Hz (default: 22050)
938
1360
  * @param kernelHarmonic - Horizontal median filter size for harmonic (default: 31)
939
1361
  * @param kernelPercussive - Vertical median filter size for percussive (default: 31)
940
1362
  * @returns Separated harmonic and percussive components
941
1363
  */
942
1364
  export function hpss(
943
1365
  samples: Float32Array,
944
- sampleRate: number,
1366
+ sampleRate = 22050,
945
1367
  kernelHarmonic = 31,
946
1368
  kernelPercussive = 31,
947
1369
  ): HpssResult {
@@ -958,10 +1380,15 @@ export function hpss(
958
1380
  * @param sampleRate - Sample rate in Hz
959
1381
  * @returns Harmonic component
960
1382
  */
961
- export function harmonic(samples: Float32Array, sampleRate: number): Float32Array {
1383
+ export function harmonic(
1384
+ samples: Float32Array,
1385
+ sampleRate: number,
1386
+ options: ValidateOptions = {},
1387
+ ): Float32Array {
962
1388
  if (!module) {
963
1389
  throw new Error('Module not initialized. Call init() first.');
964
1390
  }
1391
+ assertSamples('harmonic', samples, options.validate !== false);
965
1392
  return module.harmonic(samples, sampleRate);
966
1393
  }
967
1394
 
@@ -972,10 +1399,15 @@ export function harmonic(samples: Float32Array, sampleRate: number): Float32Arra
972
1399
  * @param sampleRate - Sample rate in Hz
973
1400
  * @returns Percussive component
974
1401
  */
975
- export function percussive(samples: Float32Array, sampleRate: number): Float32Array {
1402
+ export function percussive(
1403
+ samples: Float32Array,
1404
+ sampleRate: number,
1405
+ options: ValidateOptions = {},
1406
+ ): Float32Array {
976
1407
  if (!module) {
977
1408
  throw new Error('Module not initialized. Call init() first.');
978
1409
  }
1410
+ assertSamples('percussive', samples, options.validate !== false);
979
1411
  return module.percussive(samples, sampleRate);
980
1412
  }
981
1413
 
@@ -987,10 +1419,16 @@ export function percussive(samples: Float32Array, sampleRate: number): Float32Ar
987
1419
  * @param rate - Time stretch rate (0.5 = double duration, 2.0 = half duration)
988
1420
  * @returns Time-stretched audio
989
1421
  */
990
- export function timeStretch(samples: Float32Array, sampleRate: number, rate: number): Float32Array {
1422
+ export function timeStretch(
1423
+ samples: Float32Array,
1424
+ sampleRate: number,
1425
+ rate: number,
1426
+ options: ValidateOptions = {},
1427
+ ): Float32Array {
991
1428
  if (!module) {
992
1429
  throw new Error('Module not initialized. Call init() first.');
993
1430
  }
1431
+ assertSamples('timeStretch', samples, options.validate !== false);
994
1432
  return module.timeStretch(samples, sampleRate, rate);
995
1433
  }
996
1434
 
@@ -1006,10 +1444,12 @@ export function pitchShift(
1006
1444
  samples: Float32Array,
1007
1445
  sampleRate: number,
1008
1446
  semitones: number,
1447
+ options: ValidateOptions = {},
1009
1448
  ): Float32Array {
1010
1449
  if (!module) {
1011
1450
  throw new Error('Module not initialized. Call init() first.');
1012
1451
  }
1452
+ assertSamples('pitchShift', samples, options.validate !== false);
1013
1453
  return module.pitchShift(samples, sampleRate, semitones);
1014
1454
  }
1015
1455
 
@@ -1024,13 +1464,15 @@ export function pitchShift(
1024
1464
  */
1025
1465
  export function pitchCorrectToMidi(
1026
1466
  samples: Float32Array,
1027
- sampleRate: number,
1028
- currentMidi: number,
1029
- targetMidi: number,
1467
+ sampleRate = 22050,
1468
+ currentMidi = 69.0,
1469
+ targetMidi = 69.0,
1470
+ options: ValidateOptions = {},
1030
1471
  ): Float32Array {
1031
1472
  if (!module) {
1032
1473
  throw new Error('Module not initialized. Call init() first.');
1033
1474
  }
1475
+ assertSamples('pitchCorrectToMidi', samples, options.validate !== false);
1034
1476
  return module.pitchCorrectToMidi(samples, sampleRate, currentMidi, targetMidi);
1035
1477
  }
1036
1478
 
@@ -1046,14 +1488,16 @@ export function pitchCorrectToMidi(
1046
1488
  */
1047
1489
  export function noteStretch(
1048
1490
  samples: Float32Array,
1049
- sampleRate: number,
1050
- onsetSample: number,
1051
- offsetSample: number,
1052
- stretchRatio: number,
1491
+ sampleRate = 22050,
1492
+ onsetSample = 0,
1493
+ offsetSample = 0,
1494
+ stretchRatio = 1.0,
1495
+ options: ValidateOptions = {},
1053
1496
  ): Float32Array {
1054
1497
  if (!module) {
1055
1498
  throw new Error('Module not initialized. Call init() first.');
1056
1499
  }
1500
+ assertSamples('noteStretch', samples, options.validate !== false);
1057
1501
  return module.noteStretch(samples, sampleRate, onsetSample, offsetSample, stretchRatio);
1058
1502
  }
1059
1503
 
@@ -1068,36 +1512,103 @@ export function noteStretch(
1068
1512
  */
1069
1513
  export function voiceChange(
1070
1514
  samples: Float32Array,
1071
- sampleRate: number,
1072
- pitchSemitones: number,
1073
- formantFactor: number,
1515
+ sampleRate = 22050,
1516
+ pitchSemitones = 0.0,
1517
+ formantFactor = 1.0,
1518
+ options: ValidateOptions = {},
1074
1519
  ): Float32Array {
1075
1520
  if (!module) {
1076
1521
  throw new Error('Module not initialized. Call init() first.');
1077
1522
  }
1523
+ assertSamples('voiceChange', samples, options.validate !== false);
1078
1524
  return module.voiceChange(samples, sampleRate, pitchSemitones, formantFactor);
1079
1525
  }
1080
1526
 
1527
+ /** Options for the offline {@link voiceChangeRealtime} convenience wrapper. */
1528
+ export interface VoiceChangeRealtimeOptions extends ValidateOptions {
1529
+ sampleRate?: number;
1530
+ /** Voice-changer preset id or full config object. */
1531
+ preset?: RealtimeVoiceChangerConfigInput;
1532
+ /** Channel count (1 = mono, 2 = interleaved stereo). */
1533
+ channels?: 1 | 2;
1534
+ /** Block size for the internal render loop (default 512). */
1535
+ blockSize?: number;
1536
+ }
1537
+
1081
1538
  /**
1082
- * Normalize audio to target peak level.
1539
+ * Applies the realtime voice-changer chain to a whole buffer in one call.
1083
1540
  *
1084
- * @param samples - Audio samples (mono, float32)
1085
- * @param sampleRate - Sample rate in Hz
1086
- * @param targetDb - Target peak level in dB (default: 0 dB = full scale)
1087
- * @returns Normalized audio
1541
+ * Constructs and prepares a {@link RealtimeVoiceChanger}, runs the block loop
1542
+ * for the caller, then disposes it — matching the Python `voice_change_realtime`
1543
+ * and Node `voiceChangeRealtime` convenience wrappers. For mono, `samples` is a
1544
+ * plain mono buffer; for stereo, `samples` is interleaved (L0,R0,L1,R1,...).
1545
+ *
1546
+ * @returns The processed buffer (same layout/length as the input).
1088
1547
  */
1089
- export function normalize(samples: Float32Array, sampleRate: number, targetDb = 0.0): Float32Array {
1548
+ export function voiceChangeRealtime(
1549
+ samples: Float32Array,
1550
+ options: VoiceChangeRealtimeOptions = {},
1551
+ ): Float32Array {
1090
1552
  if (!module) {
1091
1553
  throw new Error('Module not initialized. Call init() first.');
1092
1554
  }
1093
- return module.normalize(samples, sampleRate, targetDb);
1555
+ assertSamples('voiceChangeRealtime', samples, options.validate !== false);
1556
+ const channels = options.channels ?? 1;
1557
+ if (channels !== 1 && channels !== 2) {
1558
+ throw new Error('voiceChangeRealtime: channels must be 1 or 2.');
1559
+ }
1560
+ // 48000 matches the Python voice_change_realtime and Node voiceChangeRealtime
1561
+ // convenience wrappers (and the RealtimeVoiceChanger default).
1562
+ const sampleRate = options.sampleRate ?? 48000;
1563
+ const blockSize = Math.max(1, Math.floor(options.blockSize ?? 512));
1564
+ const changer = new RealtimeVoiceChanger(options.preset ?? 'neutral-monitor');
1565
+ try {
1566
+ changer.prepare(sampleRate, blockSize, channels);
1567
+ const out = new Float32Array(samples.length);
1568
+ if (channels === 1) {
1569
+ for (let offset = 0; offset < samples.length; offset += blockSize) {
1570
+ const block = samples.subarray(offset, Math.min(offset + blockSize, samples.length));
1571
+ out.set(changer.processMono(block), offset);
1572
+ }
1573
+ } else {
1574
+ const frameStride = blockSize * 2;
1575
+ for (let offset = 0; offset < samples.length; offset += frameStride) {
1576
+ const block = samples.subarray(offset, Math.min(offset + frameStride, samples.length));
1577
+ out.set(changer.processInterleaved(block, 2), offset);
1578
+ }
1579
+ }
1580
+ return out;
1581
+ } finally {
1582
+ changer.delete();
1583
+ }
1094
1584
  }
1095
1585
 
1096
1586
  /**
1097
- * Apply mastering loudness normalization with a true-peak ceiling.
1587
+ * Normalize audio to target peak level.
1098
1588
  *
1099
1589
  * @param samples - Audio samples (mono, float32)
1100
1590
  * @param sampleRate - Sample rate in Hz
1591
+ * @param targetDb - Target peak level in dB (default: 0 dB = full scale)
1592
+ * @returns Normalized audio
1593
+ */
1594
+ export function normalize(
1595
+ samples: Float32Array,
1596
+ sampleRate: number,
1597
+ targetDb = 0.0,
1598
+ options: ValidateOptions = {},
1599
+ ): Float32Array {
1600
+ if (!module) {
1601
+ throw new Error('Module not initialized. Call init() first.');
1602
+ }
1603
+ assertSamples('normalize', samples, options.validate !== false);
1604
+ return module.normalize(samples, sampleRate, targetDb);
1605
+ }
1606
+
1607
+ /**
1608
+ * Apply mastering loudness normalization with a true-peak ceiling.
1609
+ *
1610
+ * @param samples - Audio samples (mono, float32)
1611
+ * @param sampleRate - Sample rate in Hz (default: 22050)
1101
1612
  * @param targetLufs - Target integrated LUFS (default: -14)
1102
1613
  * @param ceilingDb - True/sample peak ceiling in dBFS (default: -1)
1103
1614
  * @param truePeakOversample - Oversampling factor used for peak estimation
@@ -1105,7 +1616,7 @@ export function normalize(samples: Float32Array, sampleRate: number, targetDb =
1105
1616
  */
1106
1617
  export function mastering(
1107
1618
  samples: Float32Array,
1108
- sampleRate: number,
1619
+ sampleRate = 22050,
1109
1620
  targetLufs = -14.0,
1110
1621
  ceilingDb = -1.0,
1111
1622
  truePeakOversample = 4,
@@ -1147,7 +1658,7 @@ export function masteringStereoAnalysisNames(): StereoAnalysis[] {
1147
1658
  export function masteringProcess(
1148
1659
  processorName: SoloProcessor,
1149
1660
  samples: Float32Array,
1150
- sampleRate: number,
1661
+ sampleRate = 22050,
1151
1662
  params: MasteringProcessorParams = {},
1152
1663
  ): MasteringResult {
1153
1664
  if (!module) {
@@ -1160,7 +1671,7 @@ export function masteringProcessStereo(
1160
1671
  processorName: SoloProcessor,
1161
1672
  left: Float32Array,
1162
1673
  right: Float32Array,
1163
- sampleRate: number,
1674
+ sampleRate = 22050,
1164
1675
  params: MasteringProcessorParams = {},
1165
1676
  ): MasteringStereoResult {
1166
1677
  if (!module) {
@@ -1176,7 +1687,7 @@ export function masteringPairProcess(
1176
1687
  processorName: PairProcessor,
1177
1688
  source: Float32Array,
1178
1689
  reference: Float32Array,
1179
- sampleRate: number,
1690
+ sampleRate = 22050,
1180
1691
  params: MasteringProcessorParams = {},
1181
1692
  ): MasteringResult {
1182
1693
  if (!module) {
@@ -1189,7 +1700,7 @@ export function masteringPairAnalyze(
1189
1700
  analysisName: PairAnalysis,
1190
1701
  source: Float32Array,
1191
1702
  reference: Float32Array,
1192
- sampleRate: number,
1703
+ sampleRate = 22050,
1193
1704
  params: MasteringProcessorParams = {},
1194
1705
  ): string {
1195
1706
  if (!module) {
@@ -1202,7 +1713,7 @@ export function masteringStereoAnalyze(
1202
1713
  analysisName: StereoAnalysis,
1203
1714
  left: Float32Array,
1204
1715
  right: Float32Array,
1205
- sampleRate: number,
1716
+ sampleRate = 22050,
1206
1717
  params: MasteringProcessorParams = {},
1207
1718
  ): string {
1208
1719
  if (!module) {
@@ -1213,7 +1724,7 @@ export function masteringStereoAnalyze(
1213
1724
 
1214
1725
  export function masteringAssistantSuggest(
1215
1726
  samples: Float32Array,
1216
- sampleRate: number,
1727
+ sampleRate = 22050,
1217
1728
  params: MasteringProcessorParams = {},
1218
1729
  ): string {
1219
1730
  if (!module) {
@@ -1224,7 +1735,7 @@ export function masteringAssistantSuggest(
1224
1735
 
1225
1736
  export function masteringAudioProfile(
1226
1737
  samples: Float32Array,
1227
- sampleRate: number,
1738
+ sampleRate = 22050,
1228
1739
  params: MasteringProcessorParams = {},
1229
1740
  ): string {
1230
1741
  if (!module) {
@@ -1235,7 +1746,7 @@ export function masteringAudioProfile(
1235
1746
 
1236
1747
  export function masteringStreamingPreview(
1237
1748
  samples: Float32Array,
1238
- sampleRate: number,
1749
+ sampleRate = 22050,
1239
1750
  platforms: StreamingPlatform[] = [],
1240
1751
  ): string {
1241
1752
  if (!module) {
@@ -1244,17 +1755,271 @@ export function masteringStreamingPreview(
1244
1755
  return module.masteringStreamingPreview(samples, sampleRate, platforms);
1245
1756
  }
1246
1757
 
1758
+ // ============================================================================
1759
+ // Mastering repair (declick, denoise_classical, declip, decrackle, dehum,
1760
+ // dereverb_classical, trim_silence) — hand-written bindings.
1761
+ // ============================================================================
1762
+
1763
+ /** Options for `masteringRepairDeclick`. */
1764
+ export interface DeclickOptions {
1765
+ threshold?: number;
1766
+ neighborRatio?: number;
1767
+ maxClickSamples?: number;
1768
+ lpcOrder?: number;
1769
+ residualRatio?: number;
1770
+ }
1771
+
1772
+ /** Algorithms accepted by `masteringRepairDenoiseClassical`. */
1773
+ export type DenoiseClassicalMode = 'logMmse' | 'mmseStsa' | 'spectralSubtraction';
1774
+
1775
+ /** Noise PSD estimators accepted by `masteringRepairDenoiseClassical`. */
1776
+ export type DenoiseClassicalNoiseEstimator = 'quantile' | 'mcra' | 'imcra';
1777
+
1778
+ /** Options for `masteringRepairDenoiseClassical`. */
1779
+ export interface DenoiseClassicalOptions {
1780
+ mode?: DenoiseClassicalMode;
1781
+ noiseEstimator?: DenoiseClassicalNoiseEstimator;
1782
+ nFft?: number;
1783
+ hopLength?: number;
1784
+ ddAlpha?: number;
1785
+ gainFloor?: number;
1786
+ overSubtraction?: number;
1787
+ spectralFloor?: number;
1788
+ noiseEstimationQuantile?: number;
1789
+ speechPresenceGain?: boolean;
1790
+ gainSmoothing?: boolean;
1791
+ }
1792
+
1793
+ /** Offline LPC-based declicker. */
1794
+ export function masteringRepairDeclick(
1795
+ samples: Float32Array,
1796
+ sampleRate: number,
1797
+ options: DeclickOptions = {},
1798
+ ): Float32Array {
1799
+ return requireModule().masteringRepairDeclick(samples, sampleRate, options);
1800
+ }
1801
+
1802
+ /** Offline STFT-domain classical denoiser (LogMMSE / MMSE-STSA / SpectralSubtraction). */
1803
+ export function masteringRepairDenoiseClassical(
1804
+ samples: Float32Array,
1805
+ sampleRate: number,
1806
+ options: DenoiseClassicalOptions = {},
1807
+ ): Float32Array {
1808
+ return requireModule().masteringRepairDenoiseClassical(samples, sampleRate, options);
1809
+ }
1810
+
1811
+ /** Options for `masteringRepairDeclip`. */
1812
+ export interface DeclipOptions {
1813
+ clipThreshold?: number;
1814
+ lpcOrder?: number;
1815
+ iterations?: number;
1816
+ lpcBlend?: number;
1817
+ }
1818
+
1819
+ /** Algorithms accepted by `masteringRepairDecrackle`. */
1820
+ export type DecrackleMode = 'median' | 'waveletShrinkage';
1821
+
1822
+ /** Options for `masteringRepairDecrackle`. */
1823
+ export interface DecrackleOptions {
1824
+ threshold?: number;
1825
+ mode?: DecrackleMode;
1826
+ levels?: number;
1827
+ }
1828
+
1829
+ /** Options for `masteringRepairDehum`. */
1830
+ export interface DehumOptions {
1831
+ fundamentalHz?: number;
1832
+ harmonics?: number;
1833
+ q?: number;
1834
+ adaptive?: boolean;
1835
+ searchRangeHz?: number;
1836
+ adaptation?: number;
1837
+ frameSize?: number;
1838
+ pllBandwidth?: number;
1839
+ }
1840
+
1841
+ /** Options for `masteringRepairDereverbClassical`. */
1842
+ export interface DereverbClassicalOptions {
1843
+ threshold?: number;
1844
+ attenuation?: number;
1845
+ nFft?: number;
1846
+ hopLength?: number;
1847
+ t60Sec?: number;
1848
+ lateDelayMs?: number;
1849
+ overSubtraction?: number;
1850
+ spectralFloor?: number;
1851
+ wpeEnabled?: boolean;
1852
+ wpeIterations?: number;
1853
+ wpeTaps?: number;
1854
+ wpeStrength?: number;
1855
+ }
1856
+
1857
+ /** Trimming modes accepted by `masteringRepairTrimSilence`. */
1858
+ export type TrimSilenceMode = 'peak' | 'lufsGated';
1859
+
1860
+ /** Options for `masteringRepairTrimSilence`. */
1861
+ export interface TrimSilenceOptions {
1862
+ threshold?: number;
1863
+ paddingSamples?: number;
1864
+ mode?: TrimSilenceMode;
1865
+ gateLufs?: number;
1866
+ windowMs?: number;
1867
+ }
1868
+
1869
+ /** Offline LPC-based declipper. */
1870
+ export function masteringRepairDeclip(
1871
+ samples: Float32Array,
1872
+ sampleRate: number,
1873
+ options: DeclipOptions = {},
1874
+ ): Float32Array {
1875
+ return requireModule().masteringRepairDeclip(samples, sampleRate, options);
1876
+ }
1877
+
1878
+ /** Offline crackle suppressor (median or wavelet-shrinkage). */
1879
+ export function masteringRepairDecrackle(
1880
+ samples: Float32Array,
1881
+ sampleRate: number,
1882
+ options: DecrackleOptions = {},
1883
+ ): Float32Array {
1884
+ return requireModule().masteringRepairDecrackle(samples, sampleRate, options);
1885
+ }
1886
+
1887
+ /** Offline mains-hum remover. */
1888
+ export function masteringRepairDehum(
1889
+ samples: Float32Array,
1890
+ sampleRate: number,
1891
+ options: DehumOptions = {},
1892
+ ): Float32Array {
1893
+ return requireModule().masteringRepairDehum(samples, sampleRate, options);
1894
+ }
1895
+
1896
+ /** Offline classical dereverberator (spectral subtraction + optional WPE). */
1897
+ export function masteringRepairDereverbClassical(
1898
+ samples: Float32Array,
1899
+ sampleRate: number,
1900
+ options: DereverbClassicalOptions = {},
1901
+ ): Float32Array {
1902
+ return requireModule().masteringRepairDereverbClassical(samples, sampleRate, options);
1903
+ }
1904
+
1905
+ /** Offline silence trimmer (peak threshold or LUFS-gated). */
1906
+ export function masteringRepairTrimSilence(
1907
+ samples: Float32Array,
1908
+ sampleRate: number,
1909
+ options: TrimSilenceOptions = {},
1910
+ ): Float32Array {
1911
+ return requireModule().masteringRepairTrimSilence(samples, sampleRate, options);
1912
+ }
1913
+
1914
+ // ============================================================================
1915
+ // Mastering — offline dynamics processors (compressor / gate / transient_shaper)
1916
+ // ============================================================================
1917
+
1918
+ /** Compressor sidechain detector mode. */
1919
+ export type CompressorDetector = 'peak' | 'rms' | 'log_rms';
1920
+
1921
+ /** Options for `masteringDynamicsCompressor`. */
1922
+ export interface CompressorOptions extends ValidateOptions {
1923
+ thresholdDb?: number;
1924
+ ratio?: number;
1925
+ attackMs?: number;
1926
+ releaseMs?: number;
1927
+ kneeDb?: number;
1928
+ makeupGainDb?: number;
1929
+ autoMakeup?: boolean;
1930
+ detector?: CompressorDetector | number;
1931
+ sidechainHpfEnabled?: boolean;
1932
+ sidechainHpfHz?: number;
1933
+ pdrTimeMs?: number;
1934
+ pdrReleaseScale?: number;
1935
+ }
1936
+
1937
+ /** Options for `masteringDynamicsGate`. */
1938
+ export interface GateOptions extends ValidateOptions {
1939
+ thresholdDb?: number;
1940
+ attackMs?: number;
1941
+ releaseMs?: number;
1942
+ rangeDb?: number;
1943
+ holdMs?: number;
1944
+ closeThresholdDb?: number;
1945
+ keyHpfHz?: number;
1946
+ }
1947
+
1948
+ /** Options for `masteringDynamicsTransientShaper`. */
1949
+ export interface TransientShaperOptions extends ValidateOptions {
1950
+ attackGainDb?: number;
1951
+ sustainGainDb?: number;
1952
+ fastAttackMs?: number;
1953
+ fastReleaseMs?: number;
1954
+ slowAttackMs?: number;
1955
+ slowReleaseMs?: number;
1956
+ sensitivity?: number;
1957
+ maxGainDb?: number;
1958
+ gainSmoothingMs?: number;
1959
+ lookaheadMs?: number;
1960
+ }
1961
+
1962
+ /** Result envelope returned by offline mastering dynamics processors. */
1963
+ export interface DynamicsResult {
1964
+ samples: Float32Array;
1965
+ latencySamples: number;
1966
+ }
1967
+
1968
+ const COMPRESSOR_DETECTOR_MAP: Record<CompressorDetector, number> = {
1969
+ peak: 0,
1970
+ rms: 1,
1971
+ log_rms: 2,
1972
+ };
1973
+
1974
+ /** Offline feed-forward compressor (soft knee, optional auto-makeup / sidechain HPF). */
1975
+ export function masteringDynamicsCompressor(
1976
+ samples: Float32Array,
1977
+ sampleRate: number,
1978
+ options: CompressorOptions = {},
1979
+ ): DynamicsResult {
1980
+ assertSamples('masteringDynamicsCompressor', samples, options.validate !== false);
1981
+ const detector =
1982
+ typeof options.detector === 'string'
1983
+ ? COMPRESSOR_DETECTOR_MAP[options.detector]
1984
+ : options.detector;
1985
+ const opts: Record<string, unknown> = { ...options };
1986
+ if (detector !== undefined) {
1987
+ opts.detector = detector;
1988
+ }
1989
+ return requireModule().masteringDynamicsCompressor(samples, sampleRate, opts);
1990
+ }
1991
+
1992
+ /** Offline noise gate (hysteresis, hold, optional key HPF). */
1993
+ export function masteringDynamicsGate(
1994
+ samples: Float32Array,
1995
+ sampleRate: number,
1996
+ options: GateOptions = {},
1997
+ ): DynamicsResult {
1998
+ assertSamples('masteringDynamicsGate', samples, options.validate !== false);
1999
+ return requireModule().masteringDynamicsGate(samples, sampleRate, options);
2000
+ }
2001
+
2002
+ /** Offline transient shaper (envelope-difference attack/sustain control). */
2003
+ export function masteringDynamicsTransientShaper(
2004
+ samples: Float32Array,
2005
+ sampleRate: number,
2006
+ options: TransientShaperOptions = {},
2007
+ ): DynamicsResult {
2008
+ assertSamples('masteringDynamicsTransientShaper', samples, options.validate !== false);
2009
+ return requireModule().masteringDynamicsTransientShaper(samples, sampleRate, options);
2010
+ }
2011
+
1247
2012
  /**
1248
2013
  * Apply a configurable mastering chain in WASM.
1249
2014
  *
1250
2015
  * @param samples - Audio samples (mono, float32)
1251
- * @param sampleRate - Sample rate in Hz
2016
+ * @param sampleRate - Sample rate in Hz (default: 22050)
1252
2017
  * @param config - Chain stage configuration
1253
2018
  * @returns Processed audio, loudness metadata, and applied stage names
1254
2019
  */
1255
2020
  export function masteringChain(
1256
2021
  samples: Float32Array,
1257
- sampleRate: number,
2022
+ sampleRate = 22050,
1258
2023
  config: MasteringChainConfig,
1259
2024
  ): MasteringChainResult {
1260
2025
  if (!module) {
@@ -1275,7 +2040,7 @@ export function masteringChain(
1275
2040
  export function masteringChainStereo(
1276
2041
  left: Float32Array,
1277
2042
  right: Float32Array,
1278
- sampleRate: number,
2043
+ sampleRate = 22050,
1279
2044
  config: MasteringChainConfig,
1280
2045
  ): MasteringStereoChainResult {
1281
2046
  if (!module) {
@@ -1291,14 +2056,14 @@ export function masteringChainStereo(
1291
2056
  * Apply a configurable mastering chain in WASM with progress reporting.
1292
2057
  *
1293
2058
  * @param samples - Audio samples (mono, float32)
1294
- * @param sampleRate - Sample rate in Hz
2059
+ * @param sampleRate - Sample rate in Hz (default: 22050)
1295
2060
  * @param config - Chain stage configuration
1296
2061
  * @param onProgress - Progress callback (progress: 0-1, stage: string)
1297
2062
  * @returns Processed audio, loudness metadata, and applied stage names
1298
2063
  */
1299
2064
  export function masteringChainWithProgress(
1300
2065
  samples: Float32Array,
1301
- sampleRate: number,
2066
+ sampleRate = 22050,
1302
2067
  config: MasteringChainConfig,
1303
2068
  onProgress: ProgressCallback,
1304
2069
  ): MasteringChainResult {
@@ -1326,7 +2091,7 @@ export function masteringChainWithProgress(
1326
2091
  export function masteringChainStereoWithProgress(
1327
2092
  left: Float32Array,
1328
2093
  right: Float32Array,
1329
- sampleRate: number,
2094
+ sampleRate = 22050,
1330
2095
  config: MasteringChainConfig,
1331
2096
  onProgress: ProgressCallback,
1332
2097
  ): MasteringStereoChainResult {
@@ -1361,14 +2126,14 @@ export function masteringPresetNames(): MasteringPreset[] {
1361
2126
  * Apply a named mastering preset chain to mono audio.
1362
2127
  *
1363
2128
  * @param samples - Audio samples (mono, float32)
1364
- * @param sampleRate - Sample rate in Hz
2129
+ * @param sampleRate - Sample rate in Hz (default: 22050)
1365
2130
  * @param presetName - Preset identifier from {@link masteringPresetNames}
1366
2131
  * @param overrides - Optional flat overrides (dot-notation, e.g. `'loudness.targetLufs'`) applied on top of the preset. Pass `null` for preset defaults.
1367
2132
  * @returns Processed audio, loudness metadata, and applied stage names
1368
2133
  */
1369
2134
  export function masterAudio(
1370
2135
  samples: Float32Array,
1371
- sampleRate: number,
2136
+ sampleRate = 22050,
1372
2137
  presetName: MasteringPreset,
1373
2138
  overrides: Record<string, number | boolean> | null = null,
1374
2139
  ): MasteringChainResult {
@@ -1391,7 +2156,7 @@ export function masterAudio(
1391
2156
  export function masterAudioStereo(
1392
2157
  left: Float32Array,
1393
2158
  right: Float32Array,
1394
- sampleRate: number,
2159
+ sampleRate = 22050,
1395
2160
  presetName: MasteringPreset,
1396
2161
  overrides: Record<string, number | boolean> | null = null,
1397
2162
  ): MasteringStereoChainResult {
@@ -1404,6 +2169,50 @@ export function masterAudioStereo(
1404
2169
  return module.masterAudioStereo(presetName, left, right, sampleRate, overrides);
1405
2170
  }
1406
2171
 
2172
+ /**
2173
+ * Mono `masterAudio` with per-stage progress reporting. `onProgress` is invoked
2174
+ * with `(progress, stage)` between each chain stage (progress is in [0,1]).
2175
+ */
2176
+ export function masterAudioWithProgress(
2177
+ samples: Float32Array,
2178
+ sampleRate = 22050,
2179
+ presetName: MasteringPreset,
2180
+ onProgress: ProgressCallback,
2181
+ overrides: Record<string, number | boolean> | null = null,
2182
+ ): MasteringChainResult {
2183
+ if (!module) {
2184
+ throw new Error('Module not initialized. Call init() first.');
2185
+ }
2186
+ return module.masterAudioWithProgress(presetName, samples, sampleRate, overrides, onProgress);
2187
+ }
2188
+
2189
+ /**
2190
+ * Stereo `masterAudio` with per-stage progress reporting.
2191
+ */
2192
+ export function masterAudioStereoWithProgress(
2193
+ left: Float32Array,
2194
+ right: Float32Array,
2195
+ sampleRate = 22050,
2196
+ presetName: MasteringPreset,
2197
+ onProgress: ProgressCallback,
2198
+ overrides: Record<string, number | boolean> | null = null,
2199
+ ): MasteringStereoChainResult {
2200
+ if (!module) {
2201
+ throw new Error('Module not initialized. Call init() first.');
2202
+ }
2203
+ if (left.length !== right.length) {
2204
+ throw new Error('Stereo channel lengths must match.');
2205
+ }
2206
+ return module.masterAudioStereoWithProgress(
2207
+ presetName,
2208
+ left,
2209
+ right,
2210
+ sampleRate,
2211
+ overrides,
2212
+ onProgress,
2213
+ );
2214
+ }
2215
+
1407
2216
  export function mixingScenePresetNames(): string[] {
1408
2217
  if (!module) {
1409
2218
  throw new Error('Module not initialized. Call init() first.');
@@ -1416,14 +2225,14 @@ export function mixingScenePresetNames(): string[] {
1416
2225
  * name shared with the Node and Python bindings; the returned JSON loads
1417
2226
  * directly into a {@link Mixer} via {@link Mixer.fromSceneJson}.
1418
2227
  *
1419
- * @param preset - Preset name (see {@link mixingScenePresetNames})
2228
+ * @param presetName - Preset name (see {@link mixingScenePresetNames})
1420
2229
  * @returns Scene JSON string
1421
2230
  */
1422
- export function mixingScenePresetJson(preset: string): string {
2231
+ export function mixingScenePresetJson(presetName: string): string {
1423
2232
  if (!module) {
1424
2233
  throw new Error('Module not initialized. Call init() first.');
1425
2234
  }
1426
- return module.mixingScenePresetJson(preset);
2235
+ return module.mixingScenePresetJson(presetName);
1427
2236
  }
1428
2237
 
1429
2238
  export function mixStereo(
@@ -1472,7 +2281,7 @@ export function mixStereo(
1472
2281
  * ```
1473
2282
  */
1474
2283
  export class StreamingMasteringChain {
1475
- private chain: import('./wasm_types').WasmStreamingMasteringChain;
2284
+ private chain: import('./sonare.js').WasmStreamingMasteringChain;
1476
2285
 
1477
2286
  constructor(config: MasteringChainConfig) {
1478
2287
  if (!module) {
@@ -1559,7 +2368,7 @@ export class StreamingMasteringChain {
1559
2368
  * ```
1560
2369
  */
1561
2370
  export class StreamingEqualizer {
1562
- private eq: import('./wasm_types').WasmStreamingEqualizer;
2371
+ private eq: import('./sonare.js').WasmStreamingEqualizer;
1563
2372
 
1564
2373
  constructor(config: StreamingEqualizerConfig = {}) {
1565
2374
  if (!module) {
@@ -1697,7 +2506,7 @@ export class StreamingEqualizer {
1697
2506
  * the underlying WASM object.
1698
2507
  */
1699
2508
  export class StreamingRetune {
1700
- private retune: import('./wasm_types').WasmStreamingRetune;
2509
+ private retune: import('./sonare.js').WasmStreamingRetune;
1701
2510
 
1702
2511
  constructor(config: StreamingRetuneConfig = {}) {
1703
2512
  if (!module) {
@@ -1749,32 +2558,207 @@ export class StreamingRetune {
1749
2558
  }
1750
2559
 
1751
2560
  // ============================================================================
1752
- // Mixer Class (scene-based persistent mixer)
2561
+ // RealtimeVoiceChanger Class
1753
2562
  // ============================================================================
1754
2563
 
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 {
2564
+ export class RealtimeVoiceChanger {
2565
+ private changer: import('./sonare.js').WasmRealtimeVoiceChanger;
2566
+
2567
+ constructor(config: RealtimeVoiceChangerConfigInput = 'neutral-monitor') {
2568
+ if (!module) {
2569
+ throw new Error('Module not initialized. Call init() first.');
2570
+ }
2571
+ this.changer = module.createRealtimeVoiceChanger(config as Record<string, unknown> | string);
2572
+ }
2573
+
2574
+ prepare(sampleRate: number, maxBlockSize = 128, channels = 1): void {
2575
+ this.changer.prepare(sampleRate, maxBlockSize, channels);
2576
+ }
2577
+
2578
+ reset(): void {
2579
+ this.changer.reset();
2580
+ }
2581
+
2582
+ setConfig(config: RealtimeVoiceChangerConfigInput): void {
2583
+ this.changer.setConfig(config as Record<string, unknown> | string);
2584
+ }
2585
+
2586
+ configJson(): string {
2587
+ return this.changer.configJson();
2588
+ }
2589
+
2590
+ latencySamples(): number {
2591
+ return this.changer.latencySamples();
2592
+ }
2593
+
2594
+ processMono(samples: Float32Array): Float32Array {
2595
+ return this.changer.processMono(samples);
2596
+ }
2597
+
2598
+ processMonoInto(samples: Float32Array, output: Float32Array): void {
2599
+ this.changer.processMonoInto(samples, output);
2600
+ }
2601
+
2602
+ processInterleaved(samples: Float32Array, channels: number): Float32Array {
2603
+ return this.changer.processInterleaved(samples, channels);
2604
+ }
2605
+
2606
+ processInterleavedInto(samples: Float32Array, channels: number, output: Float32Array): void {
2607
+ this.changer.processInterleavedInto(samples, channels, output);
2608
+ }
2609
+
2610
+ /**
2611
+ * Acquire a typed-memory view onto the WASM heap for mono input.
2612
+ *
2613
+ * Write your input samples into the returned `Float32Array` directly (e.g.
2614
+ * via `input.set(source)`); no copy crosses the JS↔C++ bridge until
2615
+ * {@link processPreparedMono} is called. The view is owned by this
2616
+ * RealtimeVoiceChanger and becomes invalid after {@link delete}; it may
2617
+ * also be invalidated if you later call this method with a larger
2618
+ * `numSamples` value (the underlying buffer may be reallocated).
2619
+ */
2620
+ getMonoInputBuffer(numSamples: number): Float32Array {
2621
+ return this.changer.getMonoInputBuffer(numSamples);
2622
+ }
2623
+
2624
+ /** Mono output view counterpart to {@link getMonoInputBuffer}. */
2625
+ getMonoOutputBuffer(numSamples: number): Float32Array {
2626
+ return this.changer.getMonoOutputBuffer(numSamples);
2627
+ }
2628
+
2629
+ /**
2630
+ * Process the previously-acquired mono input buffer in place. The output
2631
+ * appears in the buffer returned by {@link getMonoOutputBuffer}. No JS↔C++
2632
+ * sample-level crossings happen on this call — it just hands control to
2633
+ * the underlying DSP on already-on-heap data.
2634
+ */
2635
+ processPreparedMono(numSamples: number): void {
2636
+ this.changer.processPreparedMono(numSamples);
2637
+ }
2638
+
2639
+ /** Interleaved input view (layout L0,R0,L1,R1,...). */
2640
+ getInterleavedInputBuffer(numFrames: number, numChannels: number): Float32Array {
2641
+ return this.changer.getInterleavedInputBuffer(numFrames, numChannels);
2642
+ }
2643
+
2644
+ /** Interleaved output view counterpart. */
2645
+ getInterleavedOutputBuffer(numFrames: number, numChannels: number): Float32Array {
2646
+ return this.changer.getInterleavedOutputBuffer(numFrames, numChannels);
2647
+ }
2648
+
2649
+ /**
2650
+ * Process the previously-acquired interleaved buffer in place. Output
2651
+ * appears in the buffer returned by {@link getInterleavedOutputBuffer}.
2652
+ */
2653
+ processPreparedInterleaved(numFrames: number, numChannels: number): void {
2654
+ this.changer.processPreparedInterleaved(numFrames, numChannels);
2655
+ }
2656
+
2657
+ /**
2658
+ * Planar-channel input/output view (one Float32Array per channel). Matches
2659
+ * AudioWorklet's native layout; processing happens in place.
2660
+ */
2661
+ getPlanarChannelBuffer(channel: number, numFrames: number): Float32Array {
2662
+ return this.changer.getPlanarChannelBuffer(channel, numFrames);
2663
+ }
2664
+
2665
+ /**
2666
+ * Process the previously-acquired planar channel buffers in place. Each
2667
+ * channel must have been obtained from {@link getPlanarChannelBuffer}
2668
+ * with the same `numFrames`. Output replaces input in the same buffers.
2669
+ */
2670
+ processPreparedPlanar(numFrames: number): void {
2671
+ this.changer.processPreparedPlanar(numFrames);
2672
+ }
2673
+
2674
+ /**
2675
+ * Convenience factory for the mono zero-copy path: returns the input/output
2676
+ * heap views plus a `process()` thunk wired to the same `numSamples`. The
2677
+ * views are reused across calls and become invalid after {@link delete}.
2678
+ */
2679
+ createRealtimeMonoBuffer(numSamples: number): RealtimeVoiceChangerMonoBuffer {
2680
+ const input = this.getMonoInputBuffer(numSamples);
2681
+ const output = this.getMonoOutputBuffer(numSamples);
2682
+ return {
2683
+ input,
2684
+ output,
2685
+ process: () => this.processPreparedMono(numSamples),
2686
+ };
2687
+ }
2688
+
2689
+ /** Same as {@link createRealtimeMonoBuffer} but for interleaved I/O. */
2690
+ createRealtimeInterleavedBuffer(
2691
+ numFrames: number,
2692
+ numChannels: number,
2693
+ ): RealtimeVoiceChangerInterleavedBuffer {
2694
+ const input = this.getInterleavedInputBuffer(numFrames, numChannels);
2695
+ const output = this.getInterleavedOutputBuffer(numFrames, numChannels);
2696
+ return {
2697
+ input,
2698
+ output,
2699
+ channels: numChannels,
2700
+ process: () => this.processPreparedInterleaved(numFrames, numChannels),
2701
+ };
2702
+ }
2703
+
2704
+ /**
2705
+ * Convenience factory for the planar zero-copy path. Acquires one
2706
+ * heap-backed Float32Array per channel and returns a `process()` thunk
2707
+ * wired to the same `numFrames`. Buffers are reused across calls and
2708
+ * become invalid after {@link delete}.
2709
+ */
2710
+ createRealtimePlanarBuffer(
2711
+ numFrames: number,
2712
+ numChannels: number,
2713
+ ): RealtimeVoiceChangerPlanarBuffer {
2714
+ const channels: Float32Array[] = [];
2715
+ for (let ch = 0; ch < numChannels; ch++) {
2716
+ channels.push(this.getPlanarChannelBuffer(ch, numFrames));
2717
+ }
2718
+ return {
2719
+ channels,
2720
+ process: () => this.processPreparedPlanar(numFrames),
2721
+ };
2722
+ }
2723
+
2724
+ delete(): void {
2725
+ this.changer.delete();
2726
+ }
2727
+ }
2728
+
2729
+ export function realtimeVoiceChangerPresetNames(): VoicePresetId[] {
2730
+ if (!module) {
2731
+ throw new Error('Module not initialized. Call init() first.');
2732
+ }
2733
+ return module.realtimeVoiceChangerPresetNames() as VoicePresetId[];
2734
+ }
2735
+
2736
+ export function realtimeVoiceChangerPresetJson(name: VoicePresetId): string {
2737
+ if (!module) {
2738
+ throw new Error('Module not initialized. Call init() first.');
2739
+ }
2740
+ return module.realtimeVoiceChangerPresetJson(name);
2741
+ }
2742
+
2743
+ export function validateRealtimeVoiceChangerPresetJson(json: string): {
2744
+ ok: boolean;
2745
+ normalizedJson?: string;
2746
+ error?: string;
2747
+ } {
1768
2748
  if (!module) {
1769
2749
  throw new Error('Module not initialized. Call init() first.');
1770
2750
  }
1771
- return module.mixerPresetJson(preset);
2751
+ return module.validateRealtimeVoiceChangerPresetJson(json);
1772
2752
  }
1773
2753
 
2754
+ // ============================================================================
2755
+ // Mixer Class (scene-based persistent mixer)
2756
+ // ============================================================================
2757
+
1774
2758
  /**
1775
2759
  * Persistent, scene-based stereo mixer.
1776
2760
  *
1777
- * Build one from a scene JSON string (e.g. {@link mixerScenePresetJson} or a
2761
+ * Build one from a scene JSON string (e.g. {@link mixingScenePresetJson} or a
1778
2762
  * hand-authored scene), then feed per-strip stereo blocks through
1779
2763
  * {@link processStereo} to get the routed stereo master. Strips, sends, buses,
1780
2764
  * and inserts are described entirely by the scene; the routing graph is
@@ -1786,7 +2770,7 @@ export function mixerScenePresetJson(preset: string): string {
1786
2770
  *
1787
2771
  * @example
1788
2772
  * ```typescript
1789
- * const mixer = Mixer.fromSceneJson(mixerScenePresetJson('basicStereo'), 48000, 512);
2773
+ * const mixer = Mixer.fromSceneJson(mixingScenePresetJson('basicStereo'), 48000, 512);
1790
2774
  * try {
1791
2775
  * const out = mixer.processStereo([stripL], [stripR]);
1792
2776
  * } finally {
@@ -1795,9 +2779,9 @@ export function mixerScenePresetJson(preset: string): string {
1795
2779
  * ```
1796
2780
  */
1797
2781
  export class Mixer {
1798
- private mixer: import('./wasm_types').WasmMixer;
2782
+ private mixer: import('./sonare.js').WasmMixer;
1799
2783
 
1800
- private constructor(mixer: import('./wasm_types').WasmMixer) {
2784
+ private constructor(mixer: import('./sonare.js').WasmMixer) {
1801
2785
  this.mixer = mixer;
1802
2786
  }
1803
2787
 
@@ -1966,6 +2950,31 @@ export class Mixer {
1966
2950
  return this.mixer.vcaGroupCount();
1967
2951
  }
1968
2952
 
2953
+ /** Set the strip's input trim in dB. */
2954
+ setInputTrimDb(stripIndex: number, db: number): void {
2955
+ this.mixer.setInputTrimDb(stripIndex, db);
2956
+ }
2957
+
2958
+ /** Set the strip's fader level in dB. */
2959
+ setFaderDb(stripIndex: number, db: number): void {
2960
+ this.mixer.setFaderDb(stripIndex, db);
2961
+ }
2962
+
2963
+ /** Set the strip's pan position. */
2964
+ setPan(stripIndex: number, pan: number, panMode: PanMode | number = 0): void {
2965
+ this.mixer.setPan(stripIndex, pan, panModeCode(panMode));
2966
+ }
2967
+
2968
+ /** Set the strip's stereo width. */
2969
+ setWidth(stripIndex: number, width: number): void {
2970
+ this.mixer.setWidth(stripIndex, width);
2971
+ }
2972
+
2973
+ /** Set the strip's mute state. */
2974
+ setMuted(stripIndex: number, muted: boolean): void {
2975
+ this.mixer.setMuted(stripIndex, muted);
2976
+ }
2977
+
1969
2978
  /**
1970
2979
  * Set a strip's solo state. Takes effect on the next process without a
1971
2980
  * graph recompile.
@@ -2179,14 +3188,14 @@ export function trim(samples: Float32Array, sampleRate: number, thresholdDb = -6
2179
3188
  * Compute Short-Time Fourier Transform (STFT).
2180
3189
  *
2181
3190
  * @param samples - Audio samples (mono, float32)
2182
- * @param sampleRate - Sample rate in Hz
3191
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2183
3192
  * @param nFft - FFT size (default: 2048)
2184
3193
  * @param hopLength - Hop length (default: 512)
2185
3194
  * @returns STFT result with magnitude and power spectrograms
2186
3195
  */
2187
3196
  export function stft(
2188
3197
  samples: Float32Array,
2189
- sampleRate: number,
3198
+ sampleRate = 22050,
2190
3199
  nFft = 2048,
2191
3200
  hopLength = 512,
2192
3201
  ): StftResult {
@@ -2200,14 +3209,14 @@ export function stft(
2200
3209
  * Compute STFT and return magnitude in decibels.
2201
3210
  *
2202
3211
  * @param samples - Audio samples (mono, float32)
2203
- * @param sampleRate - Sample rate in Hz
3212
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2204
3213
  * @param nFft - FFT size (default: 2048)
2205
3214
  * @param hopLength - Hop length (default: 512)
2206
3215
  * @returns STFT result with dB values
2207
3216
  */
2208
3217
  export function stftDb(
2209
3218
  samples: Float32Array,
2210
- sampleRate: number,
3219
+ sampleRate = 22050,
2211
3220
  nFft = 2048,
2212
3221
  hopLength = 512,
2213
3222
  ): { nBins: number; nFrames: number; db: Float32Array } {
@@ -2225,7 +3234,7 @@ export function stftDb(
2225
3234
  * Compute Mel spectrogram.
2226
3235
  *
2227
3236
  * @param samples - Audio samples (mono, float32)
2228
- * @param sampleRate - Sample rate in Hz
3237
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2229
3238
  * @param nFft - FFT size (default: 2048)
2230
3239
  * @param hopLength - Hop length (default: 512)
2231
3240
  * @param nMels - Number of Mel bands (default: 128)
@@ -2233,7 +3242,7 @@ export function stftDb(
2233
3242
  */
2234
3243
  export function melSpectrogram(
2235
3244
  samples: Float32Array,
2236
- sampleRate: number,
3245
+ sampleRate = 22050,
2237
3246
  nFft = 2048,
2238
3247
  hopLength = 512,
2239
3248
  nMels = 128,
@@ -2248,20 +3257,20 @@ export function melSpectrogram(
2248
3257
  * Compute MFCC (Mel-Frequency Cepstral Coefficients).
2249
3258
  *
2250
3259
  * @param samples - Audio samples (mono, float32)
2251
- * @param sampleRate - Sample rate in Hz
3260
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2252
3261
  * @param nFft - FFT size (default: 2048)
2253
3262
  * @param hopLength - Hop length (default: 512)
2254
3263
  * @param nMels - Number of Mel bands (default: 128)
2255
- * @param nMfcc - Number of MFCC coefficients (default: 13)
3264
+ * @param nMfcc - Number of MFCC coefficients (default: 20)
2256
3265
  * @returns MFCC result
2257
3266
  */
2258
3267
  export function mfcc(
2259
3268
  samples: Float32Array,
2260
- sampleRate: number,
3269
+ sampleRate = 22050,
2261
3270
  nFft = 2048,
2262
3271
  hopLength = 512,
2263
3272
  nMels = 128,
2264
- nMfcc = 13,
3273
+ nMfcc = 20,
2265
3274
  ): MfccResult {
2266
3275
  if (!module) {
2267
3276
  throw new Error('Module not initialized. Call init() first.');
@@ -2282,23 +3291,23 @@ export function mfcc(
2282
3291
  * @param nFrames - Number of time frames
2283
3292
  * @param sampleRate - Sample rate in Hz
2284
3293
  * @param nFft - FFT size (default: 2048)
2285
- * @param hopLength - Hop length (default: 512)
3294
+ * @param fmin - Lower Mel band edge in Hz (default: 0)
3295
+ * @param fmax - Upper Mel band edge in Hz (default: sr/2 when 0)
2286
3296
  * @returns STFT power spectrogram result
2287
3297
  */
2288
3298
  export function melToStft(
2289
3299
  melPower: Float32Array,
2290
3300
  nMels: number,
2291
3301
  nFrames: number,
2292
- sampleRate: number,
3302
+ sampleRate = 22050,
2293
3303
  nFft = 2048,
2294
- hopLength = 512,
2295
3304
  fmin = 0,
2296
3305
  fmax = 0,
2297
3306
  ): StftPowerResult {
2298
3307
  if (!module) {
2299
3308
  throw new Error('Module not initialized. Call init() first.');
2300
3309
  }
2301
- return module.melToStft(melPower, nMels, nFrames, sampleRate, nFft, hopLength, fmin, fmax);
3310
+ return module.melToStft(melPower, nMels, nFrames, sampleRate, nFft, fmin, fmax);
2302
3311
  }
2303
3312
 
2304
3313
  /**
@@ -2311,6 +3320,8 @@ export function melToStft(
2311
3320
  * @param sampleRate - Sample rate in Hz
2312
3321
  * @param nFft - FFT size (default: 2048)
2313
3322
  * @param hopLength - Hop length (default: 512)
3323
+ * @param fmin - Minimum Mel frequency in Hz (default: 0)
3324
+ * @param fmax - Maximum Mel frequency in Hz (default: 0 = sr/2)
2314
3325
  * @param nIter - Griffin-Lim iterations (default: 32)
2315
3326
  * @returns Reconstructed audio samples (mono, float32)
2316
3327
  */
@@ -2318,12 +3329,12 @@ export function melToAudio(
2318
3329
  melPower: Float32Array,
2319
3330
  nMels: number,
2320
3331
  nFrames: number,
2321
- sampleRate: number,
3332
+ sampleRate = 22050,
2322
3333
  nFft = 2048,
2323
3334
  hopLength = 512,
2324
- nIter = 32,
2325
3335
  fmin = 0,
2326
3336
  fmax = 0,
3337
+ nIter = 32,
2327
3338
  ): Float32Array {
2328
3339
  if (!module) {
2329
3340
  throw new Error('Module not initialized. Call init() first.');
@@ -2335,9 +3346,9 @@ export function melToAudio(
2335
3346
  sampleRate,
2336
3347
  nFft,
2337
3348
  hopLength,
2338
- nIter,
2339
3349
  fmin,
2340
3350
  fmax,
3351
+ nIter,
2341
3352
  );
2342
3353
  }
2343
3354
 
@@ -2360,114 +3371,318 @@ export function mfccToMel(
2360
3371
  if (!module) {
2361
3372
  throw new Error('Module not initialized. Call init() first.');
2362
3373
  }
2363
- return module.mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels);
3374
+ return module.mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels);
3375
+ }
3376
+
3377
+ /**
3378
+ * Reconstruct audio directly from MFCC coefficients via Griffin-Lim. Mirrors
3379
+ * `feature::mfcc_to_audio`.
3380
+ *
3381
+ * @param mfccCoefficients - MFCC matrix [nMfcc x nFrames] row-major
3382
+ * @param nMfcc - Number of MFCC coefficients
3383
+ * @param nFrames - Number of time frames
3384
+ * @param nMels - Number of Mel bins (default: 128)
3385
+ * @param sampleRate - Sample rate in Hz (default: 22050)
3386
+ * @param nFft - FFT size (default: 2048)
3387
+ * @param hopLength - Hop length (default: 512)
3388
+ * @param fmin - Minimum Mel frequency in Hz (default: 0)
3389
+ * @param fmax - Maximum Mel frequency in Hz (default: 0 = sr/2)
3390
+ * @param nIter - Griffin-Lim iterations (default: 32)
3391
+ * @returns Reconstructed audio samples (mono, float32)
3392
+ */
3393
+ export function mfccToAudio(
3394
+ mfccCoefficients: Float32Array,
3395
+ nMfcc: number,
3396
+ nFrames: number,
3397
+ nMels = 128,
3398
+ sampleRate = 22050,
3399
+ nFft = 2048,
3400
+ hopLength = 512,
3401
+ fmin = 0,
3402
+ fmax = 0,
3403
+ nIter = 32,
3404
+ ): Float32Array {
3405
+ if (!module) {
3406
+ throw new Error('Module not initialized. Call init() first.');
3407
+ }
3408
+ return module.mfccToAudio(
3409
+ mfccCoefficients,
3410
+ nMfcc,
3411
+ nFrames,
3412
+ nMels,
3413
+ sampleRate,
3414
+ nFft,
3415
+ hopLength,
3416
+ fmin,
3417
+ fmax,
3418
+ nIter,
3419
+ );
3420
+ }
3421
+
3422
+ // ============================================================================
3423
+ // Features - Chroma
3424
+ // ============================================================================
3425
+
3426
+ /**
3427
+ * Compute chromagram (pitch class distribution).
3428
+ *
3429
+ * @param samples - Audio samples (mono, float32)
3430
+ * @param sampleRate - Sample rate in Hz (default: 22050)
3431
+ * @param nFft - FFT size (default: 2048)
3432
+ * @param hopLength - Hop length (default: 512)
3433
+ * @returns Chroma features result
3434
+ */
3435
+ export function chroma(
3436
+ samples: Float32Array,
3437
+ sampleRate = 22050,
3438
+ nFft = 2048,
3439
+ hopLength = 512,
3440
+ ): ChromaResult {
3441
+ if (!module) {
3442
+ throw new Error('Module not initialized. Call init() first.');
3443
+ }
3444
+ return module.chroma(samples, sampleRate, nFft, hopLength);
3445
+ }
3446
+
3447
+ // ============================================================================
3448
+ // Features - Spectral
3449
+ // ============================================================================
3450
+
3451
+ /**
3452
+ * Compute spectral centroid (center of mass of spectrum).
3453
+ *
3454
+ * @param samples - Audio samples (mono, float32)
3455
+ * @param sampleRate - Sample rate in Hz (default: 22050)
3456
+ * @param nFft - FFT size (default: 2048)
3457
+ * @param hopLength - Hop length (default: 512)
3458
+ * @returns Spectral centroid in Hz for each frame
3459
+ */
3460
+ export function spectralCentroid(
3461
+ samples: Float32Array,
3462
+ sampleRate = 22050,
3463
+ nFft = 2048,
3464
+ hopLength = 512,
3465
+ ): Float32Array {
3466
+ if (!module) {
3467
+ throw new Error('Module not initialized. Call init() first.');
3468
+ }
3469
+ return module.spectralCentroid(samples, sampleRate, nFft, hopLength);
3470
+ }
3471
+
3472
+ /**
3473
+ * Compute spectral contrast (librosa.feature.spectral_contrast).
3474
+ *
3475
+ * @returns Matrix2d of shape (nBands + 1) x nFrames.
3476
+ */
3477
+ export function spectralContrast(
3478
+ samples: Float32Array,
3479
+ sampleRate = 22050,
3480
+ nFft = 2048,
3481
+ hopLength = 512,
3482
+ nBands = 6,
3483
+ fmin = 200.0,
3484
+ quantile = 0.02,
3485
+ ): WasmMatrix2dResult {
3486
+ if (!module) {
3487
+ throw new Error('Module not initialized. Call init() first.');
3488
+ }
3489
+ return module.spectralContrast(samples, sampleRate, nFft, hopLength, nBands, fmin, quantile);
3490
+ }
3491
+
3492
+ /**
3493
+ * Fit per-frame polynomial coefficients (librosa.feature.poly_features).
3494
+ *
3495
+ * @returns Matrix2d of shape (order + 1) x nFrames.
3496
+ */
3497
+ export function polyFeatures(
3498
+ samples: Float32Array,
3499
+ sampleRate = 22050,
3500
+ nFft = 2048,
3501
+ hopLength = 512,
3502
+ order = 1,
3503
+ ): WasmMatrix2dResult {
3504
+ if (!module) {
3505
+ throw new Error('Module not initialized. Call init() first.');
3506
+ }
3507
+ return module.polyFeatures(samples, sampleRate, nFft, hopLength, order);
3508
+ }
3509
+
3510
+ /**
3511
+ * Locate zero-crossing indices of a signal (librosa.zero_crossings).
3512
+ */
3513
+ export function zeroCrossings(
3514
+ samples: Float32Array,
3515
+ threshold = 1e-10,
3516
+ refMagnitude = false,
3517
+ pad = true,
3518
+ zeroPos = true,
3519
+ ): Int32Array {
3520
+ if (!module) {
3521
+ throw new Error('Module not initialized. Call init() first.');
3522
+ }
3523
+ return module.zeroCrossings(samples, threshold, refMagnitude, pad, zeroPos);
3524
+ }
3525
+
3526
+ /**
3527
+ * Estimate the global tuning offset from a set of frequencies
3528
+ * (librosa.pitch_tuning). Returns a deviation in fractions of a bin.
3529
+ */
3530
+ export function pitchTuning(
3531
+ frequencies: Float32Array,
3532
+ resolution = 0.01,
3533
+ binsPerOctave = 12,
3534
+ ): number {
3535
+ if (!module) {
3536
+ throw new Error('Module not initialized. Call init() first.');
3537
+ }
3538
+ return module.pitchTuning(frequencies, resolution, binsPerOctave);
3539
+ }
3540
+
3541
+ /**
3542
+ * Estimate the tuning offset of an audio signal (librosa.estimate_tuning).
3543
+ */
3544
+ export function estimateTuning(
3545
+ samples: Float32Array,
3546
+ sampleRate = 22050,
3547
+ nFft = 2048,
3548
+ hopLength = 512,
3549
+ resolution = 0.01,
3550
+ binsPerOctave = 12,
3551
+ ): number {
3552
+ if (!module) {
3553
+ throw new Error('Module not initialized. Call init() first.');
3554
+ }
3555
+ return module.estimateTuning(samples, sampleRate, nFft, hopLength, resolution, binsPerOctave);
3556
+ }
3557
+
3558
+ /**
3559
+ * Non-negative matrix factorisation of a flattened [nFeatures x nFrames]
3560
+ * spectrogram (librosa.decompose.decompose). Returns the W and H factors.
3561
+ */
3562
+ export function decompose(
3563
+ s: Float32Array,
3564
+ nFeatures: number,
3565
+ nFrames: number,
3566
+ nComponents: number,
3567
+ nIter = 50,
3568
+ beta = 2.0,
3569
+ ): WasmDecomposeResult {
3570
+ if (!module) {
3571
+ throw new Error('Module not initialized. Call init() first.');
3572
+ }
3573
+ return module.decompose(s, nFeatures, nFrames, nComponents, nIter, beta);
3574
+ }
3575
+
3576
+ /**
3577
+ * Nearest-neighbour filtering of a flattened [nFeatures x nFrames] spectrogram
3578
+ * (librosa.decompose.nn_filter).
3579
+ */
3580
+ export function nnFilter(
3581
+ s: Float32Array,
3582
+ nFeatures: number,
3583
+ nFrames: number,
3584
+ aggregate = 'mean',
3585
+ k = 7,
3586
+ width = 1,
3587
+ ): WasmMatrix2dResult {
3588
+ if (!module) {
3589
+ throw new Error('Module not initialized. Call init() first.');
3590
+ }
3591
+ return module.nnFilter(s, nFeatures, nFrames, aggregate, k, width);
3592
+ }
3593
+
3594
+ /**
3595
+ * Reorder/concatenate a signal by interval slices (librosa.effects.remix).
3596
+ *
3597
+ * @param intervals - Flat (start, end) sample pairs (even length).
3598
+ */
3599
+ export function remix(
3600
+ samples: Float32Array,
3601
+ intervals: Int32Array | ArrayLike<number>,
3602
+ sampleRate = 22050,
3603
+ alignZeros = false,
3604
+ ): Float32Array {
3605
+ if (!module) {
3606
+ throw new Error('Module not initialized. Call init() first.');
3607
+ }
3608
+ // Sample indices must reach the native side as exact 32-bit integers. Passing
3609
+ // a Float32Array (or a number[] holding fractional/large values) would round
3610
+ // boundaries above 2^24 and misalign the slice. Coerce to an Int32Array,
3611
+ // truncating toward zero, so callers can hand us any numeric array safely.
3612
+ const intervalsI32 =
3613
+ intervals instanceof Int32Array ? intervals : Int32Array.from(intervals, (v) => Math.trunc(v));
3614
+ return module.remix(samples, intervalsI32, sampleRate, alignZeros);
2364
3615
  }
2365
3616
 
2366
3617
  /**
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)
3618
+ * Phase-vocoder time-scale modification (rate > 1 faster, < 1 slower).
2379
3619
  */
2380
- export function mfccToAudio(
2381
- mfccCoefficients: Float32Array,
2382
- nMfcc: number,
2383
- nFrames: number,
2384
- nMels: number,
2385
- sampleRate: number,
3620
+ export function phaseVocoder(
3621
+ samples: Float32Array,
3622
+ rate: number,
3623
+ sampleRate = 22050,
2386
3624
  nFft = 2048,
2387
3625
  hopLength = 512,
2388
- nIter = 32,
2389
- fmin = 0,
2390
- fmax = 0,
2391
3626
  ): Float32Array {
2392
3627
  if (!module) {
2393
3628
  throw new Error('Module not initialized. Call init() first.');
2394
3629
  }
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
- );
3630
+ return module.phaseVocoder(samples, sampleRate, rate, nFft, hopLength);
2407
3631
  }
2408
3632
 
2409
- // ============================================================================
2410
- // Features - Chroma
2411
- // ============================================================================
2412
-
2413
3633
  /**
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
3634
+ * HPSS into harmonic / percussive / residual signals.
2421
3635
  */
2422
- export function chroma(
3636
+ export function hpssWithResidual(
2423
3637
  samples: Float32Array,
2424
- sampleRate: number,
2425
- nFft = 2048,
2426
- hopLength = 512,
2427
- ): ChromaResult {
3638
+ sampleRate = 22050,
3639
+ kernelHarmonic = 31,
3640
+ kernelPercussive = 31,
3641
+ ): WasmHpssWithResidualResult {
2428
3642
  if (!module) {
2429
3643
  throw new Error('Module not initialized. Call init() first.');
2430
3644
  }
2431
- return module.chroma(samples, sampleRate, nFft, hopLength);
3645
+ return module.hpssWithResidual(samples, sampleRate, kernelHarmonic, kernelPercussive);
2432
3646
  }
2433
3647
 
2434
- // ============================================================================
2435
- // Features - Spectral
2436
- // ============================================================================
2437
-
2438
3648
  /**
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
3649
+ * Channel-weighted multichannel integrated loudness + LRA (ITU-R BS.1770 /
3650
+ * EBU R128) from an interleaved buffer of `frames * channels` samples. The
3651
+ * per-channel frame count is derived from the buffer length and `channels`.
2446
3652
  */
2447
- export function spectralCentroid(
3653
+ export function lufsInterleaved(
2448
3654
  samples: Float32Array,
2449
- sampleRate: number,
2450
- nFft = 2048,
2451
- hopLength = 512,
2452
- ): Float32Array {
3655
+ channels: number,
3656
+ sampleRate = 22050,
3657
+ ): WasmLufsResult {
2453
3658
  if (!module) {
2454
3659
  throw new Error('Module not initialized. Call init() first.');
2455
3660
  }
2456
- return module.spectralCentroid(samples, sampleRate, nFft, hopLength);
3661
+ return module.lufsInterleaved(samples, channels, sampleRate);
3662
+ }
3663
+
3664
+ /**
3665
+ * Standards-compliant EBU R128 loudness range (LRA) in LU.
3666
+ */
3667
+ export function ebur128LoudnessRange(samples: Float32Array, sampleRate = 22050): number {
3668
+ if (!module) {
3669
+ throw new Error('Module not initialized. Call init() first.');
3670
+ }
3671
+ return module.ebur128LoudnessRange(samples, sampleRate);
2457
3672
  }
2458
3673
 
2459
3674
  /**
2460
3675
  * Compute spectral bandwidth.
2461
3676
  *
2462
3677
  * @param samples - Audio samples (mono, float32)
2463
- * @param sampleRate - Sample rate in Hz
3678
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2464
3679
  * @param nFft - FFT size (default: 2048)
2465
3680
  * @param hopLength - Hop length (default: 512)
2466
3681
  * @returns Spectral bandwidth in Hz for each frame
2467
3682
  */
2468
3683
  export function spectralBandwidth(
2469
3684
  samples: Float32Array,
2470
- sampleRate: number,
3685
+ sampleRate = 22050,
2471
3686
  nFft = 2048,
2472
3687
  hopLength = 512,
2473
3688
  ): Float32Array {
@@ -2481,7 +3696,7 @@ export function spectralBandwidth(
2481
3696
  * Compute spectral rolloff frequency.
2482
3697
  *
2483
3698
  * @param samples - Audio samples (mono, float32)
2484
- * @param sampleRate - Sample rate in Hz
3699
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2485
3700
  * @param nFft - FFT size (default: 2048)
2486
3701
  * @param hopLength - Hop length (default: 512)
2487
3702
  * @param rollPercent - Percentage threshold (default: 0.85)
@@ -2489,7 +3704,7 @@ export function spectralBandwidth(
2489
3704
  */
2490
3705
  export function spectralRolloff(
2491
3706
  samples: Float32Array,
2492
- sampleRate: number,
3707
+ sampleRate = 22050,
2493
3708
  nFft = 2048,
2494
3709
  hopLength = 512,
2495
3710
  rollPercent = 0.85,
@@ -2504,14 +3719,14 @@ export function spectralRolloff(
2504
3719
  * Compute spectral flatness.
2505
3720
  *
2506
3721
  * @param samples - Audio samples (mono, float32)
2507
- * @param sampleRate - Sample rate in Hz
3722
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2508
3723
  * @param nFft - FFT size (default: 2048)
2509
3724
  * @param hopLength - Hop length (default: 512)
2510
3725
  * @returns Spectral flatness for each frame (0 = tonal, 1 = noise-like)
2511
3726
  */
2512
3727
  export function spectralFlatness(
2513
3728
  samples: Float32Array,
2514
- sampleRate: number,
3729
+ sampleRate = 22050,
2515
3730
  nFft = 2048,
2516
3731
  hopLength = 512,
2517
3732
  ): Float32Array {
@@ -2525,14 +3740,14 @@ export function spectralFlatness(
2525
3740
  * Compute zero crossing rate.
2526
3741
  *
2527
3742
  * @param samples - Audio samples (mono, float32)
2528
- * @param sampleRate - Sample rate in Hz
3743
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2529
3744
  * @param frameLength - Frame length (default: 2048)
2530
3745
  * @param hopLength - Hop length (default: 512)
2531
3746
  * @returns Zero crossing rate for each frame
2532
3747
  */
2533
3748
  export function zeroCrossingRate(
2534
3749
  samples: Float32Array,
2535
- sampleRate: number,
3750
+ sampleRate = 22050,
2536
3751
  frameLength = 2048,
2537
3752
  hopLength = 512,
2538
3753
  ): Float32Array {
@@ -2546,14 +3761,14 @@ export function zeroCrossingRate(
2546
3761
  * Compute RMS energy.
2547
3762
  *
2548
3763
  * @param samples - Audio samples (mono, float32)
2549
- * @param sampleRate - Sample rate in Hz
3764
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2550
3765
  * @param frameLength - Frame length (default: 2048)
2551
3766
  * @param hopLength - Hop length (default: 512)
2552
3767
  * @returns RMS energy for each frame
2553
3768
  */
2554
3769
  export function rmsEnergy(
2555
3770
  samples: Float32Array,
2556
- sampleRate: number,
3771
+ sampleRate = 22050,
2557
3772
  frameLength = 2048,
2558
3773
  hopLength = 512,
2559
3774
  ): Float32Array {
@@ -2571,54 +3786,76 @@ export function rmsEnergy(
2571
3786
  * Detect pitch using YIN algorithm.
2572
3787
  *
2573
3788
  * @param samples - Audio samples (mono, float32)
2574
- * @param sampleRate - Sample rate in Hz
3789
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2575
3790
  * @param frameLength - Frame length (default: 2048)
2576
3791
  * @param hopLength - Hop length (default: 512)
2577
3792
  * @param fmin - Minimum frequency in Hz (default: 65)
2578
3793
  * @param fmax - Maximum frequency in Hz (default: 2093)
2579
3794
  * @param threshold - YIN threshold (default: 0.3)
3795
+ * @param fillNa - If true, return 0 for unvoiced f0 frames; otherwise keep NaN (default: false)
2580
3796
  * @returns Pitch detection result
2581
3797
  */
2582
3798
  export function pitchYin(
2583
3799
  samples: Float32Array,
2584
- sampleRate: number,
3800
+ sampleRate = 22050,
2585
3801
  frameLength = 2048,
2586
3802
  hopLength = 512,
2587
3803
  fmin = 65.0,
2588
3804
  fmax = 2093.0,
2589
3805
  threshold = 0.3,
3806
+ fillNa = false,
2590
3807
  ): PitchResult {
2591
3808
  if (!module) {
2592
3809
  throw new Error('Module not initialized. Call init() first.');
2593
3810
  }
2594
- return module.pitchYin(samples, sampleRate, frameLength, hopLength, fmin, fmax, threshold);
3811
+ return module.pitchYin(
3812
+ samples,
3813
+ sampleRate,
3814
+ frameLength,
3815
+ hopLength,
3816
+ fmin,
3817
+ fmax,
3818
+ threshold,
3819
+ fillNa,
3820
+ );
2595
3821
  }
2596
3822
 
2597
3823
  /**
2598
3824
  * Detect pitch using pYIN algorithm (probabilistic YIN with HMM smoothing).
2599
3825
  *
2600
3826
  * @param samples - Audio samples (mono, float32)
2601
- * @param sampleRate - Sample rate in Hz
3827
+ * @param sampleRate - Sample rate in Hz (default: 22050)
2602
3828
  * @param frameLength - Frame length (default: 2048)
2603
3829
  * @param hopLength - Hop length (default: 512)
2604
3830
  * @param fmin - Minimum frequency in Hz (default: 65)
2605
3831
  * @param fmax - Maximum frequency in Hz (default: 2093)
2606
3832
  * @param threshold - YIN threshold (default: 0.3)
3833
+ * @param fillNa - If true, return 0 for unvoiced f0 frames; otherwise keep NaN (default: false)
2607
3834
  * @returns Pitch detection result
2608
3835
  */
2609
3836
  export function pitchPyin(
2610
3837
  samples: Float32Array,
2611
- sampleRate: number,
3838
+ sampleRate = 22050,
2612
3839
  frameLength = 2048,
2613
3840
  hopLength = 512,
2614
3841
  fmin = 65.0,
2615
3842
  fmax = 2093.0,
2616
3843
  threshold = 0.3,
3844
+ fillNa = false,
2617
3845
  ): PitchResult {
2618
3846
  if (!module) {
2619
3847
  throw new Error('Module not initialized. Call init() first.');
2620
3848
  }
2621
- return module.pitchPyin(samples, sampleRate, frameLength, hopLength, fmin, fmax, threshold);
3849
+ return module.pitchPyin(
3850
+ samples,
3851
+ sampleRate,
3852
+ frameLength,
3853
+ hopLength,
3854
+ fmin,
3855
+ fmax,
3856
+ threshold,
3857
+ fillNa,
3858
+ );
2622
3859
  }
2623
3860
 
2624
3861
  // ============================================================================
@@ -2707,11 +3944,11 @@ export function noteToHz(note: string): number {
2707
3944
  * Convert frame index to time in seconds.
2708
3945
  *
2709
3946
  * @param frames - Frame index
2710
- * @param sr - Sample rate in Hz
2711
- * @param hopLength - Hop length in samples
3947
+ * @param sr - Sample rate in Hz (default: 22050)
3948
+ * @param hopLength - Hop length in samples (default: 512)
2712
3949
  * @returns Time in seconds
2713
3950
  */
2714
- export function framesToTime(frames: number, sr: number, hopLength: number): number {
3951
+ export function framesToTime(frames: number, sr = 22050, hopLength = 512): number {
2715
3952
  if (!module) {
2716
3953
  throw new Error('Module not initialized. Call init() first.');
2717
3954
  }
@@ -2722,11 +3959,11 @@ export function framesToTime(frames: number, sr: number, hopLength: number): num
2722
3959
  * Convert time in seconds to frame index.
2723
3960
  *
2724
3961
  * @param time - Time in seconds
2725
- * @param sr - Sample rate in Hz
2726
- * @param hopLength - Hop length in samples
3962
+ * @param sr - Sample rate in Hz (default: 22050)
3963
+ * @param hopLength - Hop length in samples (default: 512)
2727
3964
  * @returns Frame index
2728
3965
  */
2729
- export function timeToFrames(time: number, sr: number, hopLength: number): number {
3966
+ export function timeToFrames(time: number, sr = 22050, hopLength = 512): number {
2730
3967
  if (!module) {
2731
3968
  throw new Error('Module not initialized. Call init() first.');
2732
3969
  }
@@ -2834,18 +4071,18 @@ export function frameSignal(
2834
4071
  return module.frameSignal(samples, frameLength, hopLength);
2835
4072
  }
2836
4073
 
2837
- export function padCenter(values: Float32Array, size: number, padValue = 0.0): Float32Array {
4074
+ export function padCenter(values: Float32Array, targetSize: number, padValue = 0.0): Float32Array {
2838
4075
  if (!module) {
2839
4076
  throw new Error('Module not initialized. Call init() first.');
2840
4077
  }
2841
- return module.padCenter(values, size, padValue);
4078
+ return module.padCenter(values, targetSize, padValue);
2842
4079
  }
2843
4080
 
2844
- export function fixLength(values: Float32Array, size: number, padValue = 0.0): Float32Array {
4081
+ export function fixLength(values: Float32Array, targetSize: number, padValue = 0.0): Float32Array {
2845
4082
  if (!module) {
2846
4083
  throw new Error('Module not initialized. Call init() first.');
2847
4084
  }
2848
- return module.fixLength(values, size, padValue);
4085
+ return module.fixLength(values, targetSize, padValue);
2849
4086
  }
2850
4087
 
2851
4088
  export function fixFrames(frames: Int32Array, xMin = 0, xMax = -1, pad = true): Int32Array {
@@ -2870,11 +4107,7 @@ export function peakPick(
2870
4107
  return module.peakPick(values, preMax, postMax, preAvg, postAvg, delta, wait);
2871
4108
  }
2872
4109
 
2873
- export function vectorNormalize(
2874
- values: Float32Array,
2875
- normType = 0,
2876
- threshold = 1e-12,
2877
- ): Float32Array {
4110
+ export function vectorNormalize(values: Float32Array, normType = 0, threshold = 0.0): Float32Array {
2878
4111
  if (!module) {
2879
4112
  throw new Error('Module not initialized. Call init() first.');
2880
4113
  }
@@ -2902,7 +4135,7 @@ export function tonnetz(chromagram: Float32Array, nChroma: number, nFrames: numb
2902
4135
 
2903
4136
  export function tempogram(
2904
4137
  onsetEnvelope: Float32Array,
2905
- sampleRate: number,
4138
+ sampleRate = 22050,
2906
4139
  hopLength = 512,
2907
4140
  winLength = 384,
2908
4141
  mode: TempogramMode = 'autocorrelation',
@@ -2915,7 +4148,7 @@ export function tempogram(
2915
4148
 
2916
4149
  export function cyclicTempogram(
2917
4150
  onsetEnvelope: Float32Array,
2918
- sampleRate: number,
4151
+ sampleRate = 22050,
2919
4152
  hopLength = 512,
2920
4153
  winLength = 384,
2921
4154
  bpmMin = 60.0,
@@ -2929,7 +4162,7 @@ export function cyclicTempogram(
2929
4162
 
2930
4163
  export function plp(
2931
4164
  onsetEnvelope: Float32Array,
2932
- sampleRate: number,
4165
+ sampleRate = 22050,
2933
4166
  hopLength = 512,
2934
4167
  tempoMin = 30.0,
2935
4168
  tempoMax = 300.0,
@@ -3014,7 +4247,7 @@ export function vqt(
3014
4247
  * @param sampleRate - Sample rate in Hz (default: 22050)
3015
4248
  * @param nFft - FFT size (default: 2048)
3016
4249
  * @param hopLength - Hop length (default: 512)
3017
- * @param minSectionSec - Minimum section duration in seconds (default: 8.0)
4250
+ * @param minSectionSec - Minimum section duration in seconds (default: 4.0)
3018
4251
  * @returns Array of detected sections
3019
4252
  */
3020
4253
  export function analyzeSections(
@@ -3022,7 +4255,7 @@ export function analyzeSections(
3022
4255
  sampleRate = 22050,
3023
4256
  nFft = 2048,
3024
4257
  hopLength = 512,
3025
- minSectionSec = 8.0,
4258
+ minSectionSec = 4.0,
3026
4259
  ): Section[] {
3027
4260
  if (!module) {
3028
4261
  throw new Error('Module not initialized. Call init() first.');
@@ -3050,7 +4283,7 @@ export function analyzeMelody(
3050
4283
  fmin = 65.0,
3051
4284
  fmax = 2093.0,
3052
4285
  frameLength = 2048,
3053
- hopLength = 512,
4286
+ hopLength = 256,
3054
4287
  threshold = 0.1,
3055
4288
  ): MelodyResult {
3056
4289
  if (!module) {
@@ -3131,10 +4364,15 @@ export function tempogramRatio(
3131
4364
  * @param sampleRate - Sample rate in Hz (default: 22050)
3132
4365
  * @returns Loudness measurement result
3133
4366
  */
3134
- export function lufs(samples: Float32Array, sampleRate = 22050): LufsResult {
4367
+ export function lufs(
4368
+ samples: Float32Array,
4369
+ sampleRate = 22050,
4370
+ options: ValidateOptions = {},
4371
+ ): LufsResult {
3135
4372
  if (!module) {
3136
4373
  throw new Error('Module not initialized. Call init() first.');
3137
4374
  }
4375
+ assertSamples('lufs', samples, options.validate !== false);
3138
4376
  return module.lufs(samples, sampleRate);
3139
4377
  }
3140
4378
 
@@ -3145,10 +4383,15 @@ export function lufs(samples: Float32Array, sampleRate = 22050): LufsResult {
3145
4383
  * @param sampleRate - Sample rate in Hz (default: 22050)
3146
4384
  * @returns Momentary LUFS values over time
3147
4385
  */
3148
- export function momentaryLufs(samples: Float32Array, sampleRate = 22050): Float32Array {
4386
+ export function momentaryLufs(
4387
+ samples: Float32Array,
4388
+ sampleRate = 22050,
4389
+ options: ValidateOptions = {},
4390
+ ): Float32Array {
3149
4391
  if (!module) {
3150
4392
  throw new Error('Module not initialized. Call init() first.');
3151
4393
  }
4394
+ assertSamples('momentaryLufs', samples, options.validate !== false);
3152
4395
  return module.momentaryLufs(samples, sampleRate);
3153
4396
  }
3154
4397
 
@@ -3159,13 +4402,292 @@ export function momentaryLufs(samples: Float32Array, sampleRate = 22050): Float3
3159
4402
  * @param sampleRate - Sample rate in Hz (default: 22050)
3160
4403
  * @returns Short-term LUFS values over time
3161
4404
  */
3162
- export function shortTermLufs(samples: Float32Array, sampleRate = 22050): Float32Array {
4405
+ export function shortTermLufs(
4406
+ samples: Float32Array,
4407
+ sampleRate = 22050,
4408
+ options: ValidateOptions = {},
4409
+ ): Float32Array {
3163
4410
  if (!module) {
3164
4411
  throw new Error('Module not initialized. Call init() first.');
3165
4412
  }
4413
+ assertSamples('shortTermLufs', samples, options.validate !== false);
3166
4414
  return module.shortTermLufs(samples, sampleRate);
3167
4415
  }
3168
4416
 
4417
+ // ============================================================================
4418
+ // Metering — basic / true-peak / clipping / dynamic range
4419
+ // ============================================================================
4420
+
4421
+ /** One contiguous run of clipped samples reported by `meteringDetectClipping`. */
4422
+ export interface ClippingRegion {
4423
+ startSample: number;
4424
+ endSample: number;
4425
+ length: number;
4426
+ peak: number;
4427
+ }
4428
+
4429
+ /** Aggregated clipping report. */
4430
+ export interface ClippingReport {
4431
+ clippedSamples: number;
4432
+ clippingRatio: number;
4433
+ maxClippedPeak: number;
4434
+ regions: ClippingRegion[];
4435
+ }
4436
+
4437
+ /** Sliding-window dynamic range report. */
4438
+ export interface DynamicRangeReport {
4439
+ dynamicRangeDb: number;
4440
+ lowPercentileDb: number;
4441
+ highPercentileDb: number;
4442
+ windowRmsDb: Float32Array;
4443
+ }
4444
+
4445
+ function requireModule() {
4446
+ if (!module) {
4447
+ throw new Error('Module not initialized. Call init() first.');
4448
+ }
4449
+ return module;
4450
+ }
4451
+
4452
+ export function meteringPeakDb(
4453
+ samples: Float32Array,
4454
+ sampleRate = 22050,
4455
+ options: ValidateOptions = {},
4456
+ ): number {
4457
+ assertSamples('meteringPeakDb', samples, options.validate !== false);
4458
+ return requireModule().meteringPeakDb(samples, sampleRate);
4459
+ }
4460
+
4461
+ export function meteringRmsDb(
4462
+ samples: Float32Array,
4463
+ sampleRate = 22050,
4464
+ options: ValidateOptions = {},
4465
+ ): number {
4466
+ assertSamples('meteringRmsDb', samples, options.validate !== false);
4467
+ return requireModule().meteringRmsDb(samples, sampleRate);
4468
+ }
4469
+
4470
+ export function meteringCrestFactorDb(
4471
+ samples: Float32Array,
4472
+ sampleRate = 22050,
4473
+ options: ValidateOptions = {},
4474
+ ): number {
4475
+ assertSamples('meteringCrestFactorDb', samples, options.validate !== false);
4476
+ return requireModule().meteringCrestFactorDb(samples, sampleRate);
4477
+ }
4478
+
4479
+ export function meteringDcOffset(
4480
+ samples: Float32Array,
4481
+ sampleRate = 22050,
4482
+ options: ValidateOptions = {},
4483
+ ): number {
4484
+ assertSamples('meteringDcOffset', samples, options.validate !== false);
4485
+ return requireModule().meteringDcOffset(samples, sampleRate);
4486
+ }
4487
+
4488
+ /**
4489
+ * Inter-sample (true) peak in dBFS. `oversampleFactor` must be a power of two
4490
+ * in [1, 16]; pass 0 to use the library default (4).
4491
+ */
4492
+ export function meteringTruePeakDb(
4493
+ samples: Float32Array,
4494
+ sampleRate = 22050,
4495
+ oversampleFactor = 4,
4496
+ options: ValidateOptions = {},
4497
+ ): number {
4498
+ assertSamples('meteringTruePeakDb', samples, options.validate !== false);
4499
+ return requireModule().meteringTruePeakDb(samples, sampleRate, oversampleFactor);
4500
+ }
4501
+
4502
+ /**
4503
+ * Detect contiguous runs of clipped samples.
4504
+ *
4505
+ * @param threshold Linear absolute threshold (default 0.999).
4506
+ * @param minRegionSamples Minimum run length to report (default 1).
4507
+ */
4508
+ export function meteringDetectClipping(
4509
+ samples: Float32Array,
4510
+ sampleRate = 22050,
4511
+ threshold = 0.999,
4512
+ minRegionSamples = 1,
4513
+ options: ValidateOptions = {},
4514
+ ): ClippingReport {
4515
+ assertSamples('meteringDetectClipping', samples, options.validate !== false);
4516
+ return requireModule().meteringDetectClipping(samples, sampleRate, threshold, minRegionSamples);
4517
+ }
4518
+
4519
+ /**
4520
+ * Sliding-window dynamic range. Pass 0 for any parameter to use the library
4521
+ * default (window=3 s, hop=1 s, low=0.10, high=0.95).
4522
+ */
4523
+ export function meteringDynamicRange(
4524
+ samples: Float32Array,
4525
+ sampleRate = 22050,
4526
+ windowSec = 0,
4527
+ hopSec = 0,
4528
+ lowPercentile = 0,
4529
+ highPercentile = 0,
4530
+ options: ValidateOptions = {},
4531
+ ): DynamicRangeReport {
4532
+ assertSamples('meteringDynamicRange', samples, options.validate !== false);
4533
+ return requireModule().meteringDynamicRange(
4534
+ samples,
4535
+ sampleRate,
4536
+ windowSec,
4537
+ hopSec,
4538
+ lowPercentile,
4539
+ highPercentile,
4540
+ );
4541
+ }
4542
+
4543
+ // ============================================================================
4544
+ // Metering — stereo / phase-scope / spectrum
4545
+ // ============================================================================
4546
+
4547
+ /** Mid/side vectorscope point series for a (left, right) stereo pair. */
4548
+ export interface VectorscopeReport {
4549
+ mid: Float32Array;
4550
+ side: Float32Array;
4551
+ }
4552
+
4553
+ /** Phase-scope (Lissajous) point series plus summary stats. */
4554
+ export interface PhaseScopeReport {
4555
+ mid: Float32Array;
4556
+ side: Float32Array;
4557
+ radius: Float32Array;
4558
+ angleRad: Float32Array;
4559
+ correlation: number;
4560
+ averageAbsAngleRad: number;
4561
+ maxRadius: number;
4562
+ }
4563
+
4564
+ /** Options for `meteringSpectrum`. */
4565
+ export interface SpectrumOptions {
4566
+ /** FFT size. Pass 0 / omit for the library default (2048). */
4567
+ nFft?: number;
4568
+ /** Apply fractional-octave smoothing to magnitude. */
4569
+ applyOctaveSmoothing?: boolean;
4570
+ /** Smoothing fraction (e.g. 3 = 1/3-octave). 0 / omit = library default (3). */
4571
+ octaveFraction?: number;
4572
+ /** Linear reference for the dB conversion. 0 / omit = 1.0. */
4573
+ dbRef?: number;
4574
+ /** Linear floor used to avoid log(0). 0 / omit = library default. */
4575
+ dbAmin?: number;
4576
+ }
4577
+
4578
+ /** Single-frame magnitude / power / dB spectrum returned by `meteringSpectrum`. */
4579
+ export interface SpectrumReport {
4580
+ frequencies: Float32Array;
4581
+ magnitude: Float32Array;
4582
+ power: Float32Array;
4583
+ db: Float32Array;
4584
+ nFft: number;
4585
+ sampleRate: number;
4586
+ }
4587
+
4588
+ /** Pearson correlation in [-1, 1] between two equal-length channels. */
4589
+ export function meteringStereoCorrelation(
4590
+ left: Float32Array,
4591
+ right: Float32Array,
4592
+ sampleRate = 22050,
4593
+ options: ValidateOptions = {},
4594
+ ): number {
4595
+ const validate = options.validate !== false;
4596
+ assertSamples('meteringStereoCorrelation', left, validate, 'left');
4597
+ assertSamples('meteringStereoCorrelation', right, validate, 'right');
4598
+ return requireModule().meteringStereoCorrelation(left, right, sampleRate);
4599
+ }
4600
+
4601
+ /** Side / mid energy ratio: 0 = pure mono, ~1 = wide stereo. */
4602
+ export function meteringStereoWidth(
4603
+ left: Float32Array,
4604
+ right: Float32Array,
4605
+ sampleRate = 22050,
4606
+ options: ValidateOptions = {},
4607
+ ): number {
4608
+ const validate = options.validate !== false;
4609
+ assertSamples('meteringStereoWidth', left, validate, 'left');
4610
+ assertSamples('meteringStereoWidth', right, validate, 'right');
4611
+ return requireModule().meteringStereoWidth(left, right, sampleRate);
4612
+ }
4613
+
4614
+ /** Per-sample mid/side point series (one entry per input frame). */
4615
+ export function meteringVectorscope(
4616
+ left: Float32Array,
4617
+ right: Float32Array,
4618
+ sampleRate = 22050,
4619
+ options: ValidateOptions = {},
4620
+ ): VectorscopeReport {
4621
+ const validate = options.validate !== false;
4622
+ assertSamples('meteringVectorscope', left, validate, 'left');
4623
+ assertSamples('meteringVectorscope', right, validate, 'right');
4624
+ return requireModule().meteringVectorscope(left, right, sampleRate);
4625
+ }
4626
+
4627
+ /** Phase-scope point series plus summary stats. */
4628
+ export function meteringPhaseScope(
4629
+ left: Float32Array,
4630
+ right: Float32Array,
4631
+ sampleRate = 22050,
4632
+ options: ValidateOptions = {},
4633
+ ): PhaseScopeReport {
4634
+ const validate = options.validate !== false;
4635
+ assertSamples('meteringPhaseScope', left, validate, 'left');
4636
+ assertSamples('meteringPhaseScope', right, validate, 'right');
4637
+ return requireModule().meteringPhaseScope(left, right, sampleRate);
4638
+ }
4639
+
4640
+ /** Single-frame spectrum view (uses the first `nFft` samples of `samples`). */
4641
+ export function meteringSpectrum(
4642
+ samples: Float32Array,
4643
+ sampleRate = 22050,
4644
+ options?: SpectrumOptions & ValidateOptions,
4645
+ ): SpectrumReport {
4646
+ const validate = options?.validate !== false;
4647
+ assertSamples('meteringSpectrum', samples, validate);
4648
+ return requireModule().meteringSpectrum(samples, sampleRate, options ?? {});
4649
+ }
4650
+
4651
+ // ============================================================================
4652
+ // Editing — 12-TET scale quantizer
4653
+ // ============================================================================
4654
+
4655
+ /**
4656
+ * Snap a MIDI value to the nearest pitch class enabled by `modeMask`.
4657
+ *
4658
+ * `modeMask` is a 12-bit mask. For natural C major use `0b101010110101`.
4659
+ * `referenceMidi` defaults to A4 (69) when passed as 0.
4660
+ */
4661
+ export function scaleQuantizeMidi(
4662
+ root: number,
4663
+ modeMask: number,
4664
+ midi: number,
4665
+ referenceMidi = 0,
4666
+ ): number {
4667
+ assertFiniteScalar('scaleQuantizeMidi', midi, 'midi');
4668
+ assertFiniteScalar('scaleQuantizeMidi', referenceMidi, 'referenceMidi');
4669
+ return requireModule().scaleQuantizeMidi(root, modeMask, midi, referenceMidi);
4670
+ }
4671
+
4672
+ export function scaleCorrectionSemitones(
4673
+ root: number,
4674
+ modeMask: number,
4675
+ midi: number,
4676
+ referenceMidi = 0,
4677
+ ): number {
4678
+ assertFiniteScalar('scaleCorrectionSemitones', midi, 'midi');
4679
+ assertFiniteScalar('scaleCorrectionSemitones', referenceMidi, 'referenceMidi');
4680
+ return requireModule().scaleCorrectionSemitones(root, modeMask, midi, referenceMidi);
4681
+ }
4682
+
4683
+ export function scalePitchClassEnabled(
4684
+ root: number,
4685
+ modeMask: number,
4686
+ pitchClass: number,
4687
+ ): boolean {
4688
+ return requireModule().scalePitchClassEnabled(root, modeMask, pitchClass);
4689
+ }
4690
+
3169
4691
  // ============================================================================
3170
4692
  // Core - Resample
3171
4693
  // ============================================================================
@@ -3194,7 +4716,7 @@ export function resample(samples: Float32Array, srcSr: number, targetSr: number)
3194
4716
  *
3195
4717
  * @example
3196
4718
  * ```typescript
3197
- * import { init, Audio } from '@libraz/sonare';
4719
+ * import { init, Audio } from '@libraz/libsonare';
3198
4720
  *
3199
4721
  * await init();
3200
4722
  *
@@ -3299,15 +4821,15 @@ export class Audio {
3299
4821
  return pitchShift(this._samples, this._sampleRate, semitones);
3300
4822
  }
3301
4823
 
3302
- pitchCorrectToMidi(currentMidi: number, targetMidi: number): Float32Array {
4824
+ pitchCorrectToMidi(currentMidi = 69.0, targetMidi = 69.0): Float32Array {
3303
4825
  return pitchCorrectToMidi(this._samples, this._sampleRate, currentMidi, targetMidi);
3304
4826
  }
3305
4827
 
3306
- noteStretch(onsetSample: number, offsetSample: number, stretchRatio: number): Float32Array {
4828
+ noteStretch(onsetSample = 0, offsetSample = 0, stretchRatio = 1.0): Float32Array {
3307
4829
  return noteStretch(this._samples, this._sampleRate, onsetSample, offsetSample, stretchRatio);
3308
4830
  }
3309
4831
 
3310
- voiceChange(pitchSemitones: number, formantFactor: number): Float32Array {
4832
+ voiceChange(pitchSemitones = 0.0, formantFactor = 1.0): Float32Array {
3311
4833
  return voiceChange(this._samples, this._sampleRate, pitchSemitones, formantFactor);
3312
4834
  }
3313
4835
 
@@ -3355,7 +4877,7 @@ export class Audio {
3355
4877
  return melSpectrogram(this._samples, this._sampleRate, nFft, hopLength, nMels);
3356
4878
  }
3357
4879
 
3358
- mfcc(nFft = 2048, hopLength = 512, nMels = 128, nMfcc = 13): MfccResult {
4880
+ mfcc(nFft = 2048, hopLength = 512, nMels = 128, nMfcc = 20): MfccResult {
3359
4881
  return mfcc(this._samples, this._sampleRate, nFft, hopLength, nMels, nMfcc);
3360
4882
  }
3361
4883
 
@@ -3413,8 +4935,18 @@ export class Audio {
3413
4935
  fmin = 65.0,
3414
4936
  fmax = 2093.0,
3415
4937
  threshold = 0.3,
4938
+ fillNa = false,
3416
4939
  ): PitchResult {
3417
- return pitchYin(this._samples, this._sampleRate, frameLength, hopLength, fmin, fmax, threshold);
4940
+ return pitchYin(
4941
+ this._samples,
4942
+ this._sampleRate,
4943
+ frameLength,
4944
+ hopLength,
4945
+ fmin,
4946
+ fmax,
4947
+ threshold,
4948
+ fillNa,
4949
+ );
3418
4950
  }
3419
4951
 
3420
4952
  pitchPyin(
@@ -3423,6 +4955,7 @@ export class Audio {
3423
4955
  fmin = 65.0,
3424
4956
  fmax = 2093.0,
3425
4957
  threshold = 0.3,
4958
+ fillNa = false,
3426
4959
  ): PitchResult {
3427
4960
  return pitchPyin(
3428
4961
  this._samples,
@@ -3432,6 +4965,7 @@ export class Audio {
3432
4965
  fmin,
3433
4966
  fmax,
3434
4967
  threshold,
4968
+ fillNa,
3435
4969
  );
3436
4970
  }
3437
4971
 
@@ -3449,7 +4983,7 @@ export class Audio {
3449
4983
  *
3450
4984
  * @example
3451
4985
  * ```typescript
3452
- * import { init, StreamAnalyzer } from '@libraz/sonare';
4986
+ * import { init, StreamAnalyzer } from '@libraz/libsonare';
3453
4987
  *
3454
4988
  * await init();
3455
4989
  *
@@ -3477,9 +5011,8 @@ export class StreamAnalyzer {
3477
5011
  if (!module) {
3478
5012
  throw new Error('Module not initialized. Call init() first.');
3479
5013
  }
3480
- const wasmModule = module;
3481
- const args = [
3482
- config.sampleRate,
5014
+ this.analyzer = new module.StreamAnalyzer(
5015
+ config.sampleRate ?? 44100,
3483
5016
  config.nFft ?? 2048,
3484
5017
  config.hopLength ?? 512,
3485
5018
  config.nMels ?? 128,
@@ -3497,63 +5030,7 @@ export class StreamAnalyzer {
3497
5030
  config.bpmUpdateIntervalSec ?? 10,
3498
5031
  config.window ?? 0,
3499
5032
  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
- }
5033
+ );
3557
5034
  }
3558
5035
 
3559
5036
  /**