@libraz/libsonare 1.2.0 → 1.2.2

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