@libraz/libsonare 1.5.3 → 1.5.4

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,9 +1,9 @@
1
1
  {
2
2
  "name": "@libraz/libsonare",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
- "description": "Audio analysis library for music information retrieval",
6
+ "description": "Audio analysis, mastering, mixing, and MIDI synthesis in WebAssembly",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
9
  "exports": {
@@ -47,6 +47,17 @@
47
47
  "bpm",
48
48
  "key",
49
49
  "tempo",
50
+ "chords",
51
+ "audio-processing",
52
+ "dsp",
53
+ "mastering",
54
+ "mixing",
55
+ "loudness",
56
+ "lufs",
57
+ "midi",
58
+ "synthesizer",
59
+ "soundfont",
60
+ "room-acoustics",
50
61
  "wasm",
51
62
  "webassembly"
52
63
  ],
@@ -59,7 +70,7 @@
59
70
  "bugs": {
60
71
  "url": "https://github.com/libraz/libsonare/issues"
61
72
  },
62
- "homepage": "https://github.com/libraz/libsonare#readme",
73
+ "homepage": "https://libsonare.libraz.net",
63
74
  "engines": {
64
75
  "node": ">=18.0.0"
65
76
  },
package/src/codes.ts CHANGED
@@ -20,6 +20,8 @@ export function panLawCode(panLaw: PanLaw | number): number {
20
20
  return panLaw;
21
21
  }
22
22
  switch (panLaw) {
23
+ case 'const3dB':
24
+ return 0;
23
25
  case 'const4.5dB':
24
26
  return 1;
25
27
  case 'const6dB':
@@ -27,7 +29,7 @@ export function panLawCode(panLaw: PanLaw | number): number {
27
29
  case 'linear0dB':
28
30
  return 3;
29
31
  default:
30
- return 0;
32
+ throw new Error(`Invalid pan law: ${panLaw}`);
31
33
  }
32
34
  }
33
35
 
@@ -36,6 +38,8 @@ export function panModeCode(panMode: PanMode | number): number {
36
38
  return panMode;
37
39
  }
38
40
  switch (panMode) {
41
+ case 'balance':
42
+ return 0;
39
43
  case 'stereoPan':
40
44
  case 'stereo-pan':
41
45
  return 1;
@@ -43,19 +47,38 @@ export function panModeCode(panMode: PanMode | number): number {
43
47
  case 'dual-pan':
44
48
  return 2;
45
49
  default:
46
- return 0;
50
+ throw new Error(`Invalid pan mode: ${panMode}`);
47
51
  }
48
52
  }
49
53
 
50
54
  export function meterTapCode(tap: MeterTap | number): number {
51
- return tap === 'preFader' || tap === 0 ? 0 : 1;
55
+ if (typeof tap === 'number') {
56
+ return tap;
57
+ }
58
+ switch (tap) {
59
+ case 'preFader':
60
+ return 0;
61
+ case 'postFader':
62
+ return 1;
63
+ default:
64
+ throw new Error(`Invalid meter tap: ${tap}`);
65
+ }
52
66
  }
53
67
 
54
68
  export function sendTimingCode(timing: SendTiming | number): number {
55
69
  // Mirrors SonareSendTiming: post-fader is 0 (so an omitted/zeroed value is
56
70
  // post-fader), pre-fader is 1. A raw number is passed through as the C ABI int.
71
+ // An unknown string is rejected rather than silently routed to post-fader,
72
+ // matching the sibling enum-code helpers and Node's sendTimingValue.
57
73
  if (typeof timing === 'number') {
58
74
  return timing;
59
75
  }
60
- return timing === 'preFader' ? 1 : 0;
76
+ switch (timing) {
77
+ case 'postFader':
78
+ return 0;
79
+ case 'preFader':
80
+ return 1;
81
+ default:
82
+ throw new Error(`Invalid send timing: ${timing}`);
83
+ }
61
84
  }
@@ -34,13 +34,13 @@ export interface PercussiveRequest extends ValidateOptions {
34
34
 
35
35
  export interface TimeStretchRequest extends ValidateOptions {
36
36
  samples: Float32Array;
37
- sampleRate: number;
37
+ sampleRate?: number;
38
38
  rate: number;
39
39
  }
40
40
 
41
41
  export interface PitchShiftRequest extends ValidateOptions {
42
42
  samples: Float32Array;
43
- sampleRate: number;
43
+ sampleRate?: number;
44
44
  semitones: number;
45
45
  }
46
46
 
@@ -79,7 +79,7 @@ export interface NoteMoveRequest extends NoteMoveOptions, ValidateOptions {
79
79
 
80
80
  export interface NormalizeRequest extends ValidateOptions {
81
81
  samples: Float32Array;
82
- sampleRate: number;
82
+ sampleRate?: number;
83
83
  targetDb?: number;
84
84
  }
85
85
 
@@ -177,7 +177,7 @@ export function percussive(
177
177
  * Time-stretch audio without changing pitch.
178
178
  *
179
179
  * @param samples - Audio samples (mono, float32)
180
- * @param sampleRate - Sample rate in Hz
180
+ * @param sampleRate - Sample rate in Hz (default: 22050)
181
181
  * @param rate - Time stretch rate (0.5 = double duration, 2.0 = half duration)
182
182
  * @returns Time-stretched audio
183
183
  */
@@ -196,17 +196,17 @@ export function timeStretch(
196
196
  ): Float32Array {
197
197
  const request: TimeStretchRequest =
198
198
  samples instanceof Float32Array
199
- ? { samples, sampleRate: sampleRate as number, rate: rate as number, ...options }
199
+ ? { samples, sampleRate, rate: rate as number, ...options }
200
200
  : samples;
201
201
  assertSamples('timeStretch', request.samples, request.validate !== false);
202
- return requireModule().timeStretch(request.samples, request.sampleRate, request.rate);
202
+ return requireModule().timeStretch(request.samples, request.sampleRate ?? 22050, request.rate);
203
203
  }
204
204
 
205
205
  /**
206
206
  * Pitch-shift audio without changing duration.
207
207
  *
208
208
  * @param samples - Audio samples (mono, float32)
209
- * @param sampleRate - Sample rate in Hz
209
+ * @param sampleRate - Sample rate in Hz (default: 22050)
210
210
  * @param semitones - Pitch shift in semitones (+12 = one octave up, -12 = one octave down)
211
211
  * @returns Pitch-shifted audio
212
212
  */
@@ -225,10 +225,14 @@ export function pitchShift(
225
225
  ): Float32Array {
226
226
  const request: PitchShiftRequest =
227
227
  samples instanceof Float32Array
228
- ? { samples, sampleRate: sampleRate as number, semitones: semitones as number, ...options }
228
+ ? { samples, sampleRate, semitones: semitones as number, ...options }
229
229
  : samples;
230
230
  assertSamples('pitchShift', request.samples, request.validate !== false);
231
- return requireModule().pitchShift(request.samples, request.sampleRate, request.semitones);
231
+ return requireModule().pitchShift(
232
+ request.samples,
233
+ request.sampleRate ?? 22050,
234
+ request.semitones,
235
+ );
232
236
  }
233
237
 
234
238
  /**
@@ -459,7 +463,7 @@ export function noteMove(
459
463
  * Normalize audio to target peak level.
460
464
  *
461
465
  * @param samples - Audio samples (mono, float32)
462
- * @param sampleRate - Sample rate in Hz
466
+ * @param sampleRate - Sample rate in Hz (default: 22050)
463
467
  * @param targetDb - Target peak level in dB (default: 0 dB = full scale)
464
468
  * @returns Normalized audio
465
469
  */
@@ -477,11 +481,13 @@ export function normalize(
477
481
  options: ValidateOptions = {},
478
482
  ): Float32Array {
479
483
  const request: NormalizeRequest =
480
- samples instanceof Float32Array
481
- ? { samples, sampleRate: sampleRate as number, targetDb, ...options }
482
- : samples;
484
+ samples instanceof Float32Array ? { samples, sampleRate, targetDb, ...options } : samples;
483
485
  assertSamples('normalize', request.samples, request.validate !== false);
484
- return requireModule().normalize(request.samples, request.sampleRate, request.targetDb ?? 0.0);
486
+ return requireModule().normalize(
487
+ request.samples,
488
+ request.sampleRate ?? 22050,
489
+ request.targetDb ?? 0.0,
490
+ );
485
491
  }
486
492
 
487
493
  /**
@@ -1,3 +1,4 @@
1
+ import { ErrorCode, SonareError } from './errors';
1
2
  import { getSonareModule } from './module_state';
2
3
  import type {
3
4
  AnalyzeSectionsOptions,
@@ -614,10 +615,15 @@ export function analyzeMelody(
614
615
  const fmax = options.fmax ?? 2093.0;
615
616
  validateFrequencyBounds('analyzeMelody', fmin, fmax);
616
617
  // The melody tracker's fmin is a YIN pitch floor: 0 is meaningless, and the
617
- // flat C ABI (sonare_analyze_melody) rejects it. validateFrequencyBounds only
618
- // guards fmin >= 0, so enforce strict positivity here for parity.
618
+ // flat C ABI (sonare_analyze_melody) rejects it with InvalidParameter.
619
+ // validateFrequencyBounds only guards fmin >= 0, so enforce strict positivity
620
+ // here and report the same branded error the Node/Python surfaces raise.
619
621
  if (fmin <= 0) {
620
- throw new RangeError('analyzeMelody: fmin must be positive');
622
+ throw new SonareError(
623
+ ErrorCode.InvalidParameter,
624
+ 'InvalidParameter',
625
+ 'analyzeMelody: fmin must be positive',
626
+ );
621
627
  }
622
628
  validatePositiveIntegers('analyzeMelody', {
623
629
  frameLength: options.frameLength ?? 2048,
@@ -626,7 +632,11 @@ export function analyzeMelody(
626
632
  const threshold = options.threshold ?? 0.1;
627
633
  assertFiniteScalar('analyzeMelody', threshold, 'threshold');
628
634
  if (threshold <= 0) {
629
- throw new RangeError('analyzeMelody: threshold must be positive');
635
+ throw new SonareError(
636
+ ErrorCode.InvalidParameter,
637
+ 'InvalidParameter',
638
+ 'analyzeMelody: threshold must be positive',
639
+ );
630
640
  }
631
641
  return requireModule().analyzeMelody(
632
642
  samples,
package/src/metering.ts CHANGED
@@ -383,8 +383,10 @@ export function meteringStereoCorrelation(
383
383
  }
384
384
 
385
385
  /**
386
- * Side / mid energy ratio, clamped to `[0, 2]`: 0 = pure mono, ~1 = wide
387
- * stereo, 2 = fully decorrelated / out-of-phase.
386
+ * Side / mid energy ratio in `[0, +Infinity)`: 0 = pure mono, ~1 = wide stereo,
387
+ * larger = increasingly decorrelated / out-of-phase. The value is unbounded and
388
+ * returns `Infinity` when the mid channel is silent (a mono-collapsed / fully
389
+ * out-of-phase signal).
388
390
  */
389
391
  export function meteringStereoWidth(request: MeteringStereoRequest): number;
390
392
  export function meteringStereoWidth(
@@ -427,6 +427,19 @@ export class Project {
427
427
  this.native.redo();
428
428
  }
429
429
 
430
+ /** Clear the undo/redo history without changing the current project state. */
431
+ clearHistory(): void {
432
+ this.native.clearHistory();
433
+ }
434
+
435
+ /** Cap the undo history depth (clamped to >= 1); evicts oldest entries beyond the cap. */
436
+ setMaxUndoDepth(depth: number): void {
437
+ if (!Number.isInteger(depth) || depth < 1) {
438
+ throw new RangeError('Project.setMaxUndoDepth: depth must be an integer >= 1');
439
+ }
440
+ this.native.setMaxUndoDepth(depth);
441
+ }
442
+
430
443
  /** Replace a MIDI clip's entire event list. */
431
444
  setMidiEvents(
432
445
  clipId: number,
@@ -58,6 +58,8 @@ export interface WasmProject {
58
58
  setTrackPan: (trackId: number, pan: number) => void;
59
59
  undo: () => void;
60
60
  redo: () => void;
61
+ clearHistory: () => void;
62
+ setMaxUndoDepth: (depth: number) => void;
61
63
  setMidiEvents: (
62
64
  clipId: number,
63
65
  events: ReadonlyArray<ProjectMidiEvent | readonly [number, number, number]>,
@@ -1,5 +1,77 @@
1
1
  import { getSonareModule } from './module_state';
2
- import type { RealtimeVoiceChangerConfigInput, VoicePresetId } from './public_types';
2
+ import type {
3
+ RealtimeVoiceChangerConfigInput,
4
+ RealtimeVoiceChangerPodConfig,
5
+ VoicePresetId,
6
+ } from './public_types';
7
+
8
+ /**
9
+ * True when `config` is a flat POD (the shape `realtimeVoiceChangerPresetConfig`
10
+ * returns) rather than a preset name or a nested preset. `retuneSemitones` only
11
+ * exists on the flat POD; the nested form spells it `retune.semitones`.
12
+ */
13
+ function isFlatVoiceChangerPod(config: unknown): config is RealtimeVoiceChangerPodConfig {
14
+ return typeof config === 'object' && config !== null && 'retuneSemitones' in config;
15
+ }
16
+
17
+ /**
18
+ * Rewrite the flat 36-field POD into the nested shape the native config parser
19
+ * reads. Passing the flat POD straight to `setConfig` would leave every nested
20
+ * field at its default (only the three root fields are read flat), silently
21
+ * discarding retune/EQ/reverb/etc.
22
+ */
23
+ function flatVoiceChangerPodToNested(pod: RealtimeVoiceChangerPodConfig): Record<string, unknown> {
24
+ return {
25
+ inputGainDb: pod.inputGainDb,
26
+ outputGainDb: pod.outputGainDb,
27
+ wetMix: pod.wetMix,
28
+ retune: { semitones: pod.retuneSemitones, mix: pod.retuneMix, grainSize: pod.retuneGrainSize },
29
+ formant: {
30
+ factor: pod.formantFactor,
31
+ amount: pod.formantAmount,
32
+ body: pod.formantBody,
33
+ brightness: pod.formantBrightness,
34
+ nasal: pod.formantNasal,
35
+ },
36
+ eq: {
37
+ highpassHz: pod.eqHighpassHz,
38
+ bodyDb: pod.eqBodyDb,
39
+ presenceDb: pod.eqPresenceDb,
40
+ airDb: pod.eqAirDb,
41
+ },
42
+ gate: {
43
+ thresholdDb: pod.gateThresholdDb,
44
+ attackMs: pod.gateAttackMs,
45
+ releaseMs: pod.gateReleaseMs,
46
+ rangeDb: pod.gateRangeDb,
47
+ },
48
+ compressor: {
49
+ thresholdDb: pod.compressorThresholdDb,
50
+ ratio: pod.compressorRatio,
51
+ attackMs: pod.compressorAttackMs,
52
+ releaseMs: pod.compressorReleaseMs,
53
+ makeupGainDb: pod.compressorMakeupGainDb,
54
+ },
55
+ deesser: {
56
+ frequencyHz: pod.deesserFrequencyHz,
57
+ thresholdDb: pod.deesserThresholdDb,
58
+ ratio: pod.deesserRatio,
59
+ rangeDb: pod.deesserRangeDb,
60
+ },
61
+ reverb: {
62
+ mix: pod.reverbMix,
63
+ timeMs: pod.reverbTimeMs,
64
+ damping: pod.reverbDamping,
65
+ seed: pod.reverbSeed,
66
+ },
67
+ limiter: {
68
+ ceilingDb: pod.limiterCeilingDb,
69
+ releaseMs: pod.limiterReleaseMs,
70
+ enableIspLimiter: pod.limiterEnableIspLimiter,
71
+ ispCeilingDbtp: pod.limiterIspCeilingDbtp,
72
+ },
73
+ };
74
+ }
3
75
 
4
76
  /**
5
77
  * Zero-copy realtime buffer pair for {@link RealtimeVoiceChanger} mono
@@ -58,8 +130,12 @@ export class RealtimeVoiceChanger {
58
130
  this.changer.reset();
59
131
  }
60
132
 
61
- setConfig(config: RealtimeVoiceChangerConfigInput): void {
62
- this.changer.setConfig(config as Record<string, unknown> | string);
133
+ setConfig(config: RealtimeVoiceChangerConfigInput | RealtimeVoiceChangerPodConfig): void {
134
+ // A flat POD (from realtimeVoiceChangerPresetConfig) is rewritten to the
135
+ // nested shape the native parser reads; a preset name or nested preset is
136
+ // passed through unchanged.
137
+ const resolved = isFlatVoiceChangerPod(config) ? flatVoiceChangerPodToNested(config) : config;
138
+ this.changer.setConfig(resolved as Record<string, unknown> | string);
63
139
  }
64
140
 
65
141
  configJson(): string {
@@ -745,6 +745,10 @@ export interface WasmEngineTransportState {
745
745
  barCount: number;
746
746
  /** Time signature in effect at the current PPQ. */
747
747
  timeSignature: { numerator: number; denominator: number; confidence: number };
748
+ /** One-based beat within the current bar (`barCount` is zero-based). */
749
+ beat: number;
750
+ /** Fractional position within the current beat, in [0, 1). */
751
+ beatFraction: number;
748
752
  }
749
753
 
750
754
  export interface WasmEngineBounceOptions {