@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.
@@ -449,7 +449,11 @@ export class Project {
449
449
  this.native.setMidiEvents(clipId, events);
450
450
  }
451
451
 
452
- /** Import an in-memory SMF buffer; returns the first added clip id. */
452
+ /**
453
+ * Import an in-memory SMF buffer; returns the first added clip id.
454
+ * Malformed or partially truncated tracks are rejected instead of installing
455
+ * a silently shortened clip.
456
+ */
453
457
  importSmf(data: Uint8Array): number {
454
458
  return this.native.importSmf(data);
455
459
  }
@@ -498,7 +502,10 @@ export class Project {
498
502
  this.native.setProgramOnChannel(clipId, group, channel, program, bank);
499
503
  }
500
504
 
501
- /** Destructively bake a MIDI-FX chain into a clip's stored MIDI events. */
505
+ /**
506
+ * Destructively bake a MIDI-FX chain into all stored events. Large clips are
507
+ * drained without truncation; failure leaves the original clip unchanged.
508
+ */
502
509
  bakeMidiFx(clipId: number, configJson: string): void {
503
510
  this.native.bakeMidiFx(clipId, configJson);
504
511
  }
@@ -510,9 +517,9 @@ export class Project {
510
517
 
511
518
  /**
512
519
  * Pre-flight check for hanging / unmatched notes in a MIDI clip: reports
513
- * whether every note-on has a matching note-off (FIFO per channel+note).
514
- * Useful before bouncing to catch a stuck note. Throws if `clipId` is unknown
515
- * or not a MIDI clip.
520
+ * whether every note-on in the exported half-open playback window has a
521
+ * matching note-off (FIFO per group+channel+note). Useful before bouncing to
522
+ * catch a stuck note. Throws if `clipId` is unknown or not a MIDI clip.
516
523
  */
517
524
  validateMidiNotes(clipId: number): ProjectNotePairValidation {
518
525
  return this.native.validateMidiNotes(clipId);
@@ -138,6 +138,8 @@ export interface MasteringResult {
138
138
  inputLufs: number;
139
139
  outputLufs: number;
140
140
  appliedGainDb: number;
141
+ /** True when peak headroom prevented the requested LUFS target. */
142
+ loudnessTargetLimited?: boolean;
141
143
  latencySamples?: number;
142
144
  }
143
145
 
@@ -329,6 +331,7 @@ export interface MasteringChainConfig {
329
331
  monoMaker?: {
330
332
  enabled?: boolean;
331
333
  amount?: number;
334
+ frequencyHz?: number;
332
335
  };
333
336
  };
334
337
  maximizer?: {
@@ -407,6 +410,8 @@ export interface MasteringChainResult {
407
410
  outputTruePeakDbtp: number;
408
411
  /** EBU Tech 3342 Loudness Range of the output (LU). */
409
412
  outputLra: number;
413
+ /** True when peak headroom prevented the requested LUFS target. */
414
+ loudnessTargetLimited: boolean;
410
415
  /** Per-stage gain reductions for the dynamics/maximizer stages (a subset of `stages`). */
411
416
  stageGainReductions: StageGainReduction[];
412
417
  }
@@ -422,6 +427,7 @@ export interface MasteringChainStereoResult {
422
427
  /** See {@link MasteringChainResult} for field semantics. */
423
428
  outputTruePeakDbtp: number;
424
429
  outputLra: number;
430
+ loudnessTargetLimited: boolean;
425
431
  stageGainReductions: StageGainReduction[];
426
432
  }
427
433
 
@@ -233,6 +233,10 @@ export interface Beat {
233
233
  export interface Chord {
234
234
  root: PitchClass;
235
235
  bass: PitchClass;
236
+ /** Canonical core spelling; stable across all language bindings. */
237
+ rootName: string;
238
+ /** Canonical core spelling; stable across all language bindings. */
239
+ bassName: string;
236
240
  quality: ChordQuality;
237
241
  start: number;
238
242
  end: number;
@@ -30,15 +30,16 @@ export interface PitchCorrectOptions extends ValidateOptions {
30
30
  export interface NoteStretchOptions {
31
31
  /** Note onset position in samples (selects the region). Default 0. */
32
32
  onsetSample?: number;
33
- /** Note offset position in samples (selects the region). Default 0. */
33
+ /** Note offset position in samples (selects the region). Defaults to the input length. */
34
34
  offsetSample?: number;
35
- /** Stretch ratio (0.5 = double duration, 2.0 = half duration). Default 1. */
35
+ /** Stretch ratio (0.5 = half duration, 2.0 = double duration). Default 1. */
36
36
  stretchRatio?: number;
37
37
  }
38
38
 
39
39
  /** Options for `noteMove`. */
40
40
  export interface NoteMoveOptions {
41
41
  onsetSample?: number;
42
+ /** Defaults to the input length. */
42
43
  offsetSample?: number;
43
44
  targetOnsetSample?: number;
44
45
  }
@@ -47,7 +48,7 @@ export interface NoteMoveOptions {
47
48
  export type SpectralEditMode = 'gain' | 'attenuate' | 'mute' | 'heal';
48
49
 
49
50
  /** Analysis/synthesis window used by `spectralEdit`. */
50
- export type SpectralEditWindow = 'hann' | 'hamming' | 'blackman' | 'rectangular';
51
+ export type SpectralEditWindow = 'hann' | 'hamming' | 'blackman' | 'rectangular' | 'rect';
51
52
 
52
53
  /** One time x frequency rectangle edit op for `spectralEdit`. */
53
54
  export interface SpectralRegionOp {
@@ -186,8 +187,14 @@ export interface PitchResult {
186
187
  */
187
188
  export interface LufsResult {
188
189
  integratedLufs: number;
190
+ /** Final complete 400 ms window, not Max-M. */
189
191
  momentaryLufs: number;
192
+ /** Final complete 3 s window, not Max-S. */
190
193
  shortTermLufs: number;
194
+ /** Maximum 400 ms window (EBU R128 Max-M). */
195
+ maxMomentaryLufs: number;
196
+ /** Maximum 3 s window (EBU R128 Max-S). */
197
+ maxShortTermLufs: number;
191
198
  loudnessRange: number;
192
199
  }
193
200
 
@@ -378,20 +378,38 @@ export function chordFunctionalAnalysis(
378
378
  * the UI responsive for long inputs, drive this from a Web Worker and use
379
379
  * {@link analyzeWithProgress} to report progress.
380
380
  */
381
- export function analyze(request: SamplesRequest): AnalysisResult;
381
+ export interface MusicAnalyzeOptions {
382
+ nFft?: number;
383
+ hopLength?: number;
384
+ bpmMin?: number;
385
+ bpmMax?: number;
386
+ startBpm?: number;
387
+ useTriadsOnly?: boolean;
388
+ useHpss?: boolean;
389
+ chromaHighpassHz?: number;
390
+ useBassWeighted?: boolean;
391
+ chromaHopMultiplier?: number;
392
+ useChordHmm?: boolean;
393
+ useChordKeyContext?: boolean;
394
+ chordHmmBeamWidth?: number;
395
+ detectChordInversions?: boolean;
396
+ }
397
+ export interface MusicAnalyzeRequest extends SamplesRequest, MusicAnalyzeOptions {}
398
+
399
+ export function analyze(request: MusicAnalyzeRequest): AnalysisResult;
382
400
  export function analyze(
383
401
  samples: Float32Array,
384
402
  sampleRate?: number,
385
- options?: GuardedOptions,
403
+ options?: GuardedOptions & MusicAnalyzeOptions,
386
404
  ): AnalysisResult;
387
405
  export function analyze(
388
- samples: Float32Array | SamplesRequest,
406
+ samples: Float32Array | MusicAnalyzeRequest,
389
407
  sampleRate = 22050,
390
- options: GuardedOptions = {},
408
+ options: GuardedOptions & MusicAnalyzeOptions = {},
391
409
  ): AnalysisResult {
392
410
  const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
393
411
  validateAnalysisInput('analyze', request.samples, request.sampleRate ?? 22050, request);
394
- const result = requireModule().analyze(request.samples, request.sampleRate ?? 22050);
412
+ const result = requireModule().analyze(request.samples, request.sampleRate ?? 22050, request);
395
413
  return convertAnalysisResult(result);
396
414
  }
397
415
 
@@ -1,7 +1,7 @@
1
1
  import { panLawCode, panModeCode, sendTimingCode } from './codes';
2
2
  import { ErrorCode, SonareError } from './errors';
3
3
  import { getSonareModule } from './module_state';
4
- import type { SynthPatch } from './project';
4
+ import type { ProjectMidiCcBinding, SynthPatch } from './project';
5
5
  import type { EqBand, PanLaw, PanMode, SendTiming } from './public_types';
6
6
  import type {
7
7
  WasmClipPageRequest,
@@ -290,6 +290,11 @@ export class RealtimeEngine {
290
290
  );
291
291
  }
292
292
 
293
+ /** Bind a 7/14-bit CC, RPN, or NRPN descriptor to a live parameter. */
294
+ bindMidiCcBinding(binding: ProjectMidiCcBinding): void {
295
+ this.native.bindMidiCcBinding(binding);
296
+ }
297
+
293
298
  clearMidiCcBindings(): void {
294
299
  this.native.clearMidiCcBindings();
295
300
  }
@@ -479,6 +484,7 @@ export class RealtimeEngine {
479
484
  this.native.seekPpq(ppq, renderFrame);
480
485
  }
481
486
 
487
+ /** Set a finite tempo in the range (0, 100000] BPM. */
482
488
  setTempo(bpm: number): void {
483
489
  this.native.setTempo(bpm);
484
490
  }
@@ -551,6 +557,7 @@ export class RealtimeEngine {
551
557
  this.native.setLoopFromMarkers(startMarkerId, endMarkerId);
552
558
  }
553
559
 
560
+ /** Set a metronome config; click lengths are limited to one second. */
554
561
  setMetronome(config: EngineMetronomeConfig): void {
555
562
  this.native.setMetronome(config);
556
563
  }
@@ -833,6 +840,7 @@ export class RealtimeEngine {
833
840
  this.native.setCaptureSource(source);
834
841
  }
835
842
 
843
+ /** Positive values delay capture relative to the punch window. */
836
844
  setRecordOffsetSamples(offsetSamples: number): void {
837
845
  this.native.setRecordOffsetSamples(offsetSamples);
838
846
  }
@@ -458,6 +458,8 @@ export interface WasmLufsResult {
458
458
  integratedLufs: number;
459
459
  momentaryLufs: number;
460
460
  shortTermLufs: number;
461
+ maxMomentaryLufs: number;
462
+ maxShortTermLufs: number;
461
463
  loudnessRange: number;
462
464
  }
463
465
 
@@ -467,6 +469,7 @@ export interface WasmMasteringResult {
467
469
  inputLufs: number;
468
470
  outputLufs: number;
469
471
  appliedGainDb: number;
472
+ loudnessTargetLimited?: boolean;
470
473
  latencySamples?: number;
471
474
  }
472
475
 
@@ -479,6 +482,7 @@ export interface WasmMasteringChainResult extends WasmMasteringResult {
479
482
  stages: string[];
480
483
  outputTruePeakDbtp: number;
481
484
  outputLra: number;
485
+ loudnessTargetLimited: boolean;
482
486
  stageGainReductions: WasmStageGainReduction[];
483
487
  }
484
488
 
@@ -492,6 +496,7 @@ export interface WasmMasteringStereoChainResult {
492
496
  stages: string[];
493
497
  outputTruePeakDbtp: number;
494
498
  outputLra: number;
499
+ loudnessTargetLimited: boolean;
495
500
  stageGainReductions: WasmStageGainReduction[];
496
501
  }
497
502
 
@@ -624,8 +629,9 @@ export interface WasmEngineMetronomeConfig {
624
629
  enabled: boolean;
625
630
  beatGain?: number;
626
631
  accentGain?: number;
632
+ /** Explicit click length in samples (0..384000). The engine caps it to one second at its sample rate. */
627
633
  clickSamples?: number;
628
- /** Optional click length in seconds; > 0 overrides the engine 2 ms default. */
634
+ /** Optional click length in seconds (0..1); > 0 overrides the engine 2 ms default. */
629
635
  clickSeconds?: number;
630
636
  }
631
637
 
@@ -943,6 +949,7 @@ export interface WasmRealtimeEngine {
943
949
  minValue: number,
944
950
  maxValue: number,
945
951
  ) => void;
952
+ bindMidiCcBinding: (binding: import('./project_types').ProjectMidiCcBinding) => void;
946
953
  clearMidiCcBindings: () => void;
947
954
  midiCcBindingCount: () => number;
948
955
  setMidiFx: (destinationId: number, configJson: string) => void;
@@ -1100,7 +1107,7 @@ export interface SonareModule {
1100
1107
  detectInversions: boolean,
1101
1108
  chromaMethod: number,
1102
1109
  ) => string[];
1103
- analyze: (samples: Float32Array, sampleRate: number) => WasmAnalysisResult;
1110
+ analyze: (samples: Float32Array, sampleRate: number, options: object) => WasmAnalysisResult;
1104
1111
  _synthEnumTables: () => WasmSynthEnumTables;
1105
1112
  _synthPatchRoundTrip: (patch: unknown) => unknown;
1106
1113
  _analysisResultSchemaPaths: () => string[];
@@ -1182,6 +1189,13 @@ export interface SonareModule {
1182
1189
 
1183
1190
  meteringPeakDb: (samples: Float32Array, sampleRate: number) => number;
1184
1191
  meteringRmsDb: (samples: Float32Array, sampleRate: number) => number;
1192
+ meteringSilenceRatio: (
1193
+ samples: Float32Array,
1194
+ sampleRate: number,
1195
+ thresholdDb: number,
1196
+ frameLength: number,
1197
+ hopLength: number,
1198
+ ) => number;
1185
1199
  meteringCrestFactorDb: (samples: Float32Array, sampleRate: number) => number;
1186
1200
  meteringDcOffset: (samples: Float32Array, sampleRate: number) => number;
1187
1201
  meteringTruePeakDb: (
@@ -1695,6 +1709,7 @@ export interface SonareModule {
1695
1709
  nMfcc: number,
1696
1710
  nFrames: number,
1697
1711
  nMels: number,
1712
+ lifter: number,
1698
1713
  ) => WasmMelPowerResult;
1699
1714
  mfccToAudio: (
1700
1715
  mfcc: Float32Array,
@@ -1708,6 +1723,7 @@ export interface SonareModule {
1708
1723
  fmax: number,
1709
1724
  nIter: number,
1710
1725
  htk: boolean,
1726
+ lifter: number,
1711
1727
  ) => Float32Array;
1712
1728
 
1713
1729
  // Features - Chroma
@@ -1889,12 +1905,14 @@ export interface SonareModule {
1889
1905
  sampleRate: number,
1890
1906
  hopLength: number,
1891
1907
  nChroma: number,
1908
+ binsPerOctave: number,
1892
1909
  ) => WasmChromaResult;
1893
1910
  chromaCqt: (
1894
1911
  samples: Float32Array,
1895
1912
  sampleRate: number,
1896
1913
  hopLength: number,
1897
1914
  nChroma: number,
1915
+ binsPerOctave: number,
1898
1916
  ) => WasmChromaResult;
1899
1917
  bassChroma: (
1900
1918
  samples: Float32Array,
@@ -1902,7 +1920,13 @@ export interface SonareModule {
1902
1920
  hopLength: number,
1903
1921
  nChroma: number,
1904
1922
  ) => WasmChromaResult;
1905
- nnlsChroma: (samples: Float32Array, sampleRate: number) => WasmNnlsChromaResult;
1923
+ nnlsChroma: (
1924
+ samples: Float32Array,
1925
+ sampleRate: number,
1926
+ enableStftBlend: boolean,
1927
+ stftBlendWeight: number,
1928
+ stftBlendNFft: number,
1929
+ ) => WasmNnlsChromaResult;
1906
1930
  cqt: (
1907
1931
  samples: Float32Array,
1908
1932
  sampleRate: number,
@@ -2255,6 +2279,7 @@ export interface WasmMixer {
2255
2279
  removeSend: (stripIndex: number, sendIndex: number) => void;
2256
2280
  meterTap: (stripIndex: number, tap: number) => WasmMixMeterSnapshot;
2257
2281
  stripMeter: (stripIndex: number) => WasmMixMeterSnapshot;
2282
+ busMeter: (busId: string) => WasmMixMeterSnapshot;
2258
2283
  scheduleFaderAutomation: (
2259
2284
  stripIndex: number,
2260
2285
  samplePos: number,
@@ -2286,6 +2311,7 @@ export interface WasmMixer {
2286
2311
  busCount: () => number;
2287
2312
  addVcaGroup: (id: string, gainDb: number, members: string[]) => void;
2288
2313
  setVcaGroupGainDb: (id: string, gainDb: number) => void;
2314
+ setVcaGroupMembers: (id: string, members: string[]) => void;
2289
2315
  removeVcaGroup: (id: string) => void;
2290
2316
  vcaGroupCount: () => number;
2291
2317
  toSceneJson: () => string;
@@ -1,3 +1,4 @@
1
+ import { flattenChainConfig } from './_chain_config';
1
2
  import { getSonareModule } from './module_state';
2
3
  import type {
3
4
  EqBand,
@@ -64,7 +65,12 @@ export class StreamingMasteringChain {
64
65
 
65
66
  constructor(config: StreamingMasteringChainConfig) {
66
67
  const module = getSonareModule();
67
- this.chain = module.createStreamingMasteringChain(config as Record<string, unknown>);
68
+ const { loudnessStaticGainDb, loudnessStaticGainPeakDb, ...chainConfig } = config;
69
+ this.chain = module.createStreamingMasteringChain({
70
+ __flatParams: flattenChainConfig(chainConfig),
71
+ loudnessStaticGainDb,
72
+ loudnessStaticGainPeakDb,
73
+ });
68
74
  }
69
75
 
70
76
  /**
package/src/validation.ts CHANGED
@@ -78,6 +78,12 @@ export function assertSampleRate(fnName: string, sampleRate: number): void {
78
78
  }
79
79
  }
80
80
 
81
+ /** Validate and retain the public Audio.fromBuffer construction contract. */
82
+ export function validateAudioBuffer(samples: Float32Array, sampleRate: number): void {
83
+ assertSamples('Audio.fromBuffer', samples, true);
84
+ assertSampleRate('Audio.fromBuffer', sampleRate);
85
+ }
86
+
81
87
  export function assertNonNegativeInteger(fnName: string, value: number, argName: string): void {
82
88
  if (!Number.isInteger(value) || value < 0) {
83
89
  throw new RangeError(`${fnName}: ${argName} must be a non-negative integer`);
@@ -191,15 +191,29 @@ export interface ResolvedMetronomeConfig {
191
191
  export const DEFAULT_METRONOME_CONFIG: ResolvedMetronomeConfig = {
192
192
  beatGain: 0.35,
193
193
  accentGain: 0.7,
194
- clickSamples: 96,
194
+ clickSamples: 0,
195
195
  };
196
196
 
197
197
  export function resolveMetronomeConfig(config: EngineMetronomeConfig): ResolvedMetronomeConfig {
198
- return {
198
+ const resolved = {
199
199
  beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
200
200
  accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
201
201
  clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples,
202
202
  };
203
+ if (
204
+ !Number.isFinite(resolved.beatGain) ||
205
+ resolved.beatGain < 0 ||
206
+ !Number.isFinite(resolved.accentGain) ||
207
+ resolved.accentGain < 0 ||
208
+ !Number.isInteger(resolved.clickSamples) ||
209
+ resolved.clickSamples < 0 ||
210
+ resolved.clickSamples > 384000 ||
211
+ (config.clickSeconds !== undefined &&
212
+ (!Number.isFinite(config.clickSeconds) || config.clickSeconds < 0 || config.clickSeconds > 1))
213
+ ) {
214
+ throw new RangeError('invalid metronome gains or click length');
215
+ }
216
+ return resolved;
203
217
  }
204
218
 
205
219
  // Out-of-band control messages posted from the main-thread SonareEngine facade
@@ -122,6 +122,8 @@ export enum SonareEngineTelemetryError {
122
122
  CommandBacklogDeferred = 14,
123
123
  ClipPageUnderrun = 15,
124
124
  InsertAutomationOverflow = 16,
125
+ MidiClockOverflow = 17,
126
+ MetronomeOverflow = 18,
125
127
  }
126
128
 
127
129
  export interface SonareMeterRingBuffer {