@libraz/libsonare 1.5.4 → 1.5.5

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.5.4",
3
+ "version": "1.5.5",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
@@ -22,25 +22,5 @@ export function flattenChainConfig(config: MasteringChainConfig): Record<string,
22
22
  };
23
23
  walk(config as ChainSection, '');
24
24
 
25
- // Compatibility aliases for the original WASM-only shorthand. Normalize at
26
- // this boundary so every native entry point receives the core parser's one
27
- // canonical vocabulary; public types can migrate to the nested spelling
28
- // without preserving a second C++ parser indefinitely.
29
- const aliases: Record<string, string> = {
30
- 'repair.denoise': 'repair.denoise.enabled',
31
- 'repair.nFft': 'repair.denoise.nFft',
32
- 'repair.hopLength': 'repair.denoise.hopLength',
33
- 'repair.ddAlpha': 'repair.denoise.ddAlpha',
34
- 'repair.gainFloor': 'repair.denoise.gainFloor',
35
- 'eq.tiltDb': 'eq.tilt.tiltDb',
36
- 'eq.pivotHz': 'eq.tilt.pivotHz',
37
- };
38
- for (const [legacy, canonical] of Object.entries(aliases)) {
39
- const value = out[legacy];
40
- if (value !== undefined) {
41
- out[canonical] = value;
42
- delete out[legacy];
43
- }
44
- }
45
25
  return out;
46
26
  }
@@ -16,6 +16,25 @@ import type {
16
16
  WasmKeyCandidateResult,
17
17
  } from './sonare.js';
18
18
 
19
+ const PITCH_CLASS_NAMES = [
20
+ 'C',
21
+ 'C#',
22
+ 'D',
23
+ 'D#',
24
+ 'E',
25
+ 'F',
26
+ 'F#',
27
+ 'G',
28
+ 'G#',
29
+ 'A',
30
+ 'A#',
31
+ 'B',
32
+ ] as const;
33
+
34
+ function pitchClassName(value: number): string {
35
+ return PITCH_CLASS_NAMES[value] ?? 'C';
36
+ }
37
+
19
38
  export function convertKeyCandidate(wasm: WasmKeyCandidateResult): KeyCandidate {
20
39
  return {
21
40
  key: {
@@ -89,6 +108,8 @@ export function convertChordAnalysisResult(wasm: WasmChordAnalysisResult): Chord
89
108
  chords: wasm.chords.map((c) => ({
90
109
  root: c.root as PitchClass,
91
110
  bass: c.bass as PitchClass,
111
+ rootName: pitchClassName(c.root),
112
+ bassName: pitchClassName(c.bass),
92
113
  quality: c.quality as ChordQuality,
93
114
  start: c.start,
94
115
  end: c.end,
@@ -129,6 +150,8 @@ export function convertAnalysisResult(wasm: WasmAnalysisResult): AnalysisResult
129
150
  chords: wasm.chords.map((c) => ({
130
151
  root: c.root as PitchClass,
131
152
  bass: c.bass as PitchClass,
153
+ rootName: pitchClassName(c.root),
154
+ bassName: pitchClassName(c.bass),
132
155
  quality: c.quality as ChordQuality,
133
156
  start: c.start,
134
157
  end: c.end,
package/src/audio.ts CHANGED
@@ -76,6 +76,7 @@ import {
76
76
  detectOnsets,
77
77
  } from './quick_analysis';
78
78
  import type { ProgressCallback, WasmNnlsChromaResult } from './sonare.js';
79
+ import { validateAudioBuffer } from './validation';
79
80
 
80
81
  // ============================================================================
81
82
  // Audio Class
@@ -177,7 +178,8 @@ export class Audio {
177
178
  * Node/Python surfaces).
178
179
  */
179
180
  static fromBuffer(samples: Float32Array, sampleRate = 48000): Audio {
180
- return new Audio(samples, sampleRate);
181
+ validateAudioBuffer(samples, sampleRate);
182
+ return new Audio(samples.slice(), sampleRate);
181
183
  }
182
184
 
183
185
  /**
@@ -472,7 +474,7 @@ export class Audio {
472
474
  hopLength = 512,
473
475
  fmin = 65.0,
474
476
  fmax = 2093.0,
475
- threshold = 0.3,
477
+ threshold = 0.1,
476
478
  fillNa = false,
477
479
  ): PitchResult {
478
480
  return pitchYin(
@@ -492,7 +494,7 @@ export class Audio {
492
494
  hopLength = 512,
493
495
  fmin = 65.0,
494
496
  fmax = 2093.0,
495
- threshold = 0.3,
497
+ threshold = 0.1,
496
498
  fillNa = false,
497
499
  ): PitchResult {
498
500
  return pitchPyin(
@@ -238,6 +238,10 @@ export function pitchShift(
238
238
  /**
239
239
  * Pitch-correct audio from a current MIDI note to a target MIDI note.
240
240
  *
241
+ * Applies one constant, immediate transpose with no retune glide and preserves
242
+ * the input buffer length. Use {@link pitchCorrectToMidiTimevarying} for a
243
+ * caller-supplied pitch contour.
244
+ *
241
245
  * @param samples - Audio samples (mono, float32)
242
246
  * @param sampleRate - Sample rate in Hz
243
247
  * @param currentMidi - Detected/current MIDI note number
@@ -279,7 +283,8 @@ export function pitchCorrectToMidi(
279
283
  * the caller-supplied per-frame `f0Hz` contour and retunes every voiced frame
280
284
  * toward `targetMidi`, so vibrato/drift in the source is tracked rather than
281
285
  * flattened. `voiced` (non-zero = voiced) and `voicedProb` ([0,1]) are optional;
282
- * omitting them treats every frame as voiced.
286
+ * omitting them treats every frame as voiced. An `f0Hz` NaN is accepted only
287
+ * when the corresponding `voiced` entry is zero, matching pYIN output.
283
288
  *
284
289
  * @param samples - Audio samples (mono, float32)
285
290
  * @param f0Hz - Per-frame measured F0 in Hz (one entry per analysis frame)
@@ -354,7 +359,8 @@ export function pitchCorrectToMidiTimevarying(
354
359
  * Generalises {@link pitchCorrectToMidiTimevarying}: the same caller-supplied
355
360
  * per-frame `f0Hz` contour drives correction, but `options.mode` selects between
356
361
  * a fixed-MIDI target (`'midi'`, default) and scale quantisation (`'scale'`),
357
- * and the retune knobs shape natural-vs-robotic correction.
362
+ * and the retune knobs shape natural-vs-robotic correction. An `f0Hz` NaN is
363
+ * accepted only for a frame marked unvoiced.
358
364
  *
359
365
  * @param samples - Audio samples (mono, float32)
360
366
  * @param f0Hz - Per-frame measured F0 in Hz (one entry per analysis frame)
@@ -411,7 +417,7 @@ export function pitchCorrectTimevarying(
411
417
  * @param sampleRate - Sample rate in Hz
412
418
  * @param onsetSample - Note onset position in samples
413
419
  * @param offsetSample - Note offset position in samples
414
- * @param stretchRatio - Stretch ratio (0.5 = double duration, 2.0 = half duration)
420
+ * @param stretchRatio - Stretch ratio (0.5 = half duration, 2.0 = double duration)
415
421
  * @returns Audio with the note region stretched
416
422
  */
417
423
  export function noteStretch(request: NoteStretchRequest): Float32Array;
@@ -431,7 +437,7 @@ export function noteStretch(
431
437
  request.samples,
432
438
  request.sampleRate ?? 22050,
433
439
  request.onsetSample ?? 0,
434
- request.offsetSample ?? 0,
440
+ request.offsetSample ?? request.samples.length,
435
441
  request.stretchRatio ?? 1.0,
436
442
  );
437
443
  }
@@ -454,7 +460,7 @@ export function noteMove(
454
460
  request.samples,
455
461
  request.sampleRate ?? 22050,
456
462
  request.onsetSample ?? 0,
457
- request.offsetSample ?? 0,
463
+ request.offsetSample ?? request.samples.length,
458
464
  request.targetOnsetSample ?? 0,
459
465
  );
460
466
  }
@@ -464,7 +470,7 @@ export function noteMove(
464
470
  *
465
471
  * @param samples - Audio samples (mono, float32)
466
472
  * @param sampleRate - Sample rate in Hz (default: 22050)
467
- * @param targetDb - Target peak level in dB (default: 0 dB = full scale)
473
+ * @param targetDb - Finite target at or below 0 dBFS (default: 0 dB = full scale)
468
474
  * @returns Normalized audio
469
475
  */
470
476
  export function normalize(request: NormalizeRequest): Float32Array;
@@ -81,6 +81,14 @@ export interface PcenRequest {
81
81
  values: Float32Array;
82
82
  nBins: number;
83
83
  nFrames: number;
84
+ sampleRate?: number;
85
+ hopLength?: number;
86
+ timeConstant?: number;
87
+ gain?: number;
88
+ bias?: number;
89
+ power?: number;
90
+ eps?: number;
91
+ /** @deprecated Put PCEN fields directly on the request object. */
84
92
  options?: Record<string, number>;
85
93
  }
86
94
  export interface TempogramRequest {
@@ -449,7 +457,17 @@ export function pcen(
449
457
  ): Float32Array {
450
458
  if (!(values instanceof Float32Array)) {
451
459
  const r = values;
452
- return pcen(r.values, r.nBins, r.nFrames, r.options);
460
+ const {
461
+ values: requestValues,
462
+ nBins: requestBins,
463
+ nFrames: requestFrames,
464
+ options: legacyOptions,
465
+ ...flatOptions
466
+ } = r;
467
+ return pcen(requestValues, requestBins, requestFrames, {
468
+ ...legacyOptions,
469
+ ...flatOptions,
470
+ });
453
471
  }
454
472
  return requireModule().pcen(values, nBins, nFrames, options);
455
473
  }
@@ -98,6 +98,9 @@ export interface LufsRequest extends ValidateOptions {
98
98
  export interface NnlsChromaRequest extends GuardedOptions {
99
99
  samples: Float32Array;
100
100
  sampleRate?: number;
101
+ enableStftBlend?: boolean;
102
+ stftBlendWeight?: number;
103
+ stftBlendNFft?: number;
101
104
  }
102
105
 
103
106
  function validateMusicSamples(
@@ -140,18 +143,24 @@ export function nnlsChroma(request: NnlsChromaRequest): WasmNnlsChromaResult;
140
143
  export function nnlsChroma(
141
144
  samples: Float32Array,
142
145
  sampleRate?: number,
143
- options?: GuardedOptions,
146
+ options?: Omit<NnlsChromaRequest, 'samples' | 'sampleRate'>,
144
147
  ): WasmNnlsChromaResult;
145
148
  export function nnlsChroma(
146
149
  samples: Float32Array | NnlsChromaRequest,
147
150
  sampleRate = 22050,
148
- options: GuardedOptions = {},
151
+ options: Omit<NnlsChromaRequest, 'samples' | 'sampleRate'> = {},
149
152
  ): WasmNnlsChromaResult {
150
153
  if (!(samples instanceof Float32Array)) {
151
154
  return nnlsChroma(samples.samples, samples.sampleRate, samples);
152
155
  }
153
156
  validateMusicSamples('nnlsChroma', samples, sampleRate, options);
154
- return requireModule().nnlsChroma(samples, sampleRate);
157
+ return requireModule().nnlsChroma(
158
+ samples,
159
+ sampleRate,
160
+ options.enableStftBlend ?? true,
161
+ options.stftBlendWeight ?? 0.55,
162
+ options.stftBlendNFft ?? 4096,
163
+ );
155
164
  }
156
165
 
157
166
  /**
@@ -307,7 +316,8 @@ export function hybridCqt(
307
316
  * @param fmin - Minimum frequency in Hz (default: 32.70319566257483, C1)
308
317
  * @param nBins - Number of frequency bins (default: 84)
309
318
  * @param binsPerOctave - Bins per octave (default: 12)
310
- * @param gamma - Bandwidth offset; 0 is equivalent to CQT (default: 0)
319
+ * @param gamma - Bandwidth offset; negative selects the automatic ERB-derived
320
+ * value, while 0 is equivalent to CQT (default: -1)
311
321
  * @returns VQT magnitude result (same shape as CQT)
312
322
  */
313
323
  export function vqt(request: VqtRequest): CqtResult;
@@ -328,7 +338,7 @@ export function vqt(
328
338
  fmin = 32.70319566257483,
329
339
  nBins = 84,
330
340
  binsPerOctave = 12,
331
- gamma = 0,
341
+ gamma = -1,
332
342
  options: GuardedOptions = {},
333
343
  ): CqtResult {
334
344
  if (!(samples instanceof Float32Array)) {
@@ -348,9 +358,6 @@ export function vqt(
348
358
  validatePositiveIntegers('vqt', { hopLength, nBins, binsPerOctave });
349
359
  validateFrequencyBounds('vqt', fmin);
350
360
  assertFiniteScalar('vqt', gamma, 'gamma');
351
- if (gamma < 0) {
352
- throw new RangeError('vqt: gamma must be non-negative');
353
- }
354
361
  return requireModule().vqt(samples, sampleRate, hopLength, fmin, nBins, binsPerOctave, gamma);
355
362
  }
356
363
 
@@ -465,7 +472,7 @@ export function vqtToAudio(
465
472
  hopLength = 512,
466
473
  fmin = 32.70319566257483,
467
474
  binsPerOctave = 12,
468
- gamma = 0,
475
+ gamma = -1,
469
476
  nIter = 32,
470
477
  options: GuardedOptions = {},
471
478
  ): Float32Array {
@@ -497,9 +504,6 @@ export function vqtToAudio(
497
504
  options,
498
505
  );
499
506
  assertFiniteScalar('vqtToAudio', gamma, 'gamma');
500
- if (gamma < 0) {
501
- throw new RangeError('vqtToAudio: gamma must be non-negative');
502
- }
503
507
  return requireModule().vqtToAudio(
504
508
  magnitude,
505
509
  nBins,
@@ -544,8 +548,8 @@ export function analyzeSections(
544
548
  hopLength: options.hopLength ?? 512,
545
549
  });
546
550
  assertFiniteScalar('analyzeSections', options.minSectionSec ?? 4.0, 'minSectionSec');
547
- if ((options.minSectionSec ?? 4.0) <= 0) {
548
- throw new RangeError('analyzeSections: minSectionSec must be positive');
551
+ if ((options.minSectionSec ?? 4.0) < 0) {
552
+ throw new RangeError('analyzeSections: minSectionSec must be non-negative');
549
553
  }
550
554
  // The embind value marshalling returns an array whose constructor is not this
551
555
  // realm's Array; chaining .map() onto it propagates that constructor via
@@ -18,8 +18,8 @@ function requireModule() {
18
18
  * @param hopLength - Hop length (default: 512)
19
19
  * @param fmin - Minimum frequency in Hz (default: 65)
20
20
  * @param fmax - Maximum frequency in Hz (default: 2093)
21
- * @param threshold - YIN threshold (default: 0.3)
22
- * @param fillNa - If true, return 0 for unvoiced f0 frames; otherwise keep NaN (default: false)
21
+ * @param threshold - YIN threshold (default: 0.1)
22
+ * @param fillNa - Retained for compatibility; YIN always returns a finite per-frame estimate.
23
23
  * @returns Pitch detection result
24
24
  */
25
25
  export interface PitchYinRequest {
@@ -51,7 +51,7 @@ export function pitchYin(
51
51
  hopLength = 512,
52
52
  fmin = 65.0,
53
53
  fmax = 2093.0,
54
- threshold = 0.3,
54
+ threshold = 0.1,
55
55
  fillNa = false,
56
56
  ): PitchResult {
57
57
  if (!(samples instanceof Float32Array)) {
@@ -88,7 +88,7 @@ export function pitchYin(
88
88
  * @param hopLength - Hop length (default: 512)
89
89
  * @param fmin - Minimum frequency in Hz (default: 65)
90
90
  * @param fmax - Maximum frequency in Hz (default: 2093)
91
- * @param threshold - YIN threshold (default: 0.3)
91
+ * @param threshold - YIN threshold (default: 0.1)
92
92
  * @param fillNa - If true, return 0 for unvoiced f0 frames; otherwise keep NaN (default: false)
93
93
  * @returns Pitch detection result
94
94
  */
@@ -112,7 +112,7 @@ export function pitchPyin(
112
112
  hopLength = 512,
113
113
  fmin = 65.0,
114
114
  fmax = 2093.0,
115
- threshold = 0.3,
115
+ threshold = 0.1,
116
116
  fillNa = false,
117
117
  ): PitchResult {
118
118
  if (!(samples instanceof Float32Array)) {
@@ -438,21 +438,21 @@ export function remix(
438
438
  export function phaseVocoder(request: PhaseVocoderRequest): Float32Array;
439
439
  export function phaseVocoder(
440
440
  samples: Float32Array,
441
+ sampleRate: number,
441
442
  rate: number,
442
- sampleRate?: number,
443
443
  nFft?: number,
444
444
  hopLength?: number,
445
445
  ): Float32Array;
446
446
  export function phaseVocoder(
447
447
  samples: Float32Array | PhaseVocoderRequest,
448
- rate = 1,
449
448
  sampleRate = 22050,
449
+ rate = 1,
450
450
  nFft = 2048,
451
451
  hopLength = 512,
452
452
  ): Float32Array {
453
453
  if (!(samples instanceof Float32Array)) {
454
454
  const r = samples;
455
- return phaseVocoder(r.samples, r.rate, r.sampleRate, r.nFft, r.hopLength);
455
+ return phaseVocoder(r.samples, r.sampleRate ?? 22050, r.rate, r.nFft, r.hopLength);
456
456
  }
457
457
  return requireModule().phaseVocoder(samples, sampleRate, rate, nFft, hopLength);
458
458
  }
@@ -34,6 +34,7 @@ export interface ChromaSpectrogramRequest extends GuardedOptions {
34
34
  sampleRate?: number;
35
35
  hopLength?: number;
36
36
  nChroma?: number;
37
+ binsPerOctave?: number;
37
38
  }
38
39
 
39
40
  export interface MelSpectrogramRequest extends SpectrogramRequest {
@@ -53,6 +54,8 @@ export interface MfccToMelRequest extends GuardedOptions {
53
54
  nMfcc: number;
54
55
  nFrames: number;
55
56
  nMels?: number;
57
+ /** Lifter used by the forward MFCC transform; zero means no liftering. */
58
+ lifter?: number;
56
59
  }
57
60
 
58
61
  /** Canonical request form for reconstruction from a Mel power spectrogram. */
@@ -262,6 +265,7 @@ export function chromaCens(
262
265
  sampleRate?: number,
263
266
  hopLength?: number,
264
267
  nChroma?: number,
268
+ binsPerOctave?: number,
265
269
  options?: GuardedOptions,
266
270
  ): ChromaResult;
267
271
  export function chromaCens(
@@ -269,6 +273,7 @@ export function chromaCens(
269
273
  sampleRate = 22050,
270
274
  hopLength = 512,
271
275
  nChroma = 12,
276
+ binsPerOctave = 36,
272
277
  options: GuardedOptions = {},
273
278
  ): ChromaResult {
274
279
  if (!(samples instanceof Float32Array)) {
@@ -278,12 +283,16 @@ export function chromaCens(
278
283
  request.sampleRate,
279
284
  request.hopLength,
280
285
  request.nChroma,
286
+ request.binsPerOctave,
281
287
  request,
282
288
  );
283
289
  }
284
290
  validateSpectrogramSamples('chromaCens', samples, sampleRate, options);
285
- validatePositiveIntegers('chromaCens', { hopLength, nChroma });
286
- return requireModule().chromaCens(samples, sampleRate, hopLength, nChroma);
291
+ validatePositiveIntegers('chromaCens', { hopLength, nChroma, binsPerOctave });
292
+ if (binsPerOctave % nChroma !== 0) {
293
+ throw new RangeError('chromaCens: binsPerOctave must be a multiple of nChroma');
294
+ }
295
+ return requireModule().chromaCens(samples, sampleRate, hopLength, nChroma, binsPerOctave);
287
296
  }
288
297
 
289
298
  /**
@@ -301,6 +310,7 @@ export function chromaCqt(
301
310
  sampleRate?: number,
302
311
  hopLength?: number,
303
312
  nChroma?: number,
313
+ binsPerOctave?: number,
304
314
  options?: GuardedOptions,
305
315
  ): ChromaResult;
306
316
  export function chromaCqt(
@@ -308,6 +318,7 @@ export function chromaCqt(
308
318
  sampleRate = 22050,
309
319
  hopLength = 512,
310
320
  nChroma = 12,
321
+ binsPerOctave = 36,
311
322
  options: GuardedOptions = {},
312
323
  ): ChromaResult {
313
324
  if (!(samples instanceof Float32Array)) {
@@ -317,12 +328,16 @@ export function chromaCqt(
317
328
  request.sampleRate,
318
329
  request.hopLength,
319
330
  request.nChroma,
331
+ request.binsPerOctave,
320
332
  request,
321
333
  );
322
334
  }
323
335
  validateSpectrogramSamples('chromaCqt', samples, sampleRate, options);
324
- validatePositiveIntegers('chromaCqt', { hopLength, nChroma });
325
- return requireModule().chromaCqt(samples, sampleRate, hopLength, nChroma);
336
+ validatePositiveIntegers('chromaCqt', { hopLength, nChroma, binsPerOctave });
337
+ if (binsPerOctave % nChroma !== 0) {
338
+ throw new RangeError('chromaCqt: binsPerOctave must be a multiple of nChroma');
339
+ }
340
+ return requireModule().chromaCqt(samples, sampleRate, hopLength, nChroma, binsPerOctave);
326
341
  }
327
342
 
328
343
  /**
@@ -664,6 +679,7 @@ export function mfccToMel(
664
679
  nMfcc: number,
665
680
  nFrames: number,
666
681
  nMels?: number,
682
+ lifter?: number,
667
683
  options?: GuardedOptions,
668
684
  ): MelPowerResult;
669
685
  export function mfccToMel(
@@ -671,6 +687,7 @@ export function mfccToMel(
671
687
  nMfcc = 0,
672
688
  nFrames = 0,
673
689
  nMels = 128,
690
+ lifter = 0,
674
691
  options: GuardedOptions = {},
675
692
  ): MelPowerResult {
676
693
  if (!(mfccCoefficients instanceof Float32Array)) {
@@ -680,6 +697,7 @@ export function mfccToMel(
680
697
  request.nMfcc,
681
698
  request.nFrames,
682
699
  request.nMels,
700
+ request.lifter,
683
701
  request,
684
702
  );
685
703
  }
@@ -693,7 +711,7 @@ export function mfccToMel(
693
711
  options,
694
712
  );
695
713
  validatePositiveIntegers('mfccToMel', { nMels });
696
- return requireModule().mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels);
714
+ return requireModule().mfccToMel(mfccCoefficients, nMfcc, nFrames, nMels, lifter);
697
715
  }
698
716
 
699
717
  /**
@@ -726,6 +744,7 @@ export function mfccToAudio(
726
744
  fmax?: number,
727
745
  nIter?: number,
728
746
  htk?: boolean,
747
+ lifter?: number,
729
748
  options?: GuardedOptions,
730
749
  ): Float32Array;
731
750
  export function mfccToAudio(
@@ -740,6 +759,7 @@ export function mfccToAudio(
740
759
  fmax = 0,
741
760
  nIter = 32,
742
761
  htk = false,
762
+ lifter = 0,
743
763
  options: GuardedOptions = {},
744
764
  ): Float32Array {
745
765
  if (!(mfccCoefficients instanceof Float32Array)) {
@@ -756,6 +776,7 @@ export function mfccToAudio(
756
776
  request.fmax,
757
777
  request.nIter,
758
778
  request.htk,
779
+ request.lifter,
759
780
  request,
760
781
  );
761
782
  }
@@ -783,6 +804,7 @@ export function mfccToAudio(
783
804
  fmax,
784
805
  nIter,
785
806
  htk,
807
+ lifter,
786
808
  );
787
809
  }
788
810
 
package/src/index.ts CHANGED
@@ -312,6 +312,7 @@ export type {
312
312
  MeteringDynamicRangeOptions,
313
313
  MeteringDynamicRangeRequest,
314
314
  MeteringSamplesRequest,
315
+ MeteringSilenceRatioRequest,
315
316
  MeteringSpectrumFrameRequest,
316
317
  MeteringSpectrumRequest,
317
318
  MeteringStereoDecimatedRequest,
@@ -336,6 +337,7 @@ export {
336
337
  meteringPhaseScope,
337
338
  meteringPhaseScopeDecimated,
338
339
  meteringRmsDb,
340
+ meteringSilenceRatio,
339
341
  meteringSpectrum,
340
342
  meteringSpectrumFrame,
341
343
  meteringStereoCorrelation,
@@ -522,6 +524,7 @@ export type {
522
524
  DetectKeyRequest,
523
525
  DynamicsAnalysisResult,
524
526
  EstimateRoomRequest,
527
+ MusicAnalyzeRequest,
525
528
  RhythmAnalysisResult,
526
529
  RoomMorphRequest,
527
530
  SamplesRequest,
@@ -152,6 +152,8 @@ export interface MasteringInsertParamInfo {
152
152
  id: number;
153
153
  /** Whether the param can be changed live from the audio thread. */
154
154
  rtSafe: boolean;
155
+ /** Physical unit when the parameter is not unitless. */
156
+ unit?: string;
155
157
  }
156
158
 
157
159
  /**
package/src/metering.ts CHANGED
@@ -128,6 +128,43 @@ export function meteringRmsDb(
128
128
  return requireModule().meteringRmsDb(request.samples, request.sampleRate ?? 22050);
129
129
  }
130
130
 
131
+ export interface MeteringSilenceRatioRequest extends MeteringSamplesRequest {
132
+ thresholdDb?: number;
133
+ frameLength?: number;
134
+ hopLength?: number;
135
+ }
136
+
137
+ export function meteringSilenceRatio(request: MeteringSilenceRatioRequest): number;
138
+ export function meteringSilenceRatio(
139
+ samples: Float32Array,
140
+ sampleRate?: number,
141
+ thresholdDb?: number,
142
+ frameLength?: number,
143
+ hopLength?: number,
144
+ options?: ValidateOptions,
145
+ ): number;
146
+ export function meteringSilenceRatio(
147
+ samples: Float32Array | MeteringSilenceRatioRequest,
148
+ sampleRate = 22050,
149
+ thresholdDb = -45,
150
+ frameLength = 1024,
151
+ hopLength = 256,
152
+ options: ValidateOptions = {},
153
+ ): number {
154
+ const request =
155
+ samples instanceof Float32Array
156
+ ? { samples, sampleRate, thresholdDb, frameLength, hopLength, ...options }
157
+ : samples;
158
+ assertSamples('meteringSilenceRatio', request.samples, request.validate !== false);
159
+ return requireModule().meteringSilenceRatio(
160
+ request.samples,
161
+ request.sampleRate ?? 22050,
162
+ request.thresholdDb ?? -45,
163
+ request.frameLength ?? 1024,
164
+ request.hopLength ?? 256,
165
+ );
166
+ }
167
+
131
168
  export function meteringCrestFactorDb(request: MeteringSamplesRequest): number;
132
169
  export function meteringCrestFactorDb(
133
170
  samples: Float32Array,
@@ -206,16 +243,20 @@ export function meteringDetectClipping(
206
243
  ): ClippingReport {
207
244
  const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
208
245
  assertSamples('meteringDetectClipping', request.samples, request.validate !== false);
246
+ const minRegionSamples = request.minRegionSamples ?? 1;
247
+ if (!Number.isInteger(minRegionSamples) || minRegionSamples < 0) {
248
+ throw new RangeError('meteringDetectClipping: minRegionSamples must be a non-negative integer');
249
+ }
209
250
  return requireModule().meteringDetectClipping(
210
251
  request.samples,
211
252
  request.sampleRate ?? 22050,
212
253
  request.threshold ?? 0.999,
213
- request.minRegionSamples ?? 1,
254
+ minRegionSamples,
214
255
  );
215
256
  }
216
257
 
217
258
  /**
218
- * Sliding-window dynamic range. Pass 0 for window/hop to use the library
259
+ * Sliding-window dynamic range for mono audio. Pass 0 for window/hop to use the library
219
260
  * default (window=3 s, hop=1 s). The percentiles use a NEGATIVE sentinel for
220
261
  * "use the library default" (low=0.10, high=0.95) because 0 is a literal 0th
221
262
  * percentile; omitted percentiles therefore default to -1.
@@ -354,7 +395,7 @@ export interface WaveformPeaksReport {
354
395
  samplesPerBucket: number;
355
396
  }
356
397
 
357
- /** Pearson correlation in [-1, 1] between two equal-length channels. */
398
+ /** Uncentered correlation (cosine similarity) in [-1, 1] between equal-length channels. */
358
399
  export function meteringStereoCorrelation(request: MeteringStereoRequest): number;
359
400
  export function meteringStereoCorrelation(
360
401
  left: Float32Array,
@@ -561,7 +602,7 @@ export function meteringPhaseScopeDecimated(
561
602
  }
562
603
 
563
604
  /**
564
- * Welch-averaged magnitude / power / dB spectrum over the WHOLE signal (split
605
+ * Welch-averaged magnitude / power / dB spectrum over the WHOLE mono signal (split
565
606
  * into Hann-windowed, 50%-overlapping `nFft`-length frames whose power spectra
566
607
  * are averaged). For a true single-frame snapshot, use
567
608
  * {@link meteringSpectrumFrame}.
package/src/mixer.ts CHANGED
@@ -74,7 +74,10 @@ export class Mixer {
74
74
  return new Mixer(module.createMixerFromSceneJson(json, sampleRate, blockSize), blockSize);
75
75
  }
76
76
 
77
- /** Rebuild and compile the routing graph from the current scene topology. */
77
+ /**
78
+ * Rebuild and compile the routing graph without resetting its absolute
79
+ * automation sample position or queued strip automation.
80
+ */
78
81
  compile(): void {
79
82
  this.mixer.compile();
80
83
  }
@@ -261,6 +264,11 @@ export class Mixer {
261
264
  this.mixer.setVcaGroupGainDb(id, gainDb);
262
265
  }
263
266
 
267
+ /** Replace an existing VCA group's strip membership. */
268
+ setVcaGroupMembers(id: string, members: string[]): void {
269
+ this.mixer.setVcaGroupMembers(id, members);
270
+ }
271
+
264
272
  /** Remove a VCA group by id. */
265
273
  removeVcaGroup(id: string): void {
266
274
  this.mixer.removeVcaGroup(id);
@@ -427,6 +435,11 @@ export class Mixer {
427
435
  return this.mixer.meterTap(stripIndex, meterTapCode(tap));
428
436
  }
429
437
 
438
+ /** Read the post-insert meter for a compiled bus, including master. */
439
+ busMeter(busId: string): MixMeterSnapshot {
440
+ return this.mixer.busMeter(busId);
441
+ }
442
+
430
443
  /**
431
444
  * Schedule sample-accurate fader automation on a strip.
432
445
  *