@libraz/libsonare 1.8.0 → 1.8.1

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 (85) hide show
  1. package/README.md +2 -2
  2. package/dist/_effects_common.d.ts +2 -1
  3. package/dist/_effects_common.d.ts.map +1 -1
  4. package/dist/analysis.js +1 -0
  5. package/dist/analysis.js.map +1 -1
  6. package/dist/effects_mastering.d.ts +2 -2
  7. package/dist/effects_mastering.d.ts.map +1 -1
  8. package/dist/effects_note_ops.d.ts +55 -30
  9. package/dist/effects_note_ops.d.ts.map +1 -1
  10. package/dist/effects_timepitch.d.ts +17 -12
  11. package/dist/effects_timepitch.d.ts.map +1 -1
  12. package/dist/feature_decompose.d.ts +1 -1
  13. package/dist/feature_decompose.d.ts.map +1 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +216 -60
  17. package/dist/index.js.map +1 -1
  18. package/dist/mastering_core.d.ts +27 -1
  19. package/dist/mastering_core.d.ts.map +1 -1
  20. package/dist/mixer.d.ts.map +1 -1
  21. package/dist/project_types.d.ts +7 -5
  22. package/dist/project_types.d.ts.map +1 -1
  23. package/dist/public_types_mastering.d.ts +19 -1
  24. package/dist/public_types_mastering.d.ts.map +1 -1
  25. package/dist/public_types_playback.d.ts +1 -0
  26. package/dist/public_types_playback.d.ts.map +1 -1
  27. package/dist/public_types_spectral.d.ts +5 -5
  28. package/dist/public_types_spectral.d.ts.map +1 -1
  29. package/dist/realtime_engine.d.ts +49 -6
  30. package/dist/realtime_engine.d.ts.map +1 -1
  31. package/dist/sonare-analysis.wasm +0 -0
  32. package/dist/sonare.d.ts +36 -0
  33. package/dist/sonare.js +1 -1
  34. package/dist/sonare.wasm +0 -0
  35. package/dist/streaming_processors.d.ts +5 -0
  36. package/dist/streaming_processors.d.ts.map +1 -1
  37. package/dist/worker.js.map +1 -1
  38. package/dist/worklet/engine-mixer-facade.d.ts +40 -4
  39. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -1
  40. package/dist/worklet/engine-node.d.ts +2 -0
  41. package/dist/worklet/engine-node.d.ts.map +1 -1
  42. package/dist/worklet/engine-processor.d.ts +4 -0
  43. package/dist/worklet/engine-processor.d.ts.map +1 -1
  44. package/dist/worklet/engine-strips.d.ts +4 -2
  45. package/dist/worklet/engine-strips.d.ts.map +1 -1
  46. package/dist/worklet/engine-sync.d.ts +4 -2
  47. package/dist/worklet/engine-sync.d.ts.map +1 -1
  48. package/dist/worklet/engine.d.ts +27 -6
  49. package/dist/worklet/engine.d.ts.map +1 -1
  50. package/dist/worklet/guards.d.ts +7 -1
  51. package/dist/worklet/guards.d.ts.map +1 -1
  52. package/dist/worklet/messages.d.ts +4 -4
  53. package/dist/worklet/messages.d.ts.map +1 -1
  54. package/dist/worklet/mixer-processor.d.ts +2 -2
  55. package/dist/worklet/mixer-processor.d.ts.map +1 -1
  56. package/dist/worklet/protocol.d.ts +10 -2
  57. package/dist/worklet/protocol.d.ts.map +1 -1
  58. package/dist/worklet.js +585 -188
  59. package/dist/worklet.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/_effects_common.ts +36 -6
  62. package/src/effects_mastering.ts +4 -0
  63. package/src/effects_note_ops.ts +92 -44
  64. package/src/effects_timepitch.ts +42 -31
  65. package/src/feature_decompose.ts +2 -2
  66. package/src/index.ts +6 -0
  67. package/src/mastering_core.ts +95 -0
  68. package/src/mixer.ts +39 -19
  69. package/src/project_types.ts +7 -5
  70. package/src/public_types_mastering.ts +21 -0
  71. package/src/public_types_playback.ts +1 -0
  72. package/src/public_types_spectral.ts +5 -5
  73. package/src/realtime_engine.ts +71 -15
  74. package/src/sonare.js.d.ts +36 -0
  75. package/src/streaming_processors.ts +8 -0
  76. package/src/worklet/engine-mixer-facade.ts +420 -86
  77. package/src/worklet/engine-node.ts +22 -4
  78. package/src/worklet/engine-processor.ts +83 -33
  79. package/src/worklet/engine-strips.ts +5 -10
  80. package/src/worklet/engine-sync.ts +6 -1
  81. package/src/worklet/engine.ts +49 -37
  82. package/src/worklet/guards.ts +55 -0
  83. package/src/worklet/messages.ts +4 -2
  84. package/src/worklet/mixer-processor.ts +10 -3
  85. package/src/worklet/protocol.ts +29 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.18.0",
6
6
  "description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
@@ -4,14 +4,44 @@
4
4
 
5
5
  import type { VoicedFlags } from './public_types';
6
6
 
7
- // The embind layer reads the companion voicing array as Float32Array. A flag is
8
- // a decision, not a magnitude: collapse to 1/0 on truthiness, which is the same
9
- // reduction the Node facade applies, so both surfaces agree on every accepted
10
- // input type.
11
- export function toVoicedFloat32(voiced: VoicedFlags): Float32Array {
7
+ // The embind layer reads the companion voicing array as Float32Array. Keep the
8
+ // public union a runtime contract before conversion: strings, Float64Arrays and
9
+ // arbitrary array-like objects must not become flags through truthiness.
10
+ // Same 1/0 reduction as the Node facade's toVoicedInt32.
11
+ export function toVoicedFloat32(fnName: string, voiced: VoicedFlags): Float32Array {
12
+ if (
13
+ !(
14
+ voiced instanceof Int32Array ||
15
+ voiced instanceof Uint8Array ||
16
+ voiced instanceof Float32Array ||
17
+ Array.isArray(voiced)
18
+ )
19
+ ) {
20
+ throw new TypeError(
21
+ `${fnName}: voiced must be an Int32Array, Uint8Array, Float32Array, number[], or boolean[]`,
22
+ );
23
+ }
12
24
  const out = new Float32Array(voiced.length);
13
25
  for (let index = 0; index < voiced.length; index += 1) {
14
- out[index] = voiced[index] ? 1 : 0;
26
+ const value = voiced[index];
27
+ if (typeof value !== 'number' && typeof value !== 'boolean') {
28
+ throw new TypeError(`${fnName}: voiced array entries must be numbers or booleans`);
29
+ }
30
+ out[index] = value ? 1 : 0;
15
31
  }
16
32
  return out;
17
33
  }
34
+
35
+ export function assertPitchTrackLengths(
36
+ fnName: string,
37
+ f0Hz: Float32Array,
38
+ voiced?: VoicedFlags | null,
39
+ voicedProb?: Float32Array | null,
40
+ ): void {
41
+ if (voiced != null && voiced.length !== f0Hz.length) {
42
+ throw new RangeError(`${fnName}: voiced must have the same length as f0Hz`);
43
+ }
44
+ if (voiced == null && voicedProb != null && voicedProb.length !== f0Hz.length) {
45
+ throw new RangeError(`${fnName}: voicedProb must have the same length as f0Hz`);
46
+ }
47
+ }
@@ -43,6 +43,7 @@ export {
43
43
  } from './mastering_chain';
44
44
  export type {
45
45
  MasteringAbMatchLoudnessRequest,
46
+ MasteringAbMatchLoudnessStereoRequest,
46
47
  MasteringAmpPresetCatalogEntry,
47
48
  MasteringAssistantParamsRequest,
48
49
  MasteringAssistantStereoParamsRequest,
@@ -53,6 +54,7 @@ export type {
53
54
  MasteringInsertTiming,
54
55
  MasteringPairAnalyzeRequest,
55
56
  MasteringPairProcessRequest,
57
+ MasteringPairProcessStereoRequest,
56
58
  MasteringProcessorCatalogEntry,
57
59
  MasteringProcessorCategory,
58
60
  MasteringProcessRequest,
@@ -67,6 +69,7 @@ export type {
67
69
  export {
68
70
  mastering,
69
71
  masteringAbMatchLoudness,
72
+ masteringAbMatchLoudnessStereo,
70
73
  masteringAmpPresetCatalog,
71
74
  masteringAssistantSuggest,
72
75
  masteringAssistantSuggestChain,
@@ -82,6 +85,7 @@ export {
82
85
  masteringPairAnalyze,
83
86
  masteringPairProcess,
84
87
  masteringPairProcessorNames,
88
+ masteringPairProcessStereo,
85
89
  masteringProcess,
86
90
  masteringProcessorCatalog,
87
91
  masteringProcessorNames,
@@ -3,7 +3,7 @@
3
3
  * rendering an edited set back, and the split/merge operations over it.
4
4
  */
5
5
 
6
- import { toVoicedFloat32 } from './_effects_common';
6
+ import { assertPitchTrackLengths, toVoicedFloat32 } from './_effects_common';
7
7
  import { getSonareModule } from './module_state';
8
8
  import type {
9
9
  NoteExtractorOptions,
@@ -19,7 +19,7 @@ import type {
19
19
  VoicedFlags,
20
20
  } from './public_types';
21
21
  import type { ValidateOptions } from './validation';
22
- import { assertSampleRate, assertSamples } from './validation';
22
+ import { assertFiniteScalar, assertSampleRate, assertSamples } from './validation';
23
23
 
24
24
  function requireModule() {
25
25
  return getSonareModule();
@@ -35,13 +35,9 @@ function assertNoteTrack(
35
35
  ): Float32Array | undefined {
36
36
  assertSamples(fnName, request.samples, request.validate !== false);
37
37
  assertSampleRate(fnName, request.sampleRate);
38
- if (request.voiced && request.voiced.length !== request.f0Hz.length) {
39
- throw new RangeError(`${fnName}: voiced length must match f0Hz length`);
40
- }
41
- if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
42
- throw new RangeError(`${fnName}: voicedProb length must match f0Hz length`);
43
- }
44
- return request.voiced ? toVoicedFloat32(request.voiced) : undefined;
38
+ const voicedF32 = request.voiced == null ? undefined : toVoicedFloat32(fnName, request.voiced);
39
+ assertPitchTrackLengths(fnName, request.f0Hz, request.voiced, request.voicedProb);
40
+ return voicedF32;
45
41
  }
46
42
 
47
43
  export interface NoteStretchRequest extends NoteStretchOptions, ValidateOptions {
@@ -73,10 +69,10 @@ export interface ExtractNotesRequest extends NoteExtractorOptions, ValidateOptio
73
69
  f0Hz: Float32Array;
74
70
  /** F0 frames per second. */
75
71
  frameRate: number;
76
- /** Per-frame voiced flags (truthy = voiced). Takes precedence over `voicedProb`. */
77
- voiced?: VoicedFlags;
78
- /** Per-frame voicing probability in `[0, 1]`; read only when `voiced` is omitted. */
79
- voicedProb?: Float32Array;
72
+ /** Per-frame voiced flags (truthy = voiced); takes precedence over `voicedProb` when supplied. Omit or pass `null` to use the probability array. */
73
+ voiced?: VoicedFlags | null;
74
+ /** Per-frame voicing probability in `[0, 1]`; read only when `voiced` is omitted or `null`. */
75
+ voicedProb?: Float32Array | null;
80
76
  }
81
77
 
82
78
  /** Canonical request form for {@link renderNotes}. */
@@ -97,12 +93,16 @@ export interface RenderNotesRequest extends ValidateOptions {
97
93
  /**
98
94
  * The F0 track the notes were extracted from. Required only by
99
95
  * `vibratoDepthChange` and `driftChange`, which act on the note's own pitch
100
- * curve; every other edit ignores it. The curve is not carried on a
96
+ * curve, or when `voiced` is supplied; every other edit ignores it. The curve is not carried on a
101
97
  * {@link NoteObject} for the same reason it is not returned by
102
98
  * {@link extractNotes} — it is this array sliced by
103
99
  * `[frameStart, frameEnd)`, which the caller already holds.
100
+ * Positive finite values are candidates for pitch. Zero, negative and
101
+ * non-finite values carry no measurement.
104
102
  */
105
103
  f0Hz?: Float32Array;
104
+ /** Per-frame voiced flags for `f0Hz`; false suppresses a candidate and true cannot make an unusable F0 valid. */
105
+ voiced?: VoicedFlags | null;
106
106
  /** F0 frames per second. Required when `f0Hz` is given. */
107
107
  frameRate?: number;
108
108
  /**
@@ -120,9 +120,11 @@ export interface RenderNotesRequest extends ValidateOptions {
120
120
  export interface DecomposeNotePitchRequest {
121
121
  /**
122
122
  * The note's slice of the F0 track — `f0Hz.subarray(frameStart, frameEnd)`.
123
- * Every value must be finite and non-negative; zero denotes an unvoiced frame.
123
+ * Positive finite values carry pitch; other values carry no measurement.
124
124
  */
125
125
  f0Hz: Float32Array;
126
+ /** Per-frame voiced flags matching `f0Hz`; false suppresses a candidate and true cannot make an unusable F0 valid. */
127
+ voiced?: VoicedFlags | null;
126
128
  /** F0 frames per second. */
127
129
  frameRate: number;
128
130
  /**
@@ -152,10 +154,10 @@ export interface NoteSetRequest extends NoteExtractorOptions, ValidateOptions {
152
154
  f0Hz: Float32Array;
153
155
  /** F0 frames per second. */
154
156
  frameRate: number;
155
- /** Per-frame voiced flags (truthy = voiced). Takes precedence over `voicedProb`. */
156
- voiced?: VoicedFlags;
157
- /** Per-frame voicing probability in `[0, 1]`; read only when `voiced` is omitted. */
158
- voicedProb?: Float32Array;
157
+ /** Per-frame voiced flags (truthy = voiced); takes precedence over `voicedProb` when supplied. Omit or pass `null` to use the probability array. */
158
+ voiced?: VoicedFlags | null;
159
+ /** Per-frame voicing probability in `[0, 1]`; read only when `voiced` is omitted or `null`. */
160
+ voicedProb?: Float32Array | null;
159
161
  /** The current note set. Each note's `[frameStart, frameEnd)` must be non-empty and inside the track. */
160
162
  notes: readonly NoteSetEntry[];
161
163
  }
@@ -285,8 +287,8 @@ export function noteMove(
285
287
  * `f0Hz` sliced by `[frameStart, frameEnd)`. The amplitude curve is measured
286
288
  * here, so it is, one entry per F0 frame over that note's span.
287
289
  *
288
- * Voicing comes from `voiced` (truthy = voiced). `voicedProb` is read only when
289
- * `voiced` is absent, and then a frame counts as voiced at or above
290
+ * Voicing comes from `voiced` (truthy = voiced) when it is supplied. `voicedProb`
291
+ * is read only when `voiced` is absent or `null`, and then a frame counts as voiced at or above
290
292
  * `voicedThreshold` (default 0.5). At least one of the two is required. Because
291
293
  * `voicedProb` from pYIN rises with F0 for a fixed frame length, prefer passing
292
294
  * a {@link PitchResult}'s `voicedFlag` through `voiced`.
@@ -294,10 +296,11 @@ export function noteMove(
294
296
  * @param request - Audio, F0 track, frame cadence and segmenter options
295
297
  * @returns One {@link NoteObject} per segmented note, in time order; an empty
296
298
  * array when the track segments to nothing
297
- * @throws RangeError when `voiced` / `voicedProb` do not match `f0Hz` in length,
298
- * or the samples/sample rate fail the shared input checks
299
+ * @throws RangeError when the selected voicing array differs from `f0Hz` in
300
+ * length (`voiced`, or `voicedProb` when `voiced` is omitted or `null`), or the
301
+ * samples/sample rate fail the shared input checks
299
302
  * @throws SonareError (`InvalidParameter`) on an empty `f0Hz`, a non-positive
300
- * `frameRate`, a negative or non-finite `f0Hz` value, a `voicedProb` outside
303
+ * `frameRate`, a `voicedProb` outside
301
304
  * `[0, 1]`, or a negative option value
302
305
  *
303
306
  * @example
@@ -325,7 +328,7 @@ export function extractNotes(request: ExtractNotesRequest): NoteObject[] {
325
328
  request.samples,
326
329
  request.sampleRate,
327
330
  request.f0Hz,
328
- request.voicedProb,
331
+ request.voiced == null ? (request.voicedProb ?? undefined) : undefined,
329
332
  voicedF32,
330
333
  request.frameRate,
331
334
  request,
@@ -354,7 +357,11 @@ export function extractNotes(request: ExtractNotesRequest): NoteObject[] {
354
357
  * @param request - Source audio, the notes to render, the cross-fade length, and
355
358
  * the F0 track a vibrato or drift edit reads
356
359
  * @returns The rendered audio, the same length and sample rate as the input
357
- * @throws RangeError when the samples or sample rate fail the shared input checks
360
+ * @throws TypeError when `notes` is not an array, `f0Hz` is not a
361
+ * `Float32Array`, or `voiced` is not a supported flag array
362
+ * @throws RangeError when `frameRate` is non-finite, `voiced` is supplied
363
+ * without `f0Hz`, when its length differs from `f0Hz`, or when the samples or
364
+ * sample rate fail shared checks
358
365
  * @throws SonareError (`InvalidParameter`) on a note whose span is empty,
359
366
  * reversed or missing, overlapping source spans, a non-finite or non-positive
360
367
  * edit field, a negative or non-finite envelope value, a negative `fadeMs` or
@@ -385,9 +392,28 @@ export function extractNotes(request: ExtractNotesRequest): NoteObject[] {
385
392
  * ```
386
393
  */
387
394
  export function renderNotes(request: RenderNotesRequest): Float32Array {
388
- assertSamples('renderNotes', request.samples, request.validate !== false);
389
395
  assertSampleRate('renderNotes', request.sampleRate);
390
- return requireModule().renderNotes(request.samples, request.sampleRate, request.notes, request);
396
+ if (!Array.isArray(request.notes)) {
397
+ throw new TypeError('renderNotes: notes must be an array');
398
+ }
399
+ const f0Hz = request.f0Hz;
400
+ if (f0Hz !== undefined && !(f0Hz instanceof Float32Array)) {
401
+ throw new TypeError('renderNotes: f0Hz must be a Float32Array');
402
+ }
403
+ if (f0Hz !== undefined) {
404
+ assertFiniteScalar('renderNotes', request.frameRate as number, 'frameRate');
405
+ }
406
+ const { voiced: publicVoiced, ...withoutVoiced } = request;
407
+ const voiced = publicVoiced == null ? undefined : toVoicedFloat32('renderNotes', publicVoiced);
408
+ if (publicVoiced != null && f0Hz === undefined) {
409
+ throw new RangeError('renderNotes: voiced requires f0Hz');
410
+ }
411
+ if (f0Hz !== undefined) {
412
+ assertPitchTrackLengths('renderNotes', f0Hz, publicVoiced);
413
+ }
414
+ assertSamples('renderNotes', request.samples, request.validate !== false);
415
+ const options = { ...withoutVoiced, voiced };
416
+ return requireModule().renderNotes(request.samples, request.sampleRate, request.notes, options);
391
417
  }
392
418
 
393
419
  /**
@@ -401,7 +427,10 @@ export function renderNotes(request: RenderNotesRequest): Float32Array {
401
427
  *
402
428
  * Frames whose F0 is unusable carry no measurement, so the curve is held at the
403
429
  * nearest usable neighbour across them. Both curves therefore have an entry
404
- * everywhere; a host marking the held ones reads them off `f0Hz`, which is exact.
430
+ * everywhere; when `voiced` is supplied, a host marking held frames should
431
+ * retain both the `f0Hz` and `voiced` arrays because a positive F0 can be
432
+ * explicitly suppressed. A false flag suppresses that frame and a true flag
433
+ * cannot make an unusable F0 valid.
405
434
  *
406
435
  * A note with no usable pitch is reported as a zero `centreHz` and two empty
407
436
  * curves rather than as an error — that is a measurement which came up empty,
@@ -410,9 +439,12 @@ export function renderNotes(request: RenderNotesRequest): Float32Array {
410
439
  * @param request - The note's slice of the F0 track, its cadence, its centre, and
411
440
  * the cutoff
412
441
  * @returns The centre and the two curves, each one entry per frame of `f0Hz`
413
- * @throws SonareError (`InvalidParameter`) on an empty `f0Hz`, a negative or
414
- * non-finite `f0Hz` value, a non-positive `frameRate`, a negative `medianHz`,
415
- * or a negative `vibratoCutoffHz`
442
+ * @throws TypeError when `f0Hz` is not a `Float32Array` or `voiced` is not a
443
+ * supported flag array
444
+ * @throws RangeError when `frameRate` is not finite or `voiced` differs in
445
+ * length from `f0Hz`
446
+ * @throws SonareError (`InvalidParameter`) on an empty `f0Hz`, a non-positive
447
+ * `frameRate`, a negative `medianHz`, or a negative `vibratoCutoffHz`
416
448
  *
417
449
  * @example
418
450
  * ```ts
@@ -425,8 +457,16 @@ export function renderNotes(request: RenderNotesRequest): Float32Array {
425
457
  * ```
426
458
  */
427
459
  export function decomposeNotePitch(request: DecomposeNotePitchRequest): PitchDecompositionResult {
460
+ if (!(request.f0Hz instanceof Float32Array)) {
461
+ throw new TypeError('decomposeNotePitch: f0Hz must be a Float32Array');
462
+ }
463
+ assertFiniteScalar('decomposeNotePitch', request.frameRate, 'frameRate');
464
+ const voiced =
465
+ request.voiced == null ? undefined : toVoicedFloat32('decomposeNotePitch', request.voiced);
466
+ assertPitchTrackLengths('decomposeNotePitch', request.f0Hz, request.voiced);
428
467
  return requireModule().decomposeNotePitch(
429
468
  request.f0Hz,
469
+ voiced,
430
470
  request.frameRate,
431
471
  request.medianHz,
432
472
  request.vibratoCutoffHz ?? 0,
@@ -438,22 +478,29 @@ export function decomposeNotePitch(request: DecomposeNotePitchRequest): PitchDec
438
478
  *
439
479
  * Both halves are re-derived from the audio and the track the way
440
480
  * {@link extractNotes} derives its own, rather than by patching the fields of
441
- * the note they replace. Both inherit the source note's edit, and its amplitude
442
- * envelope is cut at the same proportion so each half keeps its own part of it —
443
- * a note whose edit is the identity therefore still renders bit for bit after
444
- * being split.
481
+ * the note they replace. Both inherit the source note's edit, and the tail's
482
+ * `timeOffsetSamples` absorbs the duration change of the stretched head, so the
483
+ * halves occupy the destination timeline the unsplit note did; a note whose edit
484
+ * is the identity still renders bit for bit after being split. A half with no
485
+ * sample span at that boundary is omitted and the other keeps the edit
486
+ * unchanged. A one-entry envelope is a constant over the span, so both halves
487
+ * get that same entry; a longer one is resampled onto a grid that preserves the
488
+ * rendered gain at every integer source sample.
445
489
  *
446
490
  * Every note in the set, not just the two halves, has its spans, curves, medians
447
491
  * and stability re-derived from `samples` and the track, because a
448
492
  * {@link NoteSetEntry} carries no curves for this call to copy through. The
449
493
  * frame bounds are therefore what a note is identified by here, and the audio
450
494
  * and track must be the ones the set was extracted from or the whole set is
451
- * re-measured against something else.
495
+ * re-measured against something else. A set whose re-derived notes would
496
+ * include one with no sample span is rejected rather than shortened.
452
497
  *
453
498
  * @param request - The source, the current note set, and where to cut
454
- * @returns The whole new note set, one note longer than the one handed in
455
- * @throws RangeError when `voiced` / `voicedProb` do not match `f0Hz` in length,
456
- * or the samples/sample rate fail the shared input checks
499
+ * @returns The whole new note set, one note longer than the one handed in, or
500
+ * the same length when a half with no sample span is omitted
501
+ * @throws RangeError when the selected voicing array differs from `f0Hz` in
502
+ * length (`voiced`, or `voicedProb` when `voiced` is omitted or `null`), or the
503
+ * samples/sample rate fail the shared input checks
457
504
  * @throws SonareError (`InvalidParameter`) on an out-of-range `index`, a `frame`
458
505
  * that is not strictly inside that note's own span, a note whose frame span is
459
506
  * empty or runs past the track, or the track arguments {@link extractNotes}
@@ -480,7 +527,7 @@ export function splitNote(request: SplitNoteRequest): NoteObject[] {
480
527
  request.samples,
481
528
  request.sampleRate,
482
529
  request.f0Hz,
483
- request.voicedProb,
530
+ request.voiced == null ? (request.voicedProb ?? undefined) : undefined,
484
531
  voicedF32,
485
532
  request.frameRate,
486
533
  request.notes,
@@ -507,8 +554,9 @@ export function splitNote(request: SplitNoteRequest): NoteObject[] {
507
554
  * @param request - The source, the current note set, and the run to join
508
555
  * @returns The whole new note set, `last - first` notes shorter than the one
509
556
  * handed in
510
- * @throws RangeError when `voiced` / `voicedProb` do not match `f0Hz` in length,
511
- * or the samples/sample rate fail the shared input checks
557
+ * @throws RangeError when the selected voicing array differs from `f0Hz` in
558
+ * length (`voiced`, or `voicedProb` when `voiced` is omitted or `null`), or the
559
+ * samples/sample rate fail the shared input checks
512
560
  * @throws SonareError (`InvalidParameter`) unless `first < last < notes.length`,
513
561
  * on a note whose frame span is empty or runs past the track, or on the track
514
562
  * arguments {@link extractNotes} itself rejects
@@ -534,7 +582,7 @@ export function mergeNotes(request: MergeNotesRequest): NoteObject[] {
534
582
  request.samples,
535
583
  request.sampleRate,
536
584
  request.f0Hz,
537
- request.voicedProb,
585
+ request.voiced == null ? (request.voicedProb ?? undefined) : undefined,
538
586
  voicedF32,
539
587
  request.frameRate,
540
588
  request.notes,
@@ -3,7 +3,7 @@
3
3
  * correction onto a target pitch.
4
4
  */
5
5
 
6
- import { toVoicedFloat32 } from './_effects_common';
6
+ import { assertPitchTrackLengths, toVoicedFloat32 } from './_effects_common';
7
7
  import { resolveFftOptions } from './_fft_options';
8
8
  import { getSonareModule } from './module_state';
9
9
  import type { PitchCorrectOptions, VoicedFlags } from './public_types';
@@ -43,8 +43,8 @@ export interface PitchCorrectToMidiTimevaryingRequest extends ValidateOptions {
43
43
  targetMidi: number;
44
44
  sampleRate?: number;
45
45
  hopLength?: number;
46
- voiced?: VoicedFlags;
47
- voicedProb?: Float32Array;
46
+ voiced?: VoicedFlags | null;
47
+ voicedProb?: Float32Array | null;
48
48
  }
49
49
 
50
50
  export interface PitchCorrectTimevaryingRequest extends PitchCorrectOptions {
@@ -250,19 +250,23 @@ export function pitchCorrectToMidi(
250
250
  * Unlike {@link pitchCorrectToMidi} (a single constant transpose), this follows
251
251
  * the caller-supplied per-frame `f0Hz` contour and retunes every voiced frame
252
252
  * toward `targetMidi`, so vibrato/drift in the source is tracked rather than
253
- * flattened. `voiced` (truthy = voiced) and `voicedProb` ([0,1]) are optional;
254
- * omitting them treats every frame as voiced. An `f0Hz` NaN is accepted only
255
- * when the corresponding `voiced` entry is falsy, matching pYIN output. The
256
- * `voicedFlag` / `voicedProb` arrays of a {@link PitchResult} can be passed
257
- * through directly.
253
+ * flattened. When `voiced` is supplied (truthy = voiced), it takes precedence
254
+ * over `voicedProb`; omitting it or passing `null` uses the probability array.
255
+ * When both are omitted, every frame is treated as voiced. An `f0Hz` NaN is
256
+ * accepted only for a frame marked unvoiced, matching pYIN output. The
257
+ * `voicedFlag` / `voicedProb` arrays of a {@link PitchResult} can
258
+ * be passed through directly.
258
259
  *
259
260
  * @param samples - Audio samples (mono, float32)
260
261
  * @param f0Hz - Per-frame measured F0 in Hz (one entry per analysis frame)
261
262
  * @param targetMidi - Desired MIDI note number
262
263
  * @param sampleRate - Sample rate in Hz
263
- * @param hopLength - F0 hop in samples (frame i covers sample i*hopLength)
264
- * @param voiced - Optional per-frame voiced flags (truthy = voiced)
265
- * @param voicedProb - Optional per-frame voicing probability in [0, 1]
264
+ * @param hopLength - F0 frame-center spacing in samples. Frame i is centered at
265
+ * sample i*hopLength; nearest-frame voicing switches halfway between centers.
266
+ * @param voiced - Optional per-frame voiced flags (truthy = voiced); takes
267
+ * precedence over `voicedProb`.
268
+ * @param voicedProb - Optional per-frame voicing probability in [0, 1]; used
269
+ * when `voiced` is omitted or `null`.
266
270
  * @returns Pitch-corrected audio
267
271
  */
268
272
  export function pitchCorrectToMidiTimevarying(
@@ -274,8 +278,8 @@ export function pitchCorrectToMidiTimevarying(
274
278
  targetMidi: number,
275
279
  sampleRate?: number,
276
280
  hopLength?: number,
277
- voiced?: VoicedFlags,
278
- voicedProb?: Float32Array,
281
+ voiced?: VoicedFlags | null,
282
+ voicedProb?: Float32Array | null,
279
283
  options?: ValidateOptions,
280
284
  ): Float32Array;
281
285
  export function pitchCorrectToMidiTimevarying(
@@ -284,8 +288,8 @@ export function pitchCorrectToMidiTimevarying(
284
288
  targetMidi?: number,
285
289
  sampleRate = 22050,
286
290
  hopLength = 512,
287
- voiced?: VoicedFlags,
288
- voicedProb?: Float32Array,
291
+ voiced?: VoicedFlags | null,
292
+ voicedProb?: Float32Array | null,
289
293
  options: ValidateOptions = {},
290
294
  ): Float32Array {
291
295
  const request: PitchCorrectToMidiTimevaryingRequest =
@@ -302,13 +306,16 @@ export function pitchCorrectToMidiTimevarying(
302
306
  }
303
307
  : samples;
304
308
  assertSamples('pitchCorrectToMidiTimevarying', request.samples, request.validate !== false);
305
- if (request.voiced && request.voiced.length !== request.f0Hz.length) {
306
- throw new RangeError('pitchCorrectToMidiTimevarying: voiced length must match f0Hz length');
307
- }
308
- if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
309
- throw new RangeError('pitchCorrectToMidiTimevarying: voicedProb length must match f0Hz length');
310
- }
311
- const voicedF32 = request.voiced ? toVoicedFloat32(request.voiced) : undefined;
309
+ const voicedF32 =
310
+ request.voiced == null
311
+ ? undefined
312
+ : toVoicedFloat32('pitchCorrectToMidiTimevarying', request.voiced);
313
+ assertPitchTrackLengths(
314
+ 'pitchCorrectToMidiTimevarying',
315
+ request.f0Hz,
316
+ request.voiced,
317
+ request.voicedProb,
318
+ );
312
319
  return requireModule().pitchCorrectToMidiTimevarying(
313
320
  request.samples,
314
321
  request.sampleRate ?? 22050,
@@ -316,7 +323,7 @@ export function pitchCorrectToMidiTimevarying(
316
323
  request.targetMidi,
317
324
  request.hopLength ?? 512,
318
325
  voicedF32,
319
- request.voicedProb,
326
+ request.voiced == null ? (request.voicedProb ?? undefined) : undefined,
320
327
  );
321
328
  }
322
329
 
@@ -333,7 +340,8 @@ export function pitchCorrectToMidiTimevarying(
333
340
  * @param samples - Audio samples (mono, float32)
334
341
  * @param f0Hz - Per-frame measured F0 in Hz (one entry per analysis frame)
335
342
  * @param sampleRate - Sample rate in Hz
336
- * @param hopLength - F0 hop in samples (frame i covers sample i*hopLength)
343
+ * @param hopLength - F0 frame-center spacing in samples. Frame i is centered at
344
+ * sample i*hopLength; nearest-frame voicing switches halfway between centers.
337
345
  * @param options - Target mode + retune knobs + optional voiced/voicedProb arrays
338
346
  * @returns Pitch-corrected audio
339
347
  */
@@ -357,15 +365,18 @@ export function pitchCorrectTimevarying(
357
365
  ? { samples, f0Hz: f0Hz as Float32Array, sampleRate, hopLength, ...options }
358
366
  : samples;
359
367
  assertSamples('pitchCorrectTimevarying', request.samples, request.validate !== false);
360
- if (request.voiced && request.voiced.length !== request.f0Hz.length) {
361
- throw new RangeError('pitchCorrectTimevarying: voiced length must match f0Hz length');
362
- }
363
- if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
364
- throw new RangeError('pitchCorrectTimevarying: voicedProb length must match f0Hz length');
365
- }
368
+ const voicedF32 =
369
+ request.voiced == null ? undefined : toVoicedFloat32('pitchCorrectTimevarying', request.voiced);
370
+ assertPitchTrackLengths(
371
+ 'pitchCorrectTimevarying',
372
+ request.f0Hz,
373
+ request.voiced,
374
+ request.voicedProb,
375
+ );
366
376
  const nativeOptions = {
367
377
  ...request,
368
- voiced: request.voiced ? toVoicedFloat32(request.voiced) : undefined,
378
+ voiced: voicedF32,
379
+ voicedProb: request.voiced == null ? (request.voicedProb ?? undefined) : undefined,
369
380
  };
370
381
  return requireModule().pitchCorrectTimevarying(
371
382
  request.samples,
@@ -233,7 +233,7 @@ export function decomposeWithInit(
233
233
  /** Options for {@link decomposeStems}. */
234
234
  export interface DecomposeStemsRequest {
235
235
  samples: Float32Array;
236
- sampleRate: number;
236
+ sampleRate?: number;
237
237
  /** Number of NMF components (default 4). */
238
238
  nComponents?: number;
239
239
  /** STFT size (default 2048). */
@@ -278,7 +278,7 @@ export interface DecomposeStemsResult {
278
278
  * to the input.
279
279
  */
280
280
  export function decomposeStems(request: DecomposeStemsRequest): DecomposeStemsResult {
281
- return requireModule().decomposeStems(request.samples, request.sampleRate, {
281
+ return requireModule().decomposeStems(request.samples, request.sampleRate ?? 22050, {
282
282
  nComponents: request.nComponents,
283
283
  nFft: request.nFft,
284
284
  hopLength: request.hopLength,
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ export type {
61
61
  DynamicsProcessorResult,
62
62
  GateOptions,
63
63
  MasteringAbMatchLoudnessRequest,
64
+ MasteringAbMatchLoudnessStereoRequest,
64
65
  MasteringAmpPresetCatalogEntry,
65
66
  MasteringAssistantParamsRequest,
66
67
  MasteringAssistantStereoParamsRequest,
@@ -74,6 +75,7 @@ export type {
74
75
  MasteringInsertTiming,
75
76
  MasteringPairAnalyzeRequest,
76
77
  MasteringPairProcessRequest,
78
+ MasteringPairProcessStereoRequest,
77
79
  MasteringProcessorCatalogEntry,
78
80
  MasteringProcessorCategory,
79
81
  MasteringProcessRequest,
@@ -132,6 +134,7 @@ export {
132
134
  masterAudioWithProgress,
133
135
  mastering,
134
136
  masteringAbMatchLoudness,
137
+ masteringAbMatchLoudnessStereo,
135
138
  masteringAmpPresetCatalog,
136
139
  masteringAssistantSuggest,
137
140
  masteringAssistantSuggestChain,
@@ -154,6 +157,7 @@ export {
154
157
  masteringPairAnalyze,
155
158
  masteringPairProcess,
156
159
  masteringPairProcessorNames,
160
+ masteringPairProcessStereo,
157
161
  masteringPlatformNames,
158
162
  masteringPresetNames,
159
163
  masteringPresetParams,
@@ -687,6 +691,7 @@ export type {
687
691
  KeyDetectionOptions,
688
692
  KeyProfileName,
689
693
  LoudnessMatchResult,
694
+ LoudnessMatchStereoResult,
690
695
  LufsResult,
691
696
  LufsSeriesResult,
692
697
  MasteringAssistantParams,
@@ -807,6 +812,7 @@ export type {
807
812
  SpectralRegionOp,
808
813
  StageGainReduction,
809
814
  StereoAnalysis,
815
+ StereoPairProcessor,
810
816
  StftPowerResult,
811
817
  StftResult,
812
818
  StreamingEqualizerConfig,