@libraz/libsonare 1.7.2 → 1.8.0

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 +6 -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 +873 -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 +477 -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 +129 -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 +3758 -1362
  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 +457 -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 +652 -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 +510 -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 +164 -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 +850 -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 +3909 -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 +335 -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 +128 -0
  179. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
  180. package/dist/worklet/engine-node.d.ts +81 -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 +68 -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 +73 -0
  193. package/dist/worklet/engine-strips.d.ts.map +1 -0
  194. package/dist/worklet/engine-sync.d.ts +37 -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 +397 -0
  199. package/dist/worklet/engine.d.ts.map +1 -0
  200. package/dist/worklet/guards.d.ts +47 -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 +323 -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 +2763 -501
  215. package/dist/worklet.js.map +1 -1
  216. package/package.json +23 -12
  217. package/src/_effects_common.ts +17 -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 +97 -22
  226. package/src/effects_note_ops.ts +635 -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 +377 -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 +282 -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 +346 -32
  246. package/src/mastering_dynamics.ts +22 -11
  247. package/src/metering.ts +67 -24
  248. package/src/mixer.ts +212 -3
  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 +268 -270
  259. package/src/public_types.ts +122 -3
  260. package/src/public_types_acoustic.ts +112 -3
  261. package/src/public_types_mastering.ts +254 -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 +195 -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 +487 -1
  268. package/src/quick_analysis.ts +203 -26
  269. package/src/realtime_engine.ts +711 -28
  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 +1122 -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 +194 -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 +444 -10
  287. package/src/worklet/engine-node.ts +78 -26
  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 +272 -83
  291. package/src/worklet/engine-register.ts +32 -18
  292. package/src/worklet/engine-strips.ts +280 -9
  293. package/src/worklet/engine-sync.ts +14 -6
  294. package/src/worklet/engine.ts +365 -31
  295. package/src/worklet/guards.ts +140 -44
  296. package/src/worklet/messages.ts +227 -2
  297. package/src/worklet/mixer-processor.ts +109 -47
  298. package/src/worklet/playback-processor.ts +300 -0
  299. package/src/worklet/protocol.ts +53 -3
  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
@@ -72,8 +72,17 @@ export interface MasteringDynamicsTransientShaperRequest extends TransientShaper
72
72
  sampleRate: number;
73
73
  }
74
74
 
75
- /** Result envelope returned by offline mastering dynamics processors. */
76
- export interface DynamicsResult {
75
+ /**
76
+ * Result envelope returned by offline mastering dynamics processors.
77
+ *
78
+ * Named for what it is rather than for the module it lives in. {@link
79
+ * DynamicsResult} is the *analysis* shape on every binding, so having that one
80
+ * identifier mean two disjoint field lists across the Node and WASM packages
81
+ * made a shared TypeScript module type-check against one and fail against the
82
+ * other — with the identifier resolving either way, so only the member list
83
+ * gave it away.
84
+ */
85
+ export interface DynamicsProcessorResult {
77
86
  samples: Float32Array;
78
87
  latencySamples: number;
79
88
  }
@@ -87,17 +96,17 @@ const COMPRESSOR_DETECTOR_MAP: Record<CompressorDetector, number> = {
87
96
  /** Offline feed-forward compressor (soft knee, optional auto-makeup / sidechain HPF). */
88
97
  export function masteringDynamicsCompressor(
89
98
  request: MasteringDynamicsCompressorRequest,
90
- ): DynamicsResult;
99
+ ): DynamicsProcessorResult;
91
100
  export function masteringDynamicsCompressor(
92
101
  samples: Float32Array,
93
102
  sampleRate: number,
94
103
  options?: CompressorOptions,
95
- ): DynamicsResult;
104
+ ): DynamicsProcessorResult;
96
105
  export function masteringDynamicsCompressor(
97
106
  samples: Float32Array | MasteringDynamicsCompressorRequest,
98
107
  sampleRate?: number,
99
108
  options: CompressorOptions = {},
100
- ): DynamicsResult {
109
+ ): DynamicsProcessorResult {
101
110
  const request =
102
111
  samples instanceof Float32Array
103
112
  ? { samples, sampleRate: sampleRate as number, ...options }
@@ -121,17 +130,19 @@ export function masteringDynamicsCompressor(
121
130
  }
122
131
 
123
132
  /** Offline noise gate (hysteresis, hold, optional key HPF). */
124
- export function masteringDynamicsGate(request: MasteringDynamicsGateRequest): DynamicsResult;
133
+ export function masteringDynamicsGate(
134
+ request: MasteringDynamicsGateRequest,
135
+ ): DynamicsProcessorResult;
125
136
  export function masteringDynamicsGate(
126
137
  samples: Float32Array,
127
138
  sampleRate: number,
128
139
  options?: GateOptions,
129
- ): DynamicsResult;
140
+ ): DynamicsProcessorResult;
130
141
  export function masteringDynamicsGate(
131
142
  samples: Float32Array | MasteringDynamicsGateRequest,
132
143
  sampleRate?: number,
133
144
  options: GateOptions = {},
134
- ): DynamicsResult {
145
+ ): DynamicsProcessorResult {
135
146
  const request =
136
147
  samples instanceof Float32Array
137
148
  ? { samples, sampleRate: sampleRate as number, ...options }
@@ -143,17 +154,17 @@ export function masteringDynamicsGate(
143
154
  /** Offline transient shaper (envelope-difference attack/sustain control). */
144
155
  export function masteringDynamicsTransientShaper(
145
156
  request: MasteringDynamicsTransientShaperRequest,
146
- ): DynamicsResult;
157
+ ): DynamicsProcessorResult;
147
158
  export function masteringDynamicsTransientShaper(
148
159
  samples: Float32Array,
149
160
  sampleRate: number,
150
161
  options?: TransientShaperOptions,
151
- ): DynamicsResult;
162
+ ): DynamicsProcessorResult;
152
163
  export function masteringDynamicsTransientShaper(
153
164
  samples: Float32Array | MasteringDynamicsTransientShaperRequest,
154
165
  sampleRate?: number,
155
166
  options: TransientShaperOptions = {},
156
- ): DynamicsResult {
167
+ ): DynamicsProcessorResult {
157
168
  const request =
158
169
  samples instanceof Float32Array
159
170
  ? { samples, sampleRate: sampleRate as number, ...options }
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)
@@ -185,6 +236,60 @@ export class Mixer {
185
236
  };
186
237
  }
187
238
 
239
+ /**
240
+ * Turn the master-output meter on or off.
241
+ *
242
+ * While on, every {@link MixerRealtimeBuffer.process} call meters the stereo
243
+ * master it just produced, so a caller reads {@link meterSnapshot} instead of
244
+ * copying the output and measuring it again. `truePeakDb*` is an inter-sample
245
+ * peak taken after oversampling (ITU-R BS.1770-4 Annex 2 requires at least
246
+ * 4x), which is a different and higher quantity than the sample peak.
247
+ *
248
+ * Enabling resets the meter, so a reading never mixes in audio from a period
249
+ * when metering was off.
250
+ *
251
+ * @param enabled - Whether to meter the master output.
252
+ * @param truePeakOversample - 0 (= 4x) or a power of two in [1, 16].
253
+ */
254
+ configureMeter(enabled: boolean, truePeakOversample = 4): void {
255
+ this.mixer.configureMeter(enabled, truePeakOversample);
256
+ }
257
+
258
+ /**
259
+ * Latest master-output meter reading, describing the most recently metered
260
+ * block. All dB fields are finite and floored at -120.
261
+ *
262
+ * @throws When the meter has never been enabled.
263
+ */
264
+ meterSnapshot(): MixerMeterSnapshot {
265
+ return this.mixer.meterSnapshot();
266
+ }
267
+
268
+ /**
269
+ * Latch the latest meter reading into the mixer's internal scratch so
270
+ * {@link meterScratchValue} can read it back one number at a time.
271
+ *
272
+ * This is the allocation-free form of {@link meterSnapshot}, for an audio
273
+ * render callback that must not create a JS object per interval. It returns
274
+ * `false` instead of throwing when the meter has never been enabled.
275
+ *
276
+ * @returns Whether a reading was latched.
277
+ */
278
+ latchMeterSnapshot(): boolean {
279
+ return this.mixer.latchMeterSnapshot();
280
+ }
281
+
282
+ /**
283
+ * Read one field of the snapshot latched by {@link latchMeterSnapshot}.
284
+ *
285
+ * @param field - `0` peakDbL, `1` peakDbR, `2` rmsDbL, `3` rmsDbR,
286
+ * `4` correlation, `5` truePeakDbL, `6` truePeakDbR. Any other index
287
+ * reads `0`.
288
+ */
289
+ meterScratchValue(field: number): number {
290
+ return this.mixer.meterScratchValue(field);
291
+ }
292
+
188
293
  /** Number of strips in the mixer (e.g. strips loaded from the scene). */
189
294
  stripCount(): number {
190
295
  return this.mixer.stripCount();
@@ -232,6 +337,18 @@ export class Mixer {
232
337
  return index < 0 ? null : index;
233
338
  }
234
339
 
340
+ /**
341
+ * Add a channel strip to the mixer topology. `metering` configures the strip's
342
+ * pre/post taps; omitting it keeps the full default (LUFS + true peak at 4x,
343
+ * about 1.4 MB per strip at 48 kHz). Marks the routing graph dirty; call
344
+ * {@link compile} (or {@link processStereo}) to rebuild.
345
+ *
346
+ * @throws If the id is already taken, or `truePeakOversample` is outside `[0, 16]`
347
+ */
348
+ addStrip(id: string, metering: StripMeteringOptions = {}): void {
349
+ this.mixer.addStrip(id, metering);
350
+ }
351
+
235
352
  /**
236
353
  * Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
237
354
  * `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
@@ -310,6 +427,31 @@ export class Mixer {
310
427
  this.mixer.setWidth(stripIndex, width);
311
428
  }
312
429
 
430
+ /**
431
+ * Snap the strip's input-trim, fader, pan and width smoothers to the values
432
+ * already set on it, so the next processed block opens at those values
433
+ * instead of gliding to them over the smoothing window (~5 ms).
434
+ *
435
+ * Call it after configuring a strip and before rendering a finite buffer: a
436
+ * strip is smoothed for a live fader, and an offline render that does not
437
+ * settle carries that glide as a level and image sweep across the head of
438
+ * its output. Unlike a reset it clears nothing — automation, meters and
439
+ * insert state are untouched.
440
+ *
441
+ * @param stripIndex - Strip index in `[0, stripCount())`
442
+ *
443
+ * @example
444
+ * ```typescript
445
+ * mixer.setFaderDb(0, -3);
446
+ * mixer.setPan(0, 0.3);
447
+ * mixer.settle(0);
448
+ * const { left, right } = mixer.processStereo([dryLeft], [dryRight]);
449
+ * ```
450
+ */
451
+ settle(stripIndex: number): void {
452
+ this.mixer.settle(stripIndex);
453
+ }
454
+
313
455
  /** Set the strip's mute state. */
314
456
  setMuted(stripIndex: number, muted: boolean): void {
315
457
  this.mixer.setMuted(stripIndex, muted);
@@ -361,7 +503,10 @@ export class Mixer {
361
503
 
362
504
  /**
363
505
  * 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.
506
+ *
507
+ * Applied when the engine's track mixer renders this strip's lane into a
508
+ * destination with more than two channels. This stereo-only mixer's own
509
+ * block entry points ignore it.
365
510
  */
366
511
  setSurroundPan(stripIndex: number, pan: SurroundPan): void {
367
512
  this.mixer.setSurroundPan(stripIndex, pan);
@@ -440,6 +585,61 @@ export class Mixer {
440
585
  return this.mixer.busMeter(busId);
441
586
  }
442
587
 
588
+ /**
589
+ * Number of blocks in which the strip discarded recursive state because a
590
+ * non-finite value had reached it.
591
+ *
592
+ * Advisory telemetry, and the only thing that separates a degraded strip
593
+ * from a clean one. A discard returns the affected state to its
594
+ * post-reset value, so the strip recovers in silence and the output stays
595
+ * finite and in range while carrying samples unrelated to the input;
596
+ * nothing else reports that this happened.
597
+ *
598
+ * The count covers the strip's own state, its EQ, every insert it owns and
599
+ * both of its meters. None of those is separately addressable here, so a
600
+ * discard inside one is observable only through this number -- and a
601
+ * meter that loses its loudness window then reports the floor, which is
602
+ * exactly what a genuinely silent strip reports, so nothing else
603
+ * distinguishes the two.
604
+ *
605
+ * A meter's own discard lags by one block: it checks its loudness state at
606
+ * the top of a block, before consuming that block's samples, so the block
607
+ * that corrupts it is not the block the count moves on -- read this again
608
+ * after one more block has processed. The EQ and inserts have no such lag;
609
+ * they discard at the end of their own process, in the same block that
610
+ * carried the poison.
611
+ *
612
+ * Cumulative since the strip was created and never cleared, so two
613
+ * readings bracket a span of audio. The unit is one processed block, never
614
+ * a channel, so a stereo block that discards on both channels adds one and
615
+ * the number does not depend on a dimension the caller did not choose.
616
+ *
617
+ * @param stripIndex - Strip index in `[0, stripCount())`
618
+ */
619
+ stripNonFiniteDiscardCount(stripIndex: number): number {
620
+ return this.mixer.stripNonFiniteDiscardCount(stripIndex);
621
+ }
622
+
623
+ /**
624
+ * Number of blocks in which a bus discarded recursive state because a
625
+ * non-finite value had reached it. Same contract as
626
+ * {@link stripNonFiniteDiscardCount}, for a bus: covers every insert the
627
+ * bus owns and its meter, neither separately addressable, so a discard
628
+ * inside one is observable only here. Cumulative across graph recompiles
629
+ * -- the count lives with the bus, not the compiled node, so an unrelated
630
+ * edit elsewhere in the mixer does not reset it.
631
+ *
632
+ * A bus's DSP record is created by the first {@link compile}. Throws for a
633
+ * bus that has been declared with {@link addBus} but never compiled --
634
+ * reading zero there would read as clean, and it is not. Also throws for
635
+ * an unknown bus id.
636
+ *
637
+ * @param busId - Bus id, as passed to {@link addBus} or declared in scene JSON
638
+ */
639
+ busNonFiniteDiscardCount(busId: string): number {
640
+ return this.mixer.busNonFiniteDiscardCount(busId);
641
+ }
642
+
443
643
  /**
444
644
  * Schedule sample-accurate fader automation on a strip.
445
645
  *
@@ -519,6 +719,11 @@ export class Mixer {
519
719
  /**
520
720
  * Read up to `maxPoints` of a strip's most recent goniometer samples
521
721
  * (oldest to newest).
722
+ *
723
+ * `maxPoints` must be a finite non-negative integer; anything else throws an
724
+ * `InvalidParameter` error. It is a request rather than an allocation size —
725
+ * a value beyond the strip's goniometer ring simply returns every point the
726
+ * ring holds.
522
727
  */
523
728
  readGoniometerLatest(stripIndex: number, maxPoints: number): GoniometerPoint[] {
524
729
  return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
@@ -559,8 +764,12 @@ export class Mixer {
559
764
  return this.mixer.drainTailStereo(numSamples);
560
765
  }
561
766
 
562
- /** Release the underlying WASM object. Safe to call only once. */
767
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
563
768
  delete(): void {
769
+ if (this.released) {
770
+ return;
771
+ }
772
+ this.released = true;
564
773
  this.mixer.delete();
565
774
  }
566
775
 
@@ -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
+ }
@@ -32,11 +32,16 @@ export interface MixStereoRequest extends MixOptions {
32
32
  * One-shot stereo mix of multiple strips through the routing graph + master bus.
33
33
  *
34
34
  * Each returned per-strip meter reflects only this single one-shot block. The
35
- * integrating-meter fields (`momentaryLufs`, `shortTermLufs`, `integratedLufs`
36
- * and the true-peak fields) require sustained streaming to populate; on a short
37
- * one-shot mix they read the -120 dB floor sentinel. Drive a streaming
38
- * {@link Mixer} block-by-block if you need meaningful loudness/true-peak
39
- * readings.
35
+ * LUFS fields (`momentaryLufs`, `shortTermLufs`, `integratedLufs`) are
36
+ * integrators whose windows that block does not fill, so they read the -120 dB
37
+ * floor sentinel; drive a streaming {@link Mixer} block-by-block if you need
38
+ * meaningful loudness.
39
+ *
40
+ * The true-peak fields are not integrators and need no streaming: each is a
41
+ * max-hold over the block just processed and is valid from the first one,
42
+ * flooring only on silence. This facade mixes the whole input as a single block,
43
+ * which is the whole-signal case, so the block-edge under-read documented on
44
+ * `MixMeterSnapshot.truePeakDbL` does not apply to the reading here.
40
45
  *
41
46
  * @param leftChannels - Per-strip left input buffers (all the same length)
42
47
  * @param rightChannels - Per-strip right input buffers (all the same length)