@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
@@ -1,13 +1,35 @@
1
- import { panLawCode, panModeCode, sendTimingCode, trackMonitorModeCode } from './codes';
1
+ import {
2
+ panLawCode,
3
+ panModeCode,
4
+ sendTimingCode,
5
+ sidechainSourceKindCode,
6
+ trackMonitorModeCode,
7
+ } from './codes';
2
8
  import { ErrorCode, SonareError } from './errors';
3
9
  import { getSonareModule } from './module_state';
4
- import type { ProjectMidiCcBinding, SynthPatch } from './project';
5
- import type { EqBand, PanLawInput, PanMode, SendTiming } from './public_types';
10
+ import type {
11
+ Articulation,
12
+ ControllerBinding,
13
+ MpeDimension,
14
+ NoteTracking,
15
+ ProjectMidiCcBinding,
16
+ SynthPatch,
17
+ } from './project';
18
+ import { normalizeSynthInstrument } from './project_internal';
19
+ import type {
20
+ EqBand,
21
+ PanLawInput,
22
+ PanMode,
23
+ SendTiming,
24
+ SidechainSourceKind,
25
+ UmpWords,
26
+ } from './public_types';
6
27
  import type {
7
28
  WasmClipPageRequest,
8
29
  WasmEngineAutomationPoint,
9
30
  WasmEngineBounceOptions,
10
31
  WasmEngineBounceResult,
32
+ WasmEngineBus,
11
33
  WasmEngineCaptureStatus,
12
34
  WasmEngineClip,
13
35
  WasmEngineFreezeOptions,
@@ -23,6 +45,7 @@ import type {
23
45
  WasmEngineTelemetry,
24
46
  WasmEngineTempoSegment,
25
47
  WasmEngineTimeSignatureSegment,
48
+ WasmEngineTrackSend,
26
49
  WasmEngineTransportState,
27
50
  WasmExternalMidiEvent,
28
51
  WasmRealtimeEngine,
@@ -94,15 +117,37 @@ export interface EngineBus {
94
117
  * to stereo.
95
118
  */
96
119
  channelLayout?: number;
120
+ /**
121
+ * Bus this bus's output sums into instead of the master mix (bus-to-bus
122
+ * routing); 0 or absent keeps it on the master mix.
123
+ */
124
+ outputBusId?: number;
125
+ /**
126
+ * Sends to other buses, in the same shape as a track lane's sends. A
127
+ * pre-fader send taps before `gainDb`, a post-fader one after it.
128
+ */
129
+ sends?: EngineTrackSend[];
97
130
  }
98
131
 
99
132
  export interface EngineMidiEvent {
100
- renderFrame: number;
133
+ /** Absolute render frame for this event. Default `0`. */
134
+ renderFrame?: number;
101
135
  word0?: number;
102
136
  word1?: number;
103
137
  word2?: number;
104
138
  word3?: number;
105
139
  wordCount?: number;
140
+ /**
141
+ * Redundant with `word0`, which already carries the UMP group in bits 24..27.
142
+ * The engine reads the group from `word0` — the form that reaches a device or
143
+ * a file — so packing it there is sufficient and a value here that contradicts
144
+ * `word0` is ignored. Must still be in `[0, 15]`; anything else is rejected as
145
+ * a malformed event. Default `0`.
146
+ *
147
+ * Utility (`word0` type nibble `0x0`) and UMP Stream (`0xF`) messages have no
148
+ * group field — those bits are Reserved and `form`/`status` respectively — so
149
+ * they always read as group `0` and packing a group into them has no effect.
150
+ */
106
151
  group?: number;
107
152
  sysexHandle?: number;
108
153
  data0?: number;
@@ -119,6 +164,20 @@ export interface EngineMidiClipSchedule {
119
164
  loop?: boolean;
120
165
  loopLengthSamples?: number;
121
166
  events: EngineMidiEvent[];
167
+ /**
168
+ * Linear gain applied to the destination instrument's rendered audio while
169
+ * this clip is the most recently started active clip on it. Absent defaults
170
+ * to `1` (unity).
171
+ */
172
+ gain?: number;
173
+ /**
174
+ * Linear fade lengths over the clip's full length (not per internal loop
175
+ * repeat). Absent defaults to `0` (no fade). `fadeOutSamples` above `0` is
176
+ * rejected when `lengthSamples` is absent or `<= 0` (open-ended): an
177
+ * open-ended clip has no end to fade out towards.
178
+ */
179
+ fadeInSamples?: number;
180
+ fadeOutSamples?: number;
122
181
  }
123
182
 
124
183
  export const EXPECTED_ENGINE_ABI_VERSION = 3;
@@ -131,6 +190,76 @@ export interface MidiCcBindOptions {
131
190
  maxValue?: number;
132
191
  }
133
192
 
193
+ /** Request form of {@link RealtimeEngine.renderOffline}. */
194
+ export interface RenderOfflineRequest {
195
+ /** One buffer per output plane; their common length is the render span. */
196
+ channels: Float32Array[];
197
+ /** Render block size. Default `128`. */
198
+ blockSize?: number;
199
+ /**
200
+ * Whether this call ends the timeline. `true` (the default, and what a
201
+ * one-shot bounce wants) releases every sounding note and flushes the PDC /
202
+ * alignment delay lines before returning. `false` renders one CHUNK of a
203
+ * longer timeline: a note held across the chunk boundary keeps sounding into
204
+ * the next call and the delay lines carry their history over, so consecutive
205
+ * chunks concatenate to exactly what one continuous render of the same span
206
+ * produces. Call {@link RealtimeEngine.finishOfflineRender} once after the
207
+ * last chunk.
208
+ *
209
+ * Sample-exact concatenation requires every chunk to use the same `blockSize`
210
+ * and a frame count that is a whole number of blocks: each call restarts the
211
+ * block grid at its own frame 0 and renders a short final block for the
212
+ * remainder, and the clip / automation / MIDI-clip snapshots are frozen once
213
+ * per block, so a chunk that ends mid-block shifts every later block
214
+ * boundary. Audio stays continuous either way; only bit-identity is lost.
215
+ */
216
+ finalize?: boolean;
217
+ }
218
+
219
+ const UMP_WORD_MIN = -0x80000000;
220
+ const UMP_WORD_MAX = 0xffffffff;
221
+
222
+ // A word may be spelled `(0x4 << 28) | …`, which is a signed int once bit 31 is
223
+ // set, so the signed 32-bit range is accepted alongside the unsigned one.
224
+ function assertUmpWords(fnName: string, words: UmpWords): UmpWords {
225
+ if (!(words instanceof Uint32Array) && !Array.isArray(words)) {
226
+ throw new TypeError(`${fnName}: words must be a Uint32Array or a number array`);
227
+ }
228
+ if (words.length < 1 || words.length > 4) {
229
+ throw new RangeError(`${fnName}: words must hold 1 to 4 words`);
230
+ }
231
+ for (let i = 0; i < words.length; i++) {
232
+ const word = words[i];
233
+ if (
234
+ typeof word !== 'number' ||
235
+ !Number.isInteger(word) ||
236
+ word < UMP_WORD_MIN ||
237
+ word > UMP_WORD_MAX
238
+ ) {
239
+ throw new RangeError(`${fnName}: words[${i}] must be an integer 32-bit word`);
240
+ }
241
+ }
242
+ return words;
243
+ }
244
+
245
+ /**
246
+ * One normalizer for both call forms, so the request object and the positional
247
+ * overload cannot drift in their defaults.
248
+ */
249
+ function normalizeRenderOfflineRequest(
250
+ channelsOrRequest: Float32Array[] | RenderOfflineRequest,
251
+ blockSize: number,
252
+ ): { channels: Float32Array[]; blockSize: number; finalize: boolean } {
253
+ const request = Array.isArray(channelsOrRequest)
254
+ ? { channels: channelsOrRequest, blockSize }
255
+ : channelsOrRequest;
256
+ return {
257
+ channels: request.channels,
258
+ blockSize: request.blockSize ?? 128,
259
+ finalize: request.finalize ?? true,
260
+ };
261
+ }
262
+
134
263
  export interface EngineCapabilities {
135
264
  engineAbiVersion: number;
136
265
  expectedEngineAbiVersion: number;
@@ -162,6 +291,7 @@ export function engineCapabilities(): EngineCapabilities {
162
291
 
163
292
  export class RealtimeEngine {
164
293
  private native: WasmRealtimeEngine;
294
+ private released = false;
165
295
 
166
296
  constructor(
167
297
  sampleRate = 48000,
@@ -186,6 +316,15 @@ export class RealtimeEngine {
186
316
  );
187
317
  }
188
318
 
319
+ /**
320
+ * Size the engine's queues and scratch for a sample rate and block size.
321
+ *
322
+ * `commandCapacity` must not exceed 65536 and `telemetryCapacity` must not
323
+ * exceed 16384; a larger value throws and leaves the engine untouched. The
324
+ * telemetry number is not a queue depth paid for one-for-one: the engine
325
+ * reserves that many meter records per metered lane, so its memory cost is
326
+ * far larger than the number given here.
327
+ */
189
328
  prepare(
190
329
  sampleRate: number,
191
330
  maxBlockSize: number,
@@ -249,12 +388,16 @@ export class RealtimeEngine {
249
388
  * scheduled MIDI clips routed to that destination render through the synth.
250
389
  * Unknown preset names throw. An object patch's `destinationId` is a JS
251
390
  * binding convenience, not part of the NativeSynth patch itself.
391
+ *
392
+ * An `engineMode: 'sample'` patch also carries the {@link SampleBank} its
393
+ * keymap names. The synth takes a share of the bank, so it may be released
394
+ * right after this call; a sample patch bound without one renders silence.
252
395
  */
253
396
  setSynthInstrument(
254
397
  patch: SynthPatch | string = {},
255
398
  destinationId = (typeof patch === 'object' ? patch.destinationId : undefined) ?? 0,
256
399
  ): void {
257
- this.native.setSynthInstrument(destinationId, patch);
400
+ this.native.setSynthInstrument(destinationId, normalizeSynthInstrument(patch));
258
401
  }
259
402
 
260
403
  /**
@@ -282,6 +425,8 @@ export class RealtimeEngine {
282
425
  gain?: number;
283
426
  polyphony?: number;
284
427
  preferModelForModeledFamilies?: boolean;
428
+ clearBankRig?: boolean;
429
+ gsEfxRealization?: 'modern' | 'classic';
285
430
  } = {},
286
431
  destinationId = config.destinationId ?? 0,
287
432
  ): void {
@@ -329,6 +474,135 @@ export class RealtimeEngine {
329
474
  return this.native.midiCcBindingCount();
330
475
  }
331
476
 
477
+ /**
478
+ * Replace a destination instrument's controller profile with a named preset
479
+ * (see {@link controllerProfileNames}). Installing a profile drops every
480
+ * channel's accumulated axis values: the new bindings say nothing about what
481
+ * the old ones had reached. An unknown name throws, and so does a destination
482
+ * with no instrument or one whose instrument holds no profile.
483
+ */
484
+ setControllerProfile(destinationId: number, presetName: string): void {
485
+ this.native.setControllerProfile(destinationId, presetName);
486
+ }
487
+
488
+ /** Add one {@link ControllerBinding} on top of the destination's current profile. */
489
+ bindController(destinationId: number, binding: ControllerBinding): void {
490
+ this.native.bindController(destinationId, binding);
491
+ }
492
+
493
+ /**
494
+ * Drop every binding of the destination's controller profile. The instrument
495
+ * keeps a profile; it resolves nothing until something is bound again.
496
+ */
497
+ clearControllerBindings(destinationId: number): void {
498
+ this.native.clearControllerBindings(destinationId);
499
+ }
500
+
501
+ controllerBindingCount(destinationId: number): number {
502
+ return this.native.controllerBindingCount(destinationId);
503
+ }
504
+
505
+ /**
506
+ * Whether note-on velocity is expression for this instrument. No fixed
507
+ * default is possible — a wind controller ships sending breath-derived
508
+ * velocity on one model and a constant on the next — so each preset states it
509
+ * and a host building its own profile sets it. When false the synth takes
510
+ * every note at full scale and the bound axes carry the dynamics alone.
511
+ */
512
+ setControllerVelocityMeaningful(destinationId: number, meaningful: boolean): void {
513
+ this.native.setControllerVelocityMeaningful(destinationId, meaningful);
514
+ }
515
+
516
+ controllerVelocityMeaningful(destinationId: number): boolean {
517
+ return this.native.controllerVelocityMeaningful(destinationId);
518
+ }
519
+
520
+ /**
521
+ * Say which note a value addressed to a whole MIDI channel belongs to when
522
+ * several are sounding on it, for one per-note dimension
523
+ * ({@link MPE_DIMENSIONS}, {@link NOTE_TRACKINGS}).
524
+ *
525
+ * Set per dimension because the useful answers differ: pressure following the
526
+ * newest note while bend reaches every one is a real configuration, not a
527
+ * mistake. MPE poses this question and declines to answer it, so this is a
528
+ * choice rather than a rule — and it is read only inside an MPE zone, and
529
+ * only while more than one note is sounding on the channel, which an MPE
530
+ * sender avoids by giving each note its own member channel.
531
+ *
532
+ * Both arguments are required and are a name or its ordinal; an unknown
533
+ * spelling is refused rather than resolved to a default, as are a destination
534
+ * with no instrument and one whose instrument holds no controller profile.
535
+ */
536
+ setControllerNoteTracking(
537
+ destinationId: number,
538
+ dimension: MpeDimension | number,
539
+ tracking: NoteTracking | number,
540
+ ): void {
541
+ this.native.setControllerNoteTracking(destinationId, dimension, tracking);
542
+ }
543
+
544
+ /**
545
+ * Read back {@link setControllerNoteTracking} for one dimension, as the
546
+ * canonical name.
547
+ */
548
+ controllerNoteTracking(
549
+ destinationId: number,
550
+ dimension: MpeDimension | number,
551
+ ): NoteTracking | number {
552
+ return this.native.controllerNoteTracking(destinationId, dimension);
553
+ }
554
+
555
+ /**
556
+ * Set how one MIDI channel (0–15) of a destination's instrument treats a
557
+ * note-on while another note on that channel is still held: `'poly'` takes a
558
+ * new voice each time, `'mono-retrigger'` stops and restarts the note (what
559
+ * GS MONO MODE and CC126 mean), `'mono-legato'` carries the sounding voice
560
+ * and only moves its pitch — a wind player's slur, which no MIDI message can
561
+ * reach by design.
562
+ *
563
+ * `'mono-legato'` is a request, not a guarantee: an engine whose exciter is
564
+ * spent at the onset — anything struck or plucked — and a target pitch below
565
+ * what the engine's delay line can hold both fall back to an ordinary note,
566
+ * which {@link legatoFallbackCount} counts. A channel outside [0,15] and an
567
+ * articulation outside the enum are refused rather than clamped, and so is a
568
+ * destination with no instrument or one whose instrument has no articulation
569
+ * of its own.
570
+ */
571
+ setArticulation(
572
+ destinationId: number,
573
+ channel: number,
574
+ articulation: Articulation | number,
575
+ ): void {
576
+ this.native.setArticulation(destinationId, channel, articulation);
577
+ }
578
+
579
+ /**
580
+ * Read back {@link setArticulation} as the canonical name. An ordinal this
581
+ * build cannot spell is handed back as the number, the way every other enum
582
+ * leaves this surface.
583
+ */
584
+ articulation(destinationId: number, channel: number): Articulation | number {
585
+ return this.native.articulation(destinationId, channel);
586
+ }
587
+
588
+ /**
589
+ * How many times a legato continuation was asked for on this destination and
590
+ * refused, so the note started a voice of its own instead. Counted rather
591
+ * than inferred: a refusal sounds like an ordinary note, so nothing in the
592
+ * audio separates "this engine declines legato" from "the mode was never
593
+ * set". Saturates at 4294967295 rather than wrapping — matching the C ABI, so
594
+ * the same phrase reports the same number on every surface — after which it
595
+ * reads as "at least this many".
596
+ *
597
+ * Throws on a destination with no instrument, and on one whose instrument has
598
+ * no articulation of its own — the same two refusals
599
+ * {@link setArticulation} keeps apart. Reporting 0 for the second would read
600
+ * as "every slur took", which is the reading this counter exists to prevent.
601
+ */
602
+ legatoFallbackCount(destinationId: number): number {
603
+ return this.native.legatoFallbackCount(destinationId);
604
+ }
605
+
332
606
  /** Install/replace a live non-destructive MIDI-FX insert for one destination. */
333
607
  setMidiFx(destinationId: number, configJson: string): void {
334
608
  this.native.setMidiFx(destinationId, configJson);
@@ -354,7 +628,10 @@ export class RealtimeEngine {
354
628
  /**
355
629
  * Route a destination's (track lane's) MIDI to the external output queue
356
630
  * instead of the internal instrument rack, so the track plays an external
357
- * device. Clearing it restores internal-synth playback.
631
+ * device. Clearing it restores internal-synth playback. Control-thread only.
632
+ * The change takes effect at the next processed block; switching a
633
+ * destination's route first releases its notes and resets its controllers
634
+ * through the old route, and drops its pending MIDI-FX events. Single writer.
358
635
  */
359
636
  setMidiDestinationExternal(destinationId: number, external: boolean): void {
360
637
  this.native.setMidiDestinationExternal(destinationId, external);
@@ -383,6 +660,12 @@ export class RealtimeEngine {
383
660
  * block / animation frame. `maxRecords` caps the number of output events
384
661
  * returned — the shared unit across every surface. Events past the cap stay
385
662
  * queued for the next call (lossless); call again to drain the rest.
663
+ *
664
+ * One queued record lowers to at most 4 MIDI 1.0 messages (a MIDI 2.0
665
+ * registered or assignable controller becomes CC 101/100 or 99/98 plus Data
666
+ * Entry 6/38), so a positive `maxRecords` below 4 could never consume a record
667
+ * and is rejected with an `InvalidParameter` `SonareError` instead of
668
+ * returning nothing forever.
386
669
  */
387
670
  drainExternalMidi(maxRecords = 1024): WasmExternalMidiEvent[] {
388
671
  return this.native.drainExternalMidi(maxRecords);
@@ -398,7 +681,11 @@ export class RealtimeEngine {
398
681
  }
399
682
 
400
683
  externalMidiScratchRenderFrame(): number {
401
- return this.native.externalMidiScratchRenderFrame();
684
+ // embind marshals the int64 render frame as a BigInt; the declared `number`
685
+ // has to be a real number or the first consumer that does arithmetic on it
686
+ // dies with "Cannot mix BigInt". Same normalization as the telemetry, meter
687
+ // and scope scratch frames.
688
+ return Number(this.native.externalMidiScratchRenderFrame());
402
689
  }
403
690
 
404
691
  externalMidiScratchByteWord(): number {
@@ -443,6 +730,50 @@ export class RealtimeEngine {
443
730
  this.native.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
444
731
  }
445
732
 
733
+ /**
734
+ * Push a live MIDI pitch bend to the engine-owned MIDI input source.
735
+ *
736
+ * `bend14` is unsigned 14-bit with centre 8192 (0..16383) — the dimension is
737
+ * not 7-bit, so a value past 16383 is refused rather than narrowed. The input
738
+ * source must be enabled with {@link setMidiInputSource} first.
739
+ */
740
+ pushMidiInputPitchBend(
741
+ group: number,
742
+ channel: number,
743
+ bend14: number,
744
+ portTimeSamples = 0,
745
+ ): void {
746
+ this.native.pushMidiInputPitchBend(group, channel, bend14, portTimeSamples);
747
+ }
748
+
749
+ /**
750
+ * Push a live MIDI channel pressure to the engine-owned MIDI input source.
751
+ * `pressure` is 7-bit (0..127). Under MPE this is the member channel's
752
+ * per-note pressure.
753
+ */
754
+ pushMidiInputChannelPressure(
755
+ group: number,
756
+ channel: number,
757
+ pressure: number,
758
+ portTimeSamples = 0,
759
+ ): void {
760
+ this.native.pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples);
761
+ }
762
+
763
+ /**
764
+ * Push a live MIDI polyphonic key pressure to the engine-owned MIDI input
765
+ * source. `note` and `pressure` are 7-bit (0..127).
766
+ */
767
+ pushMidiInputPolyPressure(
768
+ group: number,
769
+ channel: number,
770
+ note: number,
771
+ pressure: number,
772
+ portTimeSamples = 0,
773
+ ): void {
774
+ this.native.pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples);
775
+ }
776
+
446
777
  pushMidiNoteOn(
447
778
  destinationId: number,
448
779
  group: number,
@@ -482,16 +813,86 @@ export class RealtimeEngine {
482
813
  this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
483
814
  }
484
815
 
485
- /** Queue one immediate MIDI 1.0 channel-voice UMP word for a destination. */
486
- pushMidiUmp(destinationId: number, word0: number, renderFrame = -1): void {
487
- this.native.pushMidiUmp(destinationId, word0, renderFrame);
816
+ /**
817
+ * Queue an immediate (live) MIDI pitch bend to a MIDI destination. `bend14`
818
+ * is unsigned 14-bit with centre 8192 (0..16383); `renderFrame` is the frame
819
+ * to fire at, or -1 for immediate. Mirrors the Node/Python/C-ABI
820
+ * `pushMidiPitchBend`.
821
+ */
822
+ pushMidiPitchBend(
823
+ destinationId: number,
824
+ group: number,
825
+ channel: number,
826
+ bend14: number,
827
+ renderFrame = -1,
828
+ ): void {
829
+ this.native.pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame);
830
+ }
831
+
832
+ /**
833
+ * Queue an immediate (live) MIDI channel pressure to a MIDI destination.
834
+ * `pressure` is 7-bit (0..127); `renderFrame` is the frame to fire at, or -1
835
+ * for immediate. Mirrors the Node/Python/C-ABI `pushMidiChannelPressure`.
836
+ */
837
+ pushMidiChannelPressure(
838
+ destinationId: number,
839
+ group: number,
840
+ channel: number,
841
+ pressure: number,
842
+ renderFrame = -1,
843
+ ): void {
844
+ this.native.pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame);
845
+ }
846
+
847
+ /**
848
+ * Queue an immediate (live) MIDI polyphonic key pressure to a MIDI
849
+ * destination. `note` and `pressure` are 7-bit (0..127); `renderFrame` is the
850
+ * frame to fire at, or -1 for immediate. Mirrors the Node/Python/C-ABI
851
+ * `pushMidiPolyPressure`.
852
+ */
853
+ pushMidiPolyPressure(
854
+ destinationId: number,
855
+ group: number,
856
+ channel: number,
857
+ note: number,
858
+ pressure: number,
859
+ renderFrame = -1,
860
+ ): void {
861
+ this.native.pushMidiPolyPressure(destinationId, group, channel, note, pressure, renderFrame);
862
+ }
863
+
864
+ /**
865
+ * Queue an immediate (live) raw UMP message to a MIDI destination. `words` is
866
+ * 1 to 4 words, most significant first, and its length must match the message
867
+ * type of `words[0]`. MIDI 2.0 channel-voice messages (MT 0x4) arrive at full
868
+ * width; SysEx7 / data messages (MT 0x3 / 0x5) are refused, use
869
+ * {@link pushMidiSysex}. Throws when the slot ring or command queue is full
870
+ * (retry after a process block). `renderFrame` is the render-frame time to
871
+ * apply, or -1 for immediate. A bare number is accepted as a one-word
872
+ * message.
873
+ */
874
+ pushMidiUmp(destinationId: number, words: UmpWords | number, renderFrame = -1): void {
875
+ const list = typeof words === 'number' ? [words] : words;
876
+ this.native.pushMidiUmp(destinationId, assertUmpWords('pushMidiUmp', list), renderFrame);
877
+ }
878
+
879
+ /**
880
+ * Push one raw UMP message (1 to 4 words) to the engine-owned MIDI input
881
+ * source. The message rules match {@link pushMidiUmp}. `portTimeSamples` is
882
+ * the port timestamp in samples.
883
+ */
884
+ pushMidiInputUmp(words: UmpWords, portTimeSamples = 0): void {
885
+ this.native.pushMidiInputUmp(assertUmpWords('pushMidiInputUmp', words), portTimeSamples);
488
886
  }
489
887
 
490
888
  /**
491
889
  * Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
492
890
  * the full message including the leading 0xF0 and trailing 0xF7 (1..512
493
- * bytes). `renderFrame` is the frame to fire at, or -1 for immediate. Mirrors
494
- * the Node/Python/C-ABI `pushMidiSysex`.
891
+ * bytes). `renderFrame` is the frame to fire at, or -1 for immediate. Throws
892
+ * `InvalidParameter` when the destination instrument cannot prepare
893
+ * the SysEx (retrying cannot help), and `OutOfMemory` when the payload
894
+ * slots or the command queue are full (retry after a processed block).
895
+ * Mirrors the Node/Python/C-ABI `pushMidiSysex`.
495
896
  */
496
897
  pushMidiSysex(destinationId: number, data: Uint8Array, renderFrame = -1): void {
497
898
  this.native.pushMidiSysex(destinationId, data, renderFrame);
@@ -518,14 +919,34 @@ export class RealtimeEngine {
518
919
  return this.native.getTransportState();
519
920
  }
520
921
 
922
+ /** Queues an integrated-loudness reset; short-term and momentary windows are retained. */
923
+ resetMasterLoudnessMeter(renderFrame = -1): void {
924
+ this.native.resetMasterLoudnessMeter(renderFrame);
925
+ }
926
+
927
+ /** Reads the immutable factory value for a resolved insert parameter id. */
928
+ insertParameterConstructedValue(paramId: number): number {
929
+ return this.native.insertParameterConstructedValue(paramId);
930
+ }
931
+
521
932
  play(renderFrame = -1): void {
522
933
  this.native.play(renderFrame);
523
934
  }
524
935
 
936
+ /**
937
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
938
+ * centred pitch bend on every channel played since the last reset, so
939
+ * controller values set before a loop region are not restored at the wrap.
940
+ */
525
941
  stop(renderFrame = -1): void {
526
942
  this.native.stop(renderFrame);
527
943
  }
528
944
 
945
+ /**
946
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
947
+ * centred pitch bend on every channel played since the last reset, so
948
+ * controller values set before a loop region are not restored at the wrap.
949
+ */
529
950
  seekSample(timelineSample: number, renderFrame = -1): void {
530
951
  this.native.seekSample(timelineSample, renderFrame);
531
952
  }
@@ -540,11 +961,26 @@ export class RealtimeEngine {
540
961
  this.native.settleParameters();
541
962
  }
542
963
 
964
+ /** Snap only insert automation slots after structural replay. */
965
+ settleInsertParameters(): void {
966
+ this.native.settleInsertParameters();
967
+ }
968
+
543
969
  /** Drains queued commands on an offline/control-only engine immediately. */
544
970
  flushControlCommands(): void {
545
971
  this.native.flushControlCommands();
546
972
  }
547
973
 
974
+ /** Applies commands already due on a control-only mirror, retaining future commands. */
975
+ applyCommandsDueNowPreservingFuture(): void {
976
+ this.native.applyCommandsDueNowPreservingFuture();
977
+ }
978
+
979
+ /**
980
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
981
+ * centred pitch bend on every channel played since the last reset, so
982
+ * controller values set before a loop region are not restored at the wrap.
983
+ */
548
984
  seekPpq(ppq: number, renderFrame = -1): void {
549
985
  this.native.seekPpq(ppq, renderFrame);
550
986
  }
@@ -570,6 +1006,11 @@ export class RealtimeEngine {
570
1006
  return Number(this.native.sampleAtPpq(ppq));
571
1007
  }
572
1008
 
1009
+ /**
1010
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1011
+ * centred pitch bend on every channel played since the last reset, so
1012
+ * controller values set before a loop region are not restored at the wrap.
1013
+ */
573
1014
  setLoop(startPpq: number, endPpq: number, enabled = true): void {
574
1015
  this.native.setLoop(startPpq, endPpq, enabled);
575
1016
  }
@@ -582,11 +1023,11 @@ export class RealtimeEngine {
582
1023
  return this.native.parameterCount();
583
1024
  }
584
1025
 
585
- parameterInfoByIndex(index: number): EngineParameterInfo {
1026
+ parameterInfoByIndex(index: number): Required<EngineParameterInfo> {
586
1027
  return this.native.parameterInfoByIndex(index);
587
1028
  }
588
1029
 
589
- parameterInfo(id: number): EngineParameterInfo {
1030
+ parameterInfo(id: number): Required<EngineParameterInfo> {
590
1031
  return this.native.parameterInfo(id);
591
1032
  }
592
1033
 
@@ -671,24 +1112,28 @@ export class RealtimeEngine {
671
1112
  return this.native.clipCount();
672
1113
  }
673
1114
 
1115
+ /**
1116
+ * Normalizes each send's pre/post tap point to the integer the native layer
1117
+ * reads (defaults to post-fader when omitted). Shared by track lanes and
1118
+ * buses, which carry the same send shape.
1119
+ */
1120
+ private static normalizeSends(sends: EngineTrackSend[]): WasmEngineTrackSend[] {
1121
+ return sends.map((send) => ({
1122
+ ...send,
1123
+ // Post-fader (0) is the default for an omitted sendTiming.
1124
+ sendTiming: send.sendTiming === undefined ? 0 : sendTimingCode(send.sendTiming),
1125
+ }));
1126
+ }
1127
+
674
1128
  setTrackLanes(lanes: Array<number | EngineTrackLane>): void {
675
1129
  this.native.setTrackLanes(
676
1130
  lanes.map((lane) => {
677
1131
  if (typeof lane === 'number') {
678
1132
  return { trackId: lane };
679
1133
  }
680
- if (!lane.sends) {
681
- return lane;
682
- }
683
- // Normalize each send's pre/post tap point to the integer the native
684
- // layer reads (defaults to post-fader when omitted).
685
1134
  return {
686
1135
  ...lane,
687
- sends: lane.sends.map((send) => ({
688
- ...send,
689
- // Post-fader (0) is the default for an omitted sendTiming.
690
- sendTiming: send.sendTiming === undefined ? 0 : sendTimingCode(send.sendTiming),
691
- })),
1136
+ sends: lane.sends ? RealtimeEngine.normalizeSends(lane.sends) : undefined,
692
1137
  };
693
1138
  }),
694
1139
  );
@@ -703,7 +1148,43 @@ export class RealtimeEngine {
703
1148
  }
704
1149
 
705
1150
  setTrackBuses(buses: EngineBus[]): void {
706
- this.native.setTrackBuses(buses);
1151
+ // Array.isArray guards a caller-fabricated array-like (e.g. `{ length }`)
1152
+ // meant to probe the native array-length read: passing it through
1153
+ // unmodified lets that guard see the real (missing) length rather than
1154
+ // failing here on a `.map` that array-likes do not implement.
1155
+ this.native.setTrackBuses(
1156
+ Array.isArray(buses)
1157
+ ? buses.map((bus) => ({
1158
+ ...bus,
1159
+ sends: bus.sends ? RealtimeEngine.normalizeSends(bus.sends) : undefined,
1160
+ }))
1161
+ : (buses as WasmEngineBus[]),
1162
+ );
1163
+ }
1164
+
1165
+ /**
1166
+ * Keys one insert of a bus strip from a track lane or another bus
1167
+ * (ducking/sidechainRouter inserts). `sourceId` 0 removes the binding.
1168
+ */
1169
+ setBusSidechain(
1170
+ busId: number,
1171
+ insertIndex: number,
1172
+ sourceKind: SidechainSourceKind | number,
1173
+ sourceId: number,
1174
+ ): void {
1175
+ this.native.setBusSidechain(busId, insertIndex, sidechainSourceKindCode(sourceKind), sourceId);
1176
+ }
1177
+
1178
+ /**
1179
+ * Keys one insert of the master strip from a track lane or a bus. Same
1180
+ * source rules as {@link setBusSidechain}.
1181
+ */
1182
+ setMasterSidechain(
1183
+ insertIndex: number,
1184
+ sourceKind: SidechainSourceKind | number,
1185
+ sourceId: number,
1186
+ ): void {
1187
+ this.native.setMasterSidechain(insertIndex, sidechainSourceKindCode(sourceKind), sourceId);
707
1188
  }
708
1189
 
709
1190
  setBusStripJson(busId: number, sceneJson: string): void {
@@ -747,6 +1228,19 @@ export class RealtimeEngine {
747
1228
  this.native.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
748
1229
  }
749
1230
 
1231
+ /** Bus-strip counterpart of {@link setTrackStripEqBand}. */
1232
+ setBusStripEqBand(busId: number, bandIndex: number, band: EqBand | string): void {
1233
+ this.native.setBusStripEqBandJson(
1234
+ busId,
1235
+ bandIndex,
1236
+ typeof band === 'string' ? band : JSON.stringify(band),
1237
+ );
1238
+ }
1239
+
1240
+ setBusStripEqBandJson(busId: number, bandIndex: number, bandJson: string): void {
1241
+ this.native.setBusStripEqBandJson(busId, bandIndex, bandJson);
1242
+ }
1243
+
750
1244
  setMasterStripJson(sceneJson: string): void {
751
1245
  try {
752
1246
  JSON.parse(sceneJson);
@@ -778,10 +1272,12 @@ export class RealtimeEngine {
778
1272
 
779
1273
  /**
780
1274
  * Changes one track-strip insert parameter in realtime, addressed by the
781
- * processor's JSON-key parameter name (see {@link masteringInsertParamInfo}).
782
- * Applied at the next block head via the engine command queue; safe during
783
- * playback. Throws if the track, insert, or name is unknown, the param is not
784
- * realtime-safe, or the command queue is full.
1275
+ * processor's JSON-key parameter name — one of the entries
1276
+ * {@link masteringInsertParamInfo} reports with a non-null `id`; a
1277
+ * construction-only entry (`id` null) takes effect only when the insert is
1278
+ * built. Applied at the next block head via the engine command queue; safe
1279
+ * during playback. Throws if the track, insert, or name is unknown, the
1280
+ * param is not realtime-safe, or the command queue is full.
785
1281
  */
786
1282
  setTrackStripInsertParamByName(
787
1283
  trackId: number,
@@ -792,11 +1288,43 @@ export class RealtimeEngine {
792
1288
  this.native.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
793
1289
  }
794
1290
 
1291
+ /** Apply a live insert edit on this engine's owning thread without draining its command queue. */
1292
+ applyTrackStripInsertParamByNameNow(
1293
+ trackId: number,
1294
+ insertIndex: number,
1295
+ paramName: string,
1296
+ value: number,
1297
+ ): boolean {
1298
+ return this.native.applyTrackStripInsertParamByNameNow(trackId, insertIndex, paramName, value);
1299
+ }
1300
+
1301
+ /** Restore a retained insert value exactly after a strip scene is replayed. */
1302
+ restoreTrackStripInsertParamByName(
1303
+ trackId: number,
1304
+ insertIndex: number,
1305
+ paramName: string,
1306
+ value: number,
1307
+ ): void {
1308
+ this.native.restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
1309
+ }
1310
+
795
1311
  /** Master-strip counterpart of {@link setTrackStripInsertParamByName}. */
796
1312
  setMasterStripInsertParamByName(insertIndex: number, paramName: string, value: number): void {
797
1313
  this.native.setMasterStripInsertParamByName(insertIndex, paramName, value);
798
1314
  }
799
1315
 
1316
+ applyMasterStripInsertParamByNameNow(
1317
+ insertIndex: number,
1318
+ paramName: string,
1319
+ value: number,
1320
+ ): boolean {
1321
+ return this.native.applyMasterStripInsertParamByNameNow(insertIndex, paramName, value);
1322
+ }
1323
+
1324
+ restoreMasterStripInsertParamByName(insertIndex: number, paramName: string, value: number): void {
1325
+ this.native.restoreMasterStripInsertParamByName(insertIndex, paramName, value);
1326
+ }
1327
+
800
1328
  /** Bus-strip counterpart of {@link setTrackStripInsertParamByName}. */
801
1329
  setBusStripInsertParamByName(
802
1330
  busId: number,
@@ -807,6 +1335,42 @@ export class RealtimeEngine {
807
1335
  this.native.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
808
1336
  }
809
1337
 
1338
+ applyBusStripInsertParamByNameNow(
1339
+ busId: number,
1340
+ insertIndex: number,
1341
+ paramName: string,
1342
+ value: number,
1343
+ ): boolean {
1344
+ return this.native.applyBusStripInsertParamByNameNow(busId, insertIndex, paramName, value);
1345
+ }
1346
+
1347
+ restoreBusStripInsertParamByName(
1348
+ busId: number,
1349
+ insertIndex: number,
1350
+ paramName: string,
1351
+ value: number,
1352
+ ): void {
1353
+ this.native.restoreBusStripInsertParamByName(busId, insertIndex, paramName, value);
1354
+ }
1355
+
1356
+ /**
1357
+ * Forgets the remembered manual insert-parameter values of one track strip
1358
+ * and discards its queued insert edits. Call before {@link setTrackStripJson}
1359
+ * replaces the strip when its old values must not carry over; the setter
1360
+ * never does this itself, since a queued edit may already target the new chain.
1361
+ */
1362
+ clearTrackInsertParameterBases(trackId: number): void {
1363
+ this.native.clearTrackInsertParameterBases(trackId);
1364
+ }
1365
+
1366
+ clearBusInsertParameterBases(busId: number): void {
1367
+ this.native.clearBusInsertParameterBases(busId);
1368
+ }
1369
+
1370
+ clearMasterInsertParameterBases(): void {
1371
+ this.native.clearMasterInsertParameterBases();
1372
+ }
1373
+
810
1374
  /** Bus-strip counterpart of {@link setTrackStripInsertBypassed}. */
811
1375
  setBusStripInsertBypassed(
812
1376
  busId: number,
@@ -832,6 +1396,16 @@ export class RealtimeEngine {
832
1396
  * stages of the offline mastering chain (`repair.*`, `loudness`, and the
833
1397
  * match stages) have no insert form and no automation id: they buffer the
834
1398
  * entire signal by construction and do not run on the realtime path.
1399
+ *
1400
+ * The returned id uses the track's current positional lane selector. When
1401
+ * `setTrackLanes` successfully changes lane order or membership, the engine
1402
+ * remaps already queued and published track automation by track id, but it
1403
+ * cannot update a numeric id retained by the caller. Re-resolve every track
1404
+ * insert id after such a topology change before passing it to
1405
+ * `setAutomationLane`, `setParameter`, or `setParameterSmoothed`. Use
1406
+ * `setTrackStripInsertParamByName` when the operation needs a stable track
1407
+ * identity. Master and bus insert ids are separate and are not invalidated by
1408
+ * track-lane changes.
835
1409
  */
836
1410
  resolveTrackInsertAutomationId(trackId: number, insertIndex: number, paramName: string): number {
837
1411
  return this.native.resolveTrackInsertAutomationId(trackId, insertIndex, paramName);
@@ -845,6 +1419,42 @@ export class RealtimeEngine {
845
1419
  return this.native.resolveBusInsertAutomationId(busId, insertIndex, paramName);
846
1420
  }
847
1421
 
1422
+ /**
1423
+ * Resolves a hosted instrument's continuous parameter (by its JSON-key name)
1424
+ * to the reserved automation id usable with `setAutomationLane` /
1425
+ * `setParameter`, so an instrument parameter is driven at audio-block
1426
+ * precision exactly like a strip insert. Returns `-1` when the destination
1427
+ * has no bound instrument, the instrument exposes no automatable parameters,
1428
+ * or the name is unknown.
1429
+ *
1430
+ * For the NativeSynth ({@link setSynthInstrument}) the names are the
1431
+ * continuous {@link SynthPatch} fields: `gain`, `busDrive`, `cutoffHz`,
1432
+ * `resonanceQ`, `drive`, `keyTrack`, `envToCutoffCents`, `velToCutoffCents`,
1433
+ * `ampAttackMs`, `ampDecayMs`, `ampSustain`, `ampReleaseMs`,
1434
+ * `filterAttackMs`, `filterDecayMs`, `filterSustain`, `filterReleaseMs`,
1435
+ * `lfoRateHz`, `lfoToPitchCents`, `lfo2RateHz`, `glideMs`, `bodyMix`,
1436
+ * `stereoSpread`, `detuneCents`, `driftCents`, `pitchOffsetCents`,
1437
+ * `hpCutoffHz`, `sampleHoldHz`, `bitDepth`.
1438
+ *
1439
+ * Structural fields (`preset`, `engineMode`, `waveform`, `filterModel`,
1440
+ * `unison`, `polyphony`, `body`, `modRoutings`) are not automatable and
1441
+ * return `-1`: they resize voice pools or swap DSP topology, which is not
1442
+ * audio-thread safe. Rebind the instrument with a new patch instead.
1443
+ *
1444
+ * `gain`, `busDrive`, `cutoffHz`, `resonanceQ`, `envToCutoffCents`,
1445
+ * `lfoToPitchCents` and `pitchOffsetCents` reach voices that are already
1446
+ * sounding from the next block; the rest are cached into per-voice state at
1447
+ * note-on and take effect from the next note, so a lane that moves one of
1448
+ * them under a held note looks inert until the next one speaks — that is the
1449
+ * behaviour, not a dropped write.
1450
+ *
1451
+ * The id survives an unbind/rebind of the same destination and applies
1452
+ * nothing while that destination is unbound.
1453
+ */
1454
+ resolveInstrumentAutomationId(destinationId: number, paramName: string): number {
1455
+ return this.native.resolveInstrumentAutomationId(destinationId, paramName);
1456
+ }
1457
+
848
1458
  /** Sets a track lane strip's pan position in realtime (glitch-free). */
849
1459
  setTrackStripPan(trackId: number, pan: number): void {
850
1460
  this.native.setTrackStripPan(trackId, pan);
@@ -865,6 +1475,29 @@ export class RealtimeEngine {
865
1475
  this.native.setTrackStripDualPan(trackId, leftPan, rightPan);
866
1476
  }
867
1477
 
1478
+ /**
1479
+ * Sets a bus strip's output pan position in realtime (glitch-free). Throws
1480
+ * for an unknown bus or one wider than stereo.
1481
+ */
1482
+ setBusStripPan(busId: number, pan: number): void {
1483
+ this.native.setBusStripPan(busId, pan);
1484
+ }
1485
+
1486
+ /** Sets a bus strip's pan law in realtime. */
1487
+ setBusStripPanLaw(busId: number, panLaw: PanLawInput): void {
1488
+ this.native.setBusStripPanLaw(busId, panLawCode(panLaw));
1489
+ }
1490
+
1491
+ /** Sets a bus strip's pan mode in realtime. */
1492
+ setBusStripPanMode(busId: number, panMode: PanMode | number): void {
1493
+ this.native.setBusStripPanMode(busId, panModeCode(panMode));
1494
+ }
1495
+
1496
+ /** Sets a bus strip's dual-pan left/right positions in realtime. */
1497
+ setBusStripDualPan(busId: number, leftPan: number, rightPan: number): void {
1498
+ this.native.setBusStripDualPan(busId, leftPan, rightPan);
1499
+ }
1500
+
868
1501
  /**
869
1502
  * Sets a track lane strip's inter-channel alignment delay (whole samples).
870
1503
  * Adjusts strip latency, so PDC and reported graph latency are refreshed.
@@ -919,6 +1552,57 @@ export class RealtimeEngine {
919
1552
  return this.native.clipPageRequestOverflowCount();
920
1553
  }
921
1554
 
1555
+ /** Cumulative warp-stretch requests dropped because the native queue was full. */
1556
+ warpStretchOverflowCount(): number {
1557
+ return this.native.warpStretchOverflowCount();
1558
+ }
1559
+
1560
+ /**
1561
+ * Sets the number of concurrent time-stretch voices. `voices` must be an
1562
+ * integer in `[0, 64]`; a non-integer, negative, or larger value throws and
1563
+ * leaves the capacity unchanged. Default is 8. Capacity 0 disables
1564
+ * time-stretch, so every warped clip plays resampled instead and none of
1565
+ * that counts toward {@link warpStretchOverflowCount}. A change applied
1566
+ * while the engine is running restarts the splice state of any clip
1567
+ * stretching through a voice at that moment. Control-thread only.
1568
+ */
1569
+ setWarpVoiceCapacity(voices: number): void {
1570
+ this.native.setWarpVoiceCapacity(voices);
1571
+ }
1572
+
1573
+ /** Reads the current time-stretch voice capacity (default 8). */
1574
+ warpVoiceCapacity(): number {
1575
+ return this.native.warpVoiceCapacity();
1576
+ }
1577
+
1578
+ /**
1579
+ * Sets the clip-page look-ahead window in timeline frames.
1580
+ *
1581
+ * The player reports the pages it is *about to* read that are not resident
1582
+ * yet, so a streaming host can service them before the audio thread reaches
1583
+ * them. Without look-ahead a page miss is only reported after the read
1584
+ * already produced silence, which costs one block of silence at every page
1585
+ * boundary the host has not primed — the reason a sliding-window streamer
1586
+ * cannot keep a live playhead fed from miss reports alone.
1587
+ *
1588
+ * Look-ahead requests drain through the same `popClipPageRequest` queue and
1589
+ * are queued *after* the block's genuine misses, so a host that keeps only
1590
+ * the newest request per clip (as {@link ClipPageStreamer} does) tracks the
1591
+ * look-ahead frontier.
1592
+ *
1593
+ * `prepare` defaults this to half a second at the engine's sample rate. `0`
1594
+ * disables the look-ahead. A clip whose pages are all resident produces no
1595
+ * requests at all, with or without look-ahead. Safe to call during playback.
1596
+ */
1597
+ setClipPagePrefetchFrames(frames: number): void {
1598
+ this.native.setClipPagePrefetchFrames(frames);
1599
+ }
1600
+
1601
+ /** Current clip-page look-ahead window in timeline frames. */
1602
+ clipPagePrefetchFrames(): number {
1603
+ return this.native.clipPagePrefetchFrames();
1604
+ }
1605
+
922
1606
  setCaptureBuffer(numChannels: number, capacityFrames: number): void {
923
1607
  this.native.setCaptureBuffer(numChannels, capacityFrames);
924
1608
  }
@@ -1027,14 +1711,52 @@ export class RealtimeEngine {
1027
1711
  return this.native.processWithMonitor(channels);
1028
1712
  }
1029
1713
 
1030
- renderOffline(channels: Float32Array[], blockSize = 128): Float32Array[] {
1031
- return this.native.renderOffline(channels, blockSize);
1714
+ /**
1715
+ * Render `channels` offline from the current transport position. Requesting
1716
+ * more planes than `prepare` reserved throws an `InvalidParameter`
1717
+ * `SonareError` rather than returning silence that reads as a finished render.
1718
+ *
1719
+ * Set `finalize: false` to render one chunk of a longer timeline; see
1720
+ * {@link RenderOfflineRequest.finalize} and {@link finishOfflineRender}.
1721
+ */
1722
+ renderOffline(request: RenderOfflineRequest): Float32Array[];
1723
+ renderOffline(channels: Float32Array[], blockSize?: number): Float32Array[];
1724
+ renderOffline(
1725
+ channelsOrRequest: Float32Array[] | RenderOfflineRequest,
1726
+ blockSize = 128,
1727
+ ): Float32Array[] {
1728
+ const request = normalizeRenderOfflineRequest(channelsOrRequest, blockSize);
1729
+ return this.native.renderOffline(request.channels, request.blockSize, request.finalize);
1730
+ }
1731
+
1732
+ /**
1733
+ * End a chunked offline render: release every note the sequencer still holds
1734
+ * and flush the PDC / alignment delay lines. Required after
1735
+ * `renderOffline({ finalize: false })`; the finalizing form does it itself.
1736
+ *
1737
+ * Skipping it leaves every note still sounding at the last chunk held. On an
1738
+ * engine-internal instrument the tail simply never releases; on a destination
1739
+ * marked external ({@link RealtimeEngine.setMidiDestinationExternal}) the
1740
+ * note-ons already left through the external MIDI queue, so the note-offs
1741
+ * emitted here are the only ones the receiving device will get and the notes
1742
+ * otherwise hang outside the engine.
1743
+ */
1744
+ finishOfflineRender(): void {
1745
+ this.native.finishOfflineRender();
1032
1746
  }
1033
1747
 
1748
+ /**
1749
+ * Bounce the timeline to an interleaved buffer. `numChannels` above the
1750
+ * prepared channel count throws an `InvalidParameter` `SonareError`.
1751
+ */
1034
1752
  bounceOffline(options: EngineBounceOptions): EngineBounceResult {
1035
1753
  return this.native.bounceOffline(options);
1036
1754
  }
1037
1755
 
1756
+ /**
1757
+ * Freeze the current graph to audio. `numChannels` above the prepared channel
1758
+ * count throws an `InvalidParameter` `SonareError`.
1759
+ */
1038
1760
  freezeOffline(options: EngineFreezeOptions): EngineFreezeResult {
1039
1761
  return this.native.freezeOffline(options);
1040
1762
  }
@@ -1084,6 +1806,12 @@ export class RealtimeEngine {
1084
1806
  meterScratchRenderFrame(): number {
1085
1807
  return Number(this.native.meterScratchRenderFrame());
1086
1808
  }
1809
+ meterScratchInputPeakDbL(): number {
1810
+ return this.native.meterScratchInputPeakDbL();
1811
+ }
1812
+ meterScratchInputPeakDbR(): number {
1813
+ return this.native.meterScratchInputPeakDbR();
1814
+ }
1087
1815
  meterScratchValue(field: number): number {
1088
1816
  return this.native.meterScratchValue(field);
1089
1817
  }
@@ -1095,9 +1823,10 @@ export class RealtimeEngine {
1095
1823
  /**
1096
1824
  * Drains pending meter telemetry as per-plane (wide) records for a surround
1097
1825
  * target. Use this for a surround mix target; {@link drainMeterTelemetry}
1098
- * stays the stereo fast path. The two share one queue — call only one per
1099
- * target. The live AudioWorklet path owns the queue via the stereo drain, so
1100
- * this wide drain is for an offline (non-worklet) engine instance; per-plane
1826
+ * stays the stereo fast path. The two share one queue and each consumes every
1827
+ * target's records, so an engine uses only one of them. The live AudioWorklet
1828
+ * path owns the queue via the stereo drain, so this wide drain is for an
1829
+ * offline (non-worklet) engine instance; per-plane
1101
1830
  * surround meters are not delivered over the live worklet meter ring.
1102
1831
  */
1103
1832
  drainMeterTelemetryWide(maxRecords = 1024): EngineMeterTelemetryWide[] {
@@ -1144,9 +1873,19 @@ export class RealtimeEngine {
1144
1873
  return this.native.scopeScratchPointRight(index);
1145
1874
  }
1146
1875
 
1876
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
1147
1877
  destroy(): void {
1878
+ if (this.released) {
1879
+ return;
1880
+ }
1881
+ this.released = true;
1148
1882
  this.native.delete();
1149
1883
  }
1884
+
1885
+ /** Alias for {@link destroy}, matching embind's own release method name. */
1886
+ delete(): void {
1887
+ this.destroy();
1888
+ }
1150
1889
  }
1151
1890
 
1152
1891
  export class ClipPageProvider {