@libraz/libsonare 1.7.0 → 1.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
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,
@@ -566,6 +572,7 @@ export type {
566
572
  TempogramMode,
567
573
  Timbre,
568
574
  TimeSignature,
575
+ VoicedFlags,
569
576
  VoicePresetId,
570
577
  } from './public_types';
571
578
  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,
@@ -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;
@@ -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
  }
@@ -1328,6 +1328,11 @@ export interface SonareModule {
1328
1328
  hopLength: number,
1329
1329
  ) => number;
1330
1330
  meteringCrestFactorDb: (samples: Float32Array, sampleRate: number) => number;
1331
+ meteringCrestFactorDbStereo: (
1332
+ left: Float32Array,
1333
+ right: Float32Array,
1334
+ sampleRate: number,
1335
+ ) => number;
1331
1336
  meteringDcOffset: (samples: Float32Array, sampleRate: number) => number;
1332
1337
  meteringTruePeakDb: (
1333
1338
  samples: Float32Array,
@@ -1693,6 +1698,24 @@ export interface SonareModule {
1693
1698
  sampleRate: number,
1694
1699
  platforms: Array<{ name: string; targetLufs: number; ceilingDb: number }>,
1695
1700
  ) => string;
1701
+ masteringAssistantSuggestStereo: (
1702
+ left: Float32Array,
1703
+ right: Float32Array,
1704
+ sampleRate: number,
1705
+ params: Record<string, number | boolean>,
1706
+ ) => string;
1707
+ masteringAudioProfileStereo: (
1708
+ left: Float32Array,
1709
+ right: Float32Array,
1710
+ sampleRate: number,
1711
+ params: Record<string, number | boolean>,
1712
+ ) => string;
1713
+ masteringStreamingPreviewStereo: (
1714
+ left: Float32Array,
1715
+ right: Float32Array,
1716
+ sampleRate: number,
1717
+ platforms: Array<{ name: string; targetLufs: number; ceilingDb: number }>,
1718
+ ) => string;
1696
1719
  masteringRepairDeclick: (
1697
1720
  samples: Float32Array,
1698
1721
  sampleRate: number,