@libraz/libsonare 1.7.0 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.18.0",
6
6
  "description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
@@ -6,7 +6,12 @@ type ChainSection = { [key: string]: number | boolean | ChainSection | undefined
6
6
  * Flattens a nested {@link MasteringChainConfig} into the dot-notation
7
7
  * `{ "module.processor.param": value }` map the core consumes. Internal helper
8
8
  * shared by the mastering-chain / master-audio entry points. The core owns
9
- * legacy flat-key aliases, so this remains a structural flattening step.
9
+ * legacy leaf-name aliases, so this remains a structural flattening step.
10
+ *
11
+ * A key the caller already wrote in dot notation carries through untouched, so
12
+ * both spellings a {@link MasteringChainConfig} accepts reach the core as the
13
+ * same parameter — matching what the Python binding documents. An unknown key
14
+ * in either spelling is rejected by the core, not here.
10
15
  */
11
16
  export function flattenChainConfig(config: MasteringChainConfig): Record<string, number | boolean> {
12
17
  const out: Record<string, number | boolean> = {};
@@ -41,12 +41,16 @@ export type {
41
41
  MasteringRealtimeCost,
42
42
  MasteringSamplesParamsRequest,
43
43
  MasteringStereoAnalyzeRequest,
44
+ MasteringStereoParamsRequest,
44
45
  MasteringStreamingPreviewRequest,
46
+ MasteringStreamingPreviewStereoRequest,
45
47
  } from './mastering_core';
46
48
  export {
47
49
  mastering,
48
50
  masteringAssistantSuggest,
51
+ masteringAssistantSuggestStereo,
49
52
  masteringAudioProfile,
53
+ masteringAudioProfileStereo,
50
54
  masteringInsertNames,
51
55
  masteringInsertParamInfo,
52
56
  masteringInsertParamNames,
@@ -61,6 +65,7 @@ export {
61
65
  masteringStereoAnalysisNames,
62
66
  masteringStereoAnalyze,
63
67
  masteringStreamingPreview,
68
+ masteringStreamingPreviewStereo,
64
69
  } from './mastering_core';
65
70
  export type {
66
71
  CompressorDetector,
@@ -6,6 +6,7 @@ import type {
6
6
  PitchCorrectOptions,
7
7
  SpectralEditOptions,
8
8
  SpectralRegionOp,
9
+ VoicedFlags,
9
10
  } from './public_types';
10
11
  import type { ValidateOptions } from './validation';
11
12
  import { assertSampleRate, assertSamples } from './validation';
@@ -14,6 +15,18 @@ function requireModule() {
14
15
  return getSonareModule();
15
16
  }
16
17
 
18
+ // The embind layer reads the companion voicing array as Float32Array. A flag is
19
+ // a decision, not a magnitude: collapse to 1/0 on truthiness, which is the same
20
+ // reduction the Node facade applies, so both surfaces agree on every accepted
21
+ // input type.
22
+ function toVoicedFloat32(voiced: VoicedFlags): Float32Array {
23
+ const out = new Float32Array(voiced.length);
24
+ for (let index = 0; index < voiced.length; index += 1) {
25
+ out[index] = voiced[index] ? 1 : 0;
26
+ }
27
+ return out;
28
+ }
29
+
17
30
  function resolveEffectFftOptions(
18
31
  fnName: string,
19
32
  nFft: unknown,
@@ -114,7 +127,7 @@ export interface PitchCorrectToMidiTimevaryingRequest extends ValidateOptions {
114
127
  targetMidi: number;
115
128
  sampleRate?: number;
116
129
  hopLength?: number;
117
- voiced?: Int32Array;
130
+ voiced?: VoicedFlags;
118
131
  voicedProb?: Float32Array;
119
132
  }
120
133
 
@@ -423,16 +436,18 @@ export function pitchCorrectToMidi(
423
436
  * Unlike {@link pitchCorrectToMidi} (a single constant transpose), this follows
424
437
  * the caller-supplied per-frame `f0Hz` contour and retunes every voiced frame
425
438
  * toward `targetMidi`, so vibrato/drift in the source is tracked rather than
426
- * flattened. `voiced` (non-zero = voiced) and `voicedProb` ([0,1]) are optional;
439
+ * flattened. `voiced` (truthy = voiced) and `voicedProb` ([0,1]) are optional;
427
440
  * omitting them treats every frame as voiced. An `f0Hz` NaN is accepted only
428
- * when the corresponding `voiced` entry is zero, matching pYIN output.
441
+ * when the corresponding `voiced` entry is falsy, matching pYIN output. The
442
+ * `voicedFlag` / `voicedProb` arrays of a {@link PitchResult} can be passed
443
+ * through directly.
429
444
  *
430
445
  * @param samples - Audio samples (mono, float32)
431
446
  * @param f0Hz - Per-frame measured F0 in Hz (one entry per analysis frame)
432
447
  * @param targetMidi - Desired MIDI note number
433
448
  * @param sampleRate - Sample rate in Hz
434
449
  * @param hopLength - F0 hop in samples (frame i covers sample i*hopLength)
435
- * @param voiced - Optional per-frame voiced flags (non-zero = voiced)
450
+ * @param voiced - Optional per-frame voiced flags (truthy = voiced)
436
451
  * @param voicedProb - Optional per-frame voicing probability in [0, 1]
437
452
  * @returns Pitch-corrected audio
438
453
  */
@@ -445,7 +460,7 @@ export function pitchCorrectToMidiTimevarying(
445
460
  targetMidi: number,
446
461
  sampleRate?: number,
447
462
  hopLength?: number,
448
- voiced?: Int32Array,
463
+ voiced?: VoicedFlags,
449
464
  voicedProb?: Float32Array,
450
465
  options?: ValidateOptions,
451
466
  ): Float32Array;
@@ -455,7 +470,7 @@ export function pitchCorrectToMidiTimevarying(
455
470
  targetMidi?: number,
456
471
  sampleRate = 22050,
457
472
  hopLength = 512,
458
- voiced?: Int32Array,
473
+ voiced?: VoicedFlags,
459
474
  voicedProb?: Float32Array,
460
475
  options: ValidateOptions = {},
461
476
  ): Float32Array {
@@ -479,9 +494,7 @@ export function pitchCorrectToMidiTimevarying(
479
494
  if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
480
495
  throw new RangeError('pitchCorrectToMidiTimevarying: voicedProb length must match f0Hz length');
481
496
  }
482
- // The embind layer reads the companion arrays as Float32Array (voiced uses
483
- // 0.0/1.0); convert here so a single native conversion path suffices.
484
- const voicedF32 = request.voiced ? Float32Array.from(request.voiced) : undefined;
497
+ const voicedF32 = request.voiced ? toVoicedFloat32(request.voiced) : undefined;
485
498
  return requireModule().pitchCorrectToMidiTimevarying(
486
499
  request.samples,
487
500
  request.sampleRate ?? 22050,
@@ -536,11 +549,9 @@ export function pitchCorrectTimevarying(
536
549
  if (request.voicedProb && request.voicedProb.length !== request.f0Hz.length) {
537
550
  throw new RangeError('pitchCorrectTimevarying: voicedProb length must match f0Hz length');
538
551
  }
539
- // The embind layer reads the companion arrays as Float32Array (voiced uses
540
- // 0.0/1.0); convert here so a single native conversion path suffices.
541
552
  const nativeOptions = {
542
553
  ...request,
543
- voiced: request.voiced ? Float32Array.from(request.voiced) : undefined,
554
+ voiced: request.voiced ? toVoicedFloat32(request.voiced) : undefined,
544
555
  };
545
556
  return requireModule().pitchCorrectTimevarying(
546
557
  request.samples,
package/src/index.ts CHANGED
@@ -78,7 +78,9 @@ export type {
78
78
  MasteringRepairTrimSilenceRequest,
79
79
  MasteringSamplesParamsRequest,
80
80
  MasteringStereoAnalyzeRequest,
81
+ MasteringStereoParamsRequest,
81
82
  MasteringStreamingPreviewRequest,
83
+ MasteringStreamingPreviewStereoRequest,
82
84
  MixStereoRequest,
83
85
  TransientShaperOptions,
84
86
  TrimSilenceMode,
@@ -97,7 +99,9 @@ export {
97
99
  masterAudioWithProgress,
98
100
  mastering,
99
101
  masteringAssistantSuggest,
102
+ masteringAssistantSuggestStereo,
100
103
  masteringAudioProfile,
104
+ masteringAudioProfileStereo,
101
105
  masteringChain,
102
106
  masteringChainStereo,
103
107
  masteringChainStereoWithProgress,
@@ -127,6 +131,7 @@ export {
127
131
  masteringStereoAnalysisNames,
128
132
  masteringStereoAnalyze,
129
133
  masteringStreamingPreview,
134
+ masteringStreamingPreviewStereo,
130
135
  mixingScenePresetJson,
131
136
  mixingScenePresetNames,
132
137
  mixStereo,
@@ -375,6 +380,7 @@ export type {
375
380
  } from './metering';
376
381
  export {
377
382
  meteringCrestFactorDb,
383
+ meteringCrestFactorDbStereo,
378
384
  meteringDcOffset,
379
385
  meteringDetectClipping,
380
386
  meteringDynamicRange,
@@ -432,6 +438,9 @@ export type {
432
438
  ProjectMarker,
433
439
  ProjectMidiClipResult,
434
440
  ProjectMidiEvent,
441
+ ProjectMidiFxBakeRequest,
442
+ ProjectMidiFxBakeResult,
443
+ ProjectMidiFxPreviewRequest,
435
444
  ProjectNotePairValidation,
436
445
  ProjectSource,
437
446
  ProjectTrack,
@@ -566,6 +575,7 @@ export type {
566
575
  TempogramMode,
567
576
  Timbre,
568
577
  TimeSignature,
578
+ VoicedFlags,
569
579
  VoicePresetId,
570
580
  } from './public_types';
571
581
  export {
@@ -77,6 +77,22 @@ export interface MasteringStreamingPreviewRequest {
77
77
  platforms?: StreamingPlatform[];
78
78
  }
79
79
 
80
+ /** Canonical request form for the stereo analysis entry points. */
81
+ export interface MasteringStereoParamsRequest {
82
+ left: Float32Array;
83
+ right: Float32Array;
84
+ sampleRate?: number;
85
+ params?: MasteringProcessorParams;
86
+ }
87
+
88
+ /** Canonical request form for the stereo streaming-platform preview. */
89
+ export interface MasteringStreamingPreviewStereoRequest {
90
+ left: Float32Array;
91
+ right: Float32Array;
92
+ sampleRate?: number;
93
+ platforms?: StreamingPlatform[];
94
+ }
95
+
80
96
  /**
81
97
  * Apply mastering loudness normalization with a true-peak ceiling.
82
98
  *
@@ -479,3 +495,57 @@ export function masteringStreamingPreview(
479
495
  request.platforms ?? [],
480
496
  );
481
497
  }
498
+
499
+ /**
500
+ * Suggest a mastering chain for a stereo pair, as shared JSON.
501
+ *
502
+ * Profiles through {@link masteringAudioProfileStereo}, so the loudness stage
503
+ * of the suggestion is built on the channel-summed program rather than a
504
+ * downmix that reads roughly 6 dB low.
505
+ */
506
+ export function masteringAssistantSuggestStereo(request: MasteringStereoParamsRequest): string {
507
+ return requireModule().masteringAssistantSuggestStereo(
508
+ request.left,
509
+ request.right,
510
+ request.sampleRate ?? 22050,
511
+ request.params ?? {},
512
+ );
513
+ }
514
+
515
+ /**
516
+ * Mastering assistant profile of a stereo pair, as shared JSON.
517
+ *
518
+ * Only the `loudness` block is measured from the two channels: integrated LUFS
519
+ * and LRA come from the channel-summed program and the true peak is the larger
520
+ * of the two. The spectral, dynamics and tempo fields describe shape and timing
521
+ * rather than absolute level and are measured on the downmix, which keeps them
522
+ * comparable with {@link masteringAudioProfile}.
523
+ */
524
+ export function masteringAudioProfileStereo(request: MasteringStereoParamsRequest): string {
525
+ return requireModule().masteringAudioProfileStereo(
526
+ request.left,
527
+ request.right,
528
+ request.sampleRate ?? 22050,
529
+ request.params ?? {},
530
+ );
531
+ }
532
+
533
+ /**
534
+ * Preview streaming-platform normalization for a stereo pair, as shared JSON.
535
+ *
536
+ * Measures the integrated loudness with BS.1770 channel summing and reports the
537
+ * larger of the two channel true peaks. Passing a `0.5 * (left + right)` downmix
538
+ * to {@link masteringStreamingPreview} instead reads roughly 6 dB low on
539
+ * decorrelated material, and both the normalization gain and the ceiling-risk
540
+ * flag follow from that measurement.
541
+ */
542
+ export function masteringStreamingPreviewStereo(
543
+ request: MasteringStreamingPreviewStereoRequest,
544
+ ): string {
545
+ return requireModule().masteringStreamingPreviewStereo(
546
+ request.left,
547
+ request.right,
548
+ request.sampleRate ?? 22050,
549
+ request.platforms ?? [],
550
+ );
551
+ }
package/src/metering.ts CHANGED
@@ -186,6 +186,24 @@ export function meteringCrestFactorDb(
186
186
  return requireModule().meteringCrestFactorDb(request.samples, request.sampleRate ?? 22050);
187
187
  }
188
188
 
189
+ /**
190
+ * Crest factor in dB across both channels of a stereo pair.
191
+ *
192
+ * Takes the peak across both channels and the RMS over both together. An
193
+ * out-of-phase pair cancels in the `0.5 * (left + right)` downmix
194
+ * {@link meteringCrestFactorDb} would need, which understates its RMS and so
195
+ * overstates the crest factor.
196
+ */
197
+ export function meteringCrestFactorDbStereo(request: MeteringStereoRequest): number {
198
+ assertSamples('meteringCrestFactorDbStereo', request.left, request.validate !== false);
199
+ assertSamples('meteringCrestFactorDbStereo', request.right, request.validate !== false);
200
+ return requireModule().meteringCrestFactorDbStereo(
201
+ request.left,
202
+ request.right,
203
+ request.sampleRate ?? 22050,
204
+ );
205
+ }
206
+
189
207
  export function meteringDcOffset(request: MeteringSamplesRequest): number;
190
208
  export function meteringDcOffset(
191
209
  samples: Float32Array,
package/src/project.ts CHANGED
@@ -40,6 +40,9 @@ export type {
40
40
  ProjectMidiCcBindingKind,
41
41
  ProjectMidiClipResult,
42
42
  ProjectMidiEvent,
43
+ ProjectMidiFxBakeRequest,
44
+ ProjectMidiFxBakeResult,
45
+ ProjectMidiFxPreviewRequest,
43
46
  ProjectMidiRouteConfig,
44
47
  ProjectMidiRouteResult,
45
48
  ProjectNotePairValidation,
@@ -35,6 +35,9 @@ import type {
35
35
  ProjectMidiCcBinding,
36
36
  ProjectMidiClipResult,
37
37
  ProjectMidiEvent,
38
+ ProjectMidiFxBakeRequest,
39
+ ProjectMidiFxBakeResult,
40
+ ProjectMidiFxPreviewRequest,
38
41
  ProjectMidiRouteConfig,
39
42
  ProjectMidiRouteResult,
40
43
  ProjectNotePairValidation,
@@ -52,6 +55,28 @@ import type {
52
55
  SynthPatch,
53
56
  } from './project_types';
54
57
 
58
+ /**
59
+ * Folds the positional and request call forms of `bakeMidiFx` into one shape,
60
+ * so defaults, validation and errors cannot diverge between them.
61
+ */
62
+ function normalizeMidiFxBakeRequest(
63
+ clipIdOrRequest: number | ProjectMidiFxBakeRequest,
64
+ configJson?: string,
65
+ ): Required<ProjectMidiFxBakeRequest> {
66
+ if (typeof clipIdOrRequest === 'number') {
67
+ return {
68
+ clipId: clipIdOrRequest,
69
+ configJson: configJson ?? '',
70
+ withSourceIndex: false,
71
+ };
72
+ }
73
+ return {
74
+ clipId: clipIdOrRequest.clipId,
75
+ configJson: clipIdOrRequest.configJson,
76
+ withSourceIndex: clipIdOrRequest.withSourceIndex ?? false,
77
+ };
78
+ }
79
+
55
80
  function validateAssistSidecarUint32(value: unknown, field: string): number {
56
81
  if (
57
82
  typeof value !== 'number' ||
@@ -584,8 +609,33 @@ export class Project {
584
609
  * Destructively bake a MIDI-FX chain into all stored events. Large clips are
585
610
  * drained without truncation; failure leaves the original clip unchanged.
586
611
  */
587
- bakeMidiFx(clipId: number, configJson: string): void {
588
- this.native.bakeMidiFx(clipId, configJson);
612
+ bakeMidiFx(clipId: number, configJson: string): void;
613
+ /**
614
+ * Request form. Setting `withSourceIndex` also returns per-event provenance,
615
+ * so a selection or an editorial annotation can be carried across the bake.
616
+ */
617
+ bakeMidiFx(request: ProjectMidiFxBakeRequest): ProjectMidiFxBakeResult;
618
+ bakeMidiFx(
619
+ clipIdOrRequest: number | ProjectMidiFxBakeRequest,
620
+ configJson?: string,
621
+ ): ProjectMidiFxBakeResult {
622
+ const request = normalizeMidiFxBakeRequest(clipIdOrRequest, configJson);
623
+ if (!request.withSourceIndex) {
624
+ this.native.bakeMidiFx(request.clipId, request.configJson);
625
+ return {};
626
+ }
627
+ return {
628
+ sourceIndex: this.native.bakeMidiFxWithSourceIndex(request.clipId, request.configJson),
629
+ };
630
+ }
631
+
632
+ /**
633
+ * Count the events {@link bakeMidiFx} would produce for this clip and
634
+ * configuration, without mutating the project. The transform is
635
+ * deterministic, so the count matches what the bake goes on to produce.
636
+ */
637
+ previewMidiFxCount(request: ProjectMidiFxPreviewRequest): number {
638
+ return this.native.previewMidiFxCount(request.clipId, request.configJson);
589
639
  }
590
640
 
591
641
  /** Backward alias for {@link bakeMidiFx}. */
@@ -85,6 +85,8 @@ export interface WasmProject {
85
85
  bank: number,
86
86
  ) => void;
87
87
  bakeMidiFx: (clipId: number, configJson: string) => void;
88
+ bakeMidiFxWithSourceIndex: (clipId: number, configJson: string) => Int32Array;
89
+ previewMidiFxCount: (clipId: number, configJson: string) => number;
88
90
  setMidiFx: (clipId: number, configJson: string) => void;
89
91
  validateMidiNotes: (clipId: number) => ProjectNotePairValidation;
90
92
  analyzeTempo: (audio: Float32Array, sampleRate: number) => ProjectTempoCandidate[];
@@ -300,12 +300,11 @@ export interface SynthModRouting {
300
300
  *
301
301
  * The patch starts from a BASE — the named `preset` (see
302
302
  * {@link synthPresetNames}; a `"va:"` routing prefix is accepted) or, when
303
- * `preset` is omitted, the default subtractive patch. Every numeric field then
304
- * uses "0 / omit => keep the base value" (non-zero values override, clamped to
305
- * their audible ranges) and the enum fields reserve `'default'` as keep. The
306
- * frozen C ABI has no per-field presence bits, so explicit zero numeric
307
- * overrides (for example `ampSustain: 0`) cannot be represented; they keep the
308
- * base value. A non-empty `modRoutings` REPLACES the base mod matrix.
303
+ * `preset` is omitted, the default subtractive patch. Omitting a numeric field
304
+ * keeps the base value; supplying one overrides it (clamped to its audible
305
+ * range), including an explicit `0` such as `stereoSpread: 0`. The enum fields
306
+ * reserve `'default'` as keep. A `modRoutings` array REPLACES the base mod
307
+ * matrix, and an empty array clears it, while omitting the key keeps it.
309
308
  *
310
309
  * Mode-specific deep parameters (FM operator stacks, modal mode tables,
311
310
  * drawbar registrations, kit pieces, piano strings) travel inside the named
@@ -341,12 +340,10 @@ export interface SynthPatch {
341
340
  velToCutoffCents?: number;
342
341
  ampAttackMs?: number;
343
342
  ampDecayMs?: number;
344
- /** 0 / omit keeps the base value; explicit zero sustain is not representable. */
345
343
  ampSustain?: number;
346
344
  ampReleaseMs?: number;
347
345
  filterAttackMs?: number;
348
346
  filterDecayMs?: number;
349
- /** 0 / omit keeps the base value; explicit zero sustain is not representable. */
350
347
  filterSustain?: number;
351
348
  filterReleaseMs?: number;
352
349
  lfoRateHz?: number;
@@ -668,6 +665,40 @@ export interface ProjectNotePairValidation {
668
665
  unmatchedNoteOffs: number;
669
666
  }
670
667
 
668
+ /**
669
+ * Request form of {@link Project.bakeMidiFx}. The positional
670
+ * `(clipId, configJson)` call stays supported and normalizes to this shape.
671
+ */
672
+ export interface ProjectMidiFxBakeRequest {
673
+ /** Target MIDI clip id. */
674
+ clipId: number;
675
+ /** MIDI-FX chain configuration as JSON. */
676
+ configJson: string;
677
+ /** Return per-event provenance in the result. Default false. */
678
+ withSourceIndex?: boolean;
679
+ }
680
+
681
+ /** Result of the request form of {@link Project.bakeMidiFx}. */
682
+ export interface ProjectMidiFxBakeResult {
683
+ /**
684
+ * One entry per transformed event in canonical order: the index of the input
685
+ * event it derives from, or -1 for an event with no originating input. Chord
686
+ * and arpeggiator fan-out makes several outputs share one source index, so a
687
+ * caller that treats the first output per index as the same event and the
688
+ * rest as newly generated can carry a selection across the bake. Present only
689
+ * when the request set `withSourceIndex`.
690
+ */
691
+ sourceIndex?: Int32Array;
692
+ }
693
+
694
+ /** Request form of {@link Project.previewMidiFxCount}. */
695
+ export interface ProjectMidiFxPreviewRequest {
696
+ /** Target MIDI clip id. */
697
+ clipId: number;
698
+ /** MIDI-FX chain configuration as JSON. */
699
+ configJson: string;
700
+ }
701
+
671
702
  /** One compile diagnostic (mirrors SonareProjectDiagnostic). */
672
703
  export interface ProjectDiagnostic {
673
704
  code: number;
@@ -352,6 +352,15 @@ export interface MasteringChainConfig {
352
352
  releaseMs?: number;
353
353
  applyGainAtInputRate?: boolean;
354
354
  };
355
+ /**
356
+ * Dot-notation spelling of any leaf above, e.g. `'loudness.targetLufs': -20`
357
+ * beside or instead of `loudness: { targetLufs: -20 }`. It is the form the C
358
+ * ABI carries parameters in, so a caller assembling overrides dynamically can
359
+ * emit it directly; the core validates the key and rejects an unknown one.
360
+ * The nested spelling is canonical — prefer it in hand-written code, where it
361
+ * is checked field by field while a dotted key is only checked at run time.
362
+ */
363
+ [flatKey: `${string}.${string}`]: number | boolean | undefined;
355
364
  }
356
365
 
357
366
  /**
@@ -1,5 +1,19 @@
1
1
  import type { ValidateOptions } from './validation';
2
2
 
3
+ /**
4
+ * Per-frame voicing decision, one entry per `f0Hz` frame. A truthy or non-zero
5
+ * entry marks the frame voiced. The union covers what the analysis side hands
6
+ * back — `PitchResult.voicedFlag` is a `boolean[]` — as well as the typed and
7
+ * plain numeric arrays a caller may build directly, so a pitch track can be fed
8
+ * straight into pitch correction without a conversion step.
9
+ */
10
+ export type VoicedFlags =
11
+ | Int32Array
12
+ | Uint8Array
13
+ | Float32Array
14
+ | readonly number[]
15
+ | readonly boolean[];
16
+
3
17
  /** Options for `pitchCorrectTimevarying`. All fields are optional. */
4
18
  export interface PitchCorrectOptions extends ValidateOptions {
5
19
  /** `'midi'` retunes toward `targetMidi`; `'scale'` snaps to the key. Default `'midi'`. */
@@ -20,8 +34,8 @@ export interface PitchCorrectOptions extends ValidateOptions {
20
34
  retuneSpeedMs?: number;
21
35
  /** Corrections below this are bypassed to preserve vibrato (cents). Default 20. */
22
36
  vibratoThresholdCents?: number;
23
- /** Per-frame voiced flags (non-zero = voiced); omit to treat all frames as voiced. */
24
- voiced?: Int32Array;
37
+ /** Per-frame voiced flags (truthy = voiced); omit to treat all frames as voiced. */
38
+ voiced?: VoicedFlags;
25
39
  /** Per-frame voicing probability in `[0, 1]`; omit to derive from `voiced`. */
26
40
  voicedProb?: Float32Array;
27
41
  }
@@ -823,6 +823,15 @@ export class RealtimeEngine {
823
823
  * Returns `-1` when the track, insert, or name is unknown. (The Python binding
824
824
  * raises a `SonareError` for an unknown id where Node/WASM return the `-1`
825
825
  * sentinel.)
826
+ *
827
+ * This trio is how a mastering processor gets time-varying automation: the
828
+ * `eq.*`, `dynamics.*`, `saturation.*`, `spectral.*`, `stereo.*`,
829
+ * `maximizer.*` and `multiband.*` processors are all available as strip
830
+ * inserts, so placing one on a strip and resolving its parameter here drives
831
+ * it at audio-block precision, live and offline alike. The whole-signal
832
+ * stages of the offline mastering chain (`repair.*`, `loudness`, and the
833
+ * match stages) have no insert form and no automation id: they buffer the
834
+ * entire signal by construction and do not run on the realtime path.
826
835
  */
827
836
  resolveTrackInsertAutomationId(trackId: number, insertIndex: number, paramName: string): number {
828
837
  return this.native.resolveTrackInsertAutomationId(trackId, insertIndex, paramName);
@@ -984,6 +993,36 @@ export class RealtimeEngine {
984
993
  this.native.processPrepared(numFrames);
985
994
  }
986
995
 
996
+ /**
997
+ * Allocates the cue-bus counterpart of {@link prepareChannels}. Needed only
998
+ * when PFL/AFL monitoring must reach a separate output: `processPrepared`
999
+ * folds the cue bus into the program output, while
1000
+ * {@link processPreparedWithMonitor} keeps the two apart. Call once, off the
1001
+ * audio thread, with at least as many channels as `prepareChannels` got.
1002
+ */
1003
+ prepareMonitorChannels(numChannels: number, maxFrames: number): void {
1004
+ this.native.prepareMonitorChannels(numChannels, maxFrames);
1005
+ }
1006
+
1007
+ /**
1008
+ * Returns a Float32Array view onto the persistent cue-bus scratch for one
1009
+ * channel (valid for up to `numFrames`). Read it after
1010
+ * {@link processPreparedWithMonitor}. Re-acquire after WASM memory growth.
1011
+ */
1012
+ getMonitorChannelBuffer(channel: number, numFrames: number): Float32Array {
1013
+ return this.native.getMonitorChannelBuffer(channel, numFrames);
1014
+ }
1015
+
1016
+ /**
1017
+ * Runs the engine in place over the prepared scratch, writing the cue bus to
1018
+ * the monitor scratch instead of folding it into the program output.
1019
+ * Allocation-free: safe on the AudioWorklet render thread after
1020
+ * `prepareChannels` and `prepareMonitorChannels`.
1021
+ */
1022
+ processPreparedWithMonitor(numFrames: number): void {
1023
+ this.native.processPreparedWithMonitor(numFrames);
1024
+ }
1025
+
987
1026
  processWithMonitor(channels: Float32Array[]): WasmEngineProcessWithMonitorResult {
988
1027
  return this.native.processWithMonitor(channels);
989
1028
  }
@@ -1114,6 +1114,9 @@ export interface WasmRealtimeEngine {
1114
1114
  prepareChannels: (numChannels: number, maxFrames: number) => void;
1115
1115
  getChannelBuffer: (channel: number, numFrames: number) => Float32Array;
1116
1116
  processPrepared: (numFrames: number) => void;
1117
+ prepareMonitorChannels: (numChannels: number, maxFrames: number) => void;
1118
+ getMonitorChannelBuffer: (channel: number, numFrames: number) => Float32Array;
1119
+ processPreparedWithMonitor: (numFrames: number) => void;
1117
1120
  processWithMonitor: (channels: Float32Array[]) => WasmEngineProcessWithMonitorResult;
1118
1121
  renderOffline: (channels: Float32Array[], blockSize: number) => Float32Array[];
1119
1122
  bounceOffline: (options: WasmEngineBounceOptions) => WasmEngineBounceResult;
@@ -1328,6 +1331,11 @@ export interface SonareModule {
1328
1331
  hopLength: number,
1329
1332
  ) => number;
1330
1333
  meteringCrestFactorDb: (samples: Float32Array, sampleRate: number) => number;
1334
+ meteringCrestFactorDbStereo: (
1335
+ left: Float32Array,
1336
+ right: Float32Array,
1337
+ sampleRate: number,
1338
+ ) => number;
1331
1339
  meteringDcOffset: (samples: Float32Array, sampleRate: number) => number;
1332
1340
  meteringTruePeakDb: (
1333
1341
  samples: Float32Array,
@@ -1693,6 +1701,24 @@ export interface SonareModule {
1693
1701
  sampleRate: number,
1694
1702
  platforms: Array<{ name: string; targetLufs: number; ceilingDb: number }>,
1695
1703
  ) => string;
1704
+ masteringAssistantSuggestStereo: (
1705
+ left: Float32Array,
1706
+ right: Float32Array,
1707
+ sampleRate: number,
1708
+ params: Record<string, number | boolean>,
1709
+ ) => string;
1710
+ masteringAudioProfileStereo: (
1711
+ left: Float32Array,
1712
+ right: Float32Array,
1713
+ sampleRate: number,
1714
+ params: Record<string, number | boolean>,
1715
+ ) => string;
1716
+ masteringStreamingPreviewStereo: (
1717
+ left: Float32Array,
1718
+ right: Float32Array,
1719
+ sampleRate: number,
1720
+ platforms: Array<{ name: string; targetLufs: number; ceilingDb: number }>,
1721
+ ) => string;
1696
1722
  masteringRepairDeclick: (
1697
1723
  samples: Float32Array,
1698
1724
  sampleRate: number,
@@ -49,6 +49,24 @@ const EQ_PHASE_MODES: Record<string, number> = {
49
49
  * Call {@link delete} (or use a `try/finally`) to release the underlying WASM
50
50
  * object — the embind handle is not garbage-collected automatically.
51
51
  *
52
+ * Reachable from the AudioWorklet realm through the `sonare/worklet` entry, but
53
+ * the realtime contract is the caller's to keep:
54
+ *
55
+ * - {@link prepare} builds the processors and allocates. Call it once from a
56
+ * message handler, never from `AudioWorkletProcessor.process()`.
57
+ * - {@link processMono}/{@link processStereo} return fresh arrays. On the render
58
+ * thread, reuse the returned reference for the block rather than retaining it.
59
+ * - An enabled `loudness` stage needs `loudnessStaticGainDb` measured offline,
60
+ * because whole-signal integrated LUFS cannot be measured block by block. Pass
61
+ * `loudnessStaticGainPeakDb` too and the static gain is clamped exactly as the
62
+ * offline chain clamps it, so the live preview matches the render.
63
+ * - {@link flush} output starts {@link latencySamples} samples early; discard
64
+ * that many leading samples when time alignment matters.
65
+ *
66
+ * The chain is a host-side stage, not an engine insert: it does not participate
67
+ * in the engine's PDC or bypass, so latency compensation against other engine
68
+ * outputs is also the caller's.
69
+ *
52
70
  * @example
53
71
  * ```typescript
54
72
  * const chain = new StreamingMasteringChain({ eq: { tiltDb: 1.0 } });