@libraz/libsonare 1.6.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.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/dist/analysis.d.ts +139 -11
  3. package/dist/analysis.js +162 -42
  4. package/dist/analysis.js.map +1 -1
  5. package/dist/index.d.ts +211 -39
  6. package/dist/index.js +501 -142
  7. package/dist/index.js.map +1 -1
  8. package/dist/schemas/realtime-voice-changer-preset.schema.json +2 -2
  9. package/dist/sonare-analysis.js +1 -1
  10. package/dist/sonare-analysis.wasm +0 -0
  11. package/dist/sonare.js +1 -1
  12. package/dist/sonare.wasm +0 -0
  13. package/dist/worker.js +56 -29
  14. package/dist/worker.js.map +1 -1
  15. package/dist/worklet.d.ts +67 -18
  16. package/dist/worklet.js +193 -42
  17. package/dist/worklet.js.map +1 -1
  18. package/package.json +3 -3
  19. package/src/_chain_config.ts +6 -1
  20. package/src/analysis_helpers.ts +34 -29
  21. package/src/audio.ts +7 -2
  22. package/src/codes.ts +47 -11
  23. package/src/effects_mastering.ts +5 -0
  24. package/src/effects_transform.ts +202 -22
  25. package/src/feature_music.ts +8 -1
  26. package/src/feature_spectral.ts +64 -2
  27. package/src/feature_spectrogram.ts +44 -3
  28. package/src/index.ts +28 -3
  29. package/src/mastering_core.ts +70 -0
  30. package/src/metering.ts +18 -0
  31. package/src/mixer.ts +3 -3
  32. package/src/project.ts +6 -0
  33. package/src/project_class.ts +143 -16
  34. package/src/project_internal.ts +14 -5
  35. package/src/project_types.ts +50 -9
  36. package/src/public_types_mastering.ts +9 -0
  37. package/src/public_types_mixing.ts +16 -0
  38. package/src/public_types_realtime.ts +23 -5
  39. package/src/public_types_spectral.ts +16 -2
  40. package/src/quick_analysis.ts +21 -3
  41. package/src/realtime_engine.ts +14 -3
  42. package/src/sonare.js.d.ts +87 -0
  43. package/src/stream_analyzer.ts +6 -1
  44. package/src/web_midi.ts +10 -7
  45. package/src/worklet/engine-node.ts +54 -19
  46. package/src/worklet/engine-parameter-facade.ts +30 -1
  47. package/src/worklet/engine-processor.ts +52 -20
  48. package/src/worklet/engine-register.ts +1 -1
  49. package/src/worklet/engine-strips.ts +2 -2
  50. package/src/worklet/engine.ts +10 -0
  51. package/src/worklet/guards.ts +99 -7
  52. package/src/worklet/messages.ts +41 -1
  53. package/src/worklet/protocol.ts +4 -0
@@ -103,6 +103,7 @@ export interface NnlsChromaRequest extends GuardedOptions {
103
103
  enableStftBlend?: boolean;
104
104
  stftBlendWeight?: number;
105
105
  stftBlendNFft?: number;
106
+ hopLength?: number;
106
107
  }
107
108
 
108
109
  function validateMusicSamples(
@@ -156,12 +157,18 @@ export function nnlsChroma(
156
157
  return nnlsChroma(samples.samples, samples.sampleRate, samples);
157
158
  }
158
159
  validateMusicSamples('nnlsChroma', samples, sampleRate, options);
159
- return requireModule().nnlsChroma(
160
+ const hopLength = options.hopLength === undefined ? 512 : options.hopLength;
161
+ assertPositiveInteger('nnlsChroma', hopLength, 'hopLength');
162
+ if (hopLength > 2 ** 31 - 1) {
163
+ throw new RangeError('nnlsChroma: hopLength must fit in a signed 32-bit integer');
164
+ }
165
+ return requireModule().nnlsChromaEx(
160
166
  samples,
161
167
  sampleRate,
162
168
  options.enableStftBlend ?? true,
163
169
  options.stftBlendWeight ?? 0.55,
164
170
  options.stftBlendNFft ?? 4096,
171
+ hopLength,
165
172
  );
166
173
  }
167
174
 
@@ -19,6 +19,41 @@ function requireModule() {
19
19
  return getSonareModule();
20
20
  }
21
21
 
22
+ function resolveEffectFftOptions(
23
+ fnName: string,
24
+ nFft: unknown,
25
+ hopLength: unknown,
26
+ ): { nFft: number; hopLength: number } {
27
+ const resolvedNFft = nFft === undefined ? 2048 : nFft;
28
+ const resolvedHopLength = hopLength === undefined ? 512 : hopLength;
29
+ if (typeof resolvedNFft !== 'number' || !Number.isInteger(resolvedNFft)) {
30
+ throw new TypeError(`${fnName}: nFft must be an integer`);
31
+ }
32
+ if (resolvedNFft < 2 || resolvedNFft > 2 ** 30) {
33
+ throw new RangeError(`${fnName}: nFft must be an even power of two >= 2`);
34
+ }
35
+ if ((resolvedNFft & (resolvedNFft - 1)) !== 0) {
36
+ throw new RangeError(`${fnName}: nFft must be an even power of two >= 2`);
37
+ }
38
+ if (typeof resolvedHopLength !== 'number' || !Number.isInteger(resolvedHopLength)) {
39
+ throw new TypeError(`${fnName}: hopLength must be an integer`);
40
+ }
41
+ if (resolvedHopLength <= 0 || resolvedHopLength > 2 ** 31 - 1) {
42
+ throw new RangeError(`${fnName}: hopLength must be a positive integer`);
43
+ }
44
+ return { nFft: resolvedNFft, hopLength: resolvedHopLength };
45
+ }
46
+
47
+ function resolveHardMask(fnName: string, value: unknown): boolean {
48
+ if (value === undefined) {
49
+ return false;
50
+ }
51
+ if (typeof value !== 'boolean') {
52
+ throw new TypeError(`${fnName}: hardMask must be a boolean`);
53
+ }
54
+ return value;
55
+ }
56
+
22
57
  /** Canonical request form for frame-based spectral feature extraction. */
23
58
  export interface SpectralFrameRequest {
24
59
  samples: Float32Array;
@@ -166,6 +201,9 @@ export interface HpssWithResidualRequest {
166
201
  sampleRate?: number;
167
202
  kernelHarmonic?: number;
168
203
  kernelPercussive?: number;
204
+ nFft?: number;
205
+ hopLength?: number;
206
+ hardMask?: boolean;
169
207
  }
170
208
  export interface LufsInterleavedRequest extends ValidateOptions {
171
209
  samples: Float32Array;
@@ -542,18 +580,42 @@ export function hpssWithResidual(
542
580
  sampleRate?: number,
543
581
  kernelHarmonic?: number,
544
582
  kernelPercussive?: number,
583
+ nFft?: number,
584
+ hopLength?: number,
585
+ hardMask?: boolean,
545
586
  ): WasmHpssWithResidualResult;
546
587
  export function hpssWithResidual(
547
588
  samples: Float32Array | HpssWithResidualRequest,
548
589
  sampleRate = 22050,
549
590
  kernelHarmonic = 31,
550
591
  kernelPercussive = 31,
592
+ nFft?: number,
593
+ hopLength?: number,
594
+ hardMask?: boolean,
551
595
  ): WasmHpssWithResidualResult {
552
596
  if (!(samples instanceof Float32Array)) {
553
597
  const r = samples;
554
- return hpssWithResidual(r.samples, r.sampleRate, r.kernelHarmonic, r.kernelPercussive);
598
+ return hpssWithResidual(
599
+ r.samples,
600
+ r.sampleRate,
601
+ r.kernelHarmonic,
602
+ r.kernelPercussive,
603
+ r.nFft,
604
+ r.hopLength,
605
+ r.hardMask,
606
+ );
555
607
  }
556
- return requireModule().hpssWithResidual(samples, sampleRate, kernelHarmonic, kernelPercussive);
608
+ const fftOptions = resolveEffectFftOptions('hpssWithResidual', nFft, hopLength);
609
+ const resolvedHardMask = resolveHardMask('hpssWithResidual', hardMask);
610
+ return requireModule().hpssWithResidualEx(
611
+ samples,
612
+ sampleRate,
613
+ kernelHarmonic,
614
+ kernelPercussive,
615
+ fftOptions.nFft,
616
+ fftOptions.hopLength,
617
+ resolvedHardMask,
618
+ );
557
619
  }
558
620
 
559
621
  /**
@@ -113,6 +113,8 @@ export interface TrimRequest extends GuardedOptions {
113
113
  samples: Float32Array;
114
114
  sampleRate: number;
115
115
  thresholdDb?: number;
116
+ frameLength?: number;
117
+ hopLength?: number;
116
118
  }
117
119
 
118
120
  function validateSpectrogramSamples(
@@ -183,19 +185,58 @@ export function trim(
183
185
  thresholdDb?: number,
184
186
  options?: GuardedOptions,
185
187
  ): Float32Array;
188
+ export function trim(
189
+ samples: Float32Array,
190
+ sampleRate: number,
191
+ thresholdDb?: number,
192
+ frameLength?: number,
193
+ hopLength?: number,
194
+ options?: GuardedOptions,
195
+ ): Float32Array;
186
196
  export function trim(
187
197
  samples: Float32Array | TrimRequest,
188
198
  sampleRate = 22050,
189
199
  thresholdDb = -60.0,
200
+ frameLengthOrOptions?: number | GuardedOptions,
201
+ hopLength?: number,
190
202
  options: GuardedOptions = {},
191
203
  ): Float32Array {
192
204
  if (!(samples instanceof Float32Array)) {
193
205
  const r = samples;
194
- return trim(r.samples, r.sampleRate, r.thresholdDb, r);
206
+ return trim(r.samples, r.sampleRate, r.thresholdDb, r.frameLength, r.hopLength, r);
207
+ }
208
+ if (frameLengthOrOptions === null) {
209
+ throw new TypeError('trim: frameLength must be an integer or options object');
195
210
  }
196
- validateSpectrogramSamples('trim', samples, sampleRate, options);
211
+ if (
212
+ frameLengthOrOptions !== undefined &&
213
+ typeof frameLengthOrOptions !== 'number' &&
214
+ typeof frameLengthOrOptions !== 'object'
215
+ ) {
216
+ throw new TypeError('trim: frameLength must be an integer or options object');
217
+ }
218
+ const positionalOptions =
219
+ typeof frameLengthOrOptions === 'object' && frameLengthOrOptions !== null
220
+ ? frameLengthOrOptions
221
+ : options;
222
+ const positionalFrameLength =
223
+ typeof frameLengthOrOptions === 'number' ? frameLengthOrOptions : undefined;
224
+ const resolvedFrameLength = positionalFrameLength ?? 2048;
225
+ const resolvedHopLength = hopLength === undefined ? 512 : hopLength;
226
+ validateSpectrogramSamples('trim', samples, sampleRate, positionalOptions);
197
227
  assertFiniteScalar('trim', thresholdDb, 'thresholdDb');
198
- return requireModule().trim(samples, sampleRate, thresholdDb);
228
+ assertPositiveInteger('trim', resolvedFrameLength, 'frameLength');
229
+ assertPositiveInteger('trim', resolvedHopLength, 'hopLength');
230
+ if (resolvedFrameLength > 2 ** 31 - 1 || resolvedHopLength > 2 ** 31 - 1) {
231
+ throw new RangeError('trim: frameLength and hopLength must fit in a signed 32-bit integer');
232
+ }
233
+ return requireModule().trimEx(
234
+ samples,
235
+ sampleRate,
236
+ thresholdDb,
237
+ resolvedFrameLength,
238
+ resolvedHopLength,
239
+ );
199
240
  }
200
241
 
201
242
  // ============================================================================
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,
@@ -146,6 +151,7 @@ export {
146
151
  export type {
147
152
  HarmonicRequest,
148
153
  HpssRequest,
154
+ NormalizeMode,
149
155
  NormalizeRequest,
150
156
  NoteMoveRequest,
151
157
  NoteStretchRequest,
@@ -374,6 +380,7 @@ export type {
374
380
  } from './metering';
375
381
  export {
376
382
  meteringCrestFactorDb,
383
+ meteringCrestFactorDbStereo,
377
384
  meteringDcOffset,
378
385
  meteringDetectClipping,
379
386
  meteringDynamicRange,
@@ -410,9 +417,11 @@ export type {
410
417
  ExternalSeparatedStemImportResult,
411
418
  MidiCcLearnOptions,
412
419
  ProjectAssistSidecar,
420
+ ProjectAssistSidecarInput,
413
421
  ProjectAutomationCurve,
414
422
  ProjectAutomationLaneDesc,
415
423
  ProjectAutomationPoint,
424
+ ProjectAutomationTargetKind,
416
425
  ProjectBounceOptions,
417
426
  ProjectChordSymbol,
418
427
  ProjectClip,
@@ -451,9 +460,13 @@ export type {
451
460
  SynthPatch,
452
461
  } from './project';
453
462
  export {
463
+ AutomationTargetKind,
454
464
  BUILTIN_SYNTH_WAVEFORMS,
455
465
  EXPECTED_PROJECT_ABI_VERSION,
456
466
  MarkerKind,
467
+ PROJECT_AUTOMATION_TARGET_OPAQUE,
468
+ PROJECT_AUTOMATION_TARGET_TRACK_FADER_DB,
469
+ PROJECT_AUTOMATION_TARGET_TRACK_PAN,
457
470
  Project,
458
471
  projectAbiVersion,
459
472
  SYNTH_BODY_TYPES,
@@ -525,6 +538,8 @@ export type {
525
538
  PairAnalysis,
526
539
  PairProcessor,
527
540
  PanLaw,
541
+ PanLawInput,
542
+ PanLawName,
528
543
  PanMode,
529
544
  PitchCorrectOptions,
530
545
  PitchResult,
@@ -557,6 +572,7 @@ export type {
557
572
  TempogramMode,
558
573
  Timbre,
559
574
  TimeSignature,
575
+ VoicedFlags,
560
576
  VoicePresetId,
561
577
  } from './public_types';
562
578
  export {
@@ -636,10 +652,12 @@ export type {
636
652
  EngineTempoSegment,
637
653
  EngineTimeSignatureSegment,
638
654
  EngineTrackLane,
655
+ EngineTrackMonitorMode,
639
656
  EngineTrackSend,
640
657
  EngineTransportState,
641
658
  ExternalMidiEvent,
642
659
  MidiCcBindOptions,
660
+ TrackMonitorMode,
643
661
  } from './realtime_engine';
644
662
  export {
645
663
  ClipPageProvider,
@@ -840,13 +858,20 @@ function resolveVoicePresetOrdinal(preset: VoicePresetId | number): number {
840
858
 
841
859
  /**
842
860
  * Map a voice-character preset ordinal (or canonical id) to its canonical id
843
- * string (e.g. `'bright-idol'`). Invalid ordinals throw.
861
+ * string (e.g. `'bright-idol'`). Unknown numeric ordinals return `null`;
862
+ * unknown preset ids throw.
844
863
  */
845
- export function voiceCharacterPresetId(preset: VoicePresetId | number): string {
864
+ export function voiceCharacterPresetId(preset: VoicePresetId | number): VoicePresetId | null {
846
865
  if (!module) {
847
866
  throw new Error('Module not initialized. Call init() first.');
848
867
  }
849
- return module.voiceCharacterPresetId(resolveVoicePresetOrdinal(preset));
868
+ if (
869
+ typeof preset === 'number' &&
870
+ (!Number.isSafeInteger(preset) || preset < 0 || preset >= VOICE_PRESET_ORDINALS.length)
871
+ ) {
872
+ return null;
873
+ }
874
+ return module.voiceCharacterPresetId(resolveVoicePresetOrdinal(preset)) as VoicePresetId;
850
875
  }
851
876
 
852
877
  /**
@@ -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,
package/src/mixer.ts CHANGED
@@ -12,7 +12,7 @@ import type {
12
12
  MeterTap,
13
13
  MixerProcessResult,
14
14
  MixMeterSnapshot,
15
- PanLaw,
15
+ PanLawInput,
16
16
  PanMode,
17
17
  SendTiming,
18
18
  SurroundPan,
@@ -336,8 +336,8 @@ export class Mixer {
336
336
  this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
337
337
  }
338
338
 
339
- /** Set the strip's pan law. */
340
- setPanLaw(stripIndex: number, panLaw: PanLaw | number): void {
339
+ /** Set the strip's pan law (a {@link PanLawName} alias or raw C ABI ordinal). */
340
+ setPanLaw(stripIndex: number, panLaw: PanLawInput): void {
341
341
  this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
342
342
  }
343
343
 
package/src/project.ts CHANGED
@@ -15,9 +15,11 @@ export type {
15
15
  ExternalSeparatedStemImportResult,
16
16
  MidiCcLearnOptions,
17
17
  ProjectAssistSidecar,
18
+ ProjectAssistSidecarInput,
18
19
  ProjectAutomationCurve,
19
20
  ProjectAutomationLaneDesc,
20
21
  ProjectAutomationPoint,
22
+ ProjectAutomationTargetKind,
21
23
  ProjectBounceOptions,
22
24
  ProjectChordSymbol,
23
25
  ProjectClip,
@@ -65,9 +67,13 @@ export type {
65
67
  SynthPatch,
66
68
  } from './project_types';
67
69
  export {
70
+ AutomationTargetKind,
68
71
  BUILTIN_SYNTH_WAVEFORMS,
69
72
  EXPECTED_PROJECT_ABI_VERSION,
70
73
  MarkerKind,
74
+ PROJECT_AUTOMATION_TARGET_OPAQUE,
75
+ PROJECT_AUTOMATION_TARGET_TRACK_FADER_DB,
76
+ PROJECT_AUTOMATION_TARGET_TRACK_PAN,
71
77
  SYNTH_BODY_TYPES,
72
78
  SYNTH_ENGINE_MODES,
73
79
  SYNTH_FILTER_MODELS,