@libraz/libsonare 1.3.3 → 1.4.0

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.3.3",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Audio analysis library for music information retrieval",
package/src/codes.ts CHANGED
@@ -52,5 +52,10 @@ export function meterTapCode(tap: MeterTap | number): number {
52
52
  }
53
53
 
54
54
  export function sendTimingCode(timing: SendTiming | number): number {
55
- return timing === 'preFader' || timing === 0 ? 0 : 1;
55
+ // Mirrors SonareSendTiming: post-fader is 0 (so an omitted/zeroed value is
56
+ // post-fader), pre-fader is 1. A raw number is passed through as the C ABI int.
57
+ if (typeof timing === 'number') {
58
+ return timing;
59
+ }
60
+ return timing === 'preFader' ? 1 : 0;
56
61
  }
@@ -16,13 +16,15 @@ import type {
16
16
  PairProcessor,
17
17
  RealtimeVoiceChangerConfigInput,
18
18
  SoloProcessor,
19
+ SpectralEditOptions,
20
+ SpectralRegionOp,
19
21
  StereoAnalysis,
20
22
  StreamingPlatform,
21
23
  } from './public_types';
22
24
  import type { ProgressCallback } from './sonare.js';
23
25
  import { RealtimeVoiceChanger } from './streaming_mixing';
24
26
  import type { ValidateOptions } from './validation';
25
- import { assertSamples } from './validation';
27
+ import { assertSampleRate, assertSamples } from './validation';
26
28
 
27
29
  function requireModule() {
28
30
  return getSonareModule();
@@ -339,6 +341,31 @@ export function normalize(
339
341
  return requireModule().normalize(samples, sampleRate, targetDb);
340
342
  }
341
343
 
344
+ /**
345
+ * Apply region-based spectral edits (gain/attenuate/mute/heal) to mono audio.
346
+ *
347
+ * Each op is a time x frequency rectangle applied in array order over a single
348
+ * STFT buffer, so a later op observes the result of earlier ops. The output has
349
+ * the same length and sample rate as the input; an empty `ops` list is an
350
+ * identity transform (within the iSTFT's own tolerance).
351
+ *
352
+ * @param samples - Audio samples (mono, float32)
353
+ * @param sampleRate - Sample rate in Hz
354
+ * @param ops - Region edit ops applied in order ({@link SpectralRegionOp})
355
+ * @param options - STFT + heal configuration ({@link SpectralEditOptions})
356
+ * @returns Edited audio
357
+ */
358
+ export function spectralEdit(
359
+ samples: Float32Array,
360
+ sampleRate: number,
361
+ ops: SpectralRegionOp[] = [],
362
+ options: SpectralEditOptions & ValidateOptions = {},
363
+ ): Float32Array {
364
+ assertSamples('spectralEdit', samples, options.validate !== false);
365
+ assertSampleRate('spectralEdit', sampleRate);
366
+ return requireModule().spectralEdit(samples, sampleRate, ops, options as Record<string, unknown>);
367
+ }
368
+
342
369
  /**
343
370
  * Apply mastering loudness normalization with a true-peak ceiling.
344
371
  *
@@ -392,6 +419,81 @@ export function masteringInsertParamNames(name: string): string[] {
392
419
  ).masteringInsertParamNames(name);
393
420
  }
394
421
 
422
+ /** One realtime-automatable parameter of an insert processor. */
423
+ export interface MasteringInsertParamInfo {
424
+ /** JSON-key parameter name, as used in scene insert params. */
425
+ name: string;
426
+ /** Integer param id for realtime automation lanes / MIDI-CC binding. */
427
+ id: number;
428
+ /** Whether the param can be changed live from the audio thread. */
429
+ rtSafe: boolean;
430
+ }
431
+
432
+ /**
433
+ * Returns the realtime-automatable parameter descriptors for an insert / FX
434
+ * processor: each entry maps a JSON-key parameter name to the integer id used by
435
+ * realtime automation and reports whether it is realtime-safe. Unlike
436
+ * {@link masteringInsertParamNames} (every construction key), this lists only the
437
+ * realtime-controllable subset — the keys accepted by
438
+ * {@link RealtimeEngine.setTrackStripInsertParamByName}. Returns an empty array
439
+ * for an unknown name or a processor with no automatable parameters.
440
+ *
441
+ * @param name - Insert processor name (see {@link masteringInsertNames}).
442
+ */
443
+ export function masteringInsertParamInfo(name: string): MasteringInsertParamInfo[] {
444
+ const json = (
445
+ requireModule() as unknown as { masteringInsertParamInfo: (name: string) => string }
446
+ ).masteringInsertParamInfo(name);
447
+ return JSON.parse(json) as MasteringInsertParamInfo[];
448
+ }
449
+
450
+ /**
451
+ * How a processor handles a buffer with more than two channels (a surround
452
+ * bed). "multichannel" processes every plane in one call; "stereoPairOnly"
453
+ * operates on the front L/R pair and passes any surround planes through dry.
454
+ * "perChannel"/"passthrough" are reserved and unused by the current catalog.
455
+ */
456
+ export type MasteringChannelPolicy =
457
+ | 'multichannel'
458
+ | 'stereoPairOnly'
459
+ | 'perChannel'
460
+ | 'passthrough';
461
+
462
+ /** One processor's realtime/offline/pair classification in the catalog. */
463
+ export interface MasteringProcessorCatalogEntry {
464
+ /** Processor id (the name used for scene inserts / named processors). */
465
+ id: string;
466
+ /**
467
+ * Primary classification, by precedence pair > realtime > offline: "pair" for
468
+ * two-input match.* processors, "realtime" for ids that build as a realtime
469
+ * scene insert, "offline" for whole-file-only processors.
470
+ */
471
+ kind: 'realtime' | 'offline' | 'pair';
472
+ /** True exactly for ids that always succeed as a realtime scene insert. */
473
+ realtimeInsertable: boolean;
474
+ /** True for processors with no mono implementation (stereo-only). */
475
+ stereoOnly: boolean;
476
+ /**
477
+ * How the mixer wraps the processor on a >2-channel (surround) bus insert:
478
+ * "multichannel" (one full-buffer call) or "stereoPairOnly" (front L/R pair,
479
+ * surround planes passed through dry).
480
+ */
481
+ channelPolicy: MasteringChannelPolicy;
482
+ }
483
+
484
+ /**
485
+ * Returns the machine-readable classification catalog for every named processor
486
+ * id, merging the offline registry, the realtime insert factory, and the pair
487
+ * registry. Lets a host filter a processor picker by realtime insertability
488
+ * instead of offering ids the realtime strip would reject.
489
+ */
490
+ export function masteringProcessorCatalog(): MasteringProcessorCatalogEntry[] {
491
+ const json = (
492
+ requireModule() as unknown as { masteringProcessorCatalog: () => string }
493
+ ).masteringProcessorCatalog();
494
+ return JSON.parse(json) as MasteringProcessorCatalogEntry[];
495
+ }
496
+
395
497
  export function masteringPairProcessorNames(): PairProcessor[] {
396
498
  return requireModule().masteringPairProcessorNames() as PairProcessor[];
397
499
  }
@@ -257,11 +257,21 @@ export function analyzeMelody(
257
257
  const fmin = options.fmin ?? 65.0;
258
258
  const fmax = options.fmax ?? 2093.0;
259
259
  validateFrequencyBounds('analyzeMelody', fmin, fmax);
260
+ // The melody tracker's fmin is a YIN pitch floor: 0 is meaningless, and the
261
+ // flat C ABI (sonare_analyze_melody) rejects it. validateFrequencyBounds only
262
+ // guards fmin >= 0, so enforce strict positivity here for parity.
263
+ if (fmin <= 0) {
264
+ throw new RangeError('analyzeMelody: fmin must be positive');
265
+ }
260
266
  validatePositiveIntegers('analyzeMelody', {
261
267
  frameLength: options.frameLength ?? 2048,
262
268
  hopLength: options.hopLength ?? 256,
263
269
  });
264
- assertFiniteScalar('analyzeMelody', options.threshold ?? 0.1, 'threshold');
270
+ const threshold = options.threshold ?? 0.1;
271
+ assertFiniteScalar('analyzeMelody', threshold, 'threshold');
272
+ if (threshold <= 0) {
273
+ throw new RangeError('analyzeMelody: threshold must be positive');
274
+ }
265
275
  return requireModule().analyzeMelody(
266
276
  samples,
267
277
  sampleRate,
@@ -371,7 +381,9 @@ export function tempogramRatio(
371
381
  * Measure loudness (EBU R128 / ITU-R BS.1770).
372
382
  *
373
383
  * @param samples - Audio samples (mono, float32)
374
- * @param sampleRate - Sample rate in Hz (default: 22050)
384
+ * @param sampleRate - Sample rate in Hz. The default (22050) is non-standard for
385
+ * audio; pass the buffer's actual rate, as K-weighting is sample-rate
386
+ * dependent and a wrong rate yields wrong loudness.
375
387
  * @returns Loudness measurement result
376
388
  */
377
389
  export function lufs(
@@ -388,7 +400,8 @@ export function lufs(
388
400
  * Compute the momentary loudness (LUFS) over time.
389
401
  *
390
402
  * @param samples - Audio samples (mono, float32)
391
- * @param sampleRate - Sample rate in Hz (default: 22050)
403
+ * @param sampleRate - Sample rate in Hz. The default (22050) is non-standard and
404
+ * K-weighting is sample-rate dependent; pass the buffer's actual rate.
392
405
  * @returns Momentary LUFS values over time
393
406
  */
394
407
  export function momentaryLufs(
@@ -405,7 +418,8 @@ export function momentaryLufs(
405
418
  * Compute the short-term loudness (LUFS) over time.
406
419
  *
407
420
  * @param samples - Audio samples (mono, float32)
408
- * @param sampleRate - Sample rate in Hz (default: 22050)
421
+ * @param sampleRate - Sample rate in Hz. The default (22050) is non-standard and
422
+ * K-weighting is sample-rate dependent; pass the buffer's actual rate.
409
423
  * @returns Short-term LUFS values over time
410
424
  */
411
425
  export function shortTermLufs(
@@ -218,6 +218,10 @@ export function hpssWithResidual(
218
218
  * Channel-weighted multichannel integrated loudness + LRA (ITU-R BS.1770 /
219
219
  * EBU R128) from an interleaved buffer of `frames * channels` samples. The
220
220
  * per-channel frame count is derived from the buffer length and `channels`.
221
+ *
222
+ * Pass the buffer's actual `sampleRate`: the default (22050) is non-standard for
223
+ * audio, and K-weighting is sample-rate dependent, so a wrong rate yields wrong
224
+ * loudness.
221
225
  */
222
226
  export function lufsInterleaved(
223
227
  samples: Float32Array,
@@ -231,7 +235,9 @@ export function lufsInterleaved(
231
235
  }
232
236
 
233
237
  /**
234
- * Standards-compliant EBU R128 loudness range (LRA) in LU.
238
+ * Standards-compliant EBU R128 loudness range (LRA) in LU. Pass the buffer's
239
+ * actual `sampleRate`: the default (22050) is non-standard and K-weighting is
240
+ * sample-rate dependent.
235
241
  */
236
242
  export function ebur128LoudnessRange(samples: Float32Array, sampleRate = 22050): number {
237
243
  return requireModule().ebur128LoudnessRange(samples, sampleRate);
package/src/index.ts CHANGED
@@ -43,6 +43,9 @@ export type {
43
43
  DereverbClassicalOptions,
44
44
  DynamicsResult,
45
45
  GateOptions,
46
+ MasteringChannelPolicy,
47
+ MasteringInsertParamInfo,
48
+ MasteringProcessorCatalogEntry,
46
49
  TransientShaperOptions,
47
50
  TrimSilenceMode,
48
51
  TrimSilenceOptions,
@@ -67,6 +70,7 @@ export {
67
70
  masteringDynamicsGate,
68
71
  masteringDynamicsTransientShaper,
69
72
  masteringInsertNames,
73
+ masteringInsertParamInfo,
70
74
  masteringInsertParamNames,
71
75
  masteringPairAnalysisNames,
72
76
  masteringPairAnalyze,
@@ -74,6 +78,7 @@ export {
74
78
  masteringPairProcessorNames,
75
79
  masteringPresetNames,
76
80
  masteringProcess,
81
+ masteringProcessorCatalog,
77
82
  masteringProcessorNames,
78
83
  masteringProcessStereo,
79
84
  masteringRepairDeclick,
@@ -95,6 +100,7 @@ export {
95
100
  pitchCorrectToMidi,
96
101
  pitchCorrectToMidiTimevarying,
97
102
  pitchShift,
103
+ spectralEdit,
98
104
  timeStretch,
99
105
  voiceChange,
100
106
  voiceChangeRealtime,
@@ -245,6 +251,7 @@ export type {
245
251
  ProjectLoopMode,
246
252
  ProjectLoopRecordingDesc,
247
253
  ProjectLoopRecordingResult,
254
+ ProjectMarker,
248
255
  ProjectMidiClipResult,
249
256
  ProjectMidiEvent,
250
257
  ProjectNotePairValidation,
@@ -268,6 +275,7 @@ export type {
268
275
  } from './project';
269
276
  export {
270
277
  EXPECTED_PROJECT_ABI_VERSION,
278
+ MarkerKind,
271
279
  Project,
272
280
  projectAbiVersion,
273
281
  SYNTH_BODY_TYPES,
@@ -348,6 +356,10 @@ export type {
348
356
  Section,
349
357
  SendTiming,
350
358
  SoloProcessor,
359
+ SpectralEditMode,
360
+ SpectralEditOptions,
361
+ SpectralEditWindow,
362
+ SpectralRegionOp,
351
363
  StereoAnalysis,
352
364
  StftPowerResult,
353
365
  StftResult,
@@ -410,10 +422,12 @@ export type {
410
422
  EngineGraphSpec,
411
423
  EngineMarker,
412
424
  EngineMeterTelemetry,
425
+ EngineMeterTelemetryWide,
413
426
  EngineMetronomeConfig,
414
427
  EngineMidiClipSchedule,
415
428
  EngineMidiEvent,
416
429
  EngineParameterInfo,
430
+ EngineScopeTelemetry,
417
431
  EngineTelemetry,
418
432
  EngineTempoSegment,
419
433
  EngineTimeSignatureSegment,
package/src/mixer.ts CHANGED
@@ -15,6 +15,7 @@ import type {
15
15
  PanLaw,
16
16
  PanMode,
17
17
  SendTiming,
18
+ SurroundPan,
18
19
  } from './public_types';
19
20
 
20
21
  export interface MixerRealtimeBuffer {
@@ -348,6 +349,14 @@ export class Mixer {
348
349
  this.mixer.setDualPan(stripIndex, leftPan, rightPan);
349
350
  }
350
351
 
352
+ /**
353
+ * Set the strip's surround pan position, used when it feeds a >2-channel bus.
354
+ * Stored on the scene; inert until the surround DSP path applies it.
355
+ */
356
+ setSurroundPan(stripIndex: number, pan: SurroundPan): void {
357
+ this.mixer.setSurroundPan(stripIndex, pan);
358
+ }
359
+
351
360
  /**
352
361
  * Add a send to a strip after construction.
353
362
  *
package/src/project.ts CHANGED
@@ -25,6 +25,34 @@ export interface ProjectBounceOptions {
25
25
  instrumentLatencySamples?: number;
26
26
  }
27
27
 
28
+ /**
29
+ * Marker kind ordinals. Mirrors `SonareMarkerKind` in `src/sonare_c_types.h`;
30
+ * the values are part of the ABI and must not be renumbered.
31
+ */
32
+ export const MarkerKind = {
33
+ marker: 0,
34
+ text: 1,
35
+ lyric: 2,
36
+ cuePoint: 3,
37
+ keySignature: 4,
38
+ } as const;
39
+
40
+ /** A project timeline marker with its kind and (for key signatures) the key. */
41
+ export interface ProjectMarker {
42
+ /** Stable marker id (0 when allocating a new id via {@link Project.setMarkerEx}). */
43
+ id: number;
44
+ /** Marker position in PPQ (quarter notes). */
45
+ ppq: number;
46
+ /** Marker label. */
47
+ name?: string;
48
+ /** {@link MarkerKind} ordinal (default 0 = marker). */
49
+ kind?: number;
50
+ /** Key signature only: -7..7 (sharps positive). */
51
+ keyFifths?: number;
52
+ /** Key signature only: false = major, true = minor. */
53
+ keyMinor?: boolean;
54
+ }
55
+
28
56
  /** Oscillator waveform for the built-in synth. */
29
57
  export type BuiltinSynthWaveform =
30
58
  | 'sine'
@@ -544,6 +572,10 @@ interface WasmProject {
544
572
  setWarpMap: (map: ProjectWarpMapDesc) => void;
545
573
  removeWarpMap: (warpRefId: number) => void;
546
574
  setTrackMidiDestination: (trackId: number, destinationId: number) => void;
575
+ setTrackGain: (trackId: number, gain: number) => void;
576
+ setTrackMute: (trackId: number, mute: boolean) => void;
577
+ setTrackSolo: (trackId: number, solo: boolean) => void;
578
+ setTrackPan: (trackId: number, pan: number) => void;
547
579
  undo: () => void;
548
580
  redo: () => void;
549
581
  setMidiEvents: (
@@ -627,6 +659,9 @@ interface WasmProject {
627
659
  getSampleRate: () => number;
628
660
  setMixerSceneJson: (sceneJson: string) => void;
629
661
  setMarker: (markerId: number, ppq: number, name: string) => number;
662
+ setMarkerEx: (marker: ProjectMarker) => number;
663
+ markerByIndex: (index: number) => ProjectMarker;
664
+ markerCount: () => number;
630
665
  trackCount: () => number;
631
666
  sourceCount: () => number;
632
667
  tempoSegmentCount: () => number;
@@ -1166,6 +1201,26 @@ export class Project {
1166
1201
  this.native.setTrackMidiDestination(trackId, destinationId);
1167
1202
  }
1168
1203
 
1204
+ /** Set a track's linear playback gain (1.0 = unity; >= 0) via an undoable edit. */
1205
+ setTrackGain(trackId: number, gain: number): void {
1206
+ this.native.setTrackGain(trackId, gain);
1207
+ }
1208
+
1209
+ /** Set a track's mute flag via an undoable edit (a muted track is silent). */
1210
+ setTrackMute(trackId: number, mute: boolean): void {
1211
+ this.native.setTrackMute(trackId, mute);
1212
+ }
1213
+
1214
+ /** Set a track's solo flag via an undoable edit (when any track is soloed, only soloed tracks sound). */
1215
+ setTrackSolo(trackId: number, solo: boolean): void {
1216
+ this.native.setTrackSolo(trackId, solo);
1217
+ }
1218
+
1219
+ /** Set a track's stereo balance in [-1, +1] (0 = center) via an undoable edit. */
1220
+ setTrackPan(trackId: number, pan: number): void {
1221
+ this.native.setTrackPan(trackId, pan);
1222
+ }
1223
+
1169
1224
  /** Undo the most recent edit. */
1170
1225
  undo(): void {
1171
1226
  this.native.undo();
@@ -1527,6 +1582,25 @@ export class Project {
1527
1582
  return this.native.setMarker(markerId, ppq, name);
1528
1583
  }
1529
1584
 
1585
+ /**
1586
+ * Add or replace a marker from a full {@link ProjectMarker}, including its
1587
+ * {@link MarkerKind} and (for key signatures) the key. Pass `id` 0 to allocate
1588
+ * a new id; returns the stable marker id.
1589
+ */
1590
+ setMarkerEx(marker: ProjectMarker): number {
1591
+ return this.native.setMarkerEx(marker);
1592
+ }
1593
+
1594
+ /** Read a project marker by index (0-based, in stored order). */
1595
+ markerByIndex(index: number): ProjectMarker {
1596
+ return this.native.markerByIndex(index);
1597
+ }
1598
+
1599
+ /** Number of markers in the project. */
1600
+ markerCount(): number {
1601
+ return this.native.markerCount();
1602
+ }
1603
+
1530
1604
  /** Number of tracks in the project. */
1531
1605
  trackCount(): number {
1532
1606
  return this.native.trackCount();
@@ -367,6 +367,40 @@ export interface NoteStretchOptions {
367
367
  stretchRatio?: number;
368
368
  }
369
369
 
370
+ /** How a `spectralEdit` region op modifies the masked bins. */
371
+ export type SpectralEditMode = 'gain' | 'attenuate' | 'mute' | 'heal';
372
+
373
+ /** Analysis/synthesis window used by `spectralEdit`. */
374
+ export type SpectralEditWindow = 'hann' | 'hamming' | 'blackman' | 'rectangular';
375
+
376
+ /** One time x frequency rectangle edit op for `spectralEdit`. */
377
+ export interface SpectralRegionOp {
378
+ /** Region time start (input samples); clamped to [0, length]. Default 0. */
379
+ startSample?: number;
380
+ /** Region time end, exclusive (input samples); clamped to [0, length]. Default = signal length. */
381
+ endSample?: number;
382
+ /** Region frequency low edge in Hz; clamped to [0, nyquist]. Default 0. */
383
+ lowHz?: number;
384
+ /** Region frequency high edge in Hz; <=0 or >= nyquist means nyquist. Default 0. */
385
+ highHz?: number;
386
+ /** Linear gain in dB for 'gain'/'attenuate'; ignored by 'mute'/'heal'. Default 0. */
387
+ gainDb?: number;
388
+ /** Edit mode. Default 'gain'. */
389
+ mode?: SpectralEditMode;
390
+ }
391
+
392
+ /** STFT + heal parameters for `spectralEdit`. All fields are optional. */
393
+ export interface SpectralEditOptions {
394
+ /** FFT size; must be a power of two (>= 2). Default 2048. */
395
+ nFft?: number;
396
+ /** Hop length; must satisfy 0 < hop <= nFft/2. Default 512. */
397
+ hopLength?: number;
398
+ /** Analysis + synthesis window. Default 'hann'. */
399
+ window?: SpectralEditWindow;
400
+ /** Neighbour frames each side used by 'heal' (>= 1). Default 2. */
401
+ healRadiusFrames?: number;
402
+ }
403
+
370
404
  /**
371
405
  * Detected beat
372
406
  */
@@ -655,6 +689,24 @@ export type MasteringProcessorParams = Record<string, number | boolean>;
655
689
 
656
690
  export type PanMode = 'balance' | 'stereoPan' | 'stereo-pan' | 'dualPan' | 'dual-pan' | number;
657
691
 
692
+ /**
693
+ * Surround pan position for a strip feeding a >2-channel bus. Phase 1 honors
694
+ * `azimuth`/`divergence`/`lfe`; `elevation`/`distance` are reserved. All fields
695
+ * are optional and default to a centered point source.
696
+ */
697
+ export interface SurroundPan {
698
+ /** -180..180 deg, 0 = front-center, positive = right. */
699
+ azimuth?: number;
700
+ /** Reserved (no height beds in phase 1). */
701
+ elevation?: number;
702
+ /** 0 = point source, 1 = spread across the front. */
703
+ divergence?: number;
704
+ /** 0..1 scalar send into the LFE plane. */
705
+ lfe?: number;
706
+ /** Reserved (focus/spread), defaults to 1. */
707
+ distance?: number;
708
+ }
709
+
658
710
  export interface MixOptions {
659
711
  inputTrimDb?: number | number[];
660
712
  faderDb?: number | number[];