@libraz/libsonare 1.7.2 → 1.8.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 (303) hide show
  1. package/NOTICE +178 -0
  2. package/README.md +26 -203
  3. package/dist/_chain_config.d.ts +14 -0
  4. package/dist/_chain_config.d.ts.map +1 -0
  5. package/dist/_effects_common.d.ts +7 -0
  6. package/dist/_effects_common.d.ts.map +1 -0
  7. package/dist/_feature_validation.d.ts +8 -0
  8. package/dist/_feature_validation.d.ts.map +1 -0
  9. package/dist/_fft_options.d.ts +24 -0
  10. package/dist/_fft_options.d.ts.map +1 -0
  11. package/dist/align_take.d.ts +50 -0
  12. package/dist/align_take.d.ts.map +1 -0
  13. package/dist/analysis.d.ts +29 -5840
  14. package/dist/analysis.d.ts.map +1 -0
  15. package/dist/analysis.js +874 -722
  16. package/dist/analysis.js.map +1 -1
  17. package/dist/analysis_helpers.d.ts +9 -0
  18. package/dist/analysis_helpers.d.ts.map +1 -0
  19. package/dist/audio.d.ts +163 -0
  20. package/dist/audio.d.ts.map +1 -0
  21. package/dist/clip_page_streamer.d.ts +133 -0
  22. package/dist/clip_page_streamer.d.ts.map +1 -0
  23. package/dist/codes.d.ts +44 -0
  24. package/dist/codes.d.ts.map +1 -0
  25. package/dist/effects_mastering.d.ts +23 -0
  26. package/dist/effects_mastering.d.ts.map +1 -0
  27. package/dist/effects_note_ops.d.ts +502 -0
  28. package/dist/effects_note_ops.d.ts.map +1 -0
  29. package/dist/effects_percussive.d.ts +185 -0
  30. package/dist/effects_percussive.d.ts.map +1 -0
  31. package/dist/effects_separation.d.ts +65 -0
  32. package/dist/effects_separation.d.ts.map +1 -0
  33. package/dist/effects_spectral.d.ts +28 -0
  34. package/dist/effects_spectral.d.ts.map +1 -0
  35. package/dist/effects_timepitch.d.ts +134 -0
  36. package/dist/effects_timepitch.d.ts.map +1 -0
  37. package/dist/effects_voice_change.d.ts +53 -0
  38. package/dist/effects_voice_change.d.ts.map +1 -0
  39. package/dist/errors.d.ts +51 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/feature_core.d.ts +341 -0
  42. package/dist/feature_core.d.ts.map +1 -0
  43. package/dist/feature_decompose.d.ts +278 -0
  44. package/dist/feature_decompose.d.ts.map +1 -0
  45. package/dist/feature_inverse.d.ts +128 -0
  46. package/dist/feature_inverse.d.ts.map +1 -0
  47. package/dist/feature_loudness.d.ts +66 -0
  48. package/dist/feature_loudness.d.ts.map +1 -0
  49. package/dist/feature_music.d.ts +307 -0
  50. package/dist/feature_music.d.ts.map +1 -0
  51. package/dist/feature_pitch.d.ts +108 -0
  52. package/dist/feature_pitch.d.ts.map +1 -0
  53. package/dist/feature_resample.d.ts +16 -0
  54. package/dist/feature_resample.d.ts.map +1 -0
  55. package/dist/feature_spectral.d.ts +137 -0
  56. package/dist/feature_spectral.d.ts.map +1 -0
  57. package/dist/feature_spectrogram.d.ts +198 -0
  58. package/dist/feature_spectrogram.d.ts.map +1 -0
  59. package/dist/features.d.ts +10 -0
  60. package/dist/features.d.ts.map +1 -0
  61. package/dist/hrtf/default.shrf +0 -0
  62. package/dist/index.d.ts +74 -7431
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +4173 -1621
  65. package/dist/index.js.map +1 -1
  66. package/dist/instrument_types.d.ts +517 -0
  67. package/dist/instrument_types.d.ts.map +1 -0
  68. package/dist/live_audio.d.ts +35 -0
  69. package/dist/live_audio.d.ts.map +1 -0
  70. package/dist/mastering_chain.d.ts +213 -0
  71. package/dist/mastering_chain.d.ts.map +1 -0
  72. package/dist/mastering_core.d.ts +483 -0
  73. package/dist/mastering_core.d.ts.map +1 -0
  74. package/dist/mastering_dynamics.d.ts +80 -0
  75. package/dist/mastering_dynamics.d.ts.map +1 -0
  76. package/dist/metering.d.ts +287 -0
  77. package/dist/metering.d.ts.map +1 -0
  78. package/dist/mixer.d.ts +464 -0
  79. package/dist/mixer.d.ts.map +1 -0
  80. package/dist/mixing_assistant.d.ts +62 -0
  81. package/dist/mixing_assistant.d.ts.map +1 -0
  82. package/dist/mixing_oneshot.d.ts +40 -0
  83. package/dist/mixing_oneshot.d.ts.map +1 -0
  84. package/dist/module_state.d.ts +15 -0
  85. package/dist/module_state.d.ts.map +1 -0
  86. package/dist/opfs_clip_pages.d.ts +28 -0
  87. package/dist/opfs_clip_pages.d.ts.map +1 -0
  88. package/dist/playback_renderer.d.ts +128 -0
  89. package/dist/playback_renderer.d.ts.map +1 -0
  90. package/dist/polyphony.d.ts +202 -0
  91. package/dist/polyphony.d.ts.map +1 -0
  92. package/dist/project.d.ts +8 -0
  93. package/dist/project.d.ts.map +1 -0
  94. package/dist/project_class.d.ts +562 -0
  95. package/dist/project_class.d.ts.map +1 -0
  96. package/dist/project_internal.d.ts +194 -0
  97. package/dist/project_internal.d.ts.map +1 -0
  98. package/dist/project_synth.d.ts +74 -0
  99. package/dist/project_synth.d.ts.map +1 -0
  100. package/dist/project_types.d.ts +654 -0
  101. package/dist/project_types.d.ts.map +1 -0
  102. package/dist/public_types.d.ts +185 -0
  103. package/dist/public_types.d.ts.map +1 -0
  104. package/dist/public_types_acoustic.d.ts +215 -0
  105. package/dist/public_types_acoustic.d.ts.map +1 -0
  106. package/dist/public_types_mastering.d.ts +528 -0
  107. package/dist/public_types_mastering.d.ts.map +1 -0
  108. package/dist/public_types_mixing.d.ts +436 -0
  109. package/dist/public_types_mixing.d.ts.map +1 -0
  110. package/dist/public_types_music.d.ts +619 -0
  111. package/dist/public_types_music.d.ts.map +1 -0
  112. package/dist/public_types_playback.d.ts +165 -0
  113. package/dist/public_types_playback.d.ts.map +1 -0
  114. package/dist/public_types_realtime.d.ts +174 -0
  115. package/dist/public_types_realtime.d.ts.map +1 -0
  116. package/dist/public_types_repair.d.ts +424 -0
  117. package/dist/public_types_repair.d.ts.map +1 -0
  118. package/dist/public_types_spectral.d.ts +697 -0
  119. package/dist/public_types_spectral.d.ts.map +1 -0
  120. package/dist/quick_analysis.d.ts +445 -0
  121. package/dist/quick_analysis.d.ts.map +1 -0
  122. package/dist/realtime_engine.d.ts +893 -0
  123. package/dist/realtime_engine.d.ts.map +1 -0
  124. package/dist/realtime_voice_changer.d.ts +158 -0
  125. package/dist/realtime_voice_changer.d.ts.map +1 -0
  126. package/dist/repair_dereverb.d.ts +187 -0
  127. package/dist/repair_dereverb.d.ts.map +1 -0
  128. package/dist/repair_impulsive.d.ts +186 -0
  129. package/dist/repair_impulsive.d.ts.map +1 -0
  130. package/dist/repair_noise.d.ts +239 -0
  131. package/dist/repair_noise.d.ts.map +1 -0
  132. package/dist/repair_trim.d.ts +123 -0
  133. package/dist/repair_trim.d.ts.map +1 -0
  134. package/dist/sample_bank.d.ts +84 -0
  135. package/dist/sample_bank.d.ts.map +1 -0
  136. package/dist/scale.d.ts +10 -0
  137. package/dist/scale.d.ts.map +1 -0
  138. package/dist/schemas/mixer-scene.schema.json +393 -0
  139. package/dist/schemas/playback-renderer-config.schema.json +392 -0
  140. package/dist/sonare-analysis.d.ts +8 -0
  141. package/dist/sonare-analysis.js +2 -2
  142. package/dist/sonare-analysis.wasm +0 -0
  143. package/dist/sonare.d.ts +3945 -0
  144. package/dist/sonare.js +2 -2
  145. package/dist/sonare.wasm +0 -0
  146. package/dist/stream_analyzer.d.ts +163 -0
  147. package/dist/stream_analyzer.d.ts.map +1 -0
  148. package/dist/stream_types.d.ts +214 -0
  149. package/dist/stream_types.d.ts.map +1 -0
  150. package/dist/streaming_mixing.d.ts +6 -0
  151. package/dist/streaming_mixing.d.ts.map +1 -0
  152. package/dist/streaming_processors.d.ts +340 -0
  153. package/dist/streaming_processors.d.ts.map +1 -0
  154. package/dist/transcribe.d.ts +77 -0
  155. package/dist/transcribe.d.ts.map +1 -0
  156. package/dist/validation.d.ts +140 -0
  157. package/dist/validation.d.ts.map +1 -0
  158. package/dist/web_midi.d.ts +77 -0
  159. package/dist/web_midi.d.ts.map +1 -0
  160. package/dist/worker.d.ts +5 -48
  161. package/dist/worker.d.ts.map +1 -0
  162. package/dist/worker.js +94 -41
  163. package/dist/worker.js.map +1 -1
  164. package/dist/worker_client.d.ts +96 -0
  165. package/dist/worker_client.d.ts.map +1 -0
  166. package/dist/worker_protocol.d.ts +43 -0
  167. package/dist/worker_protocol.d.ts.map +1 -0
  168. package/dist/worklet/audio_types.d.ts +21 -0
  169. package/dist/worklet/audio_types.d.ts.map +1 -0
  170. package/dist/worklet/engine-automation.d.ts +29 -0
  171. package/dist/worklet/engine-automation.d.ts.map +1 -0
  172. package/dist/worklet/engine-capture-facade.d.ts +35 -0
  173. package/dist/worklet/engine-capture-facade.d.ts.map +1 -0
  174. package/dist/worklet/engine-clips.d.ts +23 -0
  175. package/dist/worklet/engine-clips.d.ts.map +1 -0
  176. package/dist/worklet/engine-markers.d.ts +40 -0
  177. package/dist/worklet/engine-markers.d.ts.map +1 -0
  178. package/dist/worklet/engine-mixer-facade.d.ts +164 -0
  179. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
  180. package/dist/worklet/engine-node.d.ts +83 -0
  181. package/dist/worklet/engine-node.d.ts.map +1 -0
  182. package/dist/worklet/engine-offline.d.ts +81 -0
  183. package/dist/worklet/engine-offline.d.ts.map +1 -0
  184. package/dist/worklet/engine-options.d.ts +12 -0
  185. package/dist/worklet/engine-options.d.ts.map +1 -0
  186. package/dist/worklet/engine-parameter-facade.d.ts +106 -0
  187. package/dist/worklet/engine-parameter-facade.d.ts.map +1 -0
  188. package/dist/worklet/engine-processor.d.ts +72 -0
  189. package/dist/worklet/engine-processor.d.ts.map +1 -0
  190. package/dist/worklet/engine-register.d.ts +2 -0
  191. package/dist/worklet/engine-register.d.ts.map +1 -0
  192. package/dist/worklet/engine-strips.d.ts +75 -0
  193. package/dist/worklet/engine-strips.d.ts.map +1 -0
  194. package/dist/worklet/engine-sync.d.ts +39 -0
  195. package/dist/worklet/engine-sync.d.ts.map +1 -0
  196. package/dist/worklet/engine-tempo-facade.d.ts +48 -0
  197. package/dist/worklet/engine-tempo-facade.d.ts.map +1 -0
  198. package/dist/worklet/engine.d.ts +418 -0
  199. package/dist/worklet/engine.d.ts.map +1 -0
  200. package/dist/worklet/guards.d.ts +53 -0
  201. package/dist/worklet/guards.d.ts.map +1 -0
  202. package/dist/worklet/messages.d.ts +710 -0
  203. package/dist/worklet/messages.d.ts.map +1 -0
  204. package/dist/worklet/mixer-processor.d.ts +46 -0
  205. package/dist/worklet/mixer-processor.d.ts.map +1 -0
  206. package/dist/worklet/playback-processor.d.ts +62 -0
  207. package/dist/worklet/playback-processor.d.ts.map +1 -0
  208. package/dist/worklet/protocol.d.ts +331 -0
  209. package/dist/worklet/protocol.d.ts.map +1 -0
  210. package/dist/worklet/voice-changer-processor.d.ts +41 -0
  211. package/dist/worklet/voice-changer-processor.d.ts.map +1 -0
  212. package/dist/worklet.d.ts +16 -2515
  213. package/dist/worklet.d.ts.map +1 -0
  214. package/dist/worklet.js +3200 -541
  215. package/dist/worklet.js.map +1 -1
  216. package/package.json +23 -12
  217. package/src/_effects_common.ts +47 -0
  218. package/src/_feature_validation.ts +34 -0
  219. package/src/_fft_options.ts +39 -0
  220. package/src/align_take.ts +64 -0
  221. package/src/analysis.ts +56 -3
  222. package/src/analysis_helpers.ts +7 -0
  223. package/src/audio.ts +106 -3
  224. package/src/codes.ts +39 -2
  225. package/src/effects_mastering.ts +101 -22
  226. package/src/effects_note_ops.ts +683 -0
  227. package/src/effects_percussive.ts +217 -0
  228. package/src/effects_separation.ts +150 -0
  229. package/src/effects_spectral.ts +60 -0
  230. package/src/effects_timepitch.ts +388 -0
  231. package/src/errors.ts +23 -1
  232. package/src/feature_core.ts +127 -2
  233. package/src/feature_decompose.ts +633 -0
  234. package/src/feature_inverse.ts +454 -0
  235. package/src/feature_loudness.ts +125 -0
  236. package/src/feature_music.ts +107 -14
  237. package/src/feature_pitch.ts +96 -1
  238. package/src/feature_spectral.ts +16 -611
  239. package/src/feature_spectrogram.ts +63 -450
  240. package/src/features.ts +36 -22
  241. package/src/index.ts +288 -30
  242. package/src/instrument_types.ts +645 -0
  243. package/src/live_audio.ts +27 -1
  244. package/src/mastering_chain.ts +184 -0
  245. package/src/mastering_core.ts +441 -32
  246. package/src/mastering_dynamics.ts +22 -11
  247. package/src/metering.ts +67 -24
  248. package/src/mixer.ts +251 -22
  249. package/src/mixing_assistant.ts +138 -0
  250. package/src/mixing_oneshot.ts +10 -5
  251. package/src/module_state.ts +24 -2
  252. package/src/playback_renderer.ts +252 -0
  253. package/src/polyphony.ts +279 -0
  254. package/src/project.ts +61 -24
  255. package/src/project_class.ts +450 -27
  256. package/src/project_internal.ts +149 -42
  257. package/src/project_synth.ts +67 -1
  258. package/src/project_types.ts +271 -271
  259. package/src/public_types.ts +122 -3
  260. package/src/public_types_acoustic.ts +112 -3
  261. package/src/public_types_mastering.ts +275 -73
  262. package/src/public_types_mixing.ts +363 -1
  263. package/src/public_types_music.ts +312 -2
  264. package/src/public_types_playback.ts +196 -0
  265. package/src/public_types_realtime.ts +39 -7
  266. package/src/public_types_repair.ts +446 -0
  267. package/src/public_types_spectral.ts +491 -5
  268. package/src/quick_analysis.ts +203 -26
  269. package/src/realtime_engine.ts +773 -34
  270. package/src/realtime_voice_changer.ts +55 -1
  271. package/src/repair_dereverb.ts +299 -0
  272. package/src/repair_impulsive.ts +395 -0
  273. package/src/repair_noise.ts +425 -0
  274. package/src/repair_trim.ts +226 -0
  275. package/src/sample_bank.ts +113 -0
  276. package/src/sonare.js.d.ts +1158 -30
  277. package/src/stream_analyzer.ts +36 -4
  278. package/src/stream_types.ts +37 -0
  279. package/src/streaming_mixing.ts +1 -1
  280. package/src/streaming_processors.ts +202 -10
  281. package/src/transcribe.ts +89 -0
  282. package/src/validation.ts +285 -11
  283. package/src/web_midi.ts +1 -6
  284. package/src/worker.ts +18 -2
  285. package/src/worklet/audio_types.ts +37 -0
  286. package/src/worklet/engine-mixer-facade.ts +800 -32
  287. package/src/worklet/engine-node.ts +99 -29
  288. package/src/worklet/engine-offline.ts +14 -8
  289. package/src/worklet/engine-parameter-facade.ts +21 -0
  290. package/src/worklet/engine-processor.ts +332 -93
  291. package/src/worklet/engine-register.ts +32 -18
  292. package/src/worklet/engine-strips.ts +275 -9
  293. package/src/worklet/engine-sync.ts +20 -7
  294. package/src/worklet/engine.ts +394 -48
  295. package/src/worklet/guards.ts +195 -44
  296. package/src/worklet/messages.ts +229 -2
  297. package/src/worklet/mixer-processor.ts +117 -48
  298. package/src/worklet/playback-processor.ts +300 -0
  299. package/src/worklet/protocol.ts +82 -11
  300. package/src/worklet/voice-changer-processor.ts +17 -11
  301. package/src/worklet.ts +17 -0
  302. package/src/effects_transform.ts +0 -718
  303. package/src/mastering_repair.ts +0 -273
package/src/metering.ts CHANGED
@@ -1,7 +1,18 @@
1
1
  import { ErrorCode, SonareError } from './errors';
2
2
  import { getSonareModule } from './module_state';
3
3
  import type { ValidateOptions } from './validation';
4
- import { assertSamples } from './validation';
4
+ import {
5
+ assertInterleavedSamples,
6
+ assertNonNegativeInteger,
7
+ assertPositiveInteger,
8
+ assertSamples,
9
+ assertSamplesInWindow,
10
+ } from './validation';
11
+
12
+ // The FFT size the library falls back to when `nFft` is 0 or omitted. Mirrored
13
+ // here so the windowed pre-scan covers exactly the span the call will read; a
14
+ // test pins it against the `nFft` the library reports back for a 0 request.
15
+ const DEFAULT_SPECTRUM_N_FFT = 2048;
5
16
 
6
17
  /**
7
18
  * Validates a true-peak oversample factor: `0` (meaning "use the default 4") or
@@ -267,9 +278,7 @@ export function meteringDetectClipping(
267
278
  const request = samples instanceof Float32Array ? { samples, sampleRate, ...options } : samples;
268
279
  assertSamples('meteringDetectClipping', request.samples, request.validate !== false);
269
280
  const minRegionSamples = request.minRegionSamples ?? 1;
270
- if (!Number.isInteger(minRegionSamples) || minRegionSamples < 0) {
271
- throw new RangeError('meteringDetectClipping: minRegionSamples must be a non-negative integer');
272
- }
281
+ assertNonNegativeInteger('meteringDetectClipping', minRegionSamples, 'minRegionSamples');
273
282
  return requireModule().meteringDetectClipping(
274
283
  request.samples,
275
284
  request.sampleRate ?? 22050,
@@ -447,10 +456,14 @@ export function meteringStereoCorrelation(
447
456
  }
448
457
 
449
458
  /**
450
- * Side / mid energy ratio in `[0, +Infinity)`: 0 = pure mono, ~1 = wide stereo,
451
- * larger = increasingly decorrelated / out-of-phase. The value is unbounded and
452
- * returns `Infinity` when the mid channel is silent (a mono-collapsed / fully
453
- * out-of-phase signal).
459
+ * Stereo width as `sqrt(side_energy / mid_energy)` in `[0, +Infinity)`: the
460
+ * side/mid RMS *amplitude* ratio, not the energy ratio. 0 = pure mono, ~1 =
461
+ * wide stereo, larger = increasingly decorrelated / out-of-phase. The value is
462
+ * unbounded and returns `Infinity` when the mid channel is silent (a
463
+ * mono-collapsed / fully out-of-phase signal).
464
+ *
465
+ * Convert to dB with `20 * Math.log10(value)`; `10 * Math.log10` would
466
+ * understate the true energy ratio by half.
454
467
  */
455
468
  export function meteringStereoWidth(request: MeteringStereoRequest): number;
456
469
  export function meteringStereoWidth(
@@ -651,6 +664,12 @@ export function meteringSpectrum(
651
664
  * `nFft`-length FFT), for spectrum-analyzer "moment" snapshots that must not be
652
665
  * time-averaged like {@link meteringSpectrum}. The analysis frame spans
653
666
  * `[frameOffset, frameOffset + nFft)`; samples past the end are zero-padded.
667
+ *
668
+ * The frame is also the only span validated: a non-finite sample inside it is
669
+ * rejected, while one outside it neither reaches the FFT nor refuses the call.
670
+ * The emptiness and `sampleRate` checks still cover the whole buffer. Cost per
671
+ * call is therefore set by `nFft` rather than by the length of the buffer, so an
672
+ * analyzer may poll a long recording frame by frame.
654
673
  */
655
674
  export function meteringSpectrumFrame(request: MeteringSpectrumFrameRequest): SpectrumReport;
656
675
  export function meteringSpectrumFrame(
@@ -667,7 +686,14 @@ export function meteringSpectrumFrame(
667
686
  ): SpectrumReport {
668
687
  const request =
669
688
  samples instanceof Float32Array ? { samples, sampleRate, frameOffset, ...options } : samples;
670
- assertSamples('meteringSpectrumFrame', request.samples, request.validate !== false);
689
+ const nFft = request.nFft ?? 0;
690
+ assertSamplesInWindow(
691
+ 'meteringSpectrumFrame',
692
+ request.samples,
693
+ request.validate !== false,
694
+ request.frameOffset ?? 0,
695
+ nFft > 0 ? nFft : DEFAULT_SPECTRUM_N_FFT,
696
+ );
671
697
  return requireModule().meteringSpectrumFrame(
672
698
  request.samples,
673
699
  request.sampleRate ?? 22050,
@@ -676,7 +702,14 @@ export function meteringSpectrumFrame(
676
702
  );
677
703
  }
678
704
 
679
- /** Compute per-channel min/max waveform buckets from interleaved audio. */
705
+ /**
706
+ * Compute per-channel min/max waveform buckets from interleaved audio.
707
+ *
708
+ * A non-finite sample is rejected rather than skipped, and `{ validate: false }`
709
+ * does not change that — it only skips the JS pre-scan that names the offending
710
+ * index. A bucket whose samples are not finite has no min/max to report, and the
711
+ * `0`/`0` it would otherwise carry is what a waveform display draws as silence.
712
+ */
680
713
  export function waveformPeaks(request: WaveformPeaksRequest): WaveformPeaksReport;
681
714
  export function waveformPeaks(
682
715
  samples: Float32Array,
@@ -692,18 +725,23 @@ export function waveformPeaks(
692
725
  samples instanceof Float32Array
693
726
  ? { samples, channels: channels as number, ...options }
694
727
  : samples;
695
- assertSamples('waveformPeaks', request.samples, request.validate !== false);
696
- if (request.channels <= 0 || request.samples.length % request.channels !== 0) {
697
- throw new RangeError('waveformPeaks: samples length must be a multiple of channels');
698
- }
728
+ assertInterleavedSamples(
729
+ 'waveformPeaks',
730
+ request.samples,
731
+ request.channels,
732
+ request.validate !== false,
733
+ );
699
734
  const samplesPerBucket = request.samplesPerBucket ?? 512;
700
- if (samplesPerBucket <= 0) {
701
- throw new RangeError('waveformPeaks: samplesPerBucket must be > 0');
702
- }
735
+ assertPositiveInteger('waveformPeaks', samplesPerBucket, 'samplesPerBucket');
703
736
  return requireModule().waveformPeaks(request.samples, request.channels, samplesPerBucket);
704
737
  }
705
738
 
706
- /** Compute waveform peak buckets for several zoom levels. */
739
+ /**
740
+ * Compute waveform peak buckets for several zoom levels.
741
+ *
742
+ * Shares {@link waveformPeaks}' bucket kernel, so a non-finite sample is
743
+ * rejected here on the same rule.
744
+ */
707
745
  export function waveformPeakPyramid(request: WaveformPeakPyramidRequest): WaveformPeaksReport[];
708
746
  export function waveformPeakPyramid(
709
747
  samples: Float32Array,
@@ -719,13 +757,18 @@ export function waveformPeakPyramid(
719
757
  samples instanceof Float32Array
720
758
  ? { samples, channels: channels as number, ...options }
721
759
  : samples;
722
- assertSamples('waveformPeakPyramid', request.samples, request.validate !== false);
723
- if (request.channels <= 0 || request.samples.length % request.channels !== 0) {
724
- throw new RangeError('waveformPeakPyramid: samples length must be a multiple of channels');
725
- }
760
+ assertInterleavedSamples(
761
+ 'waveformPeakPyramid',
762
+ request.samples,
763
+ request.channels,
764
+ request.validate !== false,
765
+ );
726
766
  const levels = request.samplesPerBucketLevels ?? [512, 1024, 2048, 4096];
727
- if (levels.length === 0 || levels.some((level) => level <= 0)) {
728
- throw new RangeError('waveformPeakPyramid: samplesPerBucketLevels must be non-empty and > 0');
767
+ if (levels.length === 0) {
768
+ throw new RangeError('waveformPeakPyramid: samplesPerBucketLevels must not be empty');
729
769
  }
770
+ levels.forEach((level, index) => {
771
+ assertPositiveInteger('waveformPeakPyramid', level, `samplesPerBucketLevels[${index}]`);
772
+ });
730
773
  return requireModule().waveformPeakPyramid(request.samples, request.channels, levels);
731
774
  }
package/src/mixer.ts CHANGED
@@ -18,6 +18,49 @@ import type {
18
18
  SurroundPan,
19
19
  } from './public_types';
20
20
 
21
+ /**
22
+ * One master-output meter reading. All dB fields are finite and floored at
23
+ * -120; `truePeakDb*` is an inter-sample peak from the ITU-R BS.1770-4
24
+ * polyphase reconstruction at 4x, not a sample peak. That reconstruction is a
25
+ * streaming measurement: its centered stencil needs a few future samples a
26
+ * realtime path does not have, so each block's last samples read marginally low
27
+ * (about 0.1 dB across 64..8192-sample blocks on a near-Nyquist tone, always
28
+ * under-reading). Use `meteringTruePeakDb` over the whole signal for an exact
29
+ * dBTP number.
30
+ */
31
+ export interface MixerMeterSnapshot {
32
+ peakDbL: number;
33
+ peakDbR: number;
34
+ rmsDbL: number;
35
+ rmsDbR: number;
36
+ correlation: number;
37
+ truePeakDbL: number;
38
+ truePeakDbR: number;
39
+ }
40
+
41
+ /**
42
+ * Meter configuration for a strip added with {@link Mixer.addStrip}.
43
+ *
44
+ * The field names and defaults are the scene document's `strips[].metering`
45
+ * object, so a strip added imperatively and one declared in a scene describe the
46
+ * same thing. A strip's meters size their buffers when the strip is built, so
47
+ * this is the only place the configuration can be chosen — there is no setter.
48
+ * A full meter costs about 646 KB at 48 kHz and a strip carries two of them.
49
+ */
50
+ export interface StripMeteringOptions {
51
+ /** Both meters; `false` drops them (about 145 KB for the strip instead of 1.4 MB). Default `true`. */
52
+ enabled?: boolean;
53
+ /** LUFS measurement; `false` takes one meter to about 83 KB. Default `true`. */
54
+ lufs?: boolean;
55
+ /** Inter-sample (true) peak measurement. Default `true`. */
56
+ truePeak?: boolean;
57
+ /**
58
+ * Requested true-peak oversampling factor in `[0, 16]`; the meter resolves it
59
+ * to the nearest of 2x / 4x / 8x. `0` selects the library default (4x).
60
+ */
61
+ truePeakOversample?: number;
62
+ }
63
+
21
64
  export interface MixerRealtimeBuffer {
22
65
  leftInputs: Float32Array[];
23
66
  rightInputs: Float32Array[];
@@ -45,7 +88,7 @@ export interface MixerRealtimeBuffer {
45
88
  *
46
89
  * @example
47
90
  * ```typescript
48
- * const mixer = Mixer.fromSceneJson(mixingScenePresetJson('basicStereo'), 48000, 512);
91
+ * const mixer = Mixer.fromSceneJson(mixingScenePresetJson('vocalReverbSend'), 48000, 512);
49
92
  * try {
50
93
  * const out = mixer.processStereo([stripL], [stripR]);
51
94
  * } finally {
@@ -55,6 +98,7 @@ export interface MixerRealtimeBuffer {
55
98
  */
56
99
  export class Mixer {
57
100
  private mixer: import('./sonare.js').WasmMixer;
101
+ private released = false;
58
102
  private readonly blockSize: number;
59
103
 
60
104
  private constructor(mixer: import('./sonare.js').WasmMixer, blockSize: number) {
@@ -65,6 +109,13 @@ export class Mixer {
65
109
  /**
66
110
  * Build a mixer from a scene JSON string.
67
111
  *
112
+ * A strip's meters are sized when the strip is built, so this is where their
113
+ * configuration is chosen: an optional `metering` object on the strip
114
+ * (`enabled` / `lufs` / `truePeak` / `truePeakOversample`) selects it, and
115
+ * leaving it out keeps the full default (LUFS + true peak at 4x, about 1.4 MB
116
+ * per strip at 48 kHz). `{"enabled": false}` drops both meters for a strip
117
+ * whose snapshots are never read.
118
+ *
68
119
  * @param json - Scene JSON (strips, buses, sends, connections, inserts)
69
120
  * @param sampleRate - Sample rate in Hz (default: 48000)
70
121
  * @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
@@ -137,54 +188,128 @@ export class Mixer {
137
188
  * after {@link delete}.
138
189
  */
139
190
  createRealtimeBuffer(): MixerRealtimeBuffer {
140
- const stripCount = this.stripCount();
141
- let leftInputs: Float32Array[] = [];
142
- let rightInputs: Float32Array[] = [];
191
+ const leftInputs: Float32Array[] = [];
192
+ const rightInputs: Float32Array[] = [];
143
193
  let outLeft = this.mixer.outputLeftView();
144
194
  let outRight = this.mixer.outputRightView();
195
+ let acquiredStripCount = -1;
196
+
197
+ // Every view shares one heap buffer, so a growth detaches the output views too.
198
+ const viewsDetached = (): boolean => outLeft.byteLength === 0 || outRight.byteLength === 0;
199
+
145
200
  const acquire = (): void => {
146
- leftInputs = [];
147
- rightInputs = [];
148
- for (let index = 0; index < stripCount; index++) {
201
+ const stripCount = this.stripCount();
202
+ const detached = viewsDetached();
203
+ if (detached) {
204
+ // A heap growth detached every view: reacquire all planes.
205
+ leftInputs.length = 0;
206
+ rightInputs.length = 0;
207
+ } else {
208
+ // Topology growth keeps existing planes in place: append views for new strips only.
209
+ leftInputs.length = Math.min(leftInputs.length, stripCount);
210
+ rightInputs.length = Math.min(rightInputs.length, stripCount);
211
+ }
212
+ for (let index = leftInputs.length; index < stripCount; index++) {
149
213
  leftInputs.push(this.mixer.inputLeftView(index));
214
+ }
215
+ for (let index = rightInputs.length; index < stripCount; index++) {
150
216
  rightInputs.push(this.mixer.inputRightView(index));
151
217
  }
152
- outLeft = this.mixer.outputLeftView();
153
- outRight = this.mixer.outputRightView();
218
+ if (detached) {
219
+ outLeft = this.mixer.outputLeftView();
220
+ outRight = this.mixer.outputRightView();
221
+ }
222
+ acquiredStripCount = stripCount;
154
223
  };
155
224
  acquire();
225
+
156
226
  // The cached heap views can detach if WASM linear memory grows (the embind
157
- // module is built ALLOW_MEMORY_GROWTH). Re-acquire them if detached
158
- // (byteLength === 0) before use, mirroring the worklet RT path.
159
- const reacquireIfDetached = (): void => {
160
- if (outLeft.byteLength === 0 || (leftInputs[0]?.byteLength ?? 1) === 0) {
227
+ // module is built ALLOW_MEMORY_GROWTH). Also refresh the view list when a
228
+ // caller adds a strip and recompiles the graph after this buffer was made.
229
+ const acquireIfNeeded = (): void => {
230
+ if (acquiredStripCount !== this.stripCount() || viewsDetached()) {
161
231
  acquire();
162
232
  }
163
233
  };
164
234
  return {
165
235
  get leftInputs(): Float32Array[] {
166
- reacquireIfDetached();
236
+ acquireIfNeeded();
167
237
  return leftInputs;
168
238
  },
169
239
  get rightInputs(): Float32Array[] {
170
- reacquireIfDetached();
240
+ acquireIfNeeded();
171
241
  return rightInputs;
172
242
  },
173
243
  get outLeft(): Float32Array {
174
- reacquireIfDetached();
244
+ acquireIfNeeded();
175
245
  return outLeft;
176
246
  },
177
247
  get outRight(): Float32Array {
178
- reacquireIfDetached();
248
+ acquireIfNeeded();
179
249
  return outRight;
180
250
  },
181
- process: (numSamples = outLeft.length) => {
182
- reacquireIfDetached();
183
- this.mixer.processPreparedStereo(numSamples);
251
+ process: (numSamples?: number) => {
252
+ acquireIfNeeded();
253
+ // Resolve the default only after reacquiring: a detached view reports length 0.
254
+ this.mixer.processPreparedStereo(numSamples ?? outLeft.length);
184
255
  },
185
256
  };
186
257
  }
187
258
 
259
+ /**
260
+ * Turn the master-output meter on or off.
261
+ *
262
+ * While on, every {@link MixerRealtimeBuffer.process} call meters the stereo
263
+ * master it just produced, so a caller reads {@link meterSnapshot} instead of
264
+ * copying the output and measuring it again. `truePeakDb*` is an inter-sample
265
+ * peak taken after oversampling (ITU-R BS.1770-4 Annex 2 requires at least
266
+ * 4x), which is a different and higher quantity than the sample peak.
267
+ *
268
+ * Enabling resets the meter, so a reading never mixes in audio from a period
269
+ * when metering was off.
270
+ *
271
+ * @param enabled - Whether to meter the master output.
272
+ * @param truePeakOversample - 0 (= 4x) or a power of two in [1, 16].
273
+ */
274
+ configureMeter(enabled: boolean, truePeakOversample = 4): void {
275
+ this.mixer.configureMeter(enabled, truePeakOversample);
276
+ }
277
+
278
+ /**
279
+ * Latest master-output meter reading, describing the most recently metered
280
+ * block. All dB fields are finite and floored at -120.
281
+ *
282
+ * @throws When the meter has never been enabled.
283
+ */
284
+ meterSnapshot(): MixerMeterSnapshot {
285
+ return this.mixer.meterSnapshot();
286
+ }
287
+
288
+ /**
289
+ * Latch the latest meter reading into the mixer's internal scratch so
290
+ * {@link meterScratchValue} can read it back one number at a time.
291
+ *
292
+ * This is the allocation-free form of {@link meterSnapshot}, for an audio
293
+ * render callback that must not create a JS object per interval. It returns
294
+ * `false` instead of throwing when the meter has never been enabled.
295
+ *
296
+ * @returns Whether a reading was latched.
297
+ */
298
+ latchMeterSnapshot(): boolean {
299
+ return this.mixer.latchMeterSnapshot();
300
+ }
301
+
302
+ /**
303
+ * Read one field of the snapshot latched by {@link latchMeterSnapshot}.
304
+ *
305
+ * @param field - `0` peakDbL, `1` peakDbR, `2` rmsDbL, `3` rmsDbR,
306
+ * `4` correlation, `5` truePeakDbL, `6` truePeakDbR. Any other index
307
+ * reads `0`.
308
+ */
309
+ meterScratchValue(field: number): number {
310
+ return this.mixer.meterScratchValue(field);
311
+ }
312
+
188
313
  /** Number of strips in the mixer (e.g. strips loaded from the scene). */
189
314
  stripCount(): number {
190
315
  return this.mixer.stripCount();
@@ -232,6 +357,18 @@ export class Mixer {
232
357
  return index < 0 ? null : index;
233
358
  }
234
359
 
360
+ /**
361
+ * Add a channel strip to the mixer topology. `metering` configures the strip's
362
+ * pre/post taps; omitting it keeps the full default (LUFS + true peak at 4x,
363
+ * about 1.4 MB per strip at 48 kHz). Marks the routing graph dirty; call
364
+ * {@link compile} (or {@link processStereo}) to rebuild.
365
+ *
366
+ * @throws If the id is already taken, or `truePeakOversample` is outside `[0, 16]`
367
+ */
368
+ addStrip(id: string, metering: StripMeteringOptions = {}): void {
369
+ this.mixer.addStrip(id, metering);
370
+ }
371
+
235
372
  /**
236
373
  * Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
237
374
  * `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
@@ -310,6 +447,31 @@ export class Mixer {
310
447
  this.mixer.setWidth(stripIndex, width);
311
448
  }
312
449
 
450
+ /**
451
+ * Snap the strip's input-trim, fader, pan and width smoothers to the values
452
+ * already set on it, so the next processed block opens at those values
453
+ * instead of gliding to them over the smoothing window (~5 ms).
454
+ *
455
+ * Call it after configuring a strip and before rendering a finite buffer: a
456
+ * strip is smoothed for a live fader, and an offline render that does not
457
+ * settle carries that glide as a level and image sweep across the head of
458
+ * its output. Unlike a reset it clears nothing — automation, meters and
459
+ * insert state are untouched.
460
+ *
461
+ * @param stripIndex - Strip index in `[0, stripCount())`
462
+ *
463
+ * @example
464
+ * ```typescript
465
+ * mixer.setFaderDb(0, -3);
466
+ * mixer.setPan(0, 0.3);
467
+ * mixer.settle(0);
468
+ * const { left, right } = mixer.processStereo([dryLeft], [dryRight]);
469
+ * ```
470
+ */
471
+ settle(stripIndex: number): void {
472
+ this.mixer.settle(stripIndex);
473
+ }
474
+
313
475
  /** Set the strip's mute state. */
314
476
  setMuted(stripIndex: number, muted: boolean): void {
315
477
  this.mixer.setMuted(stripIndex, muted);
@@ -361,7 +523,10 @@ export class Mixer {
361
523
 
362
524
  /**
363
525
  * Set the strip's surround pan position, used when it feeds a >2-channel bus.
364
- * Stored on the scene; inert until the surround DSP path applies it.
526
+ *
527
+ * Applied when the engine's track mixer renders this strip's lane into a
528
+ * destination with more than two channels. This stereo-only mixer's own
529
+ * block entry points ignore it.
365
530
  */
366
531
  setSurroundPan(stripIndex: number, pan: SurroundPan): void {
367
532
  this.mixer.setSurroundPan(stripIndex, pan);
@@ -440,6 +605,61 @@ export class Mixer {
440
605
  return this.mixer.busMeter(busId);
441
606
  }
442
607
 
608
+ /**
609
+ * Number of blocks in which the strip discarded recursive state because a
610
+ * non-finite value had reached it.
611
+ *
612
+ * Advisory telemetry, and the only thing that separates a degraded strip
613
+ * from a clean one. A discard returns the affected state to its
614
+ * post-reset value, so the strip recovers in silence and the output stays
615
+ * finite and in range while carrying samples unrelated to the input;
616
+ * nothing else reports that this happened.
617
+ *
618
+ * The count covers the strip's own state, its EQ, every insert it owns and
619
+ * both of its meters. None of those is separately addressable here, so a
620
+ * discard inside one is observable only through this number -- and a
621
+ * meter that loses its loudness window then reports the floor, which is
622
+ * exactly what a genuinely silent strip reports, so nothing else
623
+ * distinguishes the two.
624
+ *
625
+ * A meter's own discard lags by one block: it checks its loudness state at
626
+ * the top of a block, before consuming that block's samples, so the block
627
+ * that corrupts it is not the block the count moves on -- read this again
628
+ * after one more block has processed. The EQ and inserts have no such lag;
629
+ * they discard at the end of their own process, in the same block that
630
+ * carried the poison.
631
+ *
632
+ * Cumulative since the strip was created and never cleared, so two
633
+ * readings bracket a span of audio. The unit is one processed block, never
634
+ * a channel, so a stereo block that discards on both channels adds one and
635
+ * the number does not depend on a dimension the caller did not choose.
636
+ *
637
+ * @param stripIndex - Strip index in `[0, stripCount())`
638
+ */
639
+ stripNonFiniteDiscardCount(stripIndex: number): number {
640
+ return this.mixer.stripNonFiniteDiscardCount(stripIndex);
641
+ }
642
+
643
+ /**
644
+ * Number of blocks in which a bus discarded recursive state because a
645
+ * non-finite value had reached it. Same contract as
646
+ * {@link stripNonFiniteDiscardCount}, for a bus: covers every insert the
647
+ * bus owns and its meter, neither separately addressable, so a discard
648
+ * inside one is observable only here. Cumulative across graph recompiles
649
+ * -- the count lives with the bus, not the compiled node, so an unrelated
650
+ * edit elsewhere in the mixer does not reset it.
651
+ *
652
+ * A bus's DSP record is created by the first {@link compile}. Throws for a
653
+ * bus that has been declared with {@link addBus} but never compiled --
654
+ * reading zero there would read as clean, and it is not. Also throws for
655
+ * an unknown bus id.
656
+ *
657
+ * @param busId - Bus id, as passed to {@link addBus} or declared in scene JSON
658
+ */
659
+ busNonFiniteDiscardCount(busId: string): number {
660
+ return this.mixer.busNonFiniteDiscardCount(busId);
661
+ }
662
+
443
663
  /**
444
664
  * Schedule sample-accurate fader automation on a strip.
445
665
  *
@@ -519,6 +739,11 @@ export class Mixer {
519
739
  /**
520
740
  * Read up to `maxPoints` of a strip's most recent goniometer samples
521
741
  * (oldest to newest).
742
+ *
743
+ * `maxPoints` must be a finite non-negative integer; anything else throws an
744
+ * `InvalidParameter` error. It is a request rather than an allocation size —
745
+ * a value beyond the strip's goniometer ring simply returns every point the
746
+ * ring holds.
522
747
  */
523
748
  readGoniometerLatest(stripIndex: number, maxPoints: number): GoniometerPoint[] {
524
749
  return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
@@ -559,8 +784,12 @@ export class Mixer {
559
784
  return this.mixer.drainTailStereo(numSamples);
560
785
  }
561
786
 
562
- /** Release the underlying WASM object. Safe to call only once. */
787
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
563
788
  delete(): void {
789
+ if (this.released) {
790
+ return;
791
+ }
792
+ this.released = true;
564
793
  this.mixer.delete();
565
794
  }
566
795
 
@@ -0,0 +1,138 @@
1
+ import { getSonareModule } from './module_state';
2
+ import type { MixAssistantOptions, MixAssistantResult, MixAssistantTrack } from './public_types';
3
+ import { assertSampleRate } from './validation';
4
+
5
+ function requireModule() {
6
+ return getSonareModule();
7
+ }
8
+
9
+ /** Inputs for the {@link suggestMixScene} / {@link suggestMixSceneJson} facades. */
10
+ export interface SuggestMixSceneRequest {
11
+ /** Tracks to mix, in the order their profiles are reported. */
12
+ tracks: MixAssistantTrack[];
13
+ /**
14
+ * Shared sample rate in Hz for every track. Required: every band edge,
15
+ * high-pass corner, sibilance band and alignment lag is derived from it, so a
16
+ * guessed rate would silently misread 44.1 kHz material by 8.8% and report
17
+ * `channelDelaySamples` and `durationSec` wrong with it.
18
+ */
19
+ sampleRate: number;
20
+ /** Assistant tunables; every field falls back to the core default. */
21
+ options?: MixAssistantOptions;
22
+ }
23
+
24
+ /** The four parallel arrays the embind entry points take. */
25
+ interface PlanarTracks {
26
+ left: Float32Array[];
27
+ right: (Float32Array | null)[];
28
+ ids: string[];
29
+ names: (string | null)[];
30
+ }
31
+
32
+ /**
33
+ * Splits the request's track list into the planar per-track arrays the binding
34
+ * takes, rejecting the shapes that would otherwise reach the analysis as a
35
+ * missing buffer or a nameless strip.
36
+ */
37
+ function planarTracks(tracks: MixAssistantTrack[]): PlanarTracks {
38
+ if (!Array.isArray(tracks)) {
39
+ throw new Error('tracks must be an array.');
40
+ }
41
+ const left: Float32Array[] = [];
42
+ const right: (Float32Array | null)[] = [];
43
+ const ids: string[] = [];
44
+ const names: (string | null)[] = [];
45
+ for (let index = 0; index < tracks.length; index++) {
46
+ const track = tracks[index];
47
+ if (track === null || typeof track !== 'object') {
48
+ throw new Error(`tracks[${index}] must be an object.`);
49
+ }
50
+ if (typeof track.id !== 'string' || track.id.length === 0) {
51
+ throw new Error(`tracks[${index}].id must be a non-empty string.`);
52
+ }
53
+ if (!(track.left instanceof Float32Array)) {
54
+ throw new Error(`tracks[${index}].left must be a Float32Array.`);
55
+ }
56
+ if (track.right !== undefined && !(track.right instanceof Float32Array)) {
57
+ throw new Error(`tracks[${index}].right must be a Float32Array when present.`);
58
+ }
59
+ left.push(track.left);
60
+ right.push(track.right ?? null);
61
+ ids.push(track.id);
62
+ names.push(track.name ?? null);
63
+ }
64
+ return { left, right, ids, names };
65
+ }
66
+
67
+ function suggestJson(fnName: string, request: SuggestMixSceneRequest, sceneOnly: boolean): string {
68
+ // Required, not defaulted: this surface used to invent 48000, which read
69
+ // 44.1 kHz material 8.8% off across every band edge and every lag without
70
+ // saying so. Node and Python both demand it, and a request ported from either
71
+ // must not change behaviour by arriving here.
72
+ assertSampleRate(fnName, request.sampleRate);
73
+ const { left, right, ids, names } = planarTracks(request.tracks);
74
+ const sampleRate = request.sampleRate;
75
+ const params = (request.options ?? {}) as Record<string, number | boolean>;
76
+ const module = requireModule();
77
+ return sceneOnly
78
+ ? module.mixingAssistantSuggestSceneJson(left, right, ids, names, sampleRate, params)
79
+ : module.mixingAssistantSuggest(left, right, ids, names, sampleRate, params);
80
+ }
81
+
82
+ /**
83
+ * Analyze a set of tracks and suggest a mixer scene.
84
+ *
85
+ * Offline only: the pipeline runs an STFT per track and evaluates every track
86
+ * pair, so it is measured in milliseconds per track and must never be called
87
+ * from an audio callback.
88
+ *
89
+ * The assistant suggests, it does not apply. Nothing is processed and no audio
90
+ * is returned; realizing the suggestion means handing the scene to
91
+ * {@link Mixer.fromSceneJson} as an explicit second step, for which
92
+ * {@link suggestMixSceneJson} returns the scene already serialized.
93
+ *
94
+ * Degenerate input is not an error: no tracks, all-silent tracks or tracks too
95
+ * short to measure yield an empty scene and an empty `explanation`.
96
+ *
97
+ * @param request - Tracks, shared sample rate and assistant options
98
+ * @returns The suggested scene, per-track profiles, cross-track measurements
99
+ * and the explanation behind each change
100
+ */
101
+ export function suggestMixScene(request: SuggestMixSceneRequest): MixAssistantResult {
102
+ return JSON.parse(suggestJson('suggestMixScene', request, false)) as MixAssistantResult;
103
+ }
104
+
105
+ /**
106
+ * Suggest a mixer scene and return only the scene, as JSON.
107
+ *
108
+ * The same analysis as {@link suggestMixScene}, serialized in the schema
109
+ * {@link Mixer.fromSceneJson} reads, so a caller that only wants to apply the
110
+ * suggestion neither digs the scene out of the fuller result nor re-serializes
111
+ * it.
112
+ *
113
+ * @param request - Tracks, shared sample rate and assistant options
114
+ * @returns Scene JSON string
115
+ */
116
+ export function suggestMixSceneJson(request: SuggestMixSceneRequest): string {
117
+ return suggestJson('suggestMixSceneJson', request, true);
118
+ }
119
+
120
+ /**
121
+ * Source-class identifiers the assistant can report, in enum order.
122
+ *
123
+ * The index of a name in this list is the value
124
+ * {@link mixSourceClassFromName} resolves it to.
125
+ */
126
+ export function mixSourceClassNames(): string[] {
127
+ return Array.from(requireModule().mixingAssistantSourceClassNames());
128
+ }
129
+
130
+ /**
131
+ * Resolve a source-class identifier to its index in {@link mixSourceClassNames}.
132
+ *
133
+ * @param name - Source-class identifier, e.g. `"kick"`
134
+ * @returns The index, or -1 when the name is unknown
135
+ */
136
+ export function mixSourceClassFromName(name: string): number {
137
+ return requireModule().mixingAssistantSourceClassFromName(name);
138
+ }