@libraz/libsonare 1.5.2 → 1.5.4

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.
Files changed (48) hide show
  1. package/README.md +69 -725
  2. package/dist/index.d.ts +1155 -183
  3. package/dist/index.js +4924 -3935
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare.js +1 -1
  6. package/dist/sonare.wasm +0 -0
  7. package/dist/worklet.d.ts +29 -1
  8. package/dist/worklet.js +1141 -970
  9. package/dist/worklet.js.map +1 -1
  10. package/package.json +15 -3
  11. package/src/_chain_config.ts +46 -0
  12. package/src/audio.ts +24 -4
  13. package/src/codes.ts +27 -4
  14. package/src/effects_mastering.ts +25 -1
  15. package/src/effects_transform.ts +292 -46
  16. package/src/effects_voice_change.ts +46 -53
  17. package/src/feature_core.ts +316 -12
  18. package/src/feature_music.ts +371 -4
  19. package/src/feature_pitch.ts +61 -0
  20. package/src/feature_resample.ts +17 -2
  21. package/src/feature_spectral.ts +349 -2
  22. package/src/feature_spectrogram.ts +336 -0
  23. package/src/index.ts +130 -1
  24. package/src/mastering_chain.ts +305 -50
  25. package/src/mastering_core.ts +232 -16
  26. package/src/mastering_dynamics.ts +66 -10
  27. package/src/mastering_repair.ts +119 -7
  28. package/src/metering.ts +370 -83
  29. package/src/mixer.ts +9 -2
  30. package/src/mixing_oneshot.ts +25 -5
  31. package/src/module_state.ts +1 -1
  32. package/src/project_class.ts +30 -6
  33. package/src/project_internal.ts +12 -2
  34. package/src/project_types.ts +25 -2
  35. package/src/public_types_mastering.ts +114 -4
  36. package/src/public_types_spectral.ts +7 -0
  37. package/src/quick_analysis.ts +325 -117
  38. package/src/realtime_engine.ts +8 -0
  39. package/src/realtime_voice_changer.ts +79 -3
  40. package/src/sonare.js.d.ts +35 -0
  41. package/src/stream_analyzer.ts +20 -1
  42. package/src/stream_types.ts +12 -1
  43. package/src/validation.ts +17 -2
  44. package/src/worklet/engine-clips.ts +67 -2
  45. package/src/worklet/engine-processor.ts +33 -5
  46. package/src/worklet/engine.ts +7 -3
  47. package/src/worklet/guards.ts +3 -0
  48. package/src/worklet/messages.ts +27 -0
@@ -1,6 +1,7 @@
1
1
  import { getSonareModule } from './module_state';
2
2
  import type {
3
3
  HpssResult,
4
+ NoteMoveOptions,
4
5
  NoteStretchOptions,
5
6
  PitchCorrectOptions,
6
7
  SpectralEditOptions,
@@ -13,6 +14,81 @@ function requireModule() {
13
14
  return getSonareModule();
14
15
  }
15
16
 
17
+ /** Canonical request form for HPSS. */
18
+ export interface HpssRequest {
19
+ samples: Float32Array;
20
+ sampleRate?: number;
21
+ kernelHarmonic?: number;
22
+ kernelPercussive?: number;
23
+ }
24
+
25
+ export interface HarmonicRequest extends ValidateOptions {
26
+ samples: Float32Array;
27
+ sampleRate?: number;
28
+ }
29
+
30
+ export interface PercussiveRequest extends ValidateOptions {
31
+ samples: Float32Array;
32
+ sampleRate?: number;
33
+ }
34
+
35
+ export interface TimeStretchRequest extends ValidateOptions {
36
+ samples: Float32Array;
37
+ sampleRate?: number;
38
+ rate: number;
39
+ }
40
+
41
+ export interface PitchShiftRequest extends ValidateOptions {
42
+ samples: Float32Array;
43
+ sampleRate?: number;
44
+ semitones: number;
45
+ }
46
+
47
+ export interface PitchCorrectToMidiRequest extends ValidateOptions {
48
+ samples: Float32Array;
49
+ sampleRate?: number;
50
+ currentMidi?: number;
51
+ targetMidi?: number;
52
+ }
53
+
54
+ export interface PitchCorrectToMidiTimevaryingRequest extends ValidateOptions {
55
+ samples: Float32Array;
56
+ f0Hz: Float32Array;
57
+ targetMidi: number;
58
+ sampleRate?: number;
59
+ hopLength?: number;
60
+ voiced?: Int32Array;
61
+ voicedProb?: Float32Array;
62
+ }
63
+
64
+ export interface PitchCorrectTimevaryingRequest extends PitchCorrectOptions {
65
+ samples: Float32Array;
66
+ f0Hz: Float32Array;
67
+ sampleRate?: number;
68
+ hopLength?: number;
69
+ }
70
+
71
+ export interface NoteStretchRequest extends NoteStretchOptions, ValidateOptions {
72
+ samples: Float32Array;
73
+ sampleRate?: number;
74
+ }
75
+ export interface NoteMoveRequest extends NoteMoveOptions, ValidateOptions {
76
+ samples: Float32Array;
77
+ sampleRate?: number;
78
+ }
79
+
80
+ export interface NormalizeRequest extends ValidateOptions {
81
+ samples: Float32Array;
82
+ sampleRate?: number;
83
+ targetDb?: number;
84
+ }
85
+
86
+ export interface SpectralEditRequest extends SpectralEditOptions, ValidateOptions {
87
+ samples: Float32Array;
88
+ sampleRate: number;
89
+ ops?: SpectralRegionOp[];
90
+ }
91
+
16
92
  // ============================================================================
17
93
  // Effects
18
94
  // ============================================================================
@@ -26,13 +102,29 @@ function requireModule() {
26
102
  * @param kernelPercussive - Vertical median filter size for percussive (default: 31)
27
103
  * @returns Separated harmonic and percussive components
28
104
  */
105
+ export function hpss(request: HpssRequest): HpssResult;
29
106
  export function hpss(
30
107
  samples: Float32Array,
108
+ sampleRate?: number,
109
+ kernelHarmonic?: number,
110
+ kernelPercussive?: number,
111
+ ): HpssResult;
112
+ export function hpss(
113
+ samples: Float32Array | HpssRequest,
31
114
  sampleRate = 22050,
32
115
  kernelHarmonic = 31,
33
116
  kernelPercussive = 31,
34
117
  ): HpssResult {
35
- return requireModule().hpss(samples, sampleRate, kernelHarmonic, kernelPercussive);
118
+ const request =
119
+ samples instanceof Float32Array
120
+ ? { samples, sampleRate, kernelHarmonic, kernelPercussive }
121
+ : samples;
122
+ return requireModule().hpss(
123
+ request.samples,
124
+ request.sampleRate ?? 22050,
125
+ request.kernelHarmonic ?? 31,
126
+ request.kernelPercussive ?? 31,
127
+ );
36
128
  }
37
129
 
38
130
  /**
@@ -42,13 +134,20 @@ export function hpss(
42
134
  * @param sampleRate - Sample rate in Hz
43
135
  * @returns Harmonic component
44
136
  */
137
+ export function harmonic(request: HarmonicRequest): Float32Array;
45
138
  export function harmonic(
46
139
  samples: Float32Array,
47
- sampleRate: number,
140
+ sampleRate?: number,
141
+ options?: ValidateOptions,
142
+ ): Float32Array;
143
+ export function harmonic(
144
+ samples: Float32Array | HarmonicRequest,
145
+ sampleRate = 22050,
48
146
  options: ValidateOptions = {},
49
147
  ): Float32Array {
50
- assertSamples('harmonic', samples, options.validate !== false);
51
- return requireModule().harmonic(samples, sampleRate);
148
+ const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
149
+ assertSamples('harmonic', request.samples, request.validate !== false);
150
+ return requireModule().harmonic(request.samples, request.sampleRate ?? 22050);
52
151
  }
53
152
 
54
153
  /**
@@ -58,49 +157,82 @@ export function harmonic(
58
157
  * @param sampleRate - Sample rate in Hz
59
158
  * @returns Percussive component
60
159
  */
160
+ export function percussive(request: PercussiveRequest): Float32Array;
61
161
  export function percussive(
62
162
  samples: Float32Array,
63
- sampleRate: number,
163
+ sampleRate?: number,
164
+ options?: ValidateOptions,
165
+ ): Float32Array;
166
+ export function percussive(
167
+ samples: Float32Array | PercussiveRequest,
168
+ sampleRate = 22050,
64
169
  options: ValidateOptions = {},
65
170
  ): Float32Array {
66
- assertSamples('percussive', samples, options.validate !== false);
67
- return requireModule().percussive(samples, sampleRate);
171
+ const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
172
+ assertSamples('percussive', request.samples, request.validate !== false);
173
+ return requireModule().percussive(request.samples, request.sampleRate ?? 22050);
68
174
  }
69
175
 
70
176
  /**
71
177
  * Time-stretch audio without changing pitch.
72
178
  *
73
179
  * @param samples - Audio samples (mono, float32)
74
- * @param sampleRate - Sample rate in Hz
180
+ * @param sampleRate - Sample rate in Hz (default: 22050)
75
181
  * @param rate - Time stretch rate (0.5 = double duration, 2.0 = half duration)
76
182
  * @returns Time-stretched audio
77
183
  */
184
+ export function timeStretch(request: TimeStretchRequest): Float32Array;
78
185
  export function timeStretch(
79
186
  samples: Float32Array,
80
187
  sampleRate: number,
81
188
  rate: number,
189
+ options?: ValidateOptions,
190
+ ): Float32Array;
191
+ export function timeStretch(
192
+ samples: Float32Array | TimeStretchRequest,
193
+ sampleRate?: number,
194
+ rate?: number,
82
195
  options: ValidateOptions = {},
83
196
  ): Float32Array {
84
- assertSamples('timeStretch', samples, options.validate !== false);
85
- return requireModule().timeStretch(samples, sampleRate, rate);
197
+ const request: TimeStretchRequest =
198
+ samples instanceof Float32Array
199
+ ? { samples, sampleRate, rate: rate as number, ...options }
200
+ : samples;
201
+ assertSamples('timeStretch', request.samples, request.validate !== false);
202
+ return requireModule().timeStretch(request.samples, request.sampleRate ?? 22050, request.rate);
86
203
  }
87
204
 
88
205
  /**
89
206
  * Pitch-shift audio without changing duration.
90
207
  *
91
208
  * @param samples - Audio samples (mono, float32)
92
- * @param sampleRate - Sample rate in Hz
209
+ * @param sampleRate - Sample rate in Hz (default: 22050)
93
210
  * @param semitones - Pitch shift in semitones (+12 = one octave up, -12 = one octave down)
94
211
  * @returns Pitch-shifted audio
95
212
  */
213
+ export function pitchShift(request: PitchShiftRequest): Float32Array;
96
214
  export function pitchShift(
97
215
  samples: Float32Array,
98
216
  sampleRate: number,
99
217
  semitones: number,
218
+ options?: ValidateOptions,
219
+ ): Float32Array;
220
+ export function pitchShift(
221
+ samples: Float32Array | PitchShiftRequest,
222
+ sampleRate?: number,
223
+ semitones?: number,
100
224
  options: ValidateOptions = {},
101
225
  ): Float32Array {
102
- assertSamples('pitchShift', samples, options.validate !== false);
103
- return requireModule().pitchShift(samples, sampleRate, semitones);
226
+ const request: PitchShiftRequest =
227
+ samples instanceof Float32Array
228
+ ? { samples, sampleRate, semitones: semitones as number, ...options }
229
+ : samples;
230
+ assertSamples('pitchShift', request.samples, request.validate !== false);
231
+ return requireModule().pitchShift(
232
+ request.samples,
233
+ request.sampleRate ?? 22050,
234
+ request.semitones,
235
+ );
104
236
  }
105
237
 
106
238
  /**
@@ -112,15 +244,32 @@ export function pitchShift(
112
244
  * @param targetMidi - Desired MIDI note number
113
245
  * @returns Pitch-corrected audio
114
246
  */
247
+ export function pitchCorrectToMidi(request: PitchCorrectToMidiRequest): Float32Array;
115
248
  export function pitchCorrectToMidi(
116
249
  samples: Float32Array,
250
+ sampleRate?: number,
251
+ currentMidi?: number,
252
+ targetMidi?: number,
253
+ options?: ValidateOptions,
254
+ ): Float32Array;
255
+ export function pitchCorrectToMidi(
256
+ samples: Float32Array | PitchCorrectToMidiRequest,
117
257
  sampleRate = 22050,
118
258
  currentMidi = 69.0,
119
259
  targetMidi = 69.0,
120
260
  options: ValidateOptions = {},
121
261
  ): Float32Array {
122
- assertSamples('pitchCorrectToMidi', samples, options.validate !== false);
123
- return requireModule().pitchCorrectToMidi(samples, sampleRate, currentMidi, targetMidi);
262
+ const request =
263
+ samples instanceof Float32Array
264
+ ? { samples, sampleRate, currentMidi, targetMidi, ...options }
265
+ : samples;
266
+ assertSamples('pitchCorrectToMidi', request.samples, request.validate !== false);
267
+ return requireModule().pitchCorrectToMidi(
268
+ request.samples,
269
+ request.sampleRate ?? 22050,
270
+ request.currentMidi ?? 69.0,
271
+ request.targetMidi ?? 69.0,
272
+ );
124
273
  }
125
274
 
126
275
  /**
@@ -141,34 +290,60 @@ export function pitchCorrectToMidi(
141
290
  * @param voicedProb - Optional per-frame voicing probability in [0, 1]
142
291
  * @returns Pitch-corrected audio
143
292
  */
293
+ export function pitchCorrectToMidiTimevarying(
294
+ request: PitchCorrectToMidiTimevaryingRequest,
295
+ ): Float32Array;
144
296
  export function pitchCorrectToMidiTimevarying(
145
297
  samples: Float32Array,
146
298
  f0Hz: Float32Array,
147
299
  targetMidi: number,
300
+ sampleRate?: number,
301
+ hopLength?: number,
302
+ voiced?: Int32Array,
303
+ voicedProb?: Float32Array,
304
+ options?: ValidateOptions,
305
+ ): Float32Array;
306
+ export function pitchCorrectToMidiTimevarying(
307
+ samples: Float32Array | PitchCorrectToMidiTimevaryingRequest,
308
+ f0Hz?: Float32Array,
309
+ targetMidi?: number,
148
310
  sampleRate = 22050,
149
311
  hopLength = 512,
150
312
  voiced?: Int32Array,
151
313
  voicedProb?: Float32Array,
152
314
  options: ValidateOptions = {},
153
315
  ): Float32Array {
154
- assertSamples('pitchCorrectToMidiTimevarying', samples, options.validate !== false);
155
- if (voiced && voiced.length !== f0Hz.length) {
316
+ const request: PitchCorrectToMidiTimevaryingRequest =
317
+ samples instanceof Float32Array
318
+ ? {
319
+ samples,
320
+ f0Hz: f0Hz as Float32Array,
321
+ targetMidi: targetMidi as number,
322
+ sampleRate,
323
+ hopLength,
324
+ voiced,
325
+ voicedProb,
326
+ ...options,
327
+ }
328
+ : samples;
329
+ assertSamples('pitchCorrectToMidiTimevarying', request.samples, request.validate !== false);
330
+ if (request.voiced && request.voiced.length !== request.f0Hz.length) {
156
331
  throw new RangeError('pitchCorrectToMidiTimevarying: voiced length must match f0Hz length');
157
332
  }
158
- if (voicedProb && voicedProb.length !== f0Hz.length) {
333
+ if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
159
334
  throw new RangeError('pitchCorrectToMidiTimevarying: voicedProb length must match f0Hz length');
160
335
  }
161
336
  // The embind layer reads the companion arrays as Float32Array (voiced uses
162
337
  // 0.0/1.0); convert here so a single native conversion path suffices.
163
- const voicedF32 = voiced ? Float32Array.from(voiced) : undefined;
338
+ const voicedF32 = request.voiced ? Float32Array.from(request.voiced) : undefined;
164
339
  return requireModule().pitchCorrectToMidiTimevarying(
165
- samples,
166
- sampleRate,
167
- f0Hz,
168
- targetMidi,
169
- hopLength,
340
+ request.samples,
341
+ request.sampleRate ?? 22050,
342
+ request.f0Hz,
343
+ request.targetMidi,
344
+ request.hopLength ?? 512,
170
345
  voicedF32,
171
- voicedProb,
346
+ request.voicedProb,
172
347
  );
173
348
  }
174
349
 
@@ -188,31 +363,43 @@ export function pitchCorrectToMidiTimevarying(
188
363
  * @param options - Target mode + retune knobs + optional voiced/voicedProb arrays
189
364
  * @returns Pitch-corrected audio
190
365
  */
366
+ export function pitchCorrectTimevarying(request: PitchCorrectTimevaryingRequest): Float32Array;
191
367
  export function pitchCorrectTimevarying(
192
368
  samples: Float32Array,
193
369
  f0Hz: Float32Array,
370
+ sampleRate?: number,
371
+ hopLength?: number,
372
+ options?: PitchCorrectOptions,
373
+ ): Float32Array;
374
+ export function pitchCorrectTimevarying(
375
+ samples: Float32Array | PitchCorrectTimevaryingRequest,
376
+ f0Hz?: Float32Array,
194
377
  sampleRate = 22050,
195
378
  hopLength = 512,
196
379
  options: PitchCorrectOptions = {},
197
380
  ): Float32Array {
198
- assertSamples('pitchCorrectTimevarying', samples, options.validate !== false);
199
- if (options.voiced && options.voiced.length !== f0Hz.length) {
381
+ const request: PitchCorrectTimevaryingRequest =
382
+ samples instanceof Float32Array
383
+ ? { samples, f0Hz: f0Hz as Float32Array, sampleRate, hopLength, ...options }
384
+ : samples;
385
+ assertSamples('pitchCorrectTimevarying', request.samples, request.validate !== false);
386
+ if (request.voiced && request.voiced.length !== request.f0Hz.length) {
200
387
  throw new RangeError('pitchCorrectTimevarying: voiced length must match f0Hz length');
201
388
  }
202
- if (options.voicedProb && options.voicedProb.length !== f0Hz.length) {
389
+ if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
203
390
  throw new RangeError('pitchCorrectTimevarying: voicedProb length must match f0Hz length');
204
391
  }
205
392
  // The embind layer reads the companion arrays as Float32Array (voiced uses
206
393
  // 0.0/1.0); convert here so a single native conversion path suffices.
207
394
  const nativeOptions = {
208
- ...options,
209
- voiced: options.voiced ? Float32Array.from(options.voiced) : undefined,
395
+ ...request,
396
+ voiced: request.voiced ? Float32Array.from(request.voiced) : undefined,
210
397
  };
211
398
  return requireModule().pitchCorrectTimevarying(
212
- samples,
213
- sampleRate,
214
- f0Hz,
215
- hopLength,
399
+ request.samples,
400
+ request.sampleRate ?? 22050,
401
+ request.f0Hz,
402
+ request.hopLength ?? 512,
216
403
  nativeOptions,
217
404
  );
218
405
  }
@@ -227,18 +414,48 @@ export function pitchCorrectTimevarying(
227
414
  * @param stretchRatio - Stretch ratio (0.5 = double duration, 2.0 = half duration)
228
415
  * @returns Audio with the note region stretched
229
416
  */
417
+ export function noteStretch(request: NoteStretchRequest): Float32Array;
230
418
  export function noteStretch(
231
419
  samples: Float32Array,
420
+ sampleRate?: number,
421
+ options?: NoteStretchOptions & ValidateOptions,
422
+ ): Float32Array;
423
+ export function noteStretch(
424
+ samples: Float32Array | NoteStretchRequest,
232
425
  sampleRate = 22050,
233
426
  options: NoteStretchOptions & ValidateOptions = {},
234
427
  ): Float32Array {
235
- assertSamples('noteStretch', samples, options.validate !== false);
428
+ const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
429
+ assertSamples('noteStretch', request.samples, request.validate !== false);
236
430
  return requireModule().noteStretch(
237
- samples,
238
- sampleRate,
239
- options.onsetSample ?? 0,
240
- options.offsetSample ?? 0,
241
- options.stretchRatio ?? 1.0,
431
+ request.samples,
432
+ request.sampleRate ?? 22050,
433
+ request.onsetSample ?? 0,
434
+ request.offsetSample ?? 0,
435
+ request.stretchRatio ?? 1.0,
436
+ );
437
+ }
438
+
439
+ /** Move a note region to a new onset without changing its duration. */
440
+ export function noteMove(request: NoteMoveRequest): Float32Array;
441
+ export function noteMove(
442
+ samples: Float32Array,
443
+ sampleRate?: number,
444
+ options?: NoteMoveOptions & ValidateOptions,
445
+ ): Float32Array;
446
+ export function noteMove(
447
+ samples: Float32Array | NoteMoveRequest,
448
+ sampleRate = 22050,
449
+ options: NoteMoveOptions & ValidateOptions = {},
450
+ ): Float32Array {
451
+ const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
452
+ assertSamples('noteMove', request.samples, request.validate !== false);
453
+ return requireModule().noteMove(
454
+ request.samples,
455
+ request.sampleRate ?? 22050,
456
+ request.onsetSample ?? 0,
457
+ request.offsetSample ?? 0,
458
+ request.targetOnsetSample ?? 0,
242
459
  );
243
460
  }
244
461
 
@@ -246,18 +463,31 @@ export function noteStretch(
246
463
  * Normalize audio to target peak level.
247
464
  *
248
465
  * @param samples - Audio samples (mono, float32)
249
- * @param sampleRate - Sample rate in Hz
466
+ * @param sampleRate - Sample rate in Hz (default: 22050)
250
467
  * @param targetDb - Target peak level in dB (default: 0 dB = full scale)
251
468
  * @returns Normalized audio
252
469
  */
470
+ export function normalize(request: NormalizeRequest): Float32Array;
253
471
  export function normalize(
254
472
  samples: Float32Array,
255
473
  sampleRate: number,
474
+ targetDb?: number,
475
+ options?: ValidateOptions,
476
+ ): Float32Array;
477
+ export function normalize(
478
+ samples: Float32Array | NormalizeRequest,
479
+ sampleRate?: number,
256
480
  targetDb = 0.0,
257
481
  options: ValidateOptions = {},
258
482
  ): Float32Array {
259
- assertSamples('normalize', samples, options.validate !== false);
260
- return requireModule().normalize(samples, sampleRate, targetDb);
483
+ const request: NormalizeRequest =
484
+ samples instanceof Float32Array ? { samples, sampleRate, targetDb, ...options } : samples;
485
+ assertSamples('normalize', request.samples, request.validate !== false);
486
+ return requireModule().normalize(
487
+ request.samples,
488
+ request.sampleRate ?? 22050,
489
+ request.targetDb ?? 0.0,
490
+ );
261
491
  }
262
492
 
263
493
  /**
@@ -274,13 +504,29 @@ export function normalize(
274
504
  * @param options - STFT + heal configuration ({@link SpectralEditOptions})
275
505
  * @returns Edited audio
276
506
  */
507
+ export function spectralEdit(request: SpectralEditRequest): Float32Array;
277
508
  export function spectralEdit(
278
509
  samples: Float32Array,
279
510
  sampleRate: number,
511
+ ops?: SpectralRegionOp[],
512
+ options?: SpectralEditOptions & ValidateOptions,
513
+ ): Float32Array;
514
+ export function spectralEdit(
515
+ samples: Float32Array | SpectralEditRequest,
516
+ sampleRate?: number,
280
517
  ops: SpectralRegionOp[] = [],
281
518
  options: SpectralEditOptions & ValidateOptions = {},
282
519
  ): Float32Array {
283
- assertSamples('spectralEdit', samples, options.validate !== false);
284
- assertSampleRate('spectralEdit', sampleRate);
285
- return requireModule().spectralEdit(samples, sampleRate, ops, options as Record<string, unknown>);
520
+ const request: SpectralEditRequest =
521
+ samples instanceof Float32Array
522
+ ? { samples, sampleRate: sampleRate as number, ops, ...options }
523
+ : samples;
524
+ assertSamples('spectralEdit', request.samples, request.validate !== false);
525
+ assertSampleRate('spectralEdit', request.sampleRate);
526
+ return requireModule().spectralEdit(
527
+ request.samples,
528
+ request.sampleRate,
529
+ request.ops ?? [],
530
+ request as unknown as Record<string, unknown>,
531
+ );
286
532
  }
@@ -1,6 +1,5 @@
1
1
  import { getSonareModule } from './module_state';
2
2
  import type { RealtimeVoiceChangerConfigInput } from './public_types';
3
- import { RealtimeVoiceChanger } from './streaming_mixing';
4
3
  import type { ValidateOptions } from './validation';
5
4
  import { assertSamples } from './validation';
6
5
 
@@ -16,6 +15,12 @@ export interface VoiceChangeOptions extends ValidateOptions {
16
15
  formantFactor?: number;
17
16
  }
18
17
 
18
+ /** Canonical request form for one-shot voice changing. */
19
+ export interface VoiceChangeRequest extends VoiceChangeOptions {
20
+ samples: Float32Array;
21
+ sampleRate?: number;
22
+ }
23
+
19
24
  /**
20
25
  * Apply a voice change by shifting pitch and formants independently.
21
26
  *
@@ -24,17 +29,24 @@ export interface VoiceChangeOptions extends ValidateOptions {
24
29
  * @param options - Pitch/formant settings ({@link VoiceChangeOptions})
25
30
  * @returns Voice-changed audio
26
31
  */
32
+ export function voiceChange(request: VoiceChangeRequest): Float32Array;
27
33
  export function voiceChange(
28
34
  samples: Float32Array,
35
+ sampleRate?: number,
36
+ options?: VoiceChangeOptions,
37
+ ): Float32Array;
38
+ export function voiceChange(
39
+ samples: Float32Array | VoiceChangeRequest,
29
40
  sampleRate = 22050,
30
41
  options: VoiceChangeOptions = {},
31
42
  ): Float32Array {
32
- assertSamples('voiceChange', samples, options.validate !== false);
43
+ const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
44
+ assertSamples('voiceChange', request.samples, request.validate !== false);
33
45
  return requireModule().voiceChange(
34
- samples,
35
- sampleRate,
36
- options.pitchSemitones ?? 0.0,
37
- options.formantFactor ?? 1.0,
46
+ request.samples,
47
+ request.sampleRate ?? 22050,
48
+ request.pitchSemitones ?? 0.0,
49
+ request.formantFactor ?? 1.0,
38
50
  );
39
51
  }
40
52
 
@@ -42,50 +54,23 @@ export function voiceChange(
42
54
  export interface VoiceChangeRealtimeOptions extends ValidateOptions {
43
55
  /** Channel count (1 = mono, 2 = interleaved stereo). */
44
56
  channels?: 1 | 2;
45
- /** Block size for the internal render loop (default 512). */
57
+ /** @deprecated The shared C-ABI renderer uses a fixed cross-surface block size. */
46
58
  blockSize?: number;
47
59
  }
48
60
 
49
- function latencyCompensatedVoiceChange(
50
- changer: RealtimeVoiceChanger,
51
- samples: Float32Array,
52
- channels: 1 | 2,
53
- blockFrames: number,
54
- ): Float32Array {
55
- const latencyFrames = Math.max(0, changer.latencySamples());
56
- if (channels === 1) {
57
- const total = samples.length + latencyFrames;
58
- const input = new Float32Array(total);
59
- input.set(samples);
60
- const processed = new Float32Array(total);
61
- for (let offset = 0; offset < total; offset += blockFrames) {
62
- const block = input.subarray(offset, Math.min(offset + blockFrames, total));
63
- processed.set(changer.processMono(block), offset);
64
- }
65
- return processed.slice(latencyFrames, latencyFrames + samples.length);
66
- }
67
-
68
- const frames = samples.length / 2;
69
- const totalFrames = frames + latencyFrames;
70
- const input = new Float32Array(totalFrames * 2);
71
- input.set(samples);
72
- const processed = new Float32Array(totalFrames * 2);
73
- const frameStride = blockFrames * 2;
74
- for (let offset = 0; offset < input.length; offset += frameStride) {
75
- const block = input.subarray(offset, Math.min(offset + frameStride, input.length));
76
- processed.set(changer.processInterleaved(block, 2), offset);
77
- }
78
- const start = latencyFrames * 2;
79
- return processed.slice(start, start + samples.length);
61
+ /** Canonical request form for offline realtime voice changing. */
62
+ export interface VoiceChangeRealtimeRequest extends VoiceChangeRealtimeOptions {
63
+ samples: Float32Array;
64
+ sampleRate?: number;
65
+ preset?: RealtimeVoiceChangerConfigInput;
80
66
  }
81
67
 
82
68
  /**
83
69
  * Applies the realtime voice-changer chain to a whole buffer in one call.
84
70
  *
85
- * Constructs and prepares a {@link RealtimeVoiceChanger}, runs the block loop
86
- * for the caller, then disposes it — matching the Python `voice_change_realtime`
87
- * and Node `voiceChangeRealtime` convenience wrappers. For mono, `samples` is a
88
- * plain mono buffer; for stereo, `samples` is interleaved (L0,R0,L1,R1,...).
71
+ * Uses the shared C-ABI renderer, so Python, Node, and WASM use the same
72
+ * fixed block size and latency compensation. For mono, `samples` is a plain
73
+ * buffer; for stereo, it is interleaved (L0,R0,L1,R1,...).
89
74
  *
90
75
  * @param samples - Audio samples (mono, or interleaved stereo when channels=2)
91
76
  * @param sampleRate - Sample rate in Hz (default 48000, matching Python/Node)
@@ -93,26 +78,34 @@ function latencyCompensatedVoiceChange(
93
78
  * @param options - Channel count and block size ({@link VoiceChangeRealtimeOptions})
94
79
  * @returns The processed buffer (same layout/length as the input).
95
80
  */
81
+ export function voiceChangeRealtime(request: VoiceChangeRealtimeRequest): Float32Array;
96
82
  export function voiceChangeRealtime(
97
83
  samples: Float32Array,
84
+ sampleRate?: number,
85
+ preset?: RealtimeVoiceChangerConfigInput,
86
+ options?: VoiceChangeRealtimeOptions,
87
+ ): Float32Array;
88
+ export function voiceChangeRealtime(
89
+ samples: Float32Array | VoiceChangeRealtimeRequest,
98
90
  sampleRate = 48000,
99
91
  preset: RealtimeVoiceChangerConfigInput = 'neutral-monitor',
100
92
  options: VoiceChangeRealtimeOptions = {},
101
93
  ): Float32Array {
102
- assertSamples('voiceChangeRealtime', samples, options.validate !== false);
103
- const channels = options.channels ?? 1;
94
+ const request =
95
+ samples instanceof Float32Array ? { samples, sampleRate, preset, ...options } : samples;
96
+ assertSamples('voiceChangeRealtime', request.samples, request.validate !== false);
97
+ const channels = request.channels ?? 1;
104
98
  if (channels !== 1 && channels !== 2) {
105
99
  throw new Error('voiceChangeRealtime: channels must be 1 or 2.');
106
100
  }
107
- if (channels === 2 && samples.length % 2 !== 0) {
101
+ if (channels === 2 && request.samples.length % 2 !== 0) {
108
102
  throw new Error('voiceChangeRealtime: stereo input length must be a multiple of 2.');
109
103
  }
110
- const blockSize = Math.max(1, Math.floor(options.blockSize ?? 512));
111
- const changer = new RealtimeVoiceChanger(preset);
112
- try {
113
- changer.prepare(sampleRate, blockSize, channels);
114
- return latencyCompensatedVoiceChange(changer, samples, channels, blockSize);
115
- } finally {
116
- changer.delete();
117
- }
104
+ const presetConfig = request.preset ?? 'neutral-monitor';
105
+ return requireModule().voiceChangeRealtime(
106
+ request.samples,
107
+ request.sampleRate ?? 48000,
108
+ typeof presetConfig === 'string' ? presetConfig : JSON.stringify(presetConfig),
109
+ channels,
110
+ );
118
111
  }