@libraz/libsonare 1.5.3 → 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.
@@ -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,
@@ -383,8 +424,10 @@ export function meteringStereoCorrelation(
383
424
  }
384
425
 
385
426
  /**
386
- * Side / mid energy ratio, clamped to `[0, 2]`: 0 = pure mono, ~1 = wide
387
- * stereo, 2 = fully decorrelated / out-of-phase.
427
+ * Side / mid energy ratio in `[0, +Infinity)`: 0 = pure mono, ~1 = wide stereo,
428
+ * larger = increasingly decorrelated / out-of-phase. The value is unbounded and
429
+ * returns `Infinity` when the mid channel is silent (a mono-collapsed / fully
430
+ * out-of-phase signal).
388
431
  */
389
432
  export function meteringStereoWidth(request: MeteringStereoRequest): number;
390
433
  export function meteringStereoWidth(
@@ -559,7 +602,7 @@ export function meteringPhaseScopeDecimated(
559
602
  }
560
603
 
561
604
  /**
562
- * Welch-averaged magnitude / power / dB spectrum over the WHOLE signal (split
605
+ * Welch-averaged magnitude / power / dB spectrum over the WHOLE mono signal (split
563
606
  * into Hann-windowed, 50%-overlapping `nFft`-length frames whose power spectra
564
607
  * are averaged). For a true single-frame snapshot, use
565
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
  *
@@ -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,
@@ -436,7 +449,11 @@ export class Project {
436
449
  this.native.setMidiEvents(clipId, events);
437
450
  }
438
451
 
439
- /** 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
+ */
440
457
  importSmf(data: Uint8Array): number {
441
458
  return this.native.importSmf(data);
442
459
  }
@@ -485,7 +502,10 @@ export class Project {
485
502
  this.native.setProgramOnChannel(clipId, group, channel, program, bank);
486
503
  }
487
504
 
488
- /** 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
+ */
489
509
  bakeMidiFx(clipId: number, configJson: string): void {
490
510
  this.native.bakeMidiFx(clipId, configJson);
491
511
  }
@@ -497,9 +517,9 @@ export class Project {
497
517
 
498
518
  /**
499
519
  * Pre-flight check for hanging / unmatched notes in a MIDI clip: reports
500
- * whether every note-on has a matching note-off (FIFO per channel+note).
501
- * Useful before bouncing to catch a stuck note. Throws if `clipId` is unknown
502
- * 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.
503
523
  */
504
524
  validateMidiNotes(clipId: number): ProjectNotePairValidation {
505
525
  return this.native.validateMidiNotes(clipId);
@@ -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]>,
@@ -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
  }
@@ -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 {