@libraz/libsonare 1.7.1 → 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 -170
  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 -5837
  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 -7329
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +3816 -1359
  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 -2142
  213. package/dist/worklet.d.ts.map +1 -0
  214. package/dist/worklet.js +2889 -466
  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 +285 -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 +64 -24
  255. package/src/project_class.ts +502 -29
  256. package/src/project_internal.ts +151 -42
  257. package/src/project_synth.ts +67 -1
  258. package/src/project_types.ts +302 -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 +750 -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 +1125 -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 +212 -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 +85 -28
  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 +307 -71
  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 +239 -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 +22 -0
  302. package/src/effects_transform.ts +0 -718
  303. package/src/mastering_repair.ts +0 -273
package/dist/worklet.js CHANGED
@@ -400,7 +400,21 @@ var SonareError = class extends Error {
400
400
  this.code = code;
401
401
  this.codeName = codeName;
402
402
  }
403
+ /**
404
+ * Brand-based `instanceof`: an error that carries the shape narrows here even
405
+ * when it is not literally an instance of this class. That is not a
406
+ * hypothetical — an error posted from the analysis worker arrives as a
407
+ * structured clone with its prototype gone, which a prototype-based
408
+ * `instanceof` would silently miss. Delegates to {@link isSonareError} so the
409
+ * two never disagree.
410
+ */
411
+ static [Symbol.hasInstance](value) {
412
+ return isSonareError(value);
413
+ }
403
414
  };
415
+ function isSonareError(value) {
416
+ return value instanceof Error && value.name === "SonareError" && typeof value.code === "number";
417
+ }
404
418
 
405
419
  // src/module_state.ts
406
420
  var wrappedModule = null;
@@ -428,6 +442,11 @@ function makeSonareError(raw, thrown) {
428
442
  message = info.message || message;
429
443
  }
430
444
  } catch {
445
+ } finally {
446
+ try {
447
+ raw.sonareReleaseException(thrown);
448
+ } catch {
449
+ }
431
450
  }
432
451
  return new SonareError(code, codeName, message);
433
452
  }
@@ -448,6 +467,10 @@ function wrapModuleErrors(raw) {
448
467
  if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || value instanceof Promise) {
449
468
  return value;
450
469
  }
470
+ const proto = Object.getPrototypeOf(value);
471
+ if (Array.isArray(value) || proto === Object.prototype || proto === null) {
472
+ return value;
473
+ }
451
474
  const objectValue = value;
452
475
  const cached = objectCache.get(objectValue);
453
476
  if (cached) {
@@ -579,6 +602,10 @@ var AUTOMATION_CURVE_VALUES = {
579
602
  hold: 2,
580
603
  "s-curve": 3
581
604
  };
605
+ var PROJECT_AUTOMATION_CURVE_VALUES = {
606
+ ...AUTOMATION_CURVE_VALUES,
607
+ scurve: 3
608
+ };
582
609
  var PAN_LAW_VALUES = {
583
610
  const3db: 0,
584
611
  "const-3db": 0,
@@ -604,6 +631,7 @@ var PAN_MODE_VALUES = {
604
631
  };
605
632
  var METER_TAP_VALUES = { preFader: 0, postFader: 1 };
606
633
  var SEND_TIMING_VALUES = { postFader: 0, preFader: 1 };
634
+ var SIDECHAIN_SOURCE_KIND_VALUES = { track: 0, bus: 1 };
607
635
  var TRACK_MONITOR_MODE_VALUES = { off: 0, pfl: 1, afl: 2 };
608
636
  function automationCurveCode(curve) {
609
637
  return resolveEnumOrdinal(curve, AUTOMATION_CURVE_VALUES, "automation curve");
@@ -622,12 +650,276 @@ function meterTapCode(tap) {
622
650
  function sendTimingCode(timing) {
623
651
  return resolveEnumOrdinal(timing, SEND_TIMING_VALUES, "send timing");
624
652
  }
653
+ function sidechainSourceKindCode(kind) {
654
+ return resolveEnumOrdinal(kind, SIDECHAIN_SOURCE_KIND_VALUES, "sidechain source kind");
655
+ }
625
656
  function trackMonitorModeCode(mode) {
626
657
  return resolveEnumOrdinal(mode, TRACK_MONITOR_MODE_VALUES, "track monitor mode");
627
658
  }
628
659
 
660
+ // src/sample_bank.ts
661
+ var SampleBank = class {
662
+ /** Create an empty bank. */
663
+ constructor() {
664
+ this.released = false;
665
+ this.native = new (projectModule()).SampleBank();
666
+ this.nativeId = this.native.id;
667
+ }
668
+ /**
669
+ * Copy mono float frames into the bank and return the new sample's index,
670
+ * which {@link SampleZoneDesc.sampleIndex} names. The frames are copied, so
671
+ * the array may be reused afterwards.
672
+ *
673
+ * Loop points are clamped inside the sample and a loop mode whose loop
674
+ * survives the clamp empty is dropped, so a malformed loop plays as an
675
+ * unlooped sample rather than as a wrap over nothing. An empty array, and a
676
+ * bank that would exceed 67,108,864 sample points, throw.
677
+ *
678
+ * A NaN or Inf frame, `fineTuneCents` or `sourceRate` throws too, and the
679
+ * bank is left unchanged. Such a value is unattributable once stored: the
680
+ * reader's interpolation spreads one bad frame across the whole sustain, and
681
+ * a bad tuning offset renders the voice silent with no error raised.
682
+ */
683
+ addSample(data, desc = {}) {
684
+ return this.native.addSample(data, desc);
685
+ }
686
+ /**
687
+ * Append a key/velocity rectangle to a keymap set, creating any sets below
688
+ * it. A patch names a set; the first zone in it covering a note is the one
689
+ * that sounds.
690
+ *
691
+ * Every bound defaults on its own (see {@link SampleZoneDesc}), so an empty
692
+ * rectangle is the whole keyboard at every velocity and narrowing one axis
693
+ * leaves the other whole. A `sampleIndex` the bank does not have, an inverted
694
+ * key or velocity range, and a `setIndex` at or above 4096 all throw.
695
+ */
696
+ addZone(zone = {}) {
697
+ const { setIndex, ...rest } = zone;
698
+ this.native.addZone(setIndex ?? 0, rest);
699
+ }
700
+ /** Samples added so far. */
701
+ sampleCount() {
702
+ return this.native.sampleCount();
703
+ }
704
+ /** Keymap sets the bank has (one past the highest index used). */
705
+ setCount() {
706
+ return this.native.setCount();
707
+ }
708
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
709
+ delete() {
710
+ if (this.released) {
711
+ return;
712
+ }
713
+ this.released = true;
714
+ this.native.delete();
715
+ }
716
+ /** Alias for {@link SampleBank.delete}, provided for cross-binding (Node) compatibility. */
717
+ destroy() {
718
+ this.delete();
719
+ }
720
+ };
721
+
722
+ // src/project_internal.ts
723
+ function normalizeSynthInstrument(patch) {
724
+ if (patch === null || typeof patch !== "object") {
725
+ return patch;
726
+ }
727
+ const { sampleBank, ...rest } = patch;
728
+ if (!sampleBank) {
729
+ return rest;
730
+ }
731
+ if (!(sampleBank instanceof SampleBank)) {
732
+ throw new TypeError("sampleBank must be a SampleBank instance");
733
+ }
734
+ if (sampleBank.released) {
735
+ throw new TypeError("sampleBank is destroyed");
736
+ }
737
+ return { ...rest, sampleBankId: sampleBank.nativeId };
738
+ }
739
+ function projectModule() {
740
+ const candidate = getSonareModule();
741
+ if (typeof candidate.projectAbiVersion !== "function" || candidate.Project === void 0) {
742
+ throw new Error("libsonare was built without arrangement (headless DAW) support");
743
+ }
744
+ return candidate;
745
+ }
746
+
747
+ // src/_chain_config.ts
748
+ function flattenChainConfig(config) {
749
+ const out = {};
750
+ const walk = (node, prefix) => {
751
+ for (const [key, value] of Object.entries(node)) {
752
+ const path = prefix ? `${prefix}.${key}` : key;
753
+ if (typeof value === "number" || typeof value === "boolean") {
754
+ out[path] = value;
755
+ } else if (value !== null && typeof value === "object") {
756
+ walk(value, path);
757
+ } else if (value !== void 0) {
758
+ throw new TypeError(`Mastering override '${path}' must be a number or boolean.`);
759
+ }
760
+ }
761
+ };
762
+ walk(config, "");
763
+ return out;
764
+ }
765
+
766
+ // src/playback_renderer.ts
767
+ function configJsonText(config) {
768
+ return typeof config === "string" ? config : JSON.stringify(config);
769
+ }
770
+ function nativeHrtf(hrtf) {
771
+ if (hrtf === void 0) {
772
+ return null;
773
+ }
774
+ if (!(hrtf instanceof HrtfSet)) {
775
+ throw new TypeError("hrtf must be an HrtfSet");
776
+ }
777
+ return hrtf.native;
778
+ }
779
+ var HrtfSet = class _HrtfSet {
780
+ constructor(native) {
781
+ this.released = false;
782
+ this.native = native;
783
+ }
784
+ /** Builds an HRTF set from SHRF v1 bytes; malformed data throws. */
785
+ static fromBytes(bytes) {
786
+ return new _HrtfSet(getSonareModule().createHrtfSet(bytes));
787
+ }
788
+ /** Releases the native handle. Idempotent, as the Node facade is. */
789
+ delete() {
790
+ if (this.released) {
791
+ return;
792
+ }
793
+ this.released = true;
794
+ this.native.delete();
795
+ }
796
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
797
+ destroy() {
798
+ this.delete();
799
+ }
800
+ };
801
+ var PlaybackRenderer = class {
802
+ constructor(options) {
803
+ this.released = false;
804
+ this.native = getSonareModule().createPlaybackRenderer(
805
+ configJsonText(options.config),
806
+ nativeHrtf(options.hrtf),
807
+ options.sampleRate ?? 48e3,
808
+ options.maxBlockSize ?? 1024
809
+ );
810
+ }
811
+ /**
812
+ * Renders one planar block; every plane must carry the same frame count, at
813
+ * most `maxBlockSize` (0 is a no-op). With a fixed input layout the plane
814
+ * count must equal {@link inputChannels}; with `input.layout: "auto"` it
815
+ * must be 1, 2, 6 or 8, and a change switches the input layout without
816
+ * changing the latency. Non-finite input samples are replaced with 0 and
817
+ * counted ({@link nonFiniteDiscardCount}).
818
+ */
819
+ processPlanar(planes) {
820
+ return this.native.processPlanar(planes);
821
+ }
822
+ /** Interleaved variant of {@link processPlanar}. Non-finite input samples are replaced with 0 and counted. */
823
+ processInterleaved(samples, inChannels) {
824
+ return this.native.processInterleaved(samples, inChannels);
825
+ }
826
+ /** Applies a complete configuration document; a changed prepare key throws. */
827
+ setConfig(config) {
828
+ this.native.setConfig(configJsonText(config));
829
+ }
830
+ /** The current complete configuration document. */
831
+ config() {
832
+ return JSON.parse(this.native.configJson());
833
+ }
834
+ /**
835
+ * Publishes the listener head orientation in degrees: right-handed,
836
+ * positive yaw turns the head right, positive pitch looks up, positive roll
837
+ * lowers the right ear. Ignored by a speakers target; a non-finite angle is
838
+ * ignored.
839
+ */
840
+ setHeadOrientation(yawDeg, pitchDeg = 0, rollDeg = 0) {
841
+ this.native.setHeadOrientation(yawDeg, pitchDeg, rollDeg);
842
+ }
843
+ /**
844
+ * Clears DSP state (filters, FIFOs, dynamics, convolution history, pending
845
+ * input-layout drains). Configuration and head pose are kept. Call it after
846
+ * a seek, from the thread that processes.
847
+ */
848
+ reset() {
849
+ this.native.reset();
850
+ }
851
+ /**
852
+ * Renderer latency in samples (headphones: near ear). Depends only on the
853
+ * target, the sample rate and distance compensation, never on realtime keys
854
+ * or the input layout.
855
+ */
856
+ latencySamples() {
857
+ return this.native.latencySamples();
858
+ }
859
+ /**
860
+ * Channel count of the active input layout. With `input.layout: "auto"`
861
+ * this follows the channel count of the most recent non-empty process call
862
+ * (2 before the first call).
863
+ */
864
+ inputChannels() {
865
+ return this.native.inputChannels();
866
+ }
867
+ /** Channel count of the output target. */
868
+ outputChannels() {
869
+ return this.native.outputChannels();
870
+ }
871
+ /**
872
+ * Inactive stages, per-stage latency, clamps, the active input layout, and
873
+ * the layout-switch / truncated-drain counters, as a plain object.
874
+ */
875
+ diagnostics() {
876
+ return JSON.parse(this.native.diagnosticsJson());
877
+ }
878
+ /** Non-finite input samples replaced with 0 since construction. */
879
+ nonFiniteDiscardCount() {
880
+ return this.native.nonFiniteDiscardCount();
881
+ }
882
+ /** Releases the native handle. Idempotent, as the Node facade is. */
883
+ delete() {
884
+ if (this.released) {
885
+ return;
886
+ }
887
+ this.released = true;
888
+ this.native.delete();
889
+ }
890
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
891
+ destroy() {
892
+ this.delete();
893
+ }
894
+ };
895
+
629
896
  // src/realtime_engine.ts
630
897
  var EXPECTED_ENGINE_ABI_VERSION = 3;
898
+ var UMP_WORD_MIN = -2147483648;
899
+ var UMP_WORD_MAX = 4294967295;
900
+ function assertUmpWords(fnName, words) {
901
+ if (!(words instanceof Uint32Array) && !Array.isArray(words)) {
902
+ throw new TypeError(`${fnName}: words must be a Uint32Array or a number array`);
903
+ }
904
+ if (words.length < 1 || words.length > 4) {
905
+ throw new RangeError(`${fnName}: words must hold 1 to 4 words`);
906
+ }
907
+ for (let i = 0; i < words.length; i++) {
908
+ const word = words[i];
909
+ if (typeof word !== "number" || !Number.isInteger(word) || word < UMP_WORD_MIN || word > UMP_WORD_MAX) {
910
+ throw new RangeError(`${fnName}: words[${i}] must be an integer 32-bit word`);
911
+ }
912
+ }
913
+ return words;
914
+ }
915
+ function normalizeRenderOfflineRequest(channelsOrRequest, blockSize) {
916
+ const request = Array.isArray(channelsOrRequest) ? { channels: channelsOrRequest, blockSize } : channelsOrRequest;
917
+ return {
918
+ channels: request.channels,
919
+ blockSize: request.blockSize ?? 128,
920
+ finalize: request.finalize ?? true
921
+ };
922
+ }
631
923
  function engineCapabilities() {
632
924
  const abiVersion = getSonareModule().engineAbiVersion();
633
925
  const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === "function";
@@ -643,8 +935,9 @@ function engineCapabilities() {
643
935
  mode: sharedArrayBuffer && atomics ? "sab" : "postMessage"
644
936
  };
645
937
  }
646
- var RealtimeEngine = class {
938
+ var RealtimeEngine = class _RealtimeEngine {
647
939
  constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024, maxChannels = 64) {
940
+ this.released = false;
648
941
  const module2 = getSonareModule();
649
942
  const capabilities = engineCapabilities();
650
943
  if (!capabilities.abiCompatible) {
@@ -660,6 +953,15 @@ var RealtimeEngine = class {
660
953
  maxChannels
661
954
  );
662
955
  }
956
+ /**
957
+ * Size the engine's queues and scratch for a sample rate and block size.
958
+ *
959
+ * `commandCapacity` must not exceed 65536 and `telemetryCapacity` must not
960
+ * exceed 16384; a larger value throws and leaves the engine untouched. The
961
+ * telemetry number is not a queue depth paid for one-for-one: the engine
962
+ * reserves that many meter records per metered lane, so its memory cost is
963
+ * far larger than the number given here.
964
+ */
663
965
  prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024, maxChannels = 64) {
664
966
  this.native.prepareWithChannels(
665
967
  sampleRate,
@@ -706,9 +1008,13 @@ var RealtimeEngine = class {
706
1008
  * scheduled MIDI clips routed to that destination render through the synth.
707
1009
  * Unknown preset names throw. An object patch's `destinationId` is a JS
708
1010
  * binding convenience, not part of the NativeSynth patch itself.
1011
+ *
1012
+ * An `engineMode: 'sample'` patch also carries the {@link SampleBank} its
1013
+ * keymap names. The synth takes a share of the bank, so it may be released
1014
+ * right after this call; a sample patch bound without one renders silence.
709
1015
  */
710
1016
  setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
711
- this.native.setSynthInstrument(destinationId, patch);
1017
+ this.native.setSynthInstrument(destinationId, normalizeSynthInstrument(patch));
712
1018
  }
713
1019
  /**
714
1020
  * Load (parse) SoundFont 2 bytes into the engine so SF2 instruments can be
@@ -761,6 +1067,113 @@ var RealtimeEngine = class {
761
1067
  midiCcBindingCount() {
762
1068
  return this.native.midiCcBindingCount();
763
1069
  }
1070
+ /**
1071
+ * Replace a destination instrument's controller profile with a named preset
1072
+ * (see {@link controllerProfileNames}). Installing a profile drops every
1073
+ * channel's accumulated axis values: the new bindings say nothing about what
1074
+ * the old ones had reached. An unknown name throws, and so does a destination
1075
+ * with no instrument or one whose instrument holds no profile.
1076
+ */
1077
+ setControllerProfile(destinationId, presetName) {
1078
+ this.native.setControllerProfile(destinationId, presetName);
1079
+ }
1080
+ /** Add one {@link ControllerBinding} on top of the destination's current profile. */
1081
+ bindController(destinationId, binding) {
1082
+ this.native.bindController(destinationId, binding);
1083
+ }
1084
+ /**
1085
+ * Drop every binding of the destination's controller profile. The instrument
1086
+ * keeps a profile; it resolves nothing until something is bound again.
1087
+ */
1088
+ clearControllerBindings(destinationId) {
1089
+ this.native.clearControllerBindings(destinationId);
1090
+ }
1091
+ controllerBindingCount(destinationId) {
1092
+ return this.native.controllerBindingCount(destinationId);
1093
+ }
1094
+ /**
1095
+ * Whether note-on velocity is expression for this instrument. No fixed
1096
+ * default is possible — a wind controller ships sending breath-derived
1097
+ * velocity on one model and a constant on the next — so each preset states it
1098
+ * and a host building its own profile sets it. When false the synth takes
1099
+ * every note at full scale and the bound axes carry the dynamics alone.
1100
+ */
1101
+ setControllerVelocityMeaningful(destinationId, meaningful) {
1102
+ this.native.setControllerVelocityMeaningful(destinationId, meaningful);
1103
+ }
1104
+ controllerVelocityMeaningful(destinationId) {
1105
+ return this.native.controllerVelocityMeaningful(destinationId);
1106
+ }
1107
+ /**
1108
+ * Say which note a value addressed to a whole MIDI channel belongs to when
1109
+ * several are sounding on it, for one per-note dimension
1110
+ * ({@link MPE_DIMENSIONS}, {@link NOTE_TRACKINGS}).
1111
+ *
1112
+ * Set per dimension because the useful answers differ: pressure following the
1113
+ * newest note while bend reaches every one is a real configuration, not a
1114
+ * mistake. MPE poses this question and declines to answer it, so this is a
1115
+ * choice rather than a rule — and it is read only inside an MPE zone, and
1116
+ * only while more than one note is sounding on the channel, which an MPE
1117
+ * sender avoids by giving each note its own member channel.
1118
+ *
1119
+ * Both arguments are required and are a name or its ordinal; an unknown
1120
+ * spelling is refused rather than resolved to a default, as are a destination
1121
+ * with no instrument and one whose instrument holds no controller profile.
1122
+ */
1123
+ setControllerNoteTracking(destinationId, dimension, tracking) {
1124
+ this.native.setControllerNoteTracking(destinationId, dimension, tracking);
1125
+ }
1126
+ /**
1127
+ * Read back {@link setControllerNoteTracking} for one dimension, as the
1128
+ * canonical name.
1129
+ */
1130
+ controllerNoteTracking(destinationId, dimension) {
1131
+ return this.native.controllerNoteTracking(destinationId, dimension);
1132
+ }
1133
+ /**
1134
+ * Set how one MIDI channel (0–15) of a destination's instrument treats a
1135
+ * note-on while another note on that channel is still held: `'poly'` takes a
1136
+ * new voice each time, `'mono-retrigger'` stops and restarts the note (what
1137
+ * GS MONO MODE and CC126 mean), `'mono-legato'` carries the sounding voice
1138
+ * and only moves its pitch — a wind player's slur, which no MIDI message can
1139
+ * reach by design.
1140
+ *
1141
+ * `'mono-legato'` is a request, not a guarantee: an engine whose exciter is
1142
+ * spent at the onset — anything struck or plucked — and a target pitch below
1143
+ * what the engine's delay line can hold both fall back to an ordinary note,
1144
+ * which {@link legatoFallbackCount} counts. A channel outside [0,15] and an
1145
+ * articulation outside the enum are refused rather than clamped, and so is a
1146
+ * destination with no instrument or one whose instrument has no articulation
1147
+ * of its own.
1148
+ */
1149
+ setArticulation(destinationId, channel, articulation) {
1150
+ this.native.setArticulation(destinationId, channel, articulation);
1151
+ }
1152
+ /**
1153
+ * Read back {@link setArticulation} as the canonical name. An ordinal this
1154
+ * build cannot spell is handed back as the number, the way every other enum
1155
+ * leaves this surface.
1156
+ */
1157
+ articulation(destinationId, channel) {
1158
+ return this.native.articulation(destinationId, channel);
1159
+ }
1160
+ /**
1161
+ * How many times a legato continuation was asked for on this destination and
1162
+ * refused, so the note started a voice of its own instead. Counted rather
1163
+ * than inferred: a refusal sounds like an ordinary note, so nothing in the
1164
+ * audio separates "this engine declines legato" from "the mode was never
1165
+ * set". Saturates at 4294967295 rather than wrapping — matching the C ABI, so
1166
+ * the same phrase reports the same number on every surface — after which it
1167
+ * reads as "at least this many".
1168
+ *
1169
+ * Throws on a destination with no instrument, and on one whose instrument has
1170
+ * no articulation of its own — the same two refusals
1171
+ * {@link setArticulation} keeps apart. Reporting 0 for the second would read
1172
+ * as "every slur took", which is the reading this counter exists to prevent.
1173
+ */
1174
+ legatoFallbackCount(destinationId) {
1175
+ return this.native.legatoFallbackCount(destinationId);
1176
+ }
764
1177
  /** Install/replace a live non-destructive MIDI-FX insert for one destination. */
765
1178
  setMidiFx(destinationId, configJson) {
766
1179
  this.native.setMidiFx(destinationId, configJson);
@@ -806,6 +1219,12 @@ var RealtimeEngine = class {
806
1219
  * block / animation frame. `maxRecords` caps the number of output events
807
1220
  * returned — the shared unit across every surface. Events past the cap stay
808
1221
  * queued for the next call (lossless); call again to drain the rest.
1222
+ *
1223
+ * One queued record lowers to at most 4 MIDI 1.0 messages (a MIDI 2.0
1224
+ * registered or assignable controller becomes CC 101/100 or 99/98 plus Data
1225
+ * Entry 6/38), so a positive `maxRecords` below 4 could never consume a record
1226
+ * and is rejected with an `InvalidParameter` `SonareError` instead of
1227
+ * returning nothing forever.
809
1228
  */
810
1229
  drainExternalMidi(maxRecords = 1024) {
811
1230
  return this.native.drainExternalMidi(maxRecords);
@@ -818,7 +1237,7 @@ var RealtimeEngine = class {
818
1237
  return this.native.externalMidiScratchDestinationId();
819
1238
  }
820
1239
  externalMidiScratchRenderFrame() {
821
- return this.native.externalMidiScratchRenderFrame();
1240
+ return Number(this.native.externalMidiScratchRenderFrame());
822
1241
  }
823
1242
  externalMidiScratchByteWord() {
824
1243
  return this.native.externalMidiScratchByteWord();
@@ -838,6 +1257,31 @@ var RealtimeEngine = class {
838
1257
  pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
839
1258
  this.native.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
840
1259
  }
1260
+ /**
1261
+ * Push a live MIDI pitch bend to the engine-owned MIDI input source.
1262
+ *
1263
+ * `bend14` is unsigned 14-bit with centre 8192 (0..16383) — the dimension is
1264
+ * not 7-bit, so a value past 16383 is refused rather than narrowed. The input
1265
+ * source must be enabled with {@link setMidiInputSource} first.
1266
+ */
1267
+ pushMidiInputPitchBend(group, channel, bend14, portTimeSamples = 0) {
1268
+ this.native.pushMidiInputPitchBend(group, channel, bend14, portTimeSamples);
1269
+ }
1270
+ /**
1271
+ * Push a live MIDI channel pressure to the engine-owned MIDI input source.
1272
+ * `pressure` is 7-bit (0..127). Under MPE this is the member channel's
1273
+ * per-note pressure.
1274
+ */
1275
+ pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples = 0) {
1276
+ this.native.pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples);
1277
+ }
1278
+ /**
1279
+ * Push a live MIDI polyphonic key pressure to the engine-owned MIDI input
1280
+ * source. `note` and `pressure` are 7-bit (0..127).
1281
+ */
1282
+ pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples = 0) {
1283
+ this.native.pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples);
1284
+ }
841
1285
  pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
842
1286
  this.native.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
843
1287
  }
@@ -853,9 +1297,53 @@ var RealtimeEngine = class {
853
1297
  pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
854
1298
  this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
855
1299
  }
856
- /** Queue one immediate MIDI 1.0 channel-voice UMP word for a destination. */
857
- pushMidiUmp(destinationId, word0, renderFrame = -1) {
858
- this.native.pushMidiUmp(destinationId, word0, renderFrame);
1300
+ /**
1301
+ * Queue an immediate (live) MIDI pitch bend to a MIDI destination. `bend14`
1302
+ * is unsigned 14-bit with centre 8192 (0..16383); `renderFrame` is the frame
1303
+ * to fire at, or -1 for immediate. Mirrors the Node/Python/C-ABI
1304
+ * `pushMidiPitchBend`.
1305
+ */
1306
+ pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame = -1) {
1307
+ this.native.pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame);
1308
+ }
1309
+ /**
1310
+ * Queue an immediate (live) MIDI channel pressure to a MIDI destination.
1311
+ * `pressure` is 7-bit (0..127); `renderFrame` is the frame to fire at, or -1
1312
+ * for immediate. Mirrors the Node/Python/C-ABI `pushMidiChannelPressure`.
1313
+ */
1314
+ pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame = -1) {
1315
+ this.native.pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame);
1316
+ }
1317
+ /**
1318
+ * Queue an immediate (live) MIDI polyphonic key pressure to a MIDI
1319
+ * destination. `note` and `pressure` are 7-bit (0..127); `renderFrame` is the
1320
+ * frame to fire at, or -1 for immediate. Mirrors the Node/Python/C-ABI
1321
+ * `pushMidiPolyPressure`.
1322
+ */
1323
+ pushMidiPolyPressure(destinationId, group, channel, note, pressure, renderFrame = -1) {
1324
+ this.native.pushMidiPolyPressure(destinationId, group, channel, note, pressure, renderFrame);
1325
+ }
1326
+ /**
1327
+ * Queue an immediate (live) raw UMP message to a MIDI destination. `words` is
1328
+ * 1 to 4 words, most significant first, and its length must match the message
1329
+ * type of `words[0]`. MIDI 2.0 channel-voice messages (MT 0x4) arrive at full
1330
+ * width; SysEx7 / data messages (MT 0x3 / 0x5) are refused, use
1331
+ * {@link pushMidiSysex}. Throws when the slot ring or command queue is full
1332
+ * (retry after a process block). `renderFrame` is the render-frame time to
1333
+ * apply, or -1 for immediate. A bare number is accepted as a one-word
1334
+ * message.
1335
+ */
1336
+ pushMidiUmp(destinationId, words, renderFrame = -1) {
1337
+ const list = typeof words === "number" ? [words] : words;
1338
+ this.native.pushMidiUmp(destinationId, assertUmpWords("pushMidiUmp", list), renderFrame);
1339
+ }
1340
+ /**
1341
+ * Push one raw UMP message (1 to 4 words) to the engine-owned MIDI input
1342
+ * source. The message rules match {@link pushMidiUmp}. `portTimeSamples` is
1343
+ * the port timestamp in samples.
1344
+ */
1345
+ pushMidiInputUmp(words, portTimeSamples = 0) {
1346
+ this.native.pushMidiInputUmp(assertUmpWords("pushMidiInputUmp", words), portTimeSamples);
859
1347
  }
860
1348
  /**
861
1349
  * Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
@@ -902,10 +1390,18 @@ var RealtimeEngine = class {
902
1390
  settleParameters() {
903
1391
  this.native.settleParameters();
904
1392
  }
1393
+ /** Snap only insert automation slots after structural replay. */
1394
+ settleInsertParameters() {
1395
+ this.native.settleInsertParameters();
1396
+ }
905
1397
  /** Drains queued commands on an offline/control-only engine immediately. */
906
1398
  flushControlCommands() {
907
1399
  this.native.flushControlCommands();
908
1400
  }
1401
+ /** Applies commands already due on a control-only mirror, retaining future commands. */
1402
+ applyCommandsDueNowPreservingFuture() {
1403
+ this.native.applyCommandsDueNowPreservingFuture();
1404
+ }
909
1405
  seekPpq(ppq, renderFrame = -1) {
910
1406
  this.native.seekPpq(ppq, renderFrame);
911
1407
  }
@@ -1001,6 +1497,18 @@ var RealtimeEngine = class {
1001
1497
  clipCount() {
1002
1498
  return this.native.clipCount();
1003
1499
  }
1500
+ /**
1501
+ * Normalizes each send's pre/post tap point to the integer the native layer
1502
+ * reads (defaults to post-fader when omitted). Shared by track lanes and
1503
+ * buses, which carry the same send shape.
1504
+ */
1505
+ static normalizeSends(sends) {
1506
+ return sends.map((send) => ({
1507
+ ...send,
1508
+ // Post-fader (0) is the default for an omitted sendTiming.
1509
+ sendTiming: send.sendTiming === void 0 ? 0 : sendTimingCode(send.sendTiming)
1510
+ }));
1511
+ }
1004
1512
  setTrackLanes(lanes) {
1005
1513
  this.native.setTrackLanes(
1006
1514
  lanes.map((lane) => {
@@ -1010,14 +1518,7 @@ var RealtimeEngine = class {
1010
1518
  if (!lane.sends) {
1011
1519
  return lane;
1012
1520
  }
1013
- return {
1014
- ...lane,
1015
- sends: lane.sends.map((send) => ({
1016
- ...send,
1017
- // Post-fader (0) is the default for an omitted sendTiming.
1018
- sendTiming: send.sendTiming === void 0 ? 0 : sendTimingCode(send.sendTiming)
1019
- }))
1020
- };
1521
+ return { ...lane, sends: _RealtimeEngine.normalizeSends(lane.sends) };
1021
1522
  })
1022
1523
  );
1023
1524
  }
@@ -1029,7 +1530,25 @@ var RealtimeEngine = class {
1029
1530
  this.native.setLaneSidechain(trackId, insertIndex, sourceTrackId);
1030
1531
  }
1031
1532
  setTrackBuses(buses) {
1032
- this.native.setTrackBuses(buses);
1533
+ this.native.setTrackBuses(
1534
+ Array.isArray(buses) ? buses.map(
1535
+ (bus) => bus.sends ? { ...bus, sends: _RealtimeEngine.normalizeSends(bus.sends) } : bus
1536
+ ) : buses
1537
+ );
1538
+ }
1539
+ /**
1540
+ * Keys one insert of a bus strip from a track lane or another bus
1541
+ * (ducking/sidechainRouter inserts). `sourceId` 0 removes the binding.
1542
+ */
1543
+ setBusSidechain(busId, insertIndex, sourceKind, sourceId) {
1544
+ this.native.setBusSidechain(busId, insertIndex, sidechainSourceKindCode(sourceKind), sourceId);
1545
+ }
1546
+ /**
1547
+ * Keys one insert of the master strip from a track lane or a bus. Same
1548
+ * source rules as {@link setBusSidechain}.
1549
+ */
1550
+ setMasterSidechain(insertIndex, sourceKind, sourceId) {
1551
+ this.native.setMasterSidechain(insertIndex, sidechainSourceKindCode(sourceKind), sourceId);
1033
1552
  }
1034
1553
  setBusStripJson(busId, sceneJson) {
1035
1554
  try {
@@ -1062,6 +1581,17 @@ var RealtimeEngine = class {
1062
1581
  setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass = false) {
1063
1582
  this.native.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
1064
1583
  }
1584
+ /** Bus-strip counterpart of {@link setTrackStripEqBand}. */
1585
+ setBusStripEqBand(busId, bandIndex, band) {
1586
+ this.native.setBusStripEqBandJson(
1587
+ busId,
1588
+ bandIndex,
1589
+ typeof band === "string" ? band : JSON.stringify(band)
1590
+ );
1591
+ }
1592
+ setBusStripEqBandJson(busId, bandIndex, bandJson) {
1593
+ this.native.setBusStripEqBandJson(busId, bandIndex, bandJson);
1594
+ }
1065
1595
  setMasterStripJson(sceneJson) {
1066
1596
  try {
1067
1597
  JSON.parse(sceneJson);
@@ -1085,22 +1615,59 @@ var RealtimeEngine = class {
1085
1615
  }
1086
1616
  /**
1087
1617
  * Changes one track-strip insert parameter in realtime, addressed by the
1088
- * processor's JSON-key parameter name (see {@link masteringInsertParamInfo}).
1089
- * Applied at the next block head via the engine command queue; safe during
1090
- * playback. Throws if the track, insert, or name is unknown, the param is not
1091
- * realtime-safe, or the command queue is full.
1618
+ * processor's JSON-key parameter name — one of the entries
1619
+ * {@link masteringInsertParamInfo} reports with a non-null `id`; a
1620
+ * construction-only entry (`id` null) takes effect only when the insert is
1621
+ * built. Applied at the next block head via the engine command queue; safe
1622
+ * during playback. Throws if the track, insert, or name is unknown, the
1623
+ * param is not realtime-safe, or the command queue is full.
1092
1624
  */
1093
1625
  setTrackStripInsertParamByName(trackId, insertIndex, paramName, value) {
1094
1626
  this.native.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
1095
1627
  }
1628
+ /** Apply a live insert edit on this engine's owning thread without draining its command queue. */
1629
+ applyTrackStripInsertParamByNameNow(trackId, insertIndex, paramName, value) {
1630
+ return this.native.applyTrackStripInsertParamByNameNow(trackId, insertIndex, paramName, value);
1631
+ }
1632
+ /** Restore a retained insert value exactly after a strip scene is replayed. */
1633
+ restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value) {
1634
+ this.native.restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
1635
+ }
1096
1636
  /** Master-strip counterpart of {@link setTrackStripInsertParamByName}. */
1097
1637
  setMasterStripInsertParamByName(insertIndex, paramName, value) {
1098
1638
  this.native.setMasterStripInsertParamByName(insertIndex, paramName, value);
1099
1639
  }
1640
+ applyMasterStripInsertParamByNameNow(insertIndex, paramName, value) {
1641
+ return this.native.applyMasterStripInsertParamByNameNow(insertIndex, paramName, value);
1642
+ }
1643
+ restoreMasterStripInsertParamByName(insertIndex, paramName, value) {
1644
+ this.native.restoreMasterStripInsertParamByName(insertIndex, paramName, value);
1645
+ }
1100
1646
  /** Bus-strip counterpart of {@link setTrackStripInsertParamByName}. */
1101
1647
  setBusStripInsertParamByName(busId, insertIndex, paramName, value) {
1102
1648
  this.native.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
1103
1649
  }
1650
+ applyBusStripInsertParamByNameNow(busId, insertIndex, paramName, value) {
1651
+ return this.native.applyBusStripInsertParamByNameNow(busId, insertIndex, paramName, value);
1652
+ }
1653
+ restoreBusStripInsertParamByName(busId, insertIndex, paramName, value) {
1654
+ this.native.restoreBusStripInsertParamByName(busId, insertIndex, paramName, value);
1655
+ }
1656
+ /**
1657
+ * Forgets the remembered manual insert-parameter values of one track strip
1658
+ * and discards its queued insert edits. Call before {@link setTrackStripJson}
1659
+ * replaces the strip when its old values must not carry over; the setter
1660
+ * never does this itself, since a queued edit may already target the new chain.
1661
+ */
1662
+ clearTrackInsertParameterBases(trackId) {
1663
+ this.native.clearTrackInsertParameterBases(trackId);
1664
+ }
1665
+ clearBusInsertParameterBases(busId) {
1666
+ this.native.clearBusInsertParameterBases(busId);
1667
+ }
1668
+ clearMasterInsertParameterBases() {
1669
+ this.native.clearMasterInsertParameterBases();
1670
+ }
1104
1671
  /** Bus-strip counterpart of {@link setTrackStripInsertBypassed}. */
1105
1672
  setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass = false) {
1106
1673
  this.native.setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass);
@@ -1111,6 +1678,15 @@ var RealtimeEngine = class {
1111
1678
  * Returns `-1` when the track, insert, or name is unknown. (The Python binding
1112
1679
  * raises a `SonareError` for an unknown id where Node/WASM return the `-1`
1113
1680
  * sentinel.)
1681
+ *
1682
+ * This trio is how a mastering processor gets time-varying automation: the
1683
+ * `eq.*`, `dynamics.*`, `saturation.*`, `spectral.*`, `stereo.*`,
1684
+ * `maximizer.*` and `multiband.*` processors are all available as strip
1685
+ * inserts, so placing one on a strip and resolving its parameter here drives
1686
+ * it at audio-block precision, live and offline alike. The whole-signal
1687
+ * stages of the offline mastering chain (`repair.*`, `loudness`, and the
1688
+ * match stages) have no insert form and no automation id: they buffer the
1689
+ * entire signal by construction and do not run on the realtime path.
1114
1690
  */
1115
1691
  resolveTrackInsertAutomationId(trackId, insertIndex, paramName) {
1116
1692
  return this.native.resolveTrackInsertAutomationId(trackId, insertIndex, paramName);
@@ -1121,6 +1697,41 @@ var RealtimeEngine = class {
1121
1697
  resolveBusInsertAutomationId(busId, insertIndex, paramName) {
1122
1698
  return this.native.resolveBusInsertAutomationId(busId, insertIndex, paramName);
1123
1699
  }
1700
+ /**
1701
+ * Resolves a hosted instrument's continuous parameter (by its JSON-key name)
1702
+ * to the reserved automation id usable with `setAutomationLane` /
1703
+ * `setParameter`, so an instrument parameter is driven at audio-block
1704
+ * precision exactly like a strip insert. Returns `-1` when the destination
1705
+ * has no bound instrument, the instrument exposes no automatable parameters,
1706
+ * or the name is unknown.
1707
+ *
1708
+ * For the NativeSynth ({@link setSynthInstrument}) the names are the
1709
+ * continuous {@link SynthPatch} fields: `gain`, `busDrive`, `cutoffHz`,
1710
+ * `resonanceQ`, `drive`, `keyTrack`, `envToCutoffCents`, `velToCutoffCents`,
1711
+ * `ampAttackMs`, `ampDecayMs`, `ampSustain`, `ampReleaseMs`,
1712
+ * `filterAttackMs`, `filterDecayMs`, `filterSustain`, `filterReleaseMs`,
1713
+ * `lfoRateHz`, `lfoToPitchCents`, `lfo2RateHz`, `glideMs`, `bodyMix`,
1714
+ * `stereoSpread`, `detuneCents`, `driftCents`, `pitchOffsetCents`,
1715
+ * `hpCutoffHz`, `sampleHoldHz`, `bitDepth`.
1716
+ *
1717
+ * Structural fields (`preset`, `engineMode`, `waveform`, `filterModel`,
1718
+ * `unison`, `polyphony`, `body`, `modRoutings`) are not automatable and
1719
+ * return `-1`: they resize voice pools or swap DSP topology, which is not
1720
+ * audio-thread safe. Rebind the instrument with a new patch instead.
1721
+ *
1722
+ * `gain`, `busDrive`, `cutoffHz`, `resonanceQ`, `envToCutoffCents`,
1723
+ * `lfoToPitchCents` and `pitchOffsetCents` reach voices that are already
1724
+ * sounding from the next block; the rest are cached into per-voice state at
1725
+ * note-on and take effect from the next note, so a lane that moves one of
1726
+ * them under a held note looks inert until the next one speaks — that is the
1727
+ * behaviour, not a dropped write.
1728
+ *
1729
+ * The id survives an unbind/rebind of the same destination and applies
1730
+ * nothing while that destination is unbound.
1731
+ */
1732
+ resolveInstrumentAutomationId(destinationId, paramName) {
1733
+ return this.native.resolveInstrumentAutomationId(destinationId, paramName);
1734
+ }
1124
1735
  /** Sets a track lane strip's pan position in realtime (glitch-free). */
1125
1736
  setTrackStripPan(trackId, pan) {
1126
1737
  this.native.setTrackStripPan(trackId, pan);
@@ -1137,6 +1748,25 @@ var RealtimeEngine = class {
1137
1748
  setTrackStripDualPan(trackId, leftPan, rightPan) {
1138
1749
  this.native.setTrackStripDualPan(trackId, leftPan, rightPan);
1139
1750
  }
1751
+ /**
1752
+ * Sets a bus strip's output pan position in realtime (glitch-free). Throws
1753
+ * for an unknown bus or one wider than stereo.
1754
+ */
1755
+ setBusStripPan(busId, pan) {
1756
+ this.native.setBusStripPan(busId, pan);
1757
+ }
1758
+ /** Sets a bus strip's pan law in realtime. */
1759
+ setBusStripPanLaw(busId, panLaw) {
1760
+ this.native.setBusStripPanLaw(busId, panLawCode(panLaw));
1761
+ }
1762
+ /** Sets a bus strip's pan mode in realtime. */
1763
+ setBusStripPanMode(busId, panMode) {
1764
+ this.native.setBusStripPanMode(busId, panModeCode(panMode));
1765
+ }
1766
+ /** Sets a bus strip's dual-pan left/right positions in realtime. */
1767
+ setBusStripDualPan(busId, leftPan, rightPan) {
1768
+ this.native.setBusStripDualPan(busId, leftPan, rightPan);
1769
+ }
1140
1770
  /**
1141
1771
  * Sets a track lane strip's inter-channel alignment delay (whole samples).
1142
1772
  * Adjusts strip latency, so PDC and reported graph latency are refreshed.
@@ -1177,6 +1807,52 @@ var RealtimeEngine = class {
1177
1807
  clipPageRequestOverflowCount() {
1178
1808
  return this.native.clipPageRequestOverflowCount();
1179
1809
  }
1810
+ /** Cumulative warp-stretch requests dropped because the native queue was full. */
1811
+ warpStretchOverflowCount() {
1812
+ return this.native.warpStretchOverflowCount();
1813
+ }
1814
+ /**
1815
+ * Sets the number of concurrent time-stretch voices. `voices` must be an
1816
+ * integer in `[0, 64]`; a non-integer, negative, or larger value throws and
1817
+ * leaves the capacity unchanged. Default is 8. Capacity 0 disables
1818
+ * time-stretch, so every warped clip plays resampled instead and none of
1819
+ * that counts toward {@link warpStretchOverflowCount}. A change applied
1820
+ * while the engine is running restarts the splice state of any clip
1821
+ * stretching through a voice at that moment. Control-thread only.
1822
+ */
1823
+ setWarpVoiceCapacity(voices) {
1824
+ this.native.setWarpVoiceCapacity(voices);
1825
+ }
1826
+ /** Reads the current time-stretch voice capacity (default 8). */
1827
+ warpVoiceCapacity() {
1828
+ return this.native.warpVoiceCapacity();
1829
+ }
1830
+ /**
1831
+ * Sets the clip-page look-ahead window in timeline frames.
1832
+ *
1833
+ * The player reports the pages it is *about to* read that are not resident
1834
+ * yet, so a streaming host can service them before the audio thread reaches
1835
+ * them. Without look-ahead a page miss is only reported after the read
1836
+ * already produced silence, which costs one block of silence at every page
1837
+ * boundary the host has not primed — the reason a sliding-window streamer
1838
+ * cannot keep a live playhead fed from miss reports alone.
1839
+ *
1840
+ * Look-ahead requests drain through the same `popClipPageRequest` queue and
1841
+ * are queued *after* the block's genuine misses, so a host that keeps only
1842
+ * the newest request per clip (as {@link ClipPageStreamer} does) tracks the
1843
+ * look-ahead frontier.
1844
+ *
1845
+ * `prepare` defaults this to half a second at the engine's sample rate. `0`
1846
+ * disables the look-ahead. A clip whose pages are all resident produces no
1847
+ * requests at all, with or without look-ahead. Safe to call during playback.
1848
+ */
1849
+ setClipPagePrefetchFrames(frames) {
1850
+ this.native.setClipPagePrefetchFrames(frames);
1851
+ }
1852
+ /** Current clip-page look-ahead window in timeline frames. */
1853
+ clipPagePrefetchFrames() {
1854
+ return this.native.clipPagePrefetchFrames();
1855
+ }
1180
1856
  setCaptureBuffer(numChannels, capacityFrames) {
1181
1857
  this.native.setCaptureBuffer(numChannels, capacityFrames);
1182
1858
  }
@@ -1238,15 +1914,66 @@ var RealtimeEngine = class {
1238
1914
  processPrepared(numFrames) {
1239
1915
  this.native.processPrepared(numFrames);
1240
1916
  }
1917
+ /**
1918
+ * Allocates the cue-bus counterpart of {@link prepareChannels}. Needed only
1919
+ * when PFL/AFL monitoring must reach a separate output: `processPrepared`
1920
+ * folds the cue bus into the program output, while
1921
+ * {@link processPreparedWithMonitor} keeps the two apart. Call once, off the
1922
+ * audio thread, with at least as many channels as `prepareChannels` got.
1923
+ */
1924
+ prepareMonitorChannels(numChannels, maxFrames) {
1925
+ this.native.prepareMonitorChannels(numChannels, maxFrames);
1926
+ }
1927
+ /**
1928
+ * Returns a Float32Array view onto the persistent cue-bus scratch for one
1929
+ * channel (valid for up to `numFrames`). Read it after
1930
+ * {@link processPreparedWithMonitor}. Re-acquire after WASM memory growth.
1931
+ */
1932
+ getMonitorChannelBuffer(channel, numFrames) {
1933
+ return this.native.getMonitorChannelBuffer(channel, numFrames);
1934
+ }
1935
+ /**
1936
+ * Runs the engine in place over the prepared scratch, writing the cue bus to
1937
+ * the monitor scratch instead of folding it into the program output.
1938
+ * Allocation-free: safe on the AudioWorklet render thread after
1939
+ * `prepareChannels` and `prepareMonitorChannels`.
1940
+ */
1941
+ processPreparedWithMonitor(numFrames) {
1942
+ this.native.processPreparedWithMonitor(numFrames);
1943
+ }
1241
1944
  processWithMonitor(channels) {
1242
1945
  return this.native.processWithMonitor(channels);
1243
1946
  }
1244
- renderOffline(channels, blockSize = 128) {
1245
- return this.native.renderOffline(channels, blockSize);
1947
+ renderOffline(channelsOrRequest, blockSize = 128) {
1948
+ const request = normalizeRenderOfflineRequest(channelsOrRequest, blockSize);
1949
+ return this.native.renderOffline(request.channels, request.blockSize, request.finalize);
1950
+ }
1951
+ /**
1952
+ * End a chunked offline render: release every note the sequencer still holds
1953
+ * and flush the PDC / alignment delay lines. Required after
1954
+ * `renderOffline({ finalize: false })`; the finalizing form does it itself.
1955
+ *
1956
+ * Skipping it leaves every note still sounding at the last chunk held. On an
1957
+ * engine-internal instrument the tail simply never releases; on a destination
1958
+ * marked external ({@link RealtimeEngine.setMidiDestinationExternal}) the
1959
+ * note-ons already left through the external MIDI queue, so the note-offs
1960
+ * emitted here are the only ones the receiving device will get and the notes
1961
+ * otherwise hang outside the engine.
1962
+ */
1963
+ finishOfflineRender() {
1964
+ this.native.finishOfflineRender();
1246
1965
  }
1966
+ /**
1967
+ * Bounce the timeline to an interleaved buffer. `numChannels` above the
1968
+ * prepared channel count throws an `InvalidParameter` `SonareError`.
1969
+ */
1247
1970
  bounceOffline(options) {
1248
1971
  return this.native.bounceOffline(options);
1249
1972
  }
1973
+ /**
1974
+ * Freeze the current graph to audio. `numChannels` above the prepared channel
1975
+ * count throws an `InvalidParameter` `SonareError`.
1976
+ */
1250
1977
  freezeOffline(options) {
1251
1978
  return this.native.freezeOffline(options);
1252
1979
  }
@@ -1340,9 +2067,18 @@ var RealtimeEngine = class {
1340
2067
  scopeScratchPointRight(index) {
1341
2068
  return this.native.scopeScratchPointRight(index);
1342
2069
  }
2070
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
1343
2071
  destroy() {
2072
+ if (this.released) {
2073
+ return;
2074
+ }
2075
+ this.released = true;
1344
2076
  this.native.delete();
1345
2077
  }
2078
+ /** Alias for {@link destroy}, matching embind's own release method name. */
2079
+ delete() {
2080
+ this.destroy();
2081
+ }
1346
2082
  };
1347
2083
  var ClipPageProvider = class {
1348
2084
  constructor(engine, id) {
@@ -1374,12 +2110,20 @@ var ClipPageProvider = class {
1374
2110
  // src/mixer.ts
1375
2111
  var Mixer = class _Mixer {
1376
2112
  constructor(mixer, blockSize) {
2113
+ this.released = false;
1377
2114
  this.mixer = mixer;
1378
2115
  this.blockSize = blockSize;
1379
2116
  }
1380
2117
  /**
1381
2118
  * Build a mixer from a scene JSON string.
1382
2119
  *
2120
+ * A strip's meters are sized when the strip is built, so this is where their
2121
+ * configuration is chosen: an optional `metering` object on the strip
2122
+ * (`enabled` / `lufs` / `truePeak` / `truePeakOversample`) selects it, and
2123
+ * leaving it out keeps the full default (LUFS + true peak at 4x, about 1.4 MB
2124
+ * per strip at 48 kHz). `{"enabled": false}` drops both meters for a strip
2125
+ * whose snapshots are never read.
2126
+ *
1383
2127
  * @param json - Scene JSON (strips, buses, sends, connections, inserts)
1384
2128
  * @param sampleRate - Sample rate in Hz (default: 48000)
1385
2129
  * @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
@@ -1486,15 +2230,65 @@ var Mixer = class _Mixer {
1486
2230
  }
1487
2231
  };
1488
2232
  }
1489
- /** Number of strips in the mixer (e.g. strips loaded from the scene). */
1490
- stripCount() {
1491
- return this.mixer.stripCount();
1492
- }
1493
2233
  /**
1494
- * Schedule sample-accurate insert-parameter automation on a strip's insert.
2234
+ * Turn the master-output meter on or off.
1495
2235
  *
1496
- * @param stripIndex - Strip index in `[0, stripCount())`
1497
- * @param insertIndex - Index into the strip's combined insert sequence
2236
+ * While on, every {@link MixerRealtimeBuffer.process} call meters the stereo
2237
+ * master it just produced, so a caller reads {@link meterSnapshot} instead of
2238
+ * copying the output and measuring it again. `truePeakDb*` is an inter-sample
2239
+ * peak taken after oversampling (ITU-R BS.1770-4 Annex 2 requires at least
2240
+ * 4x), which is a different and higher quantity than the sample peak.
2241
+ *
2242
+ * Enabling resets the meter, so a reading never mixes in audio from a period
2243
+ * when metering was off.
2244
+ *
2245
+ * @param enabled - Whether to meter the master output.
2246
+ * @param truePeakOversample - 0 (= 4x) or a power of two in [1, 16].
2247
+ */
2248
+ configureMeter(enabled, truePeakOversample = 4) {
2249
+ this.mixer.configureMeter(enabled, truePeakOversample);
2250
+ }
2251
+ /**
2252
+ * Latest master-output meter reading, describing the most recently metered
2253
+ * block. All dB fields are finite and floored at -120.
2254
+ *
2255
+ * @throws When the meter has never been enabled.
2256
+ */
2257
+ meterSnapshot() {
2258
+ return this.mixer.meterSnapshot();
2259
+ }
2260
+ /**
2261
+ * Latch the latest meter reading into the mixer's internal scratch so
2262
+ * {@link meterScratchValue} can read it back one number at a time.
2263
+ *
2264
+ * This is the allocation-free form of {@link meterSnapshot}, for an audio
2265
+ * render callback that must not create a JS object per interval. It returns
2266
+ * `false` instead of throwing when the meter has never been enabled.
2267
+ *
2268
+ * @returns Whether a reading was latched.
2269
+ */
2270
+ latchMeterSnapshot() {
2271
+ return this.mixer.latchMeterSnapshot();
2272
+ }
2273
+ /**
2274
+ * Read one field of the snapshot latched by {@link latchMeterSnapshot}.
2275
+ *
2276
+ * @param field - `0` peakDbL, `1` peakDbR, `2` rmsDbL, `3` rmsDbR,
2277
+ * `4` correlation, `5` truePeakDbL, `6` truePeakDbR. Any other index
2278
+ * reads `0`.
2279
+ */
2280
+ meterScratchValue(field) {
2281
+ return this.mixer.meterScratchValue(field);
2282
+ }
2283
+ /** Number of strips in the mixer (e.g. strips loaded from the scene). */
2284
+ stripCount() {
2285
+ return this.mixer.stripCount();
2286
+ }
2287
+ /**
2288
+ * Schedule sample-accurate insert-parameter automation on a strip's insert.
2289
+ *
2290
+ * @param stripIndex - Strip index in `[0, stripCount())`
2291
+ * @param insertIndex - Index into the strip's combined insert sequence
1498
2292
  * (`[pre-inserts... post-inserts...]`)
1499
2293
  * @param paramId - Processor-specific parameter id
1500
2294
  * @param samplePos - Absolute samples from the start of processing (the mixer
@@ -1523,6 +2317,17 @@ var Mixer = class _Mixer {
1523
2317
  const index = this.mixer.stripById(id);
1524
2318
  return index < 0 ? null : index;
1525
2319
  }
2320
+ /**
2321
+ * Add a channel strip to the mixer topology. `metering` configures the strip's
2322
+ * pre/post taps; omitting it keeps the full default (LUFS + true peak at 4x,
2323
+ * about 1.4 MB per strip at 48 kHz). Marks the routing graph dirty; call
2324
+ * {@link compile} (or {@link processStereo}) to rebuild.
2325
+ *
2326
+ * @throws If the id is already taken, or `truePeakOversample` is outside `[0, 16]`
2327
+ */
2328
+ addStrip(id, metering = {}) {
2329
+ this.mixer.addStrip(id, metering);
2330
+ }
1526
2331
  /**
1527
2332
  * Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
1528
2333
  * `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
@@ -1588,6 +2393,30 @@ var Mixer = class _Mixer {
1588
2393
  setWidth(stripIndex, width) {
1589
2394
  this.mixer.setWidth(stripIndex, width);
1590
2395
  }
2396
+ /**
2397
+ * Snap the strip's input-trim, fader, pan and width smoothers to the values
2398
+ * already set on it, so the next processed block opens at those values
2399
+ * instead of gliding to them over the smoothing window (~5 ms).
2400
+ *
2401
+ * Call it after configuring a strip and before rendering a finite buffer: a
2402
+ * strip is smoothed for a live fader, and an offline render that does not
2403
+ * settle carries that glide as a level and image sweep across the head of
2404
+ * its output. Unlike a reset it clears nothing — automation, meters and
2405
+ * insert state are untouched.
2406
+ *
2407
+ * @param stripIndex - Strip index in `[0, stripCount())`
2408
+ *
2409
+ * @example
2410
+ * ```typescript
2411
+ * mixer.setFaderDb(0, -3);
2412
+ * mixer.setPan(0, 0.3);
2413
+ * mixer.settle(0);
2414
+ * const { left, right } = mixer.processStereo([dryLeft], [dryRight]);
2415
+ * ```
2416
+ */
2417
+ settle(stripIndex) {
2418
+ this.mixer.settle(stripIndex);
2419
+ }
1591
2420
  /** Set the strip's mute state. */
1592
2421
  setMuted(stripIndex, muted) {
1593
2422
  this.mixer.setMuted(stripIndex, muted);
@@ -1631,7 +2460,10 @@ var Mixer = class _Mixer {
1631
2460
  }
1632
2461
  /**
1633
2462
  * Set the strip's surround pan position, used when it feeds a >2-channel bus.
1634
- * Stored on the scene; inert until the surround DSP path applies it.
2463
+ *
2464
+ * Applied when the engine's track mixer renders this strip's lane into a
2465
+ * destination with more than two channels. This stereo-only mixer's own
2466
+ * block entry points ignore it.
1635
2467
  */
1636
2468
  setSurroundPan(stripIndex, pan) {
1637
2469
  this.mixer.setSurroundPan(stripIndex, pan);
@@ -1697,6 +2529,59 @@ var Mixer = class _Mixer {
1697
2529
  busMeter(busId) {
1698
2530
  return this.mixer.busMeter(busId);
1699
2531
  }
2532
+ /**
2533
+ * Number of blocks in which the strip discarded recursive state because a
2534
+ * non-finite value had reached it.
2535
+ *
2536
+ * Advisory telemetry, and the only thing that separates a degraded strip
2537
+ * from a clean one. A discard returns the affected state to its
2538
+ * post-reset value, so the strip recovers in silence and the output stays
2539
+ * finite and in range while carrying samples unrelated to the input;
2540
+ * nothing else reports that this happened.
2541
+ *
2542
+ * The count covers the strip's own state, its EQ, every insert it owns and
2543
+ * both of its meters. None of those is separately addressable here, so a
2544
+ * discard inside one is observable only through this number -- and a
2545
+ * meter that loses its loudness window then reports the floor, which is
2546
+ * exactly what a genuinely silent strip reports, so nothing else
2547
+ * distinguishes the two.
2548
+ *
2549
+ * A meter's own discard lags by one block: it checks its loudness state at
2550
+ * the top of a block, before consuming that block's samples, so the block
2551
+ * that corrupts it is not the block the count moves on -- read this again
2552
+ * after one more block has processed. The EQ and inserts have no such lag;
2553
+ * they discard at the end of their own process, in the same block that
2554
+ * carried the poison.
2555
+ *
2556
+ * Cumulative since the strip was created and never cleared, so two
2557
+ * readings bracket a span of audio. The unit is one processed block, never
2558
+ * a channel, so a stereo block that discards on both channels adds one and
2559
+ * the number does not depend on a dimension the caller did not choose.
2560
+ *
2561
+ * @param stripIndex - Strip index in `[0, stripCount())`
2562
+ */
2563
+ stripNonFiniteDiscardCount(stripIndex) {
2564
+ return this.mixer.stripNonFiniteDiscardCount(stripIndex);
2565
+ }
2566
+ /**
2567
+ * Number of blocks in which a bus discarded recursive state because a
2568
+ * non-finite value had reached it. Same contract as
2569
+ * {@link stripNonFiniteDiscardCount}, for a bus: covers every insert the
2570
+ * bus owns and its meter, neither separately addressable, so a discard
2571
+ * inside one is observable only here. Cumulative across graph recompiles
2572
+ * -- the count lives with the bus, not the compiled node, so an unrelated
2573
+ * edit elsewhere in the mixer does not reset it.
2574
+ *
2575
+ * A bus's DSP record is created by the first {@link compile}. Throws for a
2576
+ * bus that has been declared with {@link addBus} but never compiled --
2577
+ * reading zero there would read as clean, and it is not. Also throws for
2578
+ * an unknown bus id.
2579
+ *
2580
+ * @param busId - Bus id, as passed to {@link addBus} or declared in scene JSON
2581
+ */
2582
+ busNonFiniteDiscardCount(busId) {
2583
+ return this.mixer.busNonFiniteDiscardCount(busId);
2584
+ }
1700
2585
  /**
1701
2586
  * Schedule sample-accurate fader automation on a strip.
1702
2587
  *
@@ -1751,6 +2636,11 @@ var Mixer = class _Mixer {
1751
2636
  /**
1752
2637
  * Read up to `maxPoints` of a strip's most recent goniometer samples
1753
2638
  * (oldest to newest).
2639
+ *
2640
+ * `maxPoints` must be a finite non-negative integer; anything else throws an
2641
+ * `InvalidParameter` error. It is a request rather than an allocation size —
2642
+ * a value beyond the strip's goniometer ring simply returns every point the
2643
+ * ring holds.
1754
2644
  */
1755
2645
  readGoniometerLatest(stripIndex, maxPoints) {
1756
2646
  return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
@@ -1786,8 +2676,12 @@ var Mixer = class _Mixer {
1786
2676
  }
1787
2677
  return this.mixer.drainTailStereo(numSamples);
1788
2678
  }
1789
- /** Release the underlying WASM object. Safe to call only once. */
2679
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
1790
2680
  delete() {
2681
+ if (this.released) {
2682
+ return;
2683
+ }
2684
+ this.released = true;
1791
2685
  this.mixer.delete();
1792
2686
  }
1793
2687
  /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
@@ -1804,10 +2698,19 @@ var RealtimeVoiceChanger = class {
1804
2698
  * explicit {@link prepare} lifecycle for callers that configure later.
1805
2699
  */
1806
2700
  constructor(config = "neutral-monitor", sampleRate, maxBlockSize = 128, channels = 1) {
2701
+ this.released = false;
1807
2702
  const module2 = getSonareModule();
1808
2703
  this.changer = module2.createRealtimeVoiceChanger(config);
1809
2704
  if (sampleRate !== void 0) {
1810
- this.changer.prepare(sampleRate, maxBlockSize, channels);
2705
+ let prepared = false;
2706
+ try {
2707
+ this.changer.prepare(sampleRate, maxBlockSize, channels);
2708
+ prepared = true;
2709
+ } finally {
2710
+ if (!prepared) {
2711
+ this.changer.delete();
2712
+ }
2713
+ }
1811
2714
  }
1812
2715
  }
1813
2716
  prepare(sampleRate, maxBlockSize = 128, channels = 1) {
@@ -1833,6 +2736,34 @@ var RealtimeVoiceChanger = class {
1833
2736
  latencySamples() {
1834
2737
  return this.changer.latencySamples();
1835
2738
  }
2739
+ /**
2740
+ * Channel-blocks in which the chain discarded its own state because a
2741
+ * non-finite value had reached it.
2742
+ *
2743
+ * Advisory telemetry, and the only thing that separates a degraded stream
2744
+ * from a clean one. Every stage of this chain leaves an in-domain finite
2745
+ * value where a non-finite one was — the input scrub and the
2746
+ * inter-sample-peak limiter substitute silence, the sample-domain limiter
2747
+ * folds an infinity onto its ceiling — so the output stays finite, in range
2748
+ * and free of any error while carrying samples unrelated to the input. A
2749
+ * non-zero count is what says the samples in between were not computed from
2750
+ * what you supplied.
2751
+ *
2752
+ * Monotonic for the lifetime of the instance. The unit is one processed
2753
+ * block, never a channel, so a stereo block that discards on both channels
2754
+ * adds one and the number does not depend on a dimension you did not choose.
2755
+ *
2756
+ * @example
2757
+ * ```ts
2758
+ * changer.processInterleaved(block, 2);
2759
+ * if (changer.nonFiniteDiscardCount() > 0) {
2760
+ * // the audio just produced is not a function of `block`
2761
+ * }
2762
+ * ```
2763
+ */
2764
+ nonFiniteDiscardCount() {
2765
+ return this.changer.nonFiniteDiscardCount();
2766
+ }
1836
2767
  /**
1837
2768
  * Monotonically increases whenever {@link prepare} can replace the native
1838
2769
  * scratch buffers. Cached WASM heap views must be reacquired after it changes.
@@ -2000,9 +2931,151 @@ var RealtimeVoiceChanger = class {
2000
2931
  }
2001
2932
  };
2002
2933
  }
2934
+ /** Releases the native handle. Idempotent, as the Node facade is. */
2003
2935
  delete() {
2936
+ if (this.released) {
2937
+ return;
2938
+ }
2939
+ this.released = true;
2004
2940
  this.changer.delete();
2005
2941
  }
2942
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
2943
+ destroy() {
2944
+ this.delete();
2945
+ }
2946
+ };
2947
+
2948
+ // src/streaming_processors.ts
2949
+ var StreamingMasteringChain = class {
2950
+ constructor(config) {
2951
+ this.released = false;
2952
+ const module2 = getSonareModule();
2953
+ const { loudnessStaticGainDb, loudnessStaticGainPeakDb, ...chainConfig } = config;
2954
+ this.chain = module2.createStreamingMasteringChain({
2955
+ __flatParams: flattenChainConfig(chainConfig),
2956
+ loudnessStaticGainDb,
2957
+ loudnessStaticGainPeakDb
2958
+ });
2959
+ }
2960
+ /**
2961
+ * Initialize processors for the given sample rate and block layout.
2962
+ *
2963
+ * @param sampleRate - Sample rate in Hz
2964
+ * @param maxBlockSize - Maximum block size per process call
2965
+ * @param numChannels - 1 (mono) or 2 (stereo)
2966
+ */
2967
+ prepare(sampleRate, maxBlockSize, numChannels) {
2968
+ this.chain.prepare(sampleRate, maxBlockSize, numChannels);
2969
+ }
2970
+ /**
2971
+ * Process one mono block, returning the processed samples (same length).
2972
+ */
2973
+ processMono(samples) {
2974
+ return this.chain.processMono(samples);
2975
+ }
2976
+ /**
2977
+ * Process one stereo block, returning the processed channels.
2978
+ */
2979
+ processStereo(left, right) {
2980
+ if (left.length !== right.length) {
2981
+ throw new Error("Stereo channel lengths must match.");
2982
+ }
2983
+ return this.chain.processStereo(left, right);
2984
+ }
2985
+ /**
2986
+ * Emit delayed audio and finite processor tails after the final mono block.
2987
+ * Call until this returns an empty array. The initial `latencySamples()`
2988
+ * samples of the concatenated stream are delayed and should be discarded for
2989
+ * time-aligned output.
2990
+ */
2991
+ flushMono() {
2992
+ return this.chain.flushMono();
2993
+ }
2994
+ /** Stereo counterpart of {@link flushMono}. */
2995
+ flushStereo() {
2996
+ return this.chain.flushStereo();
2997
+ }
2998
+ /** Reset all processor state without rebuilding. */
2999
+ reset() {
3000
+ this.chain.reset();
3001
+ }
3002
+ /** Total reported latency in samples across all active processors. */
3003
+ latencySamples() {
3004
+ return this.chain.latencySamples();
3005
+ }
3006
+ /** Ordered stage names that will run (e.g. `"eq.tilt"`). */
3007
+ stageNames() {
3008
+ return this.chain.stageNames();
3009
+ }
3010
+ /**
3011
+ * Samples a stage replaced with a finite in-domain one, keeping the output
3012
+ * finite and in range.
3013
+ *
3014
+ * A non-finite sample supplied by the caller is rejected before any stage
3015
+ * runs, so a replacement is always of a value a stage itself produced.
3016
+ *
3017
+ * Only the true-peak limiters replace anything, so with the maximizer's
3018
+ * limiter and the loudness stage both disabled a zero here means no stage was
3019
+ * able to replace anything rather than that nothing needed replacing.
3020
+ *
3021
+ * Cumulative over every block since {@link prepare}, and aggregated over the
3022
+ * stages and channels, so it identifies neither which block nor which stage.
3023
+ * Read it per block and compare against the previous reading to localize one.
3024
+ *
3025
+ * {@link prepare} rebuilds the stages and so clears it; {@link reset} does
3026
+ * not, because it drops processor state without rebuilding.
3027
+ *
3028
+ * @example
3029
+ * ```typescript
3030
+ * chain.processMono(block);
3031
+ * if (chain.nonFiniteSubstitutionCount() > previous) {
3032
+ * // the block just produced is not derived from `block` everywhere
3033
+ * }
3034
+ * ```
3035
+ */
3036
+ nonFiniteSubstitutionCount() {
3037
+ return this.chain.nonFiniteSubstitutionCount();
3038
+ }
3039
+ /**
3040
+ * Processing calls in which a stage discarded its own recursive state
3041
+ * because a non-finite value had reached it.
3042
+ *
3043
+ * The companion to {@link nonFiniteSubstitutionCount}, and not the same
3044
+ * measurement -- a caller who assumes they are will read one and think
3045
+ * they have the other. That one counts SAMPLES a stage replaced and so
3046
+ * sums across stages; a discard is a whole stage returning to its
3047
+ * post-reset value and is counted once per call however many stages did
3048
+ * it. A stage may run more than once per call, which is why this is a
3049
+ * delta over the call and never a sum.
3050
+ *
3051
+ * Non-finite input is rejected before any stage runs, so what a stage
3052
+ * discards is always state it produced itself -- a finite sample large
3053
+ * enough to overflow inside a filter, most often. Unlike the substitution
3054
+ * count every stage can contribute, so a zero here means no stage
3055
+ * discarded rather than that none could.
3056
+ *
3057
+ * Both {@link processMono}/{@link processStereo} and
3058
+ * {@link flushMono}/{@link flushStereo} count, since a flush drives the
3059
+ * same stages. {@link prepare} rebuilds the stages and so clears it (as it
3060
+ * does {@link nonFiniteSubstitutionCount}, so the two counters on one
3061
+ * handle share an epoch); {@link reset} does not, because it drops
3062
+ * processor state without rebuilding.
3063
+ */
3064
+ nonFiniteDiscardCount() {
3065
+ return this.chain.nonFiniteDiscardCount();
3066
+ }
3067
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
3068
+ delete() {
3069
+ if (this.released) {
3070
+ return;
3071
+ }
3072
+ this.released = true;
3073
+ this.chain.delete();
3074
+ }
3075
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
3076
+ destroy() {
3077
+ this.delete();
3078
+ }
2006
3079
  };
2007
3080
 
2008
3081
  // src/index.ts
@@ -2053,11 +3126,11 @@ function buildTempoSync(tempoBpm, timeSignature, tempoSegments, timeSignatureSeg
2053
3126
  };
2054
3127
  }
2055
3128
  function resolveTargetId(target) {
2056
- if (typeof target === "number") {
2057
- return target;
3129
+ const value = typeof target === "number" ? target : target.trim() === "" ? Number.NaN : Number(target);
3130
+ if (!Number.isInteger(value)) {
3131
+ throw new RangeError(`target id must be an integer, got ${JSON.stringify(target)}`);
2058
3132
  }
2059
- const parsed = Number.parseInt(target, 10);
2060
- return Number.isFinite(parsed) ? parsed : 0;
3133
+ return value;
2061
3134
  }
2062
3135
  function resolveParamId(parameters, nodeId, param) {
2063
3136
  if (typeof param === "number") {
@@ -2100,101 +3173,6 @@ function setAutomationLane(ctx, paramId, points) {
2100
3173
  ctx.postSync({ type: "syncAutomation", paramId, points: sorted });
2101
3174
  }
2102
3175
 
2103
- // src/worklet/engine-offline.ts
2104
- function buildCaptureConfig(options, defaultChannels) {
2105
- return {
2106
- bufferFrames: Math.trunc(options.bufferFrames),
2107
- channels: Math.trunc(options.channels ?? defaultChannels),
2108
- source: options.source ?? "output",
2109
- recordOffsetSamples: Math.trunc(options.recordOffsetSamples ?? 0),
2110
- inputMonitor: {
2111
- enabled: Boolean(options.inputMonitor?.enabled),
2112
- gain: options.inputMonitor?.gain ?? 1
2113
- }
2114
- };
2115
- }
2116
- function buildTransportFacade(ctx) {
2117
- return {
2118
- play: (sampleTime = -1) => {
2119
- const ok = ctx.realtimeNode.play(sampleTime);
2120
- if (ok) {
2121
- ctx.setTransportPlaying(true);
2122
- }
2123
- return ok;
2124
- },
2125
- stop: (sampleTime = -1) => {
2126
- const ok = ctx.realtimeNode.stop(sampleTime);
2127
- if (ok) {
2128
- ctx.setTransportPlaying(false);
2129
- ctx.flushPendingInstrumentSync();
2130
- }
2131
- return ok;
2132
- },
2133
- seekPpq: (ppq, sampleTime = -1) => {
2134
- ctx.offlineEngine.seekPpq(ppq, sampleTime);
2135
- const ok = ctx.realtimeNode.seekPpq(ppq, sampleTime);
2136
- ctx.flushOfflineMirror();
2137
- return ok;
2138
- },
2139
- seekSeconds: (seconds, sampleTime = -1) => {
2140
- const timelineSample = Math.max(0, Math.round(seconds * ctx.sampleRate));
2141
- ctx.offlineEngine.seekSample(timelineSample, sampleTime);
2142
- const ok = ctx.realtimeNode.seekSample(timelineSample, sampleTime);
2143
- ctx.flushOfflineMirror();
2144
- return ok;
2145
- },
2146
- setTempo: (bpm) => ctx.setTempo(bpm),
2147
- setTempoSegments: (segments) => ctx.setTempoSegments(segments),
2148
- setLoop: (startPpq, endPpq, enabled = true) => ctx.setLoop(startPpq, endPpq, enabled)
2149
- };
2150
- }
2151
- function normalizeTrackLanes(existing, lanes) {
2152
- const entries = lanes.map((lane) => typeof lane === "number" ? { trackId: lane } : lane);
2153
- const ids = [];
2154
- for (const entry of entries) {
2155
- if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
2156
- throw new Error(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
2157
- }
2158
- ids.push(entry.trackId);
2159
- }
2160
- if (new Set(ids).size !== ids.length) {
2161
- throw new Error("Duplicate track id in mixer lane list");
2162
- }
2163
- for (let index = 0; index < existing.length; index++) {
2164
- if (ids[index] !== existing[index]) {
2165
- throw new Error(
2166
- "Mixer lanes are append-only: keep existing lanes in order and only append new track ids"
2167
- );
2168
- }
2169
- }
2170
- return { entries, ids };
2171
- }
2172
- function resolveMarkerSet(markers, nextMarkerId) {
2173
- const resolved = [];
2174
- const seen = /* @__PURE__ */ new Set();
2175
- let counter = nextMarkerId;
2176
- for (const marker2 of markers) {
2177
- if (!Number.isFinite(marker2.ppq)) {
2178
- throw new Error(`Invalid marker ppq: ${String(marker2.ppq)}`);
2179
- }
2180
- if (marker2.id !== void 0) {
2181
- if (!Number.isInteger(marker2.id) || marker2.id <= 0) {
2182
- throw new Error(`Invalid marker id: ${String(marker2.id)}`);
2183
- }
2184
- if (seen.has(marker2.id)) {
2185
- throw new Error(`Duplicate marker id: ${marker2.id}`);
2186
- }
2187
- }
2188
- const id = marker2.id ?? counter++;
2189
- seen.add(id);
2190
- if (id >= counter) {
2191
- counter = id + 1;
2192
- }
2193
- resolved.push({ id, ppq: marker2.ppq, name: marker2.name ?? "" });
2194
- }
2195
- return { resolved, nextMarkerId: counter };
2196
- }
2197
-
2198
3176
  // src/worklet/protocol.ts
2199
3177
  var ENGINE_MIXER_TARGET_BASE = 1297612800;
2200
3178
  var ENGINE_MIXER_PARAM_FADER_DB = 1;
@@ -2281,11 +3259,9 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
2281
3259
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["MetronomeOverflow"] = 18] = "MetronomeOverflow";
2282
3260
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["InvalidCommand"] = 19] = "InvalidCommand";
2283
3261
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["MaxChannelsExceeded"] = 20] = "MaxChannelsExceeded";
3262
+ SonareEngineTelemetryError2[SonareEngineTelemetryError2["ParameterBaseOverflow"] = 21] = "ParameterBaseOverflow";
2284
3263
  return SonareEngineTelemetryError2;
2285
3264
  })(SonareEngineTelemetryError || {});
2286
- function toDb(value) {
2287
- return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
2288
- }
2289
3265
  function isRecord(value) {
2290
3266
  return typeof value === "object" && value !== null;
2291
3267
  }
@@ -2764,6 +3740,16 @@ function clipPageRequestRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
2764
3740
  function recordOffset(index, capacity, recordBytes) {
2765
3741
  return index % capacity * recordBytes;
2766
3742
  }
3743
+ function isUint32Slot(value) {
3744
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 4294967295;
3745
+ }
3746
+ function toUint32Slot(value, fallback, name) {
3747
+ const resolved = value ?? fallback;
3748
+ if (!isUint32Slot(resolved)) {
3749
+ throw new RangeError(`${name} must be an integer within [0, 4294967295]`);
3750
+ }
3751
+ return resolved;
3752
+ }
2767
3753
  function toSafeInteger(value, fallback) {
2768
3754
  const resolved = typeof value === "bigint" ? Number(value) : value;
2769
3755
  if (resolved === void 0) {
@@ -2783,8 +3769,8 @@ function readInt64Words(view, offset) {
2783
3769
  return view.getInt32(offset + 4, true) * 4294967296 + view.getUint32(offset, true);
2784
3770
  }
2785
3771
  function writeEngineCommandRecord(view, offset, command) {
2786
- view.setUint32(offset, command.type, true);
2787
- view.setUint32(offset + 4, command.targetId ?? 0, true);
3772
+ view.setUint32(offset, toUint32Slot(command.type, 0, "type"), true);
3773
+ view.setUint32(offset + 4, toUint32Slot(command.targetId, 0, "targetId"), true);
2788
3774
  writeInt64Words(view, offset + 8, toSafeInteger(command.sampleTime, -1));
2789
3775
  view.setFloat64(offset + 16, command.argFloat ?? 0, true);
2790
3776
  writeInt64Words(view, offset + 24, toSafeInteger(command.argInt, 0));
@@ -2853,43 +3839,337 @@ function magnitudeToDb(value) {
2853
3839
  return value > 1e-12 ? 20 * Math.log10(value) : -120;
2854
3840
  }
2855
3841
 
2856
- // src/worklet/engine-capture-facade.ts
2857
- function configureCapture(ctx, options) {
2858
- const config = buildCaptureConfig(options, ctx.offlineChannelCount);
2859
- ctx.offlineEngine.setCaptureBuffer(config.channels, config.bufferFrames);
2860
- ctx.offlineEngine.setCaptureSource(config.source);
2861
- ctx.offlineEngine.setRecordOffsetSamples(config.recordOffsetSamples);
2862
- ctx.offlineEngine.setInputMonitor(config.inputMonitor.enabled, config.inputMonitor.gain);
2863
- ctx.setCaptureConfig(config);
2864
- ctx.postSync({ type: "syncCapture", ...config });
2865
- }
2866
- function armRecord(ctx, trackId, enabled) {
2867
- if (trackId !== 0) {
2868
- throw new RangeError("Capture is global; armRecord only accepts trackId 0");
2869
- }
2870
- if (enabled && !ctx.getCaptureConfig()) {
2871
- throw new Error("Capture buffer is not configured");
3842
+ // src/worklet/guards.ts
3843
+ function isWorkletMessage(value) {
3844
+ if (!isRecord(value) || typeof value.type !== "string") {
3845
+ return false;
2872
3846
  }
2873
- ctx.offlineEngine.armCapture(enabled);
2874
- return ctx.sendCommand({
2875
- type: 13 /* ArmRecord */,
2876
- targetId: 0,
2877
- sampleTime: -1,
2878
- argInt: enabled ? 1 : 0
2879
- });
3847
+ return value.type === "scheduleInsertAutomation" || value.type === "setMeterInterval" || value.type === "destroy";
2880
3848
  }
2881
- function punch(ctx, inPpq, outPpq) {
2882
- const inSample = ctx.offlineEngine.sampleAtPpq(inPpq);
2883
- const outSample = ctx.offlineEngine.sampleAtPpq(outPpq);
2884
- ctx.offlineEngine.setCapturePunch(inSample, outSample, true);
2885
- return ctx.sendCommand({
2886
- type: 14 /* Punch */,
2887
- sampleTime: -1,
2888
- argInt: inSample,
2889
- argFloat: outSample
2890
- });
3849
+ function isEngineCommandRecord(value) {
3850
+ return isRecord(value) && typeof value.type === "number";
2891
3851
  }
2892
- function captureStatus(ctx) {
3852
+ var ENGINE_SYNC_MESSAGE_TYPES = {
3853
+ destroy: true,
3854
+ syncAutomation: true,
3855
+ syncBuiltinInstrument: true,
3856
+ syncBusStripDualPan: true,
3857
+ syncBusStripEqBand: true,
3858
+ syncBusStripInsertBypassed: true,
3859
+ syncBusStripInsertParamByName: true,
3860
+ syncBusStripPan: true,
3861
+ syncBusStripPanLaw: true,
3862
+ syncBusStripPanMode: true,
3863
+ syncCapture: true,
3864
+ syncClearMidiFx: true,
3865
+ syncClearMidiInputSource: true,
3866
+ syncClipPage: true,
3867
+ syncClipPageClear: true,
3868
+ syncClipPageCommit: true,
3869
+ syncClipPageDestroy: true,
3870
+ syncClipPagePrefetchFrames: true,
3871
+ syncClipPageProvider: true,
3872
+ syncClips: true,
3873
+ syncClipsDelta: true,
3874
+ syncExternalMidiClock: true,
3875
+ syncLoadSoundFont: true,
3876
+ syncMarkers: true,
3877
+ syncMasterStripEqBand: true,
3878
+ syncMasterStripInsertBypassed: true,
3879
+ syncMasterStripInsertParamByName: true,
3880
+ syncMetronome: true,
3881
+ syncMidiCc: true,
3882
+ syncMidiCcBinding: true,
3883
+ syncMidiChannelPressure: true,
3884
+ syncMidiClips: true,
3885
+ syncMidiDestinationExternal: true,
3886
+ syncMidiFx: true,
3887
+ syncMidiInputChannelPressure: true,
3888
+ syncMidiInputCc: true,
3889
+ syncMidiInputNoteOff: true,
3890
+ syncMidiInputNoteOn: true,
3891
+ syncMidiInputPitchBend: true,
3892
+ syncMidiInputPolyPressure: true,
3893
+ syncMidiInputSource: true,
3894
+ syncMidiNoteOff: true,
3895
+ syncMidiNoteOn: true,
3896
+ syncMidiPanic: true,
3897
+ syncMidiPitchBend: true,
3898
+ syncMidiPolyPressure: true,
3899
+ syncMidiSysex: true,
3900
+ syncMidiUmp: true,
3901
+ syncMixer: true,
3902
+ syncParameters: true,
3903
+ syncSf2Instrument: true,
3904
+ syncSynthInstrument: true,
3905
+ syncTempo: true,
3906
+ syncTrackStripChannelDelaySamples: true,
3907
+ syncTrackStripDualPan: true,
3908
+ syncTrackStripEqBand: true,
3909
+ syncTrackStripInsertBypassed: true,
3910
+ syncTrackStripInsertParamByName: true,
3911
+ syncTrackStripPan: true,
3912
+ syncTrackStripPanLaw: true,
3913
+ syncTrackStripPanMode: true,
3914
+ syncWarpVoiceCapacity: true
3915
+ };
3916
+ var engineSyncMessageTypes = new Set(Object.keys(ENGINE_SYNC_MESSAGE_TYPES));
3917
+ function isEngineSyncMessage(value) {
3918
+ return isRecord(value) && typeof value.type === "string" && engineSyncMessageTypes.has(value.type);
3919
+ }
3920
+ function isEngineCaptureRequestMessage(value) {
3921
+ return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
3922
+ }
3923
+ function isCaptureStatus(value) {
3924
+ return isRecord(value) && typeof value.capturedFrames === "number" && Number.isSafeInteger(value.capturedFrames) && value.capturedFrames >= 0 && typeof value.overflowCount === "number" && Number.isSafeInteger(value.overflowCount) && value.overflowCount >= 0 && typeof value.armed === "boolean" && typeof value.punchEnabled === "boolean" && (value.source === "input" || value.source === "output") && typeof value.recordOffsetSamples === "number" && Number.isSafeInteger(value.recordOffsetSamples);
3925
+ }
3926
+ function isCaptureChannel(value) {
3927
+ return value instanceof Float32Array && typeof ArrayBuffer !== "undefined" && value.buffer instanceof ArrayBuffer;
3928
+ }
3929
+ function isCaptureChannels(value) {
3930
+ return Array.isArray(value) && value.every((channel) => isCaptureChannel(channel));
3931
+ }
3932
+ function hasOwn(value, key) {
3933
+ return Object.prototype.hasOwnProperty.call(value, key);
3934
+ }
3935
+ function engineCaptureResponseRequestId(value) {
3936
+ if (!isRecord(value) || value.type !== "captureResponse" || typeof value.requestId !== "number" || !Number.isSafeInteger(value.requestId)) {
3937
+ return void 0;
3938
+ }
3939
+ return value.requestId;
3940
+ }
3941
+ function isEngineCaptureResponseMessage(value) {
3942
+ const requestId = engineCaptureResponseRequestId(value);
3943
+ if (requestId === void 0 || !isRecord(value)) {
3944
+ return false;
3945
+ }
3946
+ if (value.ok === false) {
3947
+ return typeof value.error === "string" && !hasOwn(value, "status") && !hasOwn(value, "channels");
3948
+ }
3949
+ if (value.ok !== true || hasOwn(value, "error")) {
3950
+ return false;
3951
+ }
3952
+ const hasStatus = hasOwn(value, "status");
3953
+ const hasChannels = hasOwn(value, "channels");
3954
+ if (hasStatus && hasChannels) {
3955
+ return false;
3956
+ }
3957
+ if (hasStatus) {
3958
+ return isCaptureStatus(value.status);
3959
+ }
3960
+ if (hasChannels) {
3961
+ return isCaptureChannels(value.channels);
3962
+ }
3963
+ return true;
3964
+ }
3965
+ function isEngineCaptureResponseForOperation(response, op) {
3966
+ if (!response.ok) {
3967
+ return true;
3968
+ }
3969
+ switch (op) {
3970
+ case "status":
3971
+ return "status" in response;
3972
+ case "read":
3973
+ return "channels" in response;
3974
+ case "reset":
3975
+ return !("status" in response) && !("channels" in response);
3976
+ }
3977
+ }
3978
+ function isEngineTransportRequestMessage(value) {
3979
+ return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
3980
+ }
3981
+ function isEngineTransportResponseMessage(value) {
3982
+ return isRecord(value) && value.type === "transportResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
3983
+ }
3984
+ function isRealtimeVoiceChangerMessage(value) {
3985
+ if (!isRecord(value) || typeof value.type !== "string") {
3986
+ return false;
3987
+ }
3988
+ return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
3989
+ }
3990
+ var isOptionalNumber = (value) => value === void 0 || typeof value === "number";
3991
+ function isPlaybackMessage(value) {
3992
+ if (!isRecord(value) || typeof value.type !== "string") {
3993
+ return false;
3994
+ }
3995
+ switch (value.type) {
3996
+ case "config":
3997
+ return typeof value.config === "string";
3998
+ case "orientation":
3999
+ return typeof value.yaw === "number" && isOptionalNumber(value.pitch) && isOptionalNumber(value.roll);
4000
+ case "reset":
4001
+ case "diagnostics":
4002
+ case "destroy":
4003
+ return true;
4004
+ default:
4005
+ return false;
4006
+ }
4007
+ }
4008
+ function isEngineTelemetryRecord(value) {
4009
+ return isRecord(value) && typeof value.type === "number" && typeof value.error === "number" && typeof value.renderFrame === "number" && typeof value.timelineSample === "number" && typeof value.audibleTimelineSample === "number" && typeof value.graphLatencySamplesQ8 === "number" && typeof value.value === "number";
4010
+ }
4011
+ function isExternalMidiBatchMessage(value) {
4012
+ return isRecord(value) && value.type === "externalMidi" && Array.isArray(value.events);
4013
+ }
4014
+ function isClipPageRequestMessage(value) {
4015
+ return isRecord(value) && value.type === "clipPageRequest" && Array.isArray(value.requests) && value.requests.every(
4016
+ (request) => isRecord(request) && typeof request.clipId === "number" && typeof request.pageIndex === "number"
4017
+ );
4018
+ }
4019
+ function isMeterSnapshot(value) {
4020
+ return isRecord(value) && value.type === "meter" && typeof value.frame === "number" && typeof value.peakDbL === "number" && typeof value.peakDbR === "number" && typeof value.rmsDbL === "number" && typeof value.rmsDbR === "number" && typeof value.correlation === "number" && (typeof value.targetId === "number" || value.targetId === void 0);
4021
+ }
4022
+ function requireIntegerOption(value, fallback, name, minimum) {
4023
+ const resolved = value ?? fallback;
4024
+ if (!Number.isSafeInteger(resolved) || resolved < minimum) {
4025
+ throw new RangeError(`${name} must be an integer of at least ${minimum}`);
4026
+ }
4027
+ return resolved;
4028
+ }
4029
+ function requireInteger(value, fallback, name) {
4030
+ const resolved = value ?? fallback;
4031
+ if (!Number.isSafeInteger(resolved)) {
4032
+ throw new RangeError(`${name} must be an integer`);
4033
+ }
4034
+ return resolved;
4035
+ }
4036
+ function requireChannelCount(channelCount, fallback) {
4037
+ return requireIntegerOption(channelCount, fallback, "channelCount", 1);
4038
+ }
4039
+
4040
+ // src/worklet/engine-offline.ts
4041
+ function buildCaptureConfig(options, defaultChannels) {
4042
+ return {
4043
+ // bufferFrames has no default; NaN makes the guard refuse an absent value.
4044
+ bufferFrames: requireIntegerOption(options.bufferFrames, Number.NaN, "bufferFrames", 1),
4045
+ channels: requireChannelCount(options.channels, defaultChannels),
4046
+ source: options.source ?? "output",
4047
+ recordOffsetSamples: requireInteger(options.recordOffsetSamples, 0, "recordOffsetSamples"),
4048
+ inputMonitor: {
4049
+ enabled: Boolean(options.inputMonitor?.enabled),
4050
+ gain: options.inputMonitor?.gain ?? 1
4051
+ }
4052
+ };
4053
+ }
4054
+ function buildTransportFacade(ctx) {
4055
+ return {
4056
+ play: (sampleTime = -1) => {
4057
+ const ok = ctx.realtimeNode.play(sampleTime);
4058
+ if (ok) {
4059
+ ctx.setTransportPlaying(true);
4060
+ }
4061
+ return ok;
4062
+ },
4063
+ stop: (sampleTime = -1) => {
4064
+ const ok = ctx.realtimeNode.stop(sampleTime);
4065
+ if (ok) {
4066
+ ctx.setTransportPlaying(false);
4067
+ ctx.flushPendingInstrumentSync();
4068
+ }
4069
+ return ok;
4070
+ },
4071
+ seekPpq: (ppq, sampleTime = -1) => {
4072
+ ctx.offlineEngine.seekPpq(ppq, sampleTime);
4073
+ const ok = ctx.realtimeNode.seekPpq(ppq, sampleTime);
4074
+ ctx.flushOfflineMirror();
4075
+ return ok;
4076
+ },
4077
+ seekSeconds: (seconds, sampleTime = -1) => {
4078
+ const timelineSample = Math.max(0, Math.round(seconds * ctx.sampleRate));
4079
+ ctx.offlineEngine.seekSample(timelineSample, sampleTime);
4080
+ const ok = ctx.realtimeNode.seekSample(timelineSample, sampleTime);
4081
+ ctx.flushOfflineMirror();
4082
+ return ok;
4083
+ },
4084
+ setTempo: (bpm) => ctx.setTempo(bpm),
4085
+ setTempoSegments: (segments) => ctx.setTempoSegments(segments),
4086
+ setLoop: (startPpq, endPpq, enabled = true) => ctx.setLoop(startPpq, endPpq, enabled)
4087
+ };
4088
+ }
4089
+ function normalizeTrackLanes(existing, lanes) {
4090
+ const entries = lanes.map((lane) => typeof lane === "number" ? { trackId: lane } : lane);
4091
+ const ids = [];
4092
+ for (const entry of entries) {
4093
+ if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
4094
+ throw new RangeError(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
4095
+ }
4096
+ ids.push(entry.trackId);
4097
+ }
4098
+ if (new Set(ids).size !== ids.length) {
4099
+ throw new Error("Duplicate track id in mixer lane list");
4100
+ }
4101
+ for (let index = 0; index < existing.length; index++) {
4102
+ if (ids[index] !== existing[index]) {
4103
+ throw new Error(
4104
+ "Mixer lanes are append-only: keep existing lanes in order and only append new track ids"
4105
+ );
4106
+ }
4107
+ }
4108
+ return { entries, ids };
4109
+ }
4110
+ function resolveMarkerSet(markers, nextMarkerId) {
4111
+ const resolved = [];
4112
+ const seen = /* @__PURE__ */ new Set();
4113
+ let counter = nextMarkerId;
4114
+ for (const marker2 of markers) {
4115
+ if (!Number.isFinite(marker2.ppq) || marker2.ppq < 0) {
4116
+ throw new RangeError(`Invalid marker ppq: ${String(marker2.ppq)}`);
4117
+ }
4118
+ if (marker2.id !== void 0) {
4119
+ if (!Number.isInteger(marker2.id) || marker2.id <= 0) {
4120
+ throw new RangeError(`Invalid marker id: ${String(marker2.id)}`);
4121
+ }
4122
+ if (seen.has(marker2.id)) {
4123
+ throw new Error(`Duplicate marker id: ${marker2.id}`);
4124
+ }
4125
+ }
4126
+ const id = marker2.id ?? counter++;
4127
+ seen.add(id);
4128
+ if (id >= counter) {
4129
+ counter = id + 1;
4130
+ }
4131
+ resolved.push({ id, ppq: marker2.ppq, name: marker2.name ?? "" });
4132
+ }
4133
+ return { resolved, nextMarkerId: counter };
4134
+ }
4135
+
4136
+ // src/worklet/engine-capture-facade.ts
4137
+ function configureCapture(ctx, options) {
4138
+ const config = buildCaptureConfig(options, ctx.offlineChannelCount);
4139
+ ctx.offlineEngine.setCaptureBuffer(config.channels, config.bufferFrames);
4140
+ ctx.offlineEngine.setCaptureSource(config.source);
4141
+ ctx.offlineEngine.setRecordOffsetSamples(config.recordOffsetSamples);
4142
+ ctx.offlineEngine.setInputMonitor(config.inputMonitor.enabled, config.inputMonitor.gain);
4143
+ ctx.setCaptureConfig(config);
4144
+ ctx.postSync({ type: "syncCapture", ...config });
4145
+ }
4146
+ function armRecord(ctx, trackId, enabled) {
4147
+ if (trackId !== 0) {
4148
+ throw new RangeError("Capture is global; armRecord only accepts trackId 0");
4149
+ }
4150
+ if (enabled && !ctx.getCaptureConfig()) {
4151
+ throw new Error("Capture buffer is not configured");
4152
+ }
4153
+ ctx.offlineEngine.armCapture(enabled);
4154
+ return ctx.sendCommand({
4155
+ type: 13 /* ArmRecord */,
4156
+ targetId: 0,
4157
+ sampleTime: -1,
4158
+ argInt: enabled ? 1 : 0
4159
+ });
4160
+ }
4161
+ function punch(ctx, inPpq, outPpq) {
4162
+ const inSample = ctx.offlineEngine.sampleAtPpq(inPpq);
4163
+ const outSample = ctx.offlineEngine.sampleAtPpq(outPpq);
4164
+ ctx.offlineEngine.setCapturePunch(inSample, outSample, true);
4165
+ return ctx.sendCommand({
4166
+ type: 14 /* Punch */,
4167
+ sampleTime: -1,
4168
+ argInt: inSample,
4169
+ argFloat: outSample
4170
+ });
4171
+ }
4172
+ function captureStatus(ctx) {
2893
4173
  return ctx.realtimeNode.requestCaptureStatus();
2894
4174
  }
2895
4175
  function capturedAudio(ctx) {
@@ -3045,16 +4325,191 @@ function syncMarkers(ctx) {
3045
4325
  }
3046
4326
 
3047
4327
  // src/worklet/engine-mixer-facade.ts
4328
+ function emptyStripJson(target) {
4329
+ switch (target.kind) {
4330
+ case "track":
4331
+ return `{"version":1,"strips":[{"id":"track-${target.trackId}"}],"buses":[],"connections":[]}`;
4332
+ case "bus":
4333
+ return `{"version":1,"strips":[],"buses":[{"id":"bus-${target.busId}"}],"connections":[]}`;
4334
+ case "master":
4335
+ return '{"version":1,"strips":[{"id":"master"}],"buses":[],"connections":[]}';
4336
+ }
4337
+ }
4338
+ function pruneStripSidechains(ctx, target, sceneJson) {
4339
+ const scene = JSON.parse(sceneJson);
4340
+ const entry = target.kind === "bus" ? scene.buses?.[0] : scene.strips?.[0];
4341
+ const insertCount = Array.isArray(entry?.inserts) ? entry.inserts.length : 0;
4342
+ if (target.kind === "track") {
4343
+ for (const [key, binding] of ctx.laneSidechains) {
4344
+ if (binding.trackId === target.trackId && binding.insertIndex >= insertCount) {
4345
+ ctx.laneSidechains.delete(key);
4346
+ }
4347
+ }
4348
+ } else if (target.kind === "bus") {
4349
+ for (const [key, binding] of ctx.busSidechains) {
4350
+ if (binding.busId === target.busId && binding.insertIndex >= insertCount) {
4351
+ ctx.busSidechains.delete(key);
4352
+ }
4353
+ }
4354
+ } else {
4355
+ for (const [key, binding] of ctx.masterSidechains) {
4356
+ if (binding.insertIndex >= insertCount) {
4357
+ ctx.masterSidechains.delete(key);
4358
+ }
4359
+ }
4360
+ }
4361
+ }
4362
+ function hasInsertParamOverrides(overrides, target) {
4363
+ for (const override of overrides.values()) {
4364
+ if (sameStripTarget(override.target, target)) {
4365
+ return true;
4366
+ }
4367
+ }
4368
+ return false;
4369
+ }
4370
+ function applyFullStripJson(engine, target, sceneJson, resetInserts) {
4371
+ const apply = (json) => {
4372
+ switch (target.kind) {
4373
+ case "track":
4374
+ engine.setTrackStripJson(target.trackId, json);
4375
+ break;
4376
+ case "bus":
4377
+ engine.setBusStripJson(target.busId, json);
4378
+ break;
4379
+ case "master":
4380
+ engine.setMasterStripJson(json);
4381
+ break;
4382
+ }
4383
+ };
4384
+ engine.applyCommandsDueNowPreservingFuture();
4385
+ apply(sceneJson);
4386
+ if (resetInserts) {
4387
+ switch (target.kind) {
4388
+ case "track":
4389
+ engine.clearTrackInsertParameterBases(target.trackId);
4390
+ break;
4391
+ case "bus":
4392
+ engine.clearBusInsertParameterBases(target.busId);
4393
+ break;
4394
+ case "master":
4395
+ engine.clearMasterInsertParameterBases();
4396
+ break;
4397
+ }
4398
+ apply(emptyStripJson(target));
4399
+ apply(sceneJson);
4400
+ }
4401
+ }
4402
+ function insertParamOverrideKey(override) {
4403
+ const targetId = override.target.kind === "track" ? override.target.trackId : override.target.kind === "bus" ? override.target.busId : "master";
4404
+ return JSON.stringify([override.target.kind, targetId, override.insertIndex, override.paramName]);
4405
+ }
4406
+ function clearInsertParamOverrides(overrides, target) {
4407
+ for (const [key, override] of overrides) {
4408
+ if (sameStripTarget(override.target, target)) {
4409
+ overrides.delete(key);
4410
+ }
4411
+ }
4412
+ }
4413
+ function sameStripTarget(left, right) {
4414
+ if (left.kind !== right.kind) {
4415
+ return false;
4416
+ }
4417
+ if (left.kind === "track" && right.kind === "track") {
4418
+ return left.trackId === right.trackId;
4419
+ }
4420
+ if (left.kind === "bus" && right.kind === "bus") {
4421
+ return left.busId === right.busId;
4422
+ }
4423
+ return left.kind === "master" && right.kind === "master";
4424
+ }
4425
+ function flattenInsertParamOverride(override) {
4426
+ switch (override.target.kind) {
4427
+ case "track":
4428
+ return {
4429
+ kind: "track",
4430
+ trackId: override.target.trackId,
4431
+ insertIndex: override.insertIndex,
4432
+ paramName: override.paramName,
4433
+ value: override.value
4434
+ };
4435
+ case "bus":
4436
+ return {
4437
+ kind: "bus",
4438
+ busId: override.target.busId,
4439
+ insertIndex: override.insertIndex,
4440
+ paramName: override.paramName,
4441
+ value: override.value
4442
+ };
4443
+ case "master":
4444
+ return {
4445
+ kind: "master",
4446
+ insertIndex: override.insertIndex,
4447
+ paramName: override.paramName,
4448
+ value: override.value
4449
+ };
4450
+ }
4451
+ }
4452
+ function cachedStripJson(ctx, target) {
4453
+ switch (target.kind) {
4454
+ case "track":
4455
+ return ctx.trackStripJson.get(target.trackId);
4456
+ case "bus":
4457
+ return ctx.busStripJson.get(target.busId);
4458
+ case "master":
4459
+ return ctx.getMasterStripJson();
4460
+ }
4461
+ }
4462
+ function cacheStripJson(ctx, target, sceneJson) {
4463
+ switch (target.kind) {
4464
+ case "track":
4465
+ ctx.trackStripJson.set(target.trackId, sceneJson);
4466
+ return;
4467
+ case "bus":
4468
+ ctx.busStripJson.set(target.busId, sceneJson);
4469
+ return;
4470
+ case "master":
4471
+ ctx.cacheMasterStripJson(sceneJson);
4472
+ return;
4473
+ }
4474
+ }
3048
4475
  function mixerLanes(ctx) {
3049
4476
  return buildMixerLanes(ctx.trackLaneIds, ctx.trackSends, ctx.trackOutputBus);
3050
4477
  }
3051
- function syncMixer(ctx) {
4478
+ function syncMixer(ctx, busesAlreadyApplied = false, forceInsertResets = []) {
3052
4479
  const lanes = mixerLanes(ctx);
3053
4480
  const buses = ctx.buses.map((bus) => ({ ...bus }));
3054
- ctx.offlineEngine.setTrackBuses(buses);
4481
+ if (!busesAlreadyApplied) {
4482
+ ctx.offlineEngine.setTrackBuses(buses);
4483
+ }
3055
4484
  if (lanes.length > 0) {
4485
+ ctx.offlineEngine.settleInsertParameters();
3056
4486
  ctx.offlineEngine.setTrackLanes(lanes);
3057
4487
  }
4488
+ if (forceInsertResets.length > 0) {
4489
+ for (const binding of ctx.laneSidechains.values()) {
4490
+ ctx.offlineEngine.setLaneSidechain(
4491
+ binding.trackId,
4492
+ binding.insertIndex,
4493
+ binding.sourceTrackId
4494
+ );
4495
+ }
4496
+ for (const binding of ctx.busSidechains.values()) {
4497
+ ctx.offlineEngine.setBusSidechain(
4498
+ binding.busId,
4499
+ binding.insertIndex,
4500
+ binding.sourceKind,
4501
+ binding.sourceId
4502
+ );
4503
+ }
4504
+ for (const binding of ctx.masterSidechains.values()) {
4505
+ ctx.offlineEngine.setMasterSidechain(
4506
+ binding.insertIndex,
4507
+ binding.sourceKind,
4508
+ binding.sourceId
4509
+ );
4510
+ }
4511
+ }
4512
+ replayInsertParamOverrides(ctx, lanes, buses);
3058
4513
  const trackStrips = Array.from(ctx.trackStripJson, ([trackId, sceneJson]) => ({
3059
4514
  trackId,
3060
4515
  sceneJson
@@ -3063,6 +4518,10 @@ function syncMixer(ctx) {
3063
4518
  busId,
3064
4519
  sceneJson
3065
4520
  }));
4521
+ const insertParamOverrides = Array.from(
4522
+ ctx.insertParamOverrides.values(),
4523
+ flattenInsertParamOverride
4524
+ );
3066
4525
  ctx.postSync({
3067
4526
  type: "syncMixer",
3068
4527
  lanes,
@@ -3070,9 +4529,49 @@ function syncMixer(ctx) {
3070
4529
  trackStrips,
3071
4530
  laneSidechains: Array.from(ctx.laneSidechains.values()),
3072
4531
  busStrips,
3073
- masterStripJson: ctx.getMasterStripJson()
4532
+ masterStripJson: ctx.getMasterStripJson(),
4533
+ busSidechains: Array.from(ctx.busSidechains.values()),
4534
+ masterSidechains: Array.from(ctx.masterSidechains.values()),
4535
+ ...insertParamOverrides.length > 0 ? { insertParamOverrides } : {},
4536
+ ...forceInsertResets.length > 0 ? { forceInsertResets } : {}
3074
4537
  });
3075
4538
  }
4539
+ function replayInsertParamOverrides(ctx, lanes = mixerLanes(ctx), buses = ctx.buses) {
4540
+ const activeTrackIds = new Set(lanes.map((lane) => lane.trackId));
4541
+ const activeBusIds = new Set(buses.map((bus) => bus.busId));
4542
+ for (const [key, override] of ctx.insertParamOverrides) {
4543
+ const active = override.target.kind === "track" ? activeTrackIds.has(override.target.trackId) : override.target.kind === "bus" ? activeBusIds.has(override.target.busId) : true;
4544
+ if (!active) {
4545
+ ctx.insertParamOverrides.delete(key);
4546
+ continue;
4547
+ }
4548
+ switch (override.target.kind) {
4549
+ case "track":
4550
+ ctx.offlineEngine.restoreTrackStripInsertParamByName(
4551
+ override.target.trackId,
4552
+ override.insertIndex,
4553
+ override.paramName,
4554
+ override.value
4555
+ );
4556
+ break;
4557
+ case "bus":
4558
+ ctx.offlineEngine.restoreBusStripInsertParamByName(
4559
+ override.target.busId,
4560
+ override.insertIndex,
4561
+ override.paramName,
4562
+ override.value
4563
+ );
4564
+ break;
4565
+ case "master":
4566
+ ctx.offlineEngine.restoreMasterStripInsertParamByName(
4567
+ override.insertIndex,
4568
+ override.paramName,
4569
+ override.value
4570
+ );
4571
+ break;
4572
+ }
4573
+ }
4574
+ }
3076
4575
  function setTrackLanes(ctx, lanes) {
3077
4576
  const { entries, ids } = normalizeTrackLanes(ctx.trackLaneIds, lanes);
3078
4577
  for (const entry of entries) {
@@ -3112,143 +4611,116 @@ function setLaneSidechain(ctx, target, insertIndex, sourceTarget) {
3112
4611
  const sourceIndex = ctx.ensureTrackLane(sourceTarget);
3113
4612
  sourceTrackId = ctx.trackLaneIds[sourceIndex];
3114
4613
  }
4614
+ ctx.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
3115
4615
  if (sourceTrackId === 0) {
3116
4616
  ctx.laneSidechains.delete(key);
3117
4617
  } else {
3118
4618
  ctx.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
3119
4619
  }
3120
- ctx.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
3121
4620
  ctx.postSync({
3122
4621
  type: "syncMixer",
4622
+ sidechainDelta: true,
3123
4623
  lanes: ctx.mixerLanes(),
3124
- laneSidechains: [{ trackId, insertIndex, sourceTrackId }]
3125
- });
3126
- }
3127
- function setSends(ctx, target, sends) {
3128
- const laneIndex = ctx.ensureTrackLane(target);
3129
- const trackId = ctx.trackLaneIds[laneIndex];
3130
- ctx.trackSends.set(
3131
- trackId,
3132
- sends.map((send) => ({ ...send }))
3133
- );
3134
- ctx.syncMixer();
3135
- }
3136
- function setTrackBuses(ctx, buses) {
3137
- ctx.buses.splice(0, ctx.buses.length, ...buses.map((bus) => ({ ...bus })));
3138
- ctx.syncMixer();
3139
- }
3140
- function setBusGain(ctx, busId, db) {
3141
- const busIndex = ctx.ensureBus(busId);
3142
- ctx.buses[busIndex] = { ...ctx.buses[busIndex], busId, gainDb: db };
3143
- ctx.offlineEngine.setTrackBuses(ctx.buses);
3144
- return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
3145
- }
3146
- function setBusStripJson(ctx, busId, sceneJson) {
3147
- ctx.ensureBus(busId);
3148
- ctx.offlineEngine.setBusStripJson(busId, sceneJson);
3149
- ctx.busStripJson.set(busId, sceneJson);
3150
- ctx.syncMixer();
3151
- }
3152
-
3153
- // src/worklet/guards.ts
3154
- function isWorkletMessage(value) {
3155
- if (!isRecord(value) || typeof value.type !== "string") {
3156
- return false;
3157
- }
3158
- return value.type === "scheduleInsertAutomation" || value.type === "setMeterInterval" || value.type === "destroy";
3159
- }
3160
- function isEngineCommandRecord(value) {
3161
- return isRecord(value) && typeof value.type === "number";
3162
- }
3163
- function isEngineSyncMessage(value) {
3164
- if (!isRecord(value) || typeof value.type !== "string") {
3165
- return false;
3166
- }
3167
- return value.type === "syncClips" || value.type === "syncClipsDelta" || value.type === "syncClipPageProvider" || value.type === "syncClipPage" || value.type === "syncClipPageClear" || value.type === "syncClipPageCommit" || value.type === "syncClipPageDestroy" || value.type === "syncMidiClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation" || value.type === "syncTempo" || value.type === "syncMixer" || value.type === "syncCapture" || value.type === "syncTrackStripEqBand" || value.type === "syncMasterStripEqBand" || value.type === "syncTrackStripInsertBypassed" || value.type === "syncMasterStripInsertBypassed" || value.type === "syncTrackStripInsertParamByName" || value.type === "syncMasterStripInsertParamByName" || value.type === "syncBusStripInsertParamByName" || value.type === "syncTrackStripPan" || value.type === "syncTrackStripPanLaw" || value.type === "syncTrackStripPanMode" || value.type === "syncTrackStripDualPan" || value.type === "syncTrackStripChannelDelaySamples" || value.type === "syncBuiltinInstrument" || value.type === "syncSynthInstrument" || value.type === "syncSf2Instrument" || value.type === "syncLoadSoundFont" || value.type === "syncMidiFx" || value.type === "syncClearMidiFx" || value.type === "syncMidiNoteOn" || value.type === "syncMidiNoteOff" || value.type === "syncMidiCc" || value.type === "syncMidiUmp" || value.type === "syncMidiSysex" || value.type === "syncMidiPanic" || value.type === "syncMidiDestinationExternal" || value.type === "syncExternalMidiClock" || value.type === "destroy";
3168
- }
3169
- function isEngineCaptureRequestMessage(value) {
3170
- return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
3171
- }
3172
- function isCaptureStatus(value) {
3173
- return isRecord(value) && typeof value.capturedFrames === "number" && Number.isSafeInteger(value.capturedFrames) && value.capturedFrames >= 0 && typeof value.overflowCount === "number" && Number.isSafeInteger(value.overflowCount) && value.overflowCount >= 0 && typeof value.armed === "boolean" && typeof value.punchEnabled === "boolean" && (value.source === "input" || value.source === "output") && typeof value.recordOffsetSamples === "number" && Number.isSafeInteger(value.recordOffsetSamples);
3174
- }
3175
- function isCaptureChannel(value) {
3176
- return value instanceof Float32Array && typeof ArrayBuffer !== "undefined" && value.buffer instanceof ArrayBuffer;
3177
- }
3178
- function isCaptureChannels(value) {
3179
- return Array.isArray(value) && value.every((channel) => isCaptureChannel(channel));
3180
- }
3181
- function hasOwn(value, key) {
3182
- return Object.prototype.hasOwnProperty.call(value, key);
3183
- }
3184
- function engineCaptureResponseRequestId(value) {
3185
- if (!isRecord(value) || value.type !== "captureResponse" || typeof value.requestId !== "number" || !Number.isSafeInteger(value.requestId)) {
3186
- return void 0;
3187
- }
3188
- return value.requestId;
3189
- }
3190
- function isEngineCaptureResponseMessage(value) {
3191
- const requestId = engineCaptureResponseRequestId(value);
3192
- if (requestId === void 0 || !isRecord(value)) {
3193
- return false;
3194
- }
3195
- if (value.ok === false) {
3196
- return typeof value.error === "string" && !hasOwn(value, "status") && !hasOwn(value, "channels");
3197
- }
3198
- if (value.ok !== true || hasOwn(value, "error")) {
3199
- return false;
3200
- }
3201
- const hasStatus = hasOwn(value, "status");
3202
- const hasChannels = hasOwn(value, "channels");
3203
- if (hasStatus && hasChannels) {
3204
- return false;
3205
- }
3206
- if (hasStatus) {
3207
- return isCaptureStatus(value.status);
4624
+ laneSidechains: [{ trackId, insertIndex, sourceTrackId }]
4625
+ });
4626
+ }
4627
+ function setBusSidechain(ctx, busId, insertIndex, kind, sourceId) {
4628
+ const sourceKind = sidechainSourceKindCode(kind);
4629
+ ctx.ensureBus(busId);
4630
+ ensureSidechainSource(ctx, sourceKind, sourceId);
4631
+ ctx.offlineEngine.setBusSidechain(busId, insertIndex, sourceKind, sourceId);
4632
+ const key = `${busId}:${insertIndex}`;
4633
+ if (sourceId === 0) {
4634
+ ctx.busSidechains.delete(key);
4635
+ } else {
4636
+ ctx.busSidechains.set(key, { busId, insertIndex, sourceKind, sourceId });
3208
4637
  }
3209
- if (hasChannels) {
3210
- return isCaptureChannels(value.channels);
4638
+ ctx.postSync({
4639
+ type: "syncMixer",
4640
+ sidechainDelta: true,
4641
+ lanes: ctx.mixerLanes(),
4642
+ busSidechains: [{ busId, insertIndex, sourceKind, sourceId }]
4643
+ });
4644
+ }
4645
+ function setMasterSidechain(ctx, insertIndex, kind, sourceId) {
4646
+ const sourceKind = sidechainSourceKindCode(kind);
4647
+ ensureSidechainSource(ctx, sourceKind, sourceId);
4648
+ ctx.offlineEngine.setMasterSidechain(insertIndex, sourceKind, sourceId);
4649
+ if (sourceId === 0) {
4650
+ ctx.masterSidechains.delete(insertIndex);
4651
+ } else {
4652
+ ctx.masterSidechains.set(insertIndex, { insertIndex, sourceKind, sourceId });
3211
4653
  }
3212
- return true;
4654
+ ctx.postSync({
4655
+ type: "syncMixer",
4656
+ sidechainDelta: true,
4657
+ lanes: ctx.mixerLanes(),
4658
+ masterSidechains: [{ insertIndex, sourceKind, sourceId }]
4659
+ });
3213
4660
  }
3214
- function isEngineCaptureResponseForOperation(response, op) {
3215
- if (!response.ok) {
3216
- return true;
4661
+ function ensureSidechainSource(ctx, sourceKind, sourceId) {
4662
+ if (sourceId === 0) {
4663
+ return;
3217
4664
  }
3218
- switch (op) {
3219
- case "status":
3220
- return "status" in response;
3221
- case "read":
3222
- return "channels" in response;
3223
- case "reset":
3224
- return !("status" in response) && !("channels" in response);
4665
+ if (sourceKind === 1) {
4666
+ ctx.ensureBus(sourceId);
4667
+ } else {
4668
+ ctx.ensureTrackLane(sourceId);
3225
4669
  }
3226
4670
  }
3227
- function isEngineTransportRequestMessage(value) {
3228
- return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
3229
- }
3230
- function isEngineTransportResponseMessage(value) {
3231
- return isRecord(value) && value.type === "transportResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
4671
+ function setSends(ctx, target, sends) {
4672
+ const laneIndex = ctx.ensureTrackLane(target);
4673
+ const trackId = ctx.trackLaneIds[laneIndex];
4674
+ ctx.trackSends.set(
4675
+ trackId,
4676
+ sends.map((send) => ({ ...send }))
4677
+ );
4678
+ ctx.syncMixer();
3232
4679
  }
3233
- function isRealtimeVoiceChangerMessage(value) {
3234
- if (!isRecord(value) || typeof value.type !== "string") {
3235
- return false;
4680
+ function setTrackBuses(ctx, buses) {
4681
+ ctx.offlineEngine.setTrackBuses(buses);
4682
+ const retainedBusIds = new Set(buses.map((bus) => bus.busId));
4683
+ for (const bus of ctx.buses) {
4684
+ if (!retainedBusIds.has(bus.busId)) {
4685
+ ctx.clearInsertAutomationLanes({ kind: "bus", busId: bus.busId }, true);
4686
+ }
3236
4687
  }
3237
- return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
3238
- }
3239
- function isEngineTelemetryRecord(value) {
3240
- return isRecord(value) && typeof value.type === "number" && typeof value.error === "number" && typeof value.renderFrame === "number" && typeof value.timelineSample === "number" && typeof value.audibleTimelineSample === "number" && typeof value.graphLatencySamplesQ8 === "number" && typeof value.value === "number";
3241
- }
3242
- function isExternalMidiBatchMessage(value) {
3243
- return isRecord(value) && value.type === "externalMidi" && Array.isArray(value.events);
4688
+ ctx.buses.splice(0, ctx.buses.length, ...buses.map((bus) => ({ ...bus })));
4689
+ const activeBusIds = new Set(ctx.buses.map((bus) => bus.busId));
4690
+ for (const busId of ctx.busStripJson.keys()) {
4691
+ if (!activeBusIds.has(busId)) {
4692
+ ctx.busStripJson.delete(busId);
4693
+ }
4694
+ }
4695
+ for (const [key, binding] of ctx.busSidechains) {
4696
+ if (!activeBusIds.has(binding.busId) || binding.sourceKind === 1 && !activeBusIds.has(binding.sourceId)) {
4697
+ ctx.busSidechains.delete(key);
4698
+ }
4699
+ }
4700
+ for (const [key, binding] of ctx.masterSidechains) {
4701
+ if (binding.sourceKind === 1 && !activeBusIds.has(binding.sourceId)) {
4702
+ ctx.masterSidechains.delete(key);
4703
+ }
4704
+ }
4705
+ syncMixer(ctx, true);
3244
4706
  }
3245
- function isClipPageRequestMessage(value) {
3246
- return isRecord(value) && value.type === "clipPageRequest" && Array.isArray(value.requests) && value.requests.every(
3247
- (request) => isRecord(request) && typeof request.clipId === "number" && typeof request.pageIndex === "number"
3248
- );
4707
+ function setBusGain(ctx, busId, db) {
4708
+ const busIndex = ctx.ensureBus(busId);
4709
+ ctx.buses[busIndex] = { ...ctx.buses[busIndex], busId, gainDb: db };
4710
+ return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
3249
4711
  }
3250
- function isMeterSnapshot(value) {
3251
- return isRecord(value) && value.type === "meter" && typeof value.frame === "number" && typeof value.peakDbL === "number" && typeof value.peakDbR === "number" && typeof value.rmsDbL === "number" && typeof value.rmsDbR === "number" && typeof value.correlation === "number" && (typeof value.targetId === "number" || value.targetId === void 0);
4712
+ function setBusStripJson(ctx, busId, sceneJson) {
4713
+ ctx.ensureBus(busId);
4714
+ const target = { kind: "bus", busId };
4715
+ const resetInserts = ctx.busStripJson.has(busId) && ctx.busStripJson.get(busId) !== sceneJson || hasInsertParamOverrides(ctx.insertParamOverrides, target);
4716
+ applyFullStripJson(ctx.offlineEngine, target, sceneJson, resetInserts);
4717
+ if (resetInserts) {
4718
+ ctx.clearInsertAutomationLanes(target);
4719
+ }
4720
+ pruneStripSidechains(ctx, target, sceneJson);
4721
+ ctx.busStripJson.set(busId, sceneJson);
4722
+ clearInsertParamOverrides(ctx.insertParamOverrides, target);
4723
+ ctx.syncMixer(resetInserts ? [target] : []);
3252
4724
  }
3253
4725
 
3254
4726
  // src/worklet/engine-node.ts
@@ -3265,15 +4737,17 @@ function isTrackMonitorMode(value) {
3265
4737
  const mode = typeof value === "bigint" ? Number(value) : value;
3266
4738
  return typeof mode === "number" && Number.isSafeInteger(mode) && mode >= 0 && mode <= 2;
3267
4739
  }
3268
- function isTrackMonitorLaneIndex(value) {
3269
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 4294967295;
3270
- }
3271
4740
  function isValidCommandRecord(command) {
3272
4741
  const type = Number(command.type);
3273
4742
  if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */ && type !== 26 /* SetTrackMonitorMode */) {
3274
4743
  return false;
3275
4744
  }
3276
- return (command.targetId === void 0 || Number.isSafeInteger(command.targetId)) && (command.argFloat === void 0 || Number.isFinite(command.argFloat)) && isFiniteInteger(command.argInt) && isFiniteInteger(command.sampleTime) && (type !== 26 /* SetTrackMonitorMode */ || isTrackMonitorLaneIndex(command.targetId) && isTrackMonitorMode(command.argInt));
4745
+ return (
4746
+ // The record writer refuses a targetId outside the slot by throwing, and
4747
+ // this path answers false instead, so it must reject the same domain here.
4748
+ (command.targetId === void 0 || isUint32Slot(command.targetId)) && (command.argFloat === void 0 || Number.isFinite(command.argFloat)) && isFiniteInteger(command.argInt) && isFiniteInteger(command.sampleTime) && // A track-monitor command cannot infer a lane: its target is required.
4749
+ (type !== 26 /* SetTrackMonitorMode */ || isUint32Slot(command.targetId) && isTrackMonitorMode(command.argInt))
4750
+ );
3277
4751
  }
3278
4752
  var AUDIO_WORKLET_RENDER_QUANTUM = 128;
3279
4753
  function workletBlockSize(blockSize) {
@@ -3318,6 +4792,9 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3318
4792
  this.resolveReady();
3319
4793
  }
3320
4794
  this.node.port.onmessage = (event) => {
4795
+ if (this.destroyed) {
4796
+ return;
4797
+ }
3321
4798
  const captureRequestId = engineCaptureResponseRequestId(event.data);
3322
4799
  if (captureRequestId !== void 0) {
3323
4800
  const pending = this.captureRequests.get(captureRequestId);
@@ -3399,18 +4876,65 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3399
4876
  "SharedArrayBuffer mode requested but SharedArrayBuffer/Atomics are unavailable."
3400
4877
  );
3401
4878
  }
3402
- const commandRing = mode === "sab" ? createSonareEngineCommandRingBuffer(options.commandRingCapacity ?? 128) : void 0;
3403
- const telemetryRing = mode === "sab" ? createSonareEngineTelemetryRingBuffer(options.telemetryRingCapacity ?? 128) : void 0;
3404
- const meterRing = mode === "sab" ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128) : void 0;
3405
- const scopeIntervalFrames = Math.max(0, Math.floor(options.scopeIntervalFrames ?? 0));
3406
- const scopeRing = mode === "sab" && scopeIntervalFrames > 0 ? createSonareScopeRingBuffer(options.scopeRingCapacity ?? 64, options.scopeBands ?? 48) : void 0;
3407
- const clipPageRequestRing = mode === "sab" ? createSonareClipPageRequestRingBuffer(options.clipPageRequestRingCapacity ?? 128) : void 0;
3408
- const externalMidiRing = mode === "sab" ? createSonareExternalMidiRingBuffer(options.externalMidiRingCapacity ?? 256) : void 0;
3409
- const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
4879
+ const commandRingCapacity = requireIntegerOption(
4880
+ options.commandRingCapacity,
4881
+ 128,
4882
+ "commandRingCapacity",
4883
+ 1
4884
+ );
4885
+ if (commandRingCapacity > 65536) {
4886
+ throw new RangeError("commandRingCapacity must be at most 65536");
4887
+ }
4888
+ const telemetryRingCapacity = requireIntegerOption(
4889
+ options.telemetryRingCapacity,
4890
+ 128,
4891
+ "telemetryRingCapacity",
4892
+ 1
4893
+ );
4894
+ const meterRingCapacity = requireIntegerOption(
4895
+ options.meterRingCapacity,
4896
+ 128,
4897
+ "meterRingCapacity",
4898
+ 1
4899
+ );
4900
+ const scopeRingCapacity = requireIntegerOption(
4901
+ options.scopeRingCapacity,
4902
+ 64,
4903
+ "scopeRingCapacity",
4904
+ 1
4905
+ );
4906
+ const scopeBands = requireIntegerOption(options.scopeBands, 48, "scopeBands", 1);
4907
+ const clipPageRequestRingCapacity = requireIntegerOption(
4908
+ options.clipPageRequestRingCapacity,
4909
+ 128,
4910
+ "clipPageRequestRingCapacity",
4911
+ 1
4912
+ );
4913
+ const externalMidiRingCapacity = requireIntegerOption(
4914
+ options.externalMidiRingCapacity,
4915
+ 256,
4916
+ "externalMidiRingCapacity",
4917
+ 1
4918
+ );
4919
+ const commandRing = mode === "sab" ? createSonareEngineCommandRingBuffer(commandRingCapacity) : void 0;
4920
+ const telemetryRing = mode === "sab" ? createSonareEngineTelemetryRingBuffer(telemetryRingCapacity) : void 0;
4921
+ const meterRing = mode === "sab" ? createSonareMeterRingBuffer(meterRingCapacity) : void 0;
4922
+ const scopeIntervalFrames = requireIntegerOption(
4923
+ options.scopeIntervalFrames,
4924
+ 0,
4925
+ "scopeIntervalFrames",
4926
+ 0
4927
+ );
4928
+ const scopeRing = mode === "sab" && scopeIntervalFrames > 0 ? createSonareScopeRingBuffer(scopeRingCapacity, scopeBands) : void 0;
4929
+ const clipPageRequestRing = mode === "sab" ? createSonareClipPageRequestRingBuffer(clipPageRequestRingCapacity) : void 0;
4930
+ const externalMidiRing = mode === "sab" ? createSonareExternalMidiRingBuffer(externalMidiRingCapacity) : void 0;
4931
+ const channelCount = requireChannelCount(options.channelCount, 2);
4932
+ const cueOutput = options.cueOutput === true;
3410
4933
  const processorOptions = {
3411
4934
  sampleRate: options.sampleRate ?? context.sampleRate,
3412
4935
  blockSize,
3413
4936
  channelCount,
4937
+ cueOutput,
3414
4938
  commandSharedBuffer: commandRing?.sharedBuffer,
3415
4939
  commandRingCapacity: commandRing?.capacity,
3416
4940
  telemetrySharedBuffer: telemetryRing?.sharedBuffer,
@@ -3432,8 +4956,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3432
4956
  const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
3433
4957
  const node = factory(context, processorName, {
3434
4958
  numberOfInputs: 1,
3435
- numberOfOutputs: 1,
3436
- outputChannelCount: [channelCount],
4959
+ // The cue bus needs its own output; a single-output node keeps the
4960
+ // historical mix where process() folds the cue into the program.
4961
+ numberOfOutputs: cueOutput ? 2 : 1,
4962
+ outputChannelCount: cueOutput ? [channelCount, channelCount] : [channelCount],
3437
4963
  processorOptions
3438
4964
  });
3439
4965
  return new _SonareRealtimeEngineNode(
@@ -3446,6 +4972,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3446
4972
  audioWorklet,
3447
4973
  clipPageRequestsRealtimeSafe: mode === "sab",
3448
4974
  externalMidiRealtimeSafe: mode === "sab",
4975
+ cueOutput,
3449
4976
  engineAbiVersion: detectedCapabilities?.engineAbiVersion,
3450
4977
  expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
3451
4978
  abiCompatible: detectedCapabilities?.abiCompatible,
@@ -3678,6 +5205,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3678
5205
  this.scopeListeners.clear();
3679
5206
  this.midiOutListeners.clear();
3680
5207
  this.clipPageRequestListeners.clear();
5208
+ this.syncErrorListeners.clear();
5209
+ this.node.port.onmessage = null;
3681
5210
  }
3682
5211
  emitTelemetry(telemetry) {
3683
5212
  for (const listener of this.telemetryListeners) {
@@ -3817,6 +5346,9 @@ function resolveBusInsertAutomationId(ctx, busId, insertIndex, paramName) {
3817
5346
  ctx.ensureBus(busId);
3818
5347
  return ctx.offlineEngine.resolveBusInsertAutomationId(busId, insertIndex, paramName);
3819
5348
  }
5349
+ function resolveInstrumentAutomationId(ctx, destinationId, paramName) {
5350
+ return ctx.offlineEngine.resolveInstrumentAutomationId(destinationId, paramName);
5351
+ }
3820
5352
  function automationLaneCount(ctx) {
3821
5353
  return ctx.offlineEngine.automationLaneCount();
3822
5354
  }
@@ -3841,14 +5373,41 @@ function clearParameters(ctx) {
3841
5373
  function trackIdFor(ctx, target) {
3842
5374
  return ctx.trackLaneIds[ctx.ensureTrackLane(target)];
3843
5375
  }
5376
+ function mergeStripJson(ctx, target, update) {
5377
+ const scene = JSON.parse(ctx.readStripJson(target) ?? emptyStripJson(target));
5378
+ update(target.kind === "bus" ? scene.buses[0] : scene.strips[0]);
5379
+ ctx.writeStripJson(target, JSON.stringify(scene));
5380
+ }
5381
+ function mergeEqBand(ctx, target, bandIndex, bandJson) {
5382
+ mergeStripJson(ctx, target, (entry) => {
5383
+ const eq = entry.eq ?? { enabled: true, bands: [] };
5384
+ const bands = eq.bands ?? [];
5385
+ while (bands.length < bandIndex) {
5386
+ bands.push({});
5387
+ }
5388
+ bands[bandIndex] = JSON.parse(bandJson);
5389
+ entry.eq = { ...eq, bands };
5390
+ });
5391
+ }
5392
+ function setInsertParamByName(ctx, target, insertIndex, paramName, value, applyNative, message) {
5393
+ applyNative();
5394
+ const override = { target, insertIndex, paramName, value };
5395
+ ctx.insertParamOverrides.set(insertParamOverrideKey(override), override);
5396
+ ctx.postSync(message);
5397
+ }
3844
5398
  function setTrackStripJson(ctx, trackId, sceneJson, trackStripJson) {
3845
- ctx.offlineEngine.setTrackStripJson(trackId, sceneJson);
5399
+ const target = { kind: "track", trackId };
5400
+ const resetInserts = trackStripJson.has(trackId) && trackStripJson.get(trackId) !== sceneJson || hasInsertParamOverrides(ctx.insertParamOverrides, target);
5401
+ applyFullStripJson(ctx.offlineEngine, target, sceneJson, resetInserts);
5402
+ clearInsertParamOverrides(ctx.insertParamOverrides, target);
3846
5403
  trackStripJson.set(trackId, sceneJson);
5404
+ return { resetInserts };
3847
5405
  }
3848
5406
  function setTrackStripEqBand(ctx, target, bandIndex, band) {
3849
5407
  const trackId = trackIdFor(ctx, target);
3850
5408
  const bandJson = typeof band === "string" ? band : JSON.stringify(band);
3851
5409
  ctx.offlineEngine.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
5410
+ mergeEqBand(ctx, { kind: "track", trackId }, bandIndex, bandJson);
3852
5411
  ctx.postSync({ type: "syncTrackStripEqBand", trackId, bandIndex, bandJson });
3853
5412
  }
3854
5413
  function setTrackStripInsertBypassed(ctx, target, insertIndex, bypassed, resetOnBypass) {
@@ -3864,39 +5423,68 @@ function setTrackStripInsertBypassed(ctx, target, insertIndex, bypassed, resetOn
3864
5423
  }
3865
5424
  function setTrackStripInsertParamByName(ctx, target, insertIndex, paramName, value) {
3866
5425
  const trackId = trackIdFor(ctx, target);
3867
- ctx.offlineEngine.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
3868
- ctx.postSync({ type: "syncTrackStripInsertParamByName", trackId, insertIndex, paramName, value });
5426
+ setInsertParamByName(
5427
+ ctx,
5428
+ { kind: "track", trackId },
5429
+ insertIndex,
5430
+ paramName,
5431
+ value,
5432
+ () => ctx.offlineEngine.restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value),
5433
+ { type: "syncTrackStripInsertParamByName", trackId, insertIndex, paramName, value }
5434
+ );
3869
5435
  }
3870
5436
  function setTrackStripPan(ctx, target, pan) {
3871
5437
  const trackId = trackIdFor(ctx, target);
3872
5438
  ctx.offlineEngine.setTrackStripPan(trackId, pan);
5439
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5440
+ entry.pan = pan;
5441
+ });
3873
5442
  ctx.postSync({ type: "syncTrackStripPan", trackId, pan });
3874
5443
  }
3875
5444
  function setTrackStripPanLaw(ctx, target, panLaw) {
3876
5445
  const trackId = trackIdFor(ctx, target);
3877
5446
  const code = panLawCode(panLaw);
3878
5447
  ctx.offlineEngine.setTrackStripPanLaw(trackId, code);
5448
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5449
+ entry.panLaw = code;
5450
+ });
3879
5451
  ctx.postSync({ type: "syncTrackStripPanLaw", trackId, panLaw: code });
3880
5452
  }
3881
5453
  function setTrackStripPanMode(ctx, target, panMode) {
3882
5454
  const trackId = trackIdFor(ctx, target);
3883
5455
  const code = panModeCode(panMode);
3884
5456
  ctx.offlineEngine.setTrackStripPanMode(trackId, code);
5457
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5458
+ entry.panMode = code;
5459
+ });
3885
5460
  ctx.postSync({ type: "syncTrackStripPanMode", trackId, panMode: code });
3886
5461
  }
3887
5462
  function setTrackStripDualPan(ctx, target, leftPan, rightPan) {
3888
5463
  const trackId = trackIdFor(ctx, target);
3889
5464
  ctx.offlineEngine.setTrackStripDualPan(trackId, leftPan, rightPan);
5465
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5466
+ entry.dualPanLeft = leftPan;
5467
+ entry.dualPanRight = rightPan;
5468
+ });
3890
5469
  ctx.postSync({ type: "syncTrackStripDualPan", trackId, leftPan, rightPan });
3891
5470
  }
3892
5471
  function setTrackStripChannelDelaySamples(ctx, target, delaySamples) {
3893
5472
  const trackId = trackIdFor(ctx, target);
3894
5473
  ctx.offlineEngine.setTrackStripChannelDelaySamples(trackId, delaySamples);
5474
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5475
+ entry.channelDelaySamples = delaySamples;
5476
+ });
3895
5477
  ctx.postSync({ type: "syncTrackStripChannelDelaySamples", trackId, delaySamples });
3896
5478
  }
5479
+ function cacheMasterStripScalar(ctx, field, value) {
5480
+ mergeStripJson(ctx, { kind: "master" }, (entry) => {
5481
+ entry[field] = value;
5482
+ });
5483
+ }
3897
5484
  function setMasterStripEqBand(ctx, bandIndex, band) {
3898
5485
  const bandJson = typeof band === "string" ? band : JSON.stringify(band);
3899
5486
  ctx.offlineEngine.setMasterStripEqBandJson(bandIndex, bandJson);
5487
+ mergeEqBand(ctx, { kind: "master" }, bandIndex, bandJson);
3900
5488
  ctx.postSync({ type: "syncMasterStripEqBand", bandIndex, bandJson });
3901
5489
  }
3902
5490
  function setMasterStripInsertBypassed(ctx, insertIndex, bypassed, resetOnBypass) {
@@ -3904,12 +5492,67 @@ function setMasterStripInsertBypassed(ctx, insertIndex, bypassed, resetOnBypass)
3904
5492
  ctx.postSync({ type: "syncMasterStripInsertBypassed", insertIndex, bypassed, resetOnBypass });
3905
5493
  }
3906
5494
  function setMasterStripInsertParamByName(ctx, insertIndex, paramName, value) {
3907
- ctx.offlineEngine.setMasterStripInsertParamByName(insertIndex, paramName, value);
3908
- ctx.postSync({ type: "syncMasterStripInsertParamByName", insertIndex, paramName, value });
5495
+ setInsertParamByName(
5496
+ ctx,
5497
+ { kind: "master" },
5498
+ insertIndex,
5499
+ paramName,
5500
+ value,
5501
+ () => ctx.offlineEngine.restoreMasterStripInsertParamByName(insertIndex, paramName, value),
5502
+ { type: "syncMasterStripInsertParamByName", insertIndex, paramName, value }
5503
+ );
3909
5504
  }
3910
5505
  function setBusStripInsertParamByName(ctx, busId, insertIndex, paramName, value) {
3911
- ctx.offlineEngine.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
3912
- ctx.postSync({ type: "syncBusStripInsertParamByName", busId, insertIndex, paramName, value });
5506
+ setInsertParamByName(
5507
+ ctx,
5508
+ { kind: "bus", busId },
5509
+ insertIndex,
5510
+ paramName,
5511
+ value,
5512
+ () => ctx.offlineEngine.restoreBusStripInsertParamByName(busId, insertIndex, paramName, value),
5513
+ { type: "syncBusStripInsertParamByName", busId, insertIndex, paramName, value }
5514
+ );
5515
+ }
5516
+ function setBusStripInsertBypassed(ctx, busId, insertIndex, bypassed, resetOnBypass) {
5517
+ ctx.offlineEngine.setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass);
5518
+ ctx.postSync({ type: "syncBusStripInsertBypassed", busId, insertIndex, bypassed, resetOnBypass });
5519
+ }
5520
+ function setBusStripEqBand(ctx, busId, bandIndex, band) {
5521
+ const bandJson = typeof band === "string" ? band : JSON.stringify(band);
5522
+ ctx.offlineEngine.setBusStripEqBandJson(busId, bandIndex, bandJson);
5523
+ mergeEqBand(ctx, { kind: "bus", busId }, bandIndex, bandJson);
5524
+ ctx.postSync({ type: "syncBusStripEqBand", busId, bandIndex, bandJson });
5525
+ }
5526
+ function setBusStripPan(ctx, busId, pan) {
5527
+ ctx.offlineEngine.setBusStripPan(busId, pan);
5528
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5529
+ entry.pan = pan;
5530
+ });
5531
+ ctx.postSync({ type: "syncBusStripPan", busId, pan });
5532
+ }
5533
+ function setBusStripPanLaw(ctx, busId, panLaw) {
5534
+ const code = panLawCode(panLaw);
5535
+ ctx.offlineEngine.setBusStripPanLaw(busId, code);
5536
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5537
+ entry.panLaw = code;
5538
+ });
5539
+ ctx.postSync({ type: "syncBusStripPanLaw", busId, panLaw: code });
5540
+ }
5541
+ function setBusStripPanMode(ctx, busId, panMode) {
5542
+ const code = panModeCode(panMode);
5543
+ ctx.offlineEngine.setBusStripPanMode(busId, code);
5544
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5545
+ entry.panMode = code;
5546
+ });
5547
+ ctx.postSync({ type: "syncBusStripPanMode", busId, panMode: code });
5548
+ }
5549
+ function setBusStripDualPan(ctx, busId, leftPan, rightPan) {
5550
+ ctx.offlineEngine.setBusStripDualPan(busId, leftPan, rightPan);
5551
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5552
+ entry.dualPanLeft = leftPan;
5553
+ entry.dualPanRight = rightPan;
5554
+ });
5555
+ ctx.postSync({ type: "syncBusStripDualPan", busId, leftPan, rightPan });
3913
5556
  }
3914
5557
  function pushMidiNoteOn(ctx, trackId, group, channel, note, velocity, renderFrame) {
3915
5558
  const destinationId = ctx.resolveTargetId(trackId);
@@ -3950,9 +5593,55 @@ function pushMidiCc(ctx, trackId, group, channel, controller, value, renderFrame
3950
5593
  renderFrame
3951
5594
  });
3952
5595
  }
5596
+ function pushMidiPitchBend(ctx, trackId, group, channel, bend14, renderFrame) {
5597
+ const destinationId = ctx.resolveTargetId(trackId);
5598
+ ctx.offlineEngine.pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame);
5599
+ ctx.postSync({
5600
+ type: "syncMidiPitchBend",
5601
+ destinationId,
5602
+ group,
5603
+ channel,
5604
+ data0: bend14,
5605
+ data1: 0,
5606
+ renderFrame
5607
+ });
5608
+ }
5609
+ function pushMidiChannelPressure(ctx, trackId, group, channel, pressure, renderFrame) {
5610
+ const destinationId = ctx.resolveTargetId(trackId);
5611
+ ctx.offlineEngine.pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame);
5612
+ ctx.postSync({
5613
+ type: "syncMidiChannelPressure",
5614
+ destinationId,
5615
+ group,
5616
+ channel,
5617
+ data0: pressure,
5618
+ data1: 0,
5619
+ renderFrame
5620
+ });
5621
+ }
5622
+ function pushMidiPolyPressure(ctx, trackId, group, channel, note, pressure, renderFrame) {
5623
+ const destinationId = ctx.resolveTargetId(trackId);
5624
+ ctx.offlineEngine.pushMidiPolyPressure(
5625
+ destinationId,
5626
+ group,
5627
+ channel,
5628
+ note,
5629
+ pressure,
5630
+ renderFrame
5631
+ );
5632
+ ctx.postSync({
5633
+ type: "syncMidiPolyPressure",
5634
+ destinationId,
5635
+ group,
5636
+ channel,
5637
+ data0: note,
5638
+ data1: pressure,
5639
+ renderFrame
5640
+ });
5641
+ }
3953
5642
  function pushMidiUmp(ctx, trackId, word0, renderFrame) {
3954
5643
  const destinationId = ctx.resolveTargetId(trackId);
3955
- ctx.offlineEngine.pushMidiUmp(destinationId, word0, renderFrame);
5644
+ ctx.offlineEngine.pushMidiUmp(destinationId, [word0], renderFrame);
3956
5645
  ctx.postSync({ type: "syncMidiUmp", destinationId, word0, renderFrame });
3957
5646
  }
3958
5647
  function setBuiltinInstrument(ctx, trackId, config) {
@@ -4078,6 +5767,7 @@ function transferableAudioBuffers(channels) {
4078
5767
  var SonareEngine = class _SonareEngine {
4079
5768
  constructor(context, realtimeNode, offlineEngine, sampleRate, offlineBlockSize, offlineChannelCount) {
4080
5769
  this.automationLanes = /* @__PURE__ */ new Map();
5770
+ this.insertAutomationIdsByTarget = /* @__PURE__ */ new Map();
4081
5771
  this.clips = /* @__PURE__ */ new Map();
4082
5772
  this.midiClips = /* @__PURE__ */ new Map();
4083
5773
  this.markers = /* @__PURE__ */ new Map();
@@ -4085,9 +5775,12 @@ var SonareEngine = class _SonareEngine {
4085
5775
  this.trackSends = /* @__PURE__ */ new Map();
4086
5776
  this.trackOutputBus = /* @__PURE__ */ new Map();
4087
5777
  this.laneSidechains = /* @__PURE__ */ new Map();
5778
+ this.busSidechains = /* @__PURE__ */ new Map();
5779
+ this.masterSidechains = /* @__PURE__ */ new Map();
4088
5780
  this.buses = [];
4089
5781
  this.trackStripJson = /* @__PURE__ */ new Map();
4090
5782
  this.busStripJson = /* @__PURE__ */ new Map();
5783
+ this.insertParamOverrides = /* @__PURE__ */ new Map();
4091
5784
  this.tempoBpm = 120;
4092
5785
  this.timeSignature = { numerator: 4, denominator: 4 };
4093
5786
  this.tempoSegments = [{ startPpq: 0, bpm: 120 }];
@@ -4246,12 +5939,17 @@ var SonareEngine = class _SonareEngine {
4246
5939
  * @returns Reserved insert-automation id, or -1 when strip/insert/key unknown.
4247
5940
  */
4248
5941
  resolveTrackInsertAutomationId(target, insertIndex, paramName) {
4249
- return resolveTrackInsertAutomationId(
5942
+ const id = resolveTrackInsertAutomationId(
4250
5943
  this.parameterContext,
4251
5944
  target,
4252
5945
  insertIndex,
4253
5946
  paramName
4254
5947
  );
5948
+ if (id >= 0) {
5949
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
5950
+ this.rememberInsertAutomationId({ kind: "track", trackId }, id);
5951
+ }
5952
+ return id;
4255
5953
  }
4256
5954
  /**
4257
5955
  * Resolves a master-strip insert parameter to its reserved insert-automation
@@ -4262,7 +5960,15 @@ var SonareEngine = class _SonareEngine {
4262
5960
  * @returns Reserved insert-automation id, or -1 when insert/key unknown.
4263
5961
  */
4264
5962
  resolveMasterInsertAutomationId(insertIndex, paramName) {
4265
- return resolveMasterInsertAutomationId(this.parameterContext, insertIndex, paramName);
5963
+ const id = resolveMasterInsertAutomationId(
5964
+ this.parameterContext,
5965
+ insertIndex,
5966
+ paramName
5967
+ );
5968
+ if (id >= 0) {
5969
+ this.rememberInsertAutomationId({ kind: "master" }, id);
5970
+ }
5971
+ return id;
4266
5972
  }
4267
5973
  /**
4268
5974
  * Resolves a bus-strip insert parameter to its reserved insert-automation id.
@@ -4275,12 +5981,29 @@ var SonareEngine = class _SonareEngine {
4275
5981
  * @returns Reserved insert-automation id, or -1 when bus/insert/key unknown.
4276
5982
  */
4277
5983
  resolveBusInsertAutomationId(busId, insertIndex, paramName) {
4278
- return resolveBusInsertAutomationId(
5984
+ const id = resolveBusInsertAutomationId(
4279
5985
  this.parameterContext,
4280
5986
  busId,
4281
5987
  insertIndex,
4282
5988
  paramName
4283
5989
  );
5990
+ if (id >= 0) {
5991
+ this.rememberInsertAutomationId({ kind: "bus", busId }, id);
5992
+ }
5993
+ return id;
5994
+ }
5995
+ /**
5996
+ * Resolves a hosted instrument's continuous parameter to its reserved
5997
+ * instrument-automation id, so an instrument parameter follows a breakpoint
5998
+ * lane at audio-block precision like a strip insert. Bind the instrument
5999
+ * (`setSynthInstrument`) before resolving.
6000
+ *
6001
+ * @param destinationId MIDI destination the instrument is bound to.
6002
+ * @param paramName Instrument JSON-key parameter name (e.g. `cutoffHz`).
6003
+ * @returns Reserved instrument-automation id, or -1 when destination/key unknown.
6004
+ */
6005
+ resolveInstrumentAutomationId(destinationId, paramName) {
6006
+ return resolveInstrumentAutomationId(this.parameterContext, destinationId, paramName);
4284
6007
  }
4285
6008
  /**
4286
6009
  * Returns the number of automation lanes installed on the engine, including
@@ -4310,10 +6033,18 @@ var SonareEngine = class _SonareEngine {
4310
6033
  return setTrackMonitorMode(this.parameterContext, target, mode, renderFrame);
4311
6034
  }
4312
6035
  setStripGain(target, db) {
4313
- return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
6036
+ const sent = this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
6037
+ if (sent && target === "master") {
6038
+ cacheMasterStripScalar(this.stripContext, "faderDb", db);
6039
+ }
6040
+ return sent;
4314
6041
  }
4315
6042
  setStripPan(target, pan) {
4316
- return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_PAN), pan);
6043
+ const sent = this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_PAN), pan);
6044
+ if (sent && target === "master") {
6045
+ cacheMasterStripScalar(this.stripContext, "pan", pan);
6046
+ }
6047
+ return sent;
4317
6048
  }
4318
6049
  /**
4319
6050
  * Declares the mixer track lanes in an explicit order.
@@ -4350,14 +6081,37 @@ var SonareEngine = class _SonareEngine {
4350
6081
  setTrackBuses(buses) {
4351
6082
  setTrackBuses(this.mixerContext, buses);
4352
6083
  }
6084
+ /**
6085
+ * Keys one insert of a bus strip from a track lane or another bus
6086
+ * (ducking/sidechainRouter inserts). `sourceId` 0 removes the binding.
6087
+ */
6088
+ setBusSidechain(busId, insertIndex, sourceKind, sourceId) {
6089
+ setBusSidechain(this.mixerContext, busId, insertIndex, sourceKind, sourceId);
6090
+ }
6091
+ /**
6092
+ * Keys one insert of the master strip from a track lane or a bus. Same
6093
+ * source rules as {@link setBusSidechain}.
6094
+ */
6095
+ setMasterSidechain(insertIndex, sourceKind, sourceId) {
6096
+ setMasterSidechain(this.mixerContext, insertIndex, sourceKind, sourceId);
6097
+ }
4353
6098
  setBusGain(busId, db) {
4354
6099
  return setBusGain(this.mixerContext, busId, db);
4355
6100
  }
4356
6101
  setTrackStripJson(target, sceneJson) {
4357
6102
  const laneIndex = this.ensureTrackLane(target);
4358
6103
  const trackId = this.trackLaneIds[laneIndex];
4359
- setTrackStripJson(this.stripContext, trackId, sceneJson, this.trackStripJson);
4360
- this.syncMixer();
6104
+ const { resetInserts } = setTrackStripJson(
6105
+ this.stripContext,
6106
+ trackId,
6107
+ sceneJson,
6108
+ this.trackStripJson
6109
+ );
6110
+ if (resetInserts) {
6111
+ this.clearInsertAutomationLanes({ kind: "track", trackId });
6112
+ }
6113
+ pruneStripSidechains(this.mixerContext, { kind: "track", trackId }, sceneJson);
6114
+ this.syncMixer(resetInserts ? [{ kind: "track", trackId }] : []);
4361
6115
  }
4362
6116
  setTrackStripEqBand(target, bandIndex, band) {
4363
6117
  setTrackStripEqBand(this.stripContext, target, bandIndex, band);
@@ -4414,9 +6168,16 @@ var SonareEngine = class _SonareEngine {
4414
6168
  setBusStripJson(this.mixerContext, busId, sceneJson);
4415
6169
  }
4416
6170
  setMasterStripJson(sceneJson) {
4417
- this.offlineEngine.setMasterStripJson(sceneJson);
6171
+ const target = { kind: "master" };
6172
+ const resetInserts = this.masterStripJson !== void 0 && this.masterStripJson !== sceneJson || hasInsertParamOverrides(this.insertParamOverrides, target);
6173
+ applyFullStripJson(this.offlineEngine, target, sceneJson, resetInserts);
6174
+ if (resetInserts) {
6175
+ this.clearInsertAutomationLanes(target);
6176
+ }
6177
+ pruneStripSidechains(this.mixerContext, target, sceneJson);
6178
+ clearInsertParamOverrides(this.insertParamOverrides, target);
4418
6179
  this.masterStripJson = sceneJson;
4419
- this.syncMixer();
6180
+ this.syncMixer(resetInserts ? [target] : []);
4420
6181
  }
4421
6182
  setMasterStripEqBand(bandIndex, band) {
4422
6183
  setMasterStripEqBand(this.stripContext, bandIndex, band);
@@ -4431,6 +6192,47 @@ var SonareEngine = class _SonareEngine {
4431
6192
  this.ensureBus(busId);
4432
6193
  setBusStripInsertParamByName(this.stripContext, busId, insertIndex, paramName, value);
4433
6194
  }
6195
+ /**
6196
+ * Bus-strip counterpart of {@link setStripInsertBypassed}. A bus is addressed
6197
+ * by its numeric id, which the `target` of the track/master form cannot
6198
+ * express, so it is its own method exactly like
6199
+ * {@link setBusStripInsertParamByName}. Bypassing keeps the insert's internal
6200
+ * state (reverb tail, compressor envelope) unless `resetOnBypass` is set;
6201
+ * re-posting the bus scene JSON, the only workaround before this existed,
6202
+ * rebuilds the chain and loses it.
6203
+ */
6204
+ setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass = false) {
6205
+ this.ensureBus(busId);
6206
+ setBusStripInsertBypassed(
6207
+ this.stripContext,
6208
+ busId,
6209
+ insertIndex,
6210
+ bypassed,
6211
+ resetOnBypass
6212
+ );
6213
+ }
6214
+ /** Bus-strip counterpart of {@link setTrackStripEqBand}. */
6215
+ setBusStripEqBand(busId, bandIndex, band) {
6216
+ this.ensureBus(busId);
6217
+ setBusStripEqBand(this.stripContext, busId, bandIndex, band);
6218
+ }
6219
+ /** Bus-strip counterpart of {@link setTrackStripPan}; refused on a surround bus. */
6220
+ setBusStripPan(busId, pan) {
6221
+ this.ensureBus(busId);
6222
+ setBusStripPan(this.stripContext, busId, pan);
6223
+ }
6224
+ setBusStripPanLaw(busId, panLaw) {
6225
+ this.ensureBus(busId);
6226
+ setBusStripPanLaw(this.stripContext, busId, panLaw);
6227
+ }
6228
+ setBusStripPanMode(busId, panMode) {
6229
+ this.ensureBus(busId);
6230
+ setBusStripPanMode(this.stripContext, busId, panMode);
6231
+ }
6232
+ setBusStripDualPan(busId, leftPan, rightPan) {
6233
+ this.ensureBus(busId);
6234
+ setBusStripDualPan(this.stripContext, busId, leftPan, rightPan);
6235
+ }
4434
6236
  setStripInsertParamByName(target, insertIndex, paramName, value) {
4435
6237
  if (target === "master") {
4436
6238
  this.setMasterStripInsertParamByName(insertIndex, paramName, value);
@@ -4511,6 +6313,49 @@ var SonareEngine = class _SonareEngine {
4511
6313
  }
4512
6314
  return { binding, provider: binding.provider };
4513
6315
  }
6316
+ /**
6317
+ * Sets the clip-page look-ahead window in timeline frames on both the worklet
6318
+ * engine and this thread's offline engine.
6319
+ *
6320
+ * The audio thread reports the pages it is *about to* read that are not
6321
+ * resident yet, so the sliding-window streamer can service them before the
6322
+ * playhead reaches them. Without look-ahead a page miss is only reported
6323
+ * after the read already produced silence, which costs one block of silence
6324
+ * at every page boundary that was not primed.
6325
+ *
6326
+ * Defaults to half a second at the engine's sample rate. `0` disables it.
6327
+ * Safe to call during playback.
6328
+ */
6329
+ setClipPagePrefetchFrames(frames) {
6330
+ if (this.destroyed) {
6331
+ throw new Error("SonareEngine is destroyed.");
6332
+ }
6333
+ if (!Number.isFinite(frames) || frames < 0) {
6334
+ throw new Error("clip page prefetch frames must be a finite value >= 0.");
6335
+ }
6336
+ this.offlineEngine.setClipPagePrefetchFrames(frames);
6337
+ this.postSync({ type: "syncClipPagePrefetchFrames", frames });
6338
+ }
6339
+ /**
6340
+ * Sets the number of concurrent time-stretch voices on both this thread's
6341
+ * offline engine and the live worklet engine. `voices` must be an integer
6342
+ * in `[0, 64]`; a non-integer, negative, or larger value throws and leaves
6343
+ * the capacity unchanged on both engines. Default is 8. Capacity 0 disables
6344
+ * time-stretch, so every warped clip plays resampled instead. A change
6345
+ * applied while the engine is running restarts the splice state of any clip
6346
+ * stretching through a voice at that moment.
6347
+ */
6348
+ setWarpVoiceCapacity(voices) {
6349
+ if (this.destroyed) {
6350
+ throw new Error("SonareEngine is destroyed.");
6351
+ }
6352
+ this.offlineEngine.setWarpVoiceCapacity(voices);
6353
+ this.postSync({ type: "syncWarpVoiceCapacity", voices });
6354
+ }
6355
+ /** Reads the current time-stretch voice capacity (default 8). */
6356
+ warpVoiceCapacity() {
6357
+ return this.offlineEngine.warpVoiceCapacity();
6358
+ }
4514
6359
  addClip(trackId, buffer, startPpq, opts = {}) {
4515
6360
  return addClip(this.clipContext, trackId, buffer, startPpq, opts);
4516
6361
  }
@@ -4572,6 +6417,30 @@ var SonareEngine = class _SonareEngine {
4572
6417
  pushMidiCc(trackId, group, channel, controller, value, renderFrame = -1) {
4573
6418
  pushMidiCc(this.stripContext, trackId, group, channel, controller, value, renderFrame);
4574
6419
  }
6420
+ pushMidiPitchBend(trackId, group, channel, bend14, renderFrame = -1) {
6421
+ pushMidiPitchBend(this.stripContext, trackId, group, channel, bend14, renderFrame);
6422
+ }
6423
+ pushMidiChannelPressure(trackId, group, channel, pressure, renderFrame = -1) {
6424
+ pushMidiChannelPressure(
6425
+ this.stripContext,
6426
+ trackId,
6427
+ group,
6428
+ channel,
6429
+ pressure,
6430
+ renderFrame
6431
+ );
6432
+ }
6433
+ pushMidiPolyPressure(trackId, group, channel, note, pressure, renderFrame = -1) {
6434
+ pushMidiPolyPressure(
6435
+ this.stripContext,
6436
+ trackId,
6437
+ group,
6438
+ channel,
6439
+ note,
6440
+ pressure,
6441
+ renderFrame
6442
+ );
6443
+ }
4575
6444
  pushMidiUmp(trackId, word0, renderFrame = -1) {
4576
6445
  pushMidiUmp(this.stripContext, trackId, word0, renderFrame);
4577
6446
  }
@@ -4625,6 +6494,39 @@ var SonareEngine = class _SonareEngine {
4625
6494
  portTimeSamples
4626
6495
  });
4627
6496
  }
6497
+ pushMidiInputPitchBend(group, channel, bend14, portTimeSamples = 0) {
6498
+ this.offlineEngine.pushMidiInputPitchBend(group, channel, bend14, portTimeSamples);
6499
+ this.postSync({
6500
+ type: "syncMidiInputPitchBend",
6501
+ group,
6502
+ channel,
6503
+ data0: bend14,
6504
+ data1: 0,
6505
+ portTimeSamples
6506
+ });
6507
+ }
6508
+ pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples = 0) {
6509
+ this.offlineEngine.pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples);
6510
+ this.postSync({
6511
+ type: "syncMidiInputChannelPressure",
6512
+ group,
6513
+ channel,
6514
+ data0: pressure,
6515
+ data1: 0,
6516
+ portTimeSamples
6517
+ });
6518
+ }
6519
+ pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples = 0) {
6520
+ this.offlineEngine.pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples);
6521
+ this.postSync({
6522
+ type: "syncMidiInputPolyPressure",
6523
+ group,
6524
+ channel,
6525
+ data0: note,
6526
+ data1: pressure,
6527
+ portTimeSamples
6528
+ });
6529
+ }
4628
6530
  pushMidiPanic(renderFrame = -1) {
4629
6531
  this.offlineEngine.pushMidiPanic(renderFrame);
4630
6532
  this.postSync({ type: "syncMidiPanic", renderFrame });
@@ -4822,8 +6724,32 @@ var SonareEngine = class _SonareEngine {
4822
6724
  mixerLanes() {
4823
6725
  return mixerLanes(this.mixerContext);
4824
6726
  }
4825
- syncMixer() {
4826
- syncMixer(this.mixerContext);
6727
+ syncMixer(forceInsertResets = []) {
6728
+ syncMixer(this.mixerContext, false, forceInsertResets);
6729
+ }
6730
+ insertAutomationTargetKey(target) {
6731
+ return target.kind === "track" ? `track:${target.trackId}` : target.kind === "bus" ? `bus:${target.busId}` : "master";
6732
+ }
6733
+ rememberInsertAutomationId(target, id) {
6734
+ const key = this.insertAutomationTargetKey(target);
6735
+ const ids = this.insertAutomationIdsByTarget.get(key) ?? /* @__PURE__ */ new Set();
6736
+ ids.add(id);
6737
+ this.insertAutomationIdsByTarget.set(key, ids);
6738
+ }
6739
+ clearInsertAutomationLanes(target, forgetResolvedIds = false) {
6740
+ const key = this.insertAutomationTargetKey(target);
6741
+ const ids = this.insertAutomationIdsByTarget.get(key);
6742
+ if (!ids) {
6743
+ return;
6744
+ }
6745
+ for (const id of ids) {
6746
+ if (this.automationLanes.has(id)) {
6747
+ this.setAutomationLane(id, []);
6748
+ }
6749
+ }
6750
+ if (forgetResolvedIds) {
6751
+ this.insertAutomationIdsByTarget.delete(key);
6752
+ }
4827
6753
  }
4828
6754
  postInstrumentSync(message) {
4829
6755
  if (this.destroyed) {
@@ -4851,21 +6777,16 @@ var SonareEngine = class _SonareEngine {
4851
6777
  if (this.destroyed) {
4852
6778
  return;
4853
6779
  }
4854
- try {
4855
- if (transfer && transfer.length > 0) {
4856
- this.realtimeNode.node.port.postMessage(message, transfer);
4857
- } else {
4858
- this.realtimeNode.node.port.postMessage(message);
4859
- }
4860
- } finally {
4861
- this.flushOfflineMirror();
6780
+ if (transfer && transfer.length > 0) {
6781
+ this.realtimeNode.node.port.postMessage(message, transfer);
6782
+ } else {
6783
+ this.realtimeNode.node.port.postMessage(message);
4862
6784
  }
4863
6785
  }
4864
- // The offline engine is a control-thread mirror. It has no render loop to
4865
- // drain its command ring, so drain it after each control operation. Never
4866
- // call this from the worklet/audio path: it is intentionally control-only.
6786
+ // The control-only mirror applies due commands immediately while preserving
6787
+ // scheduled ones for its next render block.
4867
6788
  flushOfflineMirror() {
4868
- this.offlineEngine.flushControlCommands();
6789
+ this.offlineEngine.applyCommandsDueNowPreservingFuture();
4869
6790
  }
4870
6791
  sendMirroredCommand(command) {
4871
6792
  const accepted = this.realtimeNode.sendCommand(command);
@@ -4883,16 +6804,24 @@ var SonareEngine = class _SonareEngine {
4883
6804
  trackSends: this.trackSends,
4884
6805
  trackOutputBus: this.trackOutputBus,
4885
6806
  laneSidechains: this.laneSidechains,
6807
+ busSidechains: this.busSidechains,
6808
+ masterSidechains: this.masterSidechains,
4886
6809
  buses: this.buses,
4887
6810
  trackStripJson: this.trackStripJson,
4888
6811
  busStripJson: this.busStripJson,
6812
+ insertParamOverrides: this.insertParamOverrides,
6813
+ flushOfflineMirror: () => this.flushOfflineMirror(),
4889
6814
  postSync: (message) => this.postSync(message),
4890
6815
  ensureTrackLane: (target) => this.ensureTrackLane(target),
4891
6816
  ensureBus: (busId) => this.ensureBus(busId),
4892
6817
  mixerLanes: () => this.mixerLanes(),
4893
- syncMixer: () => this.syncMixer(),
6818
+ syncMixer: (forceInsertResets) => this.syncMixer(forceInsertResets),
6819
+ clearInsertAutomationLanes: (target, forgetResolvedIds) => this.clearInsertAutomationLanes(target, forgetResolvedIds),
4894
6820
  sendSmoothedParam: (paramId, value) => this.sendSmoothedParam(paramId, value),
4895
- getMasterStripJson: () => this.masterStripJson
6821
+ getMasterStripJson: () => this.masterStripJson,
6822
+ cacheMasterStripJson: (sceneJson) => {
6823
+ this.masterStripJson = sceneJson;
6824
+ }
4896
6825
  };
4897
6826
  }
4898
6827
  // Collaborator surface handed to the strip/pan/EQ/insert/MIDI free functions
@@ -4905,7 +6834,10 @@ var SonareEngine = class _SonareEngine {
4905
6834
  postSync: (message) => this.postSync(message),
4906
6835
  postInstrumentSync: (message) => this.postInstrumentSync(message),
4907
6836
  ensureTrackLane: (target) => this.ensureTrackLane(target),
4908
- resolveTargetId: (target) => this.resolveTargetId(target)
6837
+ resolveTargetId: (target) => this.resolveTargetId(target),
6838
+ readStripJson: (target) => cachedStripJson(this.mixerContext, target),
6839
+ writeStripJson: (target, sceneJson) => cacheStripJson(this.mixerContext, target, sceneJson),
6840
+ insertParamOverrides: this.insertParamOverrides
4909
6841
  };
4910
6842
  }
4911
6843
  // Collaborator surface handed to the automation-lane free functions so they
@@ -5047,7 +6979,7 @@ var SonareEngine = class _SonareEngine {
5047
6979
  ensureTrackLane(target) {
5048
6980
  const trackId = this.resolveTargetId(target);
5049
6981
  if (!Number.isInteger(trackId) || trackId <= 0) {
5050
- throw new Error(`Invalid track id for mixer lane: ${String(target)}`);
6982
+ throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
5051
6983
  }
5052
6984
  const existing = this.trackLaneIds.indexOf(trackId);
5053
6985
  if (existing >= 0) {
@@ -5058,20 +6990,36 @@ var SonareEngine = class _SonareEngine {
5058
6990
  return this.trackLaneIds.length - 1;
5059
6991
  }
5060
6992
  ensureBus(busId) {
5061
- const resolved = Math.trunc(busId);
5062
- if (!Number.isInteger(resolved) || resolved <= 0) {
5063
- throw new Error(`Invalid bus id for mixer bus: ${String(busId)}`);
6993
+ if (!Number.isInteger(busId) || busId <= 0) {
6994
+ throw new RangeError(`Invalid bus id for mixer bus: ${String(busId)}`);
5064
6995
  }
5065
- const existing = this.buses.findIndex((bus) => bus.busId === resolved);
6996
+ const existing = this.buses.findIndex((bus) => bus.busId === busId);
5066
6997
  if (existing >= 0) {
5067
6998
  return existing;
5068
6999
  }
5069
- this.buses.push({ busId: resolved });
7000
+ this.buses.push({ busId });
5070
7001
  this.syncMixer();
5071
7002
  return this.buses.length - 1;
5072
7003
  }
5073
7004
  };
5074
7005
 
7006
+ // src/worklet/audio_types.ts
7007
+ function copyPlanesToOutput(output, planes, frames) {
7008
+ const monoFanOut = planes.length === 1;
7009
+ for (let ch = 0; ch < output.length; ch++) {
7010
+ const target = output[ch];
7011
+ const source = monoFanOut ? planes[0] : planes[ch];
7012
+ let copied = 0;
7013
+ if (source) {
7014
+ copied = Math.min(target.length, frames, source.length);
7015
+ target.set(source.subarray(0, copied));
7016
+ }
7017
+ if (copied < target.length) {
7018
+ target.fill(0, copied);
7019
+ }
7020
+ }
7021
+ }
7022
+
5075
7023
  // src/worklet/messages.ts
5076
7024
  var DEFAULT_METRONOME_CONFIG = {
5077
7025
  beatGain: 0.35,
@@ -5115,6 +7063,10 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5115
7063
  // Latest metronome gains/click length pushed via 'syncMetronome'. The
5116
7064
  // SetMetronome command only toggles enabled state; the config arrives here.
5117
7065
  this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
7066
+ // Cue-bus plane, allocated only when the host asked for a separate PFL/AFL
7067
+ // output. Empty otherwise, so a single-output host pays no heap and keeps the
7068
+ // historical behaviour where process() folds the cue into the program mix.
7069
+ this.monitorBuffers = [];
5118
7070
  this.liveClips = /* @__PURE__ */ new Map();
5119
7071
  this.pagedClipProviders = /* @__PURE__ */ new Map();
5120
7072
  this.pagedClipPageFrames = /* @__PURE__ */ new Map();
@@ -5128,9 +7080,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5128
7080
  this.clipPageRequestOverflowReported = 0;
5129
7081
  this.sampleRate = options.sampleRate ?? 48e3;
5130
7082
  this.blockSize = options.blockSize ?? 128;
5131
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
7083
+ this.channelCount = requireChannelCount(options.channelCount, 2);
5132
7084
  this.transport = transport;
5133
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
7085
+ this.meterIntervalFrames = requireIntegerOption(
7086
+ options.meterIntervalFrames,
7087
+ 2048,
7088
+ "meterIntervalFrames",
7089
+ 0
7090
+ );
5134
7091
  this.commandRing = options.commandSharedBuffer ? this.commandRingFromSharedBuffer(options.commandSharedBuffer, options.commandRingCapacity) : void 0;
5135
7092
  this.telemetryRing = options.telemetrySharedBuffer ? this.telemetryRingFromSharedBuffer(
5136
7093
  options.telemetrySharedBuffer,
@@ -5150,10 +7107,13 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5150
7107
  options.externalMidiSharedBuffer,
5151
7108
  options.externalMidiRingCapacity
5152
7109
  ) : void 0;
7110
+ if (this.commandRing && this.commandRing.capacity > 65536) {
7111
+ throw new RangeError("commandRingCapacity must be at most 65536");
7112
+ }
5153
7113
  this.engine = new RealtimeEngine(
5154
7114
  this.sampleRate,
5155
7115
  this.blockSize,
5156
- 1024,
7116
+ Math.max(1024, this.commandRing?.capacity ?? 0),
5157
7117
  1024,
5158
7118
  this.channelCount
5159
7119
  );
@@ -5162,8 +7122,21 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5162
7122
  for (let ch = 0; ch < this.channelCount; ch++) {
5163
7123
  this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
5164
7124
  }
7125
+ this.cueOutput = options.cueOutput === true;
7126
+ if (this.cueOutput) {
7127
+ this.engine.prepareMonitorChannels(this.channelCount, this.blockSize);
7128
+ this.monitorBuffers = new Array(this.channelCount);
7129
+ for (let ch = 0; ch < this.channelCount; ch++) {
7130
+ this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
7131
+ }
7132
+ }
5165
7133
  if (this.scopeRing) {
5166
- const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
7134
+ const interval = requireIntegerOption(
7135
+ options.scopeIntervalFrames,
7136
+ this.blockSize,
7137
+ "scopeIntervalFrames",
7138
+ 0
7139
+ );
5167
7140
  this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
5168
7141
  }
5169
7142
  }
@@ -5198,6 +7171,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5198
7171
  if ((this.channelBuffers[0]?.byteLength ?? 0) === 0) {
5199
7172
  this.reacquireChannelBuffers();
5200
7173
  }
7174
+ if (this.cueOutput && (this.monitorBuffers[0]?.byteLength ?? 0) === 0) {
7175
+ this.reacquireMonitorBuffers();
7176
+ }
5201
7177
  const input = inputs[0];
5202
7178
  for (let ch = 0; ch < this.channelCount; ch++) {
5203
7179
  const dst = this.channelBuffers[ch];
@@ -5208,17 +7184,16 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5208
7184
  dst.fill(0, 0, usableFrames);
5209
7185
  }
5210
7186
  }
5211
- this.engine.processPrepared(usableFrames);
5212
- for (let ch = 0; ch < output.length; ch++) {
5213
- const target = output[ch];
5214
- const source = this.channelBuffers[ch] ?? this.channelBuffers[0];
5215
- if (source) {
5216
- target.set(source.subarray(0, Math.min(target.length, usableFrames)));
5217
- if (target.length > usableFrames) {
5218
- target.fill(0, usableFrames);
5219
- }
5220
- } else {
5221
- target.fill(0);
7187
+ if (this.cueOutput) {
7188
+ this.engine.processPreparedWithMonitor(usableFrames);
7189
+ } else {
7190
+ this.engine.processPrepared(usableFrames);
7191
+ }
7192
+ copyPlanesToOutput(output, this.channelBuffers, usableFrames);
7193
+ if (this.cueOutput) {
7194
+ const cue = outputs[1];
7195
+ if (cue) {
7196
+ copyPlanesToOutput(cue, this.monitorBuffers, usableFrames);
5222
7197
  }
5223
7198
  }
5224
7199
  this.publishClipPageRequests();
@@ -5233,6 +7208,11 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5233
7208
  this.channelBuffers[ch] = this.engine.getChannelBuffer(ch, this.blockSize);
5234
7209
  }
5235
7210
  }
7211
+ reacquireMonitorBuffers() {
7212
+ for (let ch = 0; ch < this.channelCount; ch++) {
7213
+ this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
7214
+ }
7215
+ }
5236
7216
  receiveCommand(command) {
5237
7217
  if (!this.closed) {
5238
7218
  this.safeApplyCommand(command);
@@ -5320,6 +7300,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5320
7300
  }
5321
7301
  break;
5322
7302
  }
7303
+ case "syncClipPagePrefetchFrames": {
7304
+ this.engine.setClipPagePrefetchFrames(message.frames);
7305
+ break;
7306
+ }
7307
+ case "syncWarpVoiceCapacity": {
7308
+ this.engine.setWarpVoiceCapacity(message.voices);
7309
+ break;
7310
+ }
5323
7311
  case "syncClipPageCommit": {
5324
7312
  const providerId = this.pagedClipProviders.get(message.clipId);
5325
7313
  const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
@@ -5378,11 +7366,60 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5378
7366
  );
5379
7367
  }
5380
7368
  break;
5381
- case "syncMixer":
7369
+ case "syncMixer": {
7370
+ if (message.sidechainDelta === true) {
7371
+ for (const binding of message.laneSidechains ?? []) {
7372
+ this.engine.setLaneSidechain(
7373
+ binding.trackId,
7374
+ binding.insertIndex,
7375
+ binding.sourceTrackId
7376
+ );
7377
+ }
7378
+ for (const binding of message.busSidechains ?? []) {
7379
+ this.engine.setBusSidechain(
7380
+ binding.busId,
7381
+ binding.insertIndex,
7382
+ binding.sourceKind,
7383
+ binding.sourceId
7384
+ );
7385
+ }
7386
+ for (const binding of message.masterSidechains ?? []) {
7387
+ this.engine.setMasterSidechain(
7388
+ binding.insertIndex,
7389
+ binding.sourceKind,
7390
+ binding.sourceId
7391
+ );
7392
+ }
7393
+ break;
7394
+ }
7395
+ if (this.commandRing) {
7396
+ const write = Atomics.load(this.commandRing.header, 0);
7397
+ const read = Atomics.load(this.commandRing.header, 1);
7398
+ this.drainCommands(Math.min(this.commandRing.capacity, Math.max(0, write - read)));
7399
+ }
7400
+ this.engine.applyCommandsDueNowPreservingFuture();
7401
+ this.engine.settleInsertParameters();
5382
7402
  if (message.buses) {
5383
7403
  this.engine.setTrackBuses(message.buses);
5384
7404
  }
5385
7405
  this.engine.setTrackLanes(message.lanes);
7406
+ for (const target of message.forceInsertResets ?? []) {
7407
+ const emptyJson = emptyStripJson(target);
7408
+ switch (target.kind) {
7409
+ case "track":
7410
+ this.engine.clearTrackInsertParameterBases(target.trackId);
7411
+ this.engine.setTrackStripJson(target.trackId, emptyJson);
7412
+ break;
7413
+ case "bus":
7414
+ this.engine.clearBusInsertParameterBases(target.busId);
7415
+ this.engine.setBusStripJson(target.busId, emptyJson);
7416
+ break;
7417
+ case "master":
7418
+ this.engine.clearMasterInsertParameterBases();
7419
+ this.engine.setMasterStripJson(emptyJson);
7420
+ break;
7421
+ }
7422
+ }
5386
7423
  for (const strip of message.trackStrips ?? []) {
5387
7424
  this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
5388
7425
  }
@@ -5395,7 +7432,46 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5395
7432
  for (const binding of message.laneSidechains ?? []) {
5396
7433
  this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
5397
7434
  }
7435
+ for (const binding of message.busSidechains ?? []) {
7436
+ this.engine.setBusSidechain(
7437
+ binding.busId,
7438
+ binding.insertIndex,
7439
+ binding.sourceKind,
7440
+ binding.sourceId
7441
+ );
7442
+ }
7443
+ for (const binding of message.masterSidechains ?? []) {
7444
+ this.engine.setMasterSidechain(binding.insertIndex, binding.sourceKind, binding.sourceId);
7445
+ }
7446
+ for (const override of message.insertParamOverrides ?? []) {
7447
+ switch (override.kind) {
7448
+ case "track":
7449
+ this.engine.restoreTrackStripInsertParamByName(
7450
+ override.trackId,
7451
+ override.insertIndex,
7452
+ override.paramName,
7453
+ override.value
7454
+ );
7455
+ break;
7456
+ case "bus":
7457
+ this.engine.restoreBusStripInsertParamByName(
7458
+ override.busId,
7459
+ override.insertIndex,
7460
+ override.paramName,
7461
+ override.value
7462
+ );
7463
+ break;
7464
+ case "master":
7465
+ this.engine.restoreMasterStripInsertParamByName(
7466
+ override.insertIndex,
7467
+ override.paramName,
7468
+ override.value
7469
+ );
7470
+ break;
7471
+ }
7472
+ }
5398
7473
  break;
7474
+ }
5399
7475
  case "syncCapture":
5400
7476
  this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
5401
7477
  this.engine.setCaptureSource(message.source);
@@ -5408,6 +7484,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5408
7484
  case "syncMasterStripEqBand":
5409
7485
  this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
5410
7486
  break;
7487
+ case "syncBusStripEqBand":
7488
+ this.engine.setBusStripEqBandJson(message.busId, message.bandIndex, message.bandJson);
7489
+ break;
5411
7490
  case "syncTrackStripInsertBypassed":
5412
7491
  this.engine.setTrackStripInsertBypassed(
5413
7492
  message.trackId,
@@ -5424,26 +7503,57 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5424
7503
  );
5425
7504
  break;
5426
7505
  case "syncTrackStripInsertParamByName":
5427
- this.engine.setTrackStripInsertParamByName(
7506
+ this.engine.applyCommandsDueNowPreservingFuture();
7507
+ if (!this.engine.applyTrackStripInsertParamByNameNow(
5428
7508
  message.trackId,
5429
7509
  message.insertIndex,
5430
7510
  message.paramName,
5431
7511
  message.value
5432
- );
7512
+ )) {
7513
+ this.engine.restoreTrackStripInsertParamByName(
7514
+ message.trackId,
7515
+ message.insertIndex,
7516
+ message.paramName,
7517
+ message.value
7518
+ );
7519
+ }
5433
7520
  break;
5434
7521
  case "syncMasterStripInsertParamByName":
5435
- this.engine.setMasterStripInsertParamByName(
7522
+ this.engine.applyCommandsDueNowPreservingFuture();
7523
+ if (!this.engine.applyMasterStripInsertParamByNameNow(
5436
7524
  message.insertIndex,
5437
7525
  message.paramName,
5438
7526
  message.value
5439
- );
7527
+ )) {
7528
+ this.engine.restoreMasterStripInsertParamByName(
7529
+ message.insertIndex,
7530
+ message.paramName,
7531
+ message.value
7532
+ );
7533
+ }
5440
7534
  break;
5441
7535
  case "syncBusStripInsertParamByName":
5442
- this.engine.setBusStripInsertParamByName(
7536
+ this.engine.applyCommandsDueNowPreservingFuture();
7537
+ if (!this.engine.applyBusStripInsertParamByNameNow(
5443
7538
  message.busId,
5444
7539
  message.insertIndex,
5445
7540
  message.paramName,
5446
7541
  message.value
7542
+ )) {
7543
+ this.engine.restoreBusStripInsertParamByName(
7544
+ message.busId,
7545
+ message.insertIndex,
7546
+ message.paramName,
7547
+ message.value
7548
+ );
7549
+ }
7550
+ break;
7551
+ case "syncBusStripInsertBypassed":
7552
+ this.engine.setBusStripInsertBypassed(
7553
+ message.busId,
7554
+ message.insertIndex,
7555
+ message.bypassed,
7556
+ message.resetOnBypass
5447
7557
  );
5448
7558
  break;
5449
7559
  case "syncTrackStripPan":
@@ -5458,6 +7568,18 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5458
7568
  case "syncTrackStripDualPan":
5459
7569
  this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
5460
7570
  break;
7571
+ case "syncBusStripPan":
7572
+ this.engine.setBusStripPan(message.busId, message.pan);
7573
+ break;
7574
+ case "syncBusStripPanLaw":
7575
+ this.engine.setBusStripPanLaw(message.busId, message.panLaw);
7576
+ break;
7577
+ case "syncBusStripPanMode":
7578
+ this.engine.setBusStripPanMode(message.busId, message.panMode);
7579
+ break;
7580
+ case "syncBusStripDualPan":
7581
+ this.engine.setBusStripDualPan(message.busId, message.leftPan, message.rightPan);
7582
+ break;
5461
7583
  case "syncTrackStripChannelDelaySamples":
5462
7584
  this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
5463
7585
  break;
@@ -5509,8 +7631,36 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5509
7631
  message.renderFrame
5510
7632
  );
5511
7633
  break;
7634
+ case "syncMidiPitchBend":
7635
+ this.engine.pushMidiPitchBend(
7636
+ message.destinationId,
7637
+ message.group,
7638
+ message.channel,
7639
+ message.data0,
7640
+ message.renderFrame
7641
+ );
7642
+ break;
7643
+ case "syncMidiChannelPressure":
7644
+ this.engine.pushMidiChannelPressure(
7645
+ message.destinationId,
7646
+ message.group,
7647
+ message.channel,
7648
+ message.data0,
7649
+ message.renderFrame
7650
+ );
7651
+ break;
7652
+ case "syncMidiPolyPressure":
7653
+ this.engine.pushMidiPolyPressure(
7654
+ message.destinationId,
7655
+ message.group,
7656
+ message.channel,
7657
+ message.data0,
7658
+ message.data1,
7659
+ message.renderFrame
7660
+ );
7661
+ break;
5512
7662
  case "syncMidiUmp":
5513
- this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
7663
+ this.engine.pushMidiUmp(message.destinationId, [message.word0], message.renderFrame);
5514
7664
  break;
5515
7665
  case "syncMidiSysex":
5516
7666
  this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
@@ -5563,6 +7713,31 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5563
7713
  message.portTimeSamples
5564
7714
  );
5565
7715
  break;
7716
+ case "syncMidiInputPitchBend":
7717
+ this.engine.pushMidiInputPitchBend(
7718
+ message.group,
7719
+ message.channel,
7720
+ message.data0,
7721
+ message.portTimeSamples
7722
+ );
7723
+ break;
7724
+ case "syncMidiInputChannelPressure":
7725
+ this.engine.pushMidiInputChannelPressure(
7726
+ message.group,
7727
+ message.channel,
7728
+ message.data0,
7729
+ message.portTimeSamples
7730
+ );
7731
+ break;
7732
+ case "syncMidiInputPolyPressure":
7733
+ this.engine.pushMidiInputPolyPressure(
7734
+ message.group,
7735
+ message.channel,
7736
+ message.data0,
7737
+ message.data1,
7738
+ message.portTimeSamples
7739
+ );
7740
+ break;
5566
7741
  }
5567
7742
  }
5568
7743
  receiveCaptureRequest(message) {
@@ -5643,11 +7818,11 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5643
7818
  this.closed = true;
5644
7819
  }
5645
7820
  }
5646
- drainCommands() {
7821
+ drainCommands(limit = 64) {
5647
7822
  if (!this.commandRing) {
5648
7823
  return;
5649
7824
  }
5650
- for (let i = 0; i < 64; i++) {
7825
+ for (let i = 0; i < limit; i++) {
5651
7826
  const command = popSonareEngineCommandRingBuffer(this.commandRing);
5652
7827
  if (!command) {
5653
7828
  return;
@@ -5834,10 +8009,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5834
8009
  if (meter.frame !== this.lastMeterFrame) {
5835
8010
  this.lastMeterFrame = meter.frame;
5836
8011
  }
5837
- if (this.meterRing) {
5838
- this.writeMeterRing(meter);
8012
+ if (this.transport?.onMeter) {
8013
+ this.transport.onMeter(meter);
5839
8014
  } else {
5840
- this.transport?.onMeter?.(meter);
5841
8015
  this.transport?.postMessage?.(meter);
5842
8016
  }
5843
8017
  }
@@ -5854,29 +8028,18 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5854
8028
  }
5855
8029
  Atomics.store(ring.header, 0, writeIndex + 1);
5856
8030
  }
5857
- writeMeterRing(meter) {
5858
- const ring = this.meterRing;
5859
- if (!ring) {
5860
- return;
5861
- }
5862
- const writeIndex = Atomics.load(ring.header, 0);
5863
- const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
5864
- ring.records[offset] = encodeFrameLo(meter.frame);
5865
- ring.records[offset + 1] = encodeFrameHi(meter.frame);
5866
- ring.records[offset + 2] = meter.targetId;
5867
- ring.records[offset + 3] = meter.peakDbL;
5868
- ring.records[offset + 4] = meter.peakDbR;
5869
- ring.records[offset + 5] = meter.rmsDbL;
5870
- ring.records[offset + 6] = meter.rmsDbR;
5871
- ring.records[offset + 7] = meter.correlation;
5872
- ring.records[offset + 8] = meter.truePeakDbL;
5873
- ring.records[offset + 9] = meter.truePeakDbR;
5874
- ring.records[offset + 10] = meter.momentaryLufs;
5875
- ring.records[offset + 11] = meter.shortTermLufs;
5876
- ring.records[offset + 12] = meter.integratedLufs;
5877
- ring.records[offset + 13] = meter.gainReductionDb;
5878
- Atomics.store(ring.header, 0, writeIndex + 1);
5879
- }
8031
+ // A snapshot-object ring writer used to live here as well. It was
8032
+ // unreachable: publishMeters returns after the writeMeterScratch loop
8033
+ // whenever a ring exists, so the ring is never fed from a materialised
8034
+ // snapshot. writeMeterScratch above is the only ring writer, and it keeps the
8035
+ // render callback allocation-free by reading the engine's scratch registers
8036
+ // directly.
8037
+ //
8038
+ // writeIndex is a free-running monotonic counter, so an overflow guard would
8039
+ // fire on essentially every write past the first `capacity` records and store
8040
+ // an ever-growing value, not a dropped-record count. Readers already detect
8041
+ // silent overrun via firstReadable = max(readIndex, writeIndex - capacity),
8042
+ // so header slot 3 is left at its initial 0.
5880
8043
  // Drains the engine's scope producer (FFT spectrum + goniometer points) into
5881
8044
  // the lock-free SAB scope ring. No allocation on the render path: records are
5882
8045
  // written field-by-field into the ring.
@@ -6084,15 +8247,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
6084
8247
  }
6085
8248
  return;
6086
8249
  }
6087
- if (isEngineCommandRecord(event.data)) {
6088
- this.bridge.receiveCommand(event.data);
6089
- } else if (isEngineSyncMessage(event.data)) {
6090
- this.bridge.receiveSync(event.data);
6091
- } else if (isEngineCaptureRequestMessage(event.data)) {
6092
- this.bridge.receiveCaptureRequest(event.data);
6093
- } else if (isEngineTransportRequestMessage(event.data)) {
6094
- this.bridge.receiveTransportRequest(event.data);
6095
- }
8250
+ this.routeMessage(event.data);
6096
8251
  };
6097
8252
  if (port?.addEventListener) {
6098
8253
  port.addEventListener("message", onMessage);
@@ -6111,18 +8266,35 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
6111
8266
  }
6112
8267
  return true;
6113
8268
  }
8269
+ /**
8270
+ * Single dispatch point for the port, shared with the buffered replay so
8271
+ * the two cannot recognize different message sets.
8272
+ */
8273
+ routeMessage(data) {
8274
+ const bridge = this.bridge;
8275
+ if (!bridge) {
8276
+ return;
8277
+ }
8278
+ if (isEngineCommandRecord(data)) {
8279
+ bridge.receiveCommand(data);
8280
+ } else if (isEngineSyncMessage(data)) {
8281
+ bridge.receiveSync(data);
8282
+ } else if (isEngineCaptureRequestMessage(data)) {
8283
+ bridge.receiveCaptureRequest(data);
8284
+ } else if (isEngineTransportRequestMessage(data)) {
8285
+ bridge.receiveTransportRequest(data);
8286
+ } else if (isRecord(data) && typeof data.type === "string" && data.type.startsWith("sync")) {
8287
+ this.port?.postMessage?.({
8288
+ type: "syncError",
8289
+ syncType: data.type,
8290
+ message: `Unrecognized worklet sync message: ${data.type}`
8291
+ });
8292
+ }
8293
+ }
6114
8294
  replayPendingMessages() {
6115
8295
  const messages = this.pendingMessages.splice(0);
6116
8296
  for (const data of messages) {
6117
- if (isEngineCommandRecord(data)) {
6118
- this.bridge?.receiveCommand(data);
6119
- } else if (isEngineSyncMessage(data)) {
6120
- this.bridge?.receiveSync(data);
6121
- } else if (isEngineCaptureRequestMessage(data)) {
6122
- this.bridge?.receiveCaptureRequest(data);
6123
- } else if (isEngineTransportRequestMessage(data)) {
6124
- this.bridge?.receiveTransportRequest(data);
6125
- }
8297
+ this.routeMessage(data);
6126
8298
  }
6127
8299
  }
6128
8300
  async initializeEmbind(options, port) {
@@ -6175,15 +8347,48 @@ var SonareWorkletProcessor = class {
6175
8347
  this.processedFrames = 0;
6176
8348
  this.lastMeterFrame = 0;
6177
8349
  this.lastSpectrumFrame = 0;
8350
+ /**
8351
+ * Reused meter record, so a publish writes fields instead of allocating one.
8352
+ *
8353
+ * `targetId` is always the master and the four LUFS / gain-reduction fields
8354
+ * are always unavailable here — the mixer worklet does not run the
8355
+ * K-weighting filters, and a floor value would read as silence — so both are
8356
+ * set once rather than per interval.
8357
+ */
8358
+ this.meterScratch = {
8359
+ type: "meter",
8360
+ targetId: 0,
8361
+ frame: 0,
8362
+ peakDbL: 0,
8363
+ peakDbR: 0,
8364
+ rmsDbL: 0,
8365
+ rmsDbR: 0,
8366
+ correlation: 0,
8367
+ truePeakDbL: 0,
8368
+ truePeakDbR: 0,
8369
+ momentaryLufs: Number.NaN,
8370
+ shortTermLufs: Number.NaN,
8371
+ integratedLufs: Number.NaN,
8372
+ gainReductionDb: Number.NaN
8373
+ };
6178
8374
  if (!options.sceneJson) {
6179
8375
  throw new Error("sceneJson is required.");
6180
8376
  }
6181
8377
  this.sampleRate = options.sampleRate ?? 48e3;
6182
8378
  this.blockSize = options.blockSize ?? 128;
6183
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
6184
- this.spectrumIntervalFrames = Math.max(0, Math.floor(options.spectrumIntervalFrames ?? 0));
8379
+ this.meterIntervalFrames = requireIntegerOption(
8380
+ options.meterIntervalFrames,
8381
+ 2048,
8382
+ "meterIntervalFrames",
8383
+ 0
8384
+ );
8385
+ this.spectrumIntervalFrames = requireIntegerOption(
8386
+ options.spectrumIntervalFrames,
8387
+ 0,
8388
+ "spectrumIntervalFrames",
8389
+ 0
8390
+ );
6185
8391
  this.transport = transport;
6186
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
6187
8392
  this.meterRing = options.meterSharedBuffer ? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity) : void 0;
6188
8393
  this.spectrumRing = options.spectrumSharedBuffer ? spectrumRingFromSharedBuffer(
6189
8394
  options.spectrumSharedBuffer,
@@ -6200,6 +8405,7 @@ var SonareWorkletProcessor = class {
6200
8405
  throw new Error("stripCount must match the scene strip count.");
6201
8406
  }
6202
8407
  this.realtime = this.mixer.createRealtimeBuffer();
8408
+ this.mixer.configureMeter(this.meterIntervalFrames > 0, 4);
6203
8409
  }
6204
8410
  process(inputs, outputs) {
6205
8411
  if (this.closed) {
@@ -6246,10 +8452,7 @@ var SonareWorkletProcessor = class {
6246
8452
  }
6247
8453
  }
6248
8454
  this.processedFrames += usable;
6249
- this.publishMeter(
6250
- this.realtime.outLeft.subarray(0, usable),
6251
- this.realtime.outRight.subarray(0, usable)
6252
- );
8455
+ this.publishMeter();
6253
8456
  this.publishSpectrum(
6254
8457
  this.realtime.outLeft.subarray(0, usable),
6255
8458
  this.realtime.outRight.subarray(0, usable)
@@ -6265,7 +8468,11 @@ var SonareWorkletProcessor = class {
6265
8468
  return;
6266
8469
  }
6267
8470
  if (message.type === "setMeterInterval") {
6268
- this.meterIntervalFrames = Math.max(0, Math.floor(message.frames));
8471
+ const frames = Math.max(0, Math.floor(message.frames));
8472
+ if (frames > 0 !== this.meterIntervalFrames > 0) {
8473
+ this.mixer.configureMeter(frames > 0, 4);
8474
+ }
8475
+ this.meterIntervalFrames = frames;
6269
8476
  return;
6270
8477
  }
6271
8478
  if (message.type === "scheduleInsertAutomation") {
@@ -6285,58 +8492,52 @@ var SonareWorkletProcessor = class {
6285
8492
  this.closed = true;
6286
8493
  }
6287
8494
  }
6288
- publishMeter(left, right) {
6289
- if (!this.transport || this.meterIntervalFrames <= 0) {
8495
+ publishMeter() {
8496
+ if (!this.transport && !this.meterRing || this.meterIntervalFrames <= 0) {
6290
8497
  return;
6291
8498
  }
6292
8499
  if (this.processedFrames - this.lastMeterFrame < this.meterIntervalFrames) {
6293
8500
  return;
6294
8501
  }
6295
8502
  this.lastMeterFrame = this.processedFrames;
6296
- let peakL = 0;
6297
- let peakR = 0;
6298
- let sumL = 0;
6299
- let sumR = 0;
6300
- let sumLR = 0;
6301
- for (let i = 0; i < left.length; i++) {
6302
- const l = left[i] ?? 0;
6303
- const r = right[i] ?? 0;
6304
- const absL = Math.abs(l);
6305
- const absR = Math.abs(r);
6306
- if (absL > peakL) {
6307
- peakL = absL;
6308
- }
6309
- if (absR > peakR) {
6310
- peakR = absR;
6311
- }
6312
- sumL += l * l;
6313
- sumR += r * r;
6314
- sumLR += l * r;
8503
+ if (!this.mixer.latchMeterSnapshot()) {
8504
+ return;
8505
+ }
8506
+ if (this.meterRing) {
8507
+ const meter2 = this.meterScratch;
8508
+ meter2.frame = this.processedFrames;
8509
+ meter2.peakDbL = this.mixer.meterScratchValue(0);
8510
+ meter2.peakDbR = this.mixer.meterScratchValue(1);
8511
+ meter2.rmsDbL = this.mixer.meterScratchValue(2);
8512
+ meter2.rmsDbR = this.mixer.meterScratchValue(3);
8513
+ meter2.correlation = this.mixer.meterScratchValue(4);
8514
+ meter2.truePeakDbL = this.mixer.meterScratchValue(5);
8515
+ meter2.truePeakDbR = this.mixer.meterScratchValue(6);
8516
+ this.writeMeterRing(meter2);
8517
+ return;
6315
8518
  }
6316
- const rmsL = Math.sqrt(sumL / Math.max(1, left.length));
6317
- const rmsR = Math.sqrt(sumR / Math.max(1, right.length));
6318
- const denominator = Math.sqrt(sumL * sumR);
6319
8519
  const meter = {
6320
8520
  type: "meter",
6321
8521
  targetId: 0,
6322
8522
  frame: this.processedFrames,
6323
- peakDbL: toDb(peakL),
6324
- peakDbR: toDb(peakR),
6325
- rmsDbL: toDb(rmsL),
6326
- rmsDbR: toDb(rmsR),
6327
- correlation: denominator > 0 ? sumLR / denominator : 0,
6328
- truePeakDbL: toDb(peakL),
6329
- truePeakDbR: toDb(peakR),
8523
+ peakDbL: this.mixer.meterScratchValue(0),
8524
+ peakDbR: this.mixer.meterScratchValue(1),
8525
+ rmsDbL: this.mixer.meterScratchValue(2),
8526
+ rmsDbR: this.mixer.meterScratchValue(3),
8527
+ correlation: this.mixer.meterScratchValue(4),
8528
+ truePeakDbL: this.mixer.meterScratchValue(5),
8529
+ truePeakDbR: this.mixer.meterScratchValue(6),
8530
+ // Declared unavailable rather than floored: the mixer worklet does not run
8531
+ // the K-weighting filters, and a floor value would read as silence.
6330
8532
  momentaryLufs: Number.NaN,
6331
8533
  shortTermLufs: Number.NaN,
6332
8534
  integratedLufs: Number.NaN,
6333
8535
  gainReductionDb: Number.NaN
6334
8536
  };
6335
- this.transport.onMeter?.(meter);
6336
- if (this.meterRing) {
6337
- this.writeMeterRing(meter);
8537
+ if (this.transport?.onMeter) {
8538
+ this.transport.onMeter(meter);
6338
8539
  } else {
6339
- this.transport.postMessage?.(meter);
8540
+ this.transport?.postMessage?.(meter);
6340
8541
  }
6341
8542
  }
6342
8543
  writeMeterRing(meter) {
@@ -6380,8 +8581,11 @@ var SonareWorkletProcessor = class {
6380
8581
  frame: this.processedFrames,
6381
8582
  bands: new Float32Array(this.spectrumBands)
6382
8583
  };
6383
- this.transport?.onSpectrum?.(spectrum);
6384
- this.transport?.postMessage?.(spectrum);
8584
+ if (this.transport?.onSpectrum) {
8585
+ this.transport.onSpectrum(spectrum);
8586
+ } else {
8587
+ this.transport?.postMessage?.(spectrum);
8588
+ }
6385
8589
  }
6386
8590
  computeSpectrum(left, right) {
6387
8591
  const n = Math.max(1, Math.min(left.length, right.length));
@@ -6449,13 +8653,232 @@ function registerSonareWorkletProcessor(name = "sonare-worklet-processor") {
6449
8653
  scope.registerProcessor(name, RegisteredSonareWorkletProcessor);
6450
8654
  }
6451
8655
 
8656
+ // src/worklet/playback-processor.ts
8657
+ var MAX_INPUT_CHANNELS = 8;
8658
+ var SPEAKER_LAYOUT_CHANNELS = { stereo: 2, "5.1": 6, "7.1": 8 };
8659
+ function configObject(config) {
8660
+ if (config === void 0) {
8661
+ return {};
8662
+ }
8663
+ return typeof config === "string" ? JSON.parse(config) : config;
8664
+ }
8665
+ function outputChannelCount(config) {
8666
+ if (config.target?.kind !== "speakers") {
8667
+ return 2;
8668
+ }
8669
+ const count = SPEAKER_LAYOUT_CHANNELS[config.target.layout ?? ""];
8670
+ if (count === void 0) {
8671
+ throw new RangeError('target.layout must be "stereo", "5.1" or "7.1" for a speakers target');
8672
+ }
8673
+ return count;
8674
+ }
8675
+ var _SonarePlaybackWorkletProcessor = class _SonarePlaybackWorkletProcessor {
8676
+ constructor(options = {}, port) {
8677
+ this.inputPlanes = [];
8678
+ this.outputPlanes = [];
8679
+ this.unsupportedInputBlocks = 0;
8680
+ this.destroyed = false;
8681
+ this.port = port;
8682
+ this.maxBlockSize = requireIntegerOption(options.maxBlockSize, 128, "maxBlockSize", 1);
8683
+ const scopeRate = globalThis.sampleRate;
8684
+ const sampleRate = options.sampleRate ?? (typeof scopeRate === "number" ? scopeRate : 48e3);
8685
+ const hrtf = options.hrtf ? HrtfSet.fromBytes(new Uint8Array(options.hrtf)) : void 0;
8686
+ try {
8687
+ this.renderer = new PlaybackRenderer({
8688
+ config: options.config ?? {},
8689
+ hrtf,
8690
+ sampleRate,
8691
+ maxBlockSize: this.maxBlockSize
8692
+ });
8693
+ } finally {
8694
+ hrtf?.delete();
8695
+ }
8696
+ this.native = this.renderer.native;
8697
+ this.outputChannels = this.native.outputChannels();
8698
+ this.acquirePlanes();
8699
+ }
8700
+ /**
8701
+ * Handles a control-plane message. AudioWorklet port handlers run on the
8702
+ * rendering thread between `process()` calls, which is what makes `reset`
8703
+ * safe here. A refused message is answered with an `error` message.
8704
+ */
8705
+ receiveMessage(message) {
8706
+ if (this.destroyed) {
8707
+ return;
8708
+ }
8709
+ try {
8710
+ switch (message.type) {
8711
+ case "config":
8712
+ this.renderer.setConfig(message.config);
8713
+ break;
8714
+ case "orientation":
8715
+ this.renderer.setHeadOrientation(message.yaw, message.pitch ?? 0, message.roll ?? 0);
8716
+ break;
8717
+ case "reset":
8718
+ this.renderer.reset();
8719
+ break;
8720
+ case "diagnostics":
8721
+ this.port?.postMessage?.({
8722
+ type: "diagnostics",
8723
+ diagnostics: this.diagnostics()
8724
+ });
8725
+ break;
8726
+ case "destroy":
8727
+ this.destroy();
8728
+ break;
8729
+ }
8730
+ } catch (error) {
8731
+ this.port?.postMessage?.({
8732
+ type: "error",
8733
+ request: message.type,
8734
+ message: error instanceof Error ? error.message : String(error)
8735
+ });
8736
+ }
8737
+ }
8738
+ /** The renderer's diagnostics plus `unsupported_input_blocks`, as a plain object. */
8739
+ diagnostics() {
8740
+ return {
8741
+ ...this.renderer.diagnostics(),
8742
+ unsupported_input_blocks: this.unsupportedInputBlocks
8743
+ };
8744
+ }
8745
+ process(inputs, outputs) {
8746
+ if (this.destroyed) {
8747
+ return false;
8748
+ }
8749
+ const output = outputs[0];
8750
+ const requested = output?.[0]?.length ?? 0;
8751
+ if (!output || requested === 0) {
8752
+ return true;
8753
+ }
8754
+ const frames = this.clampFrames(requested);
8755
+ if (this.inputPlanes[0]?.byteLength === 0 || this.outputPlanes[0]?.byteLength === 0) {
8756
+ this.acquirePlanes();
8757
+ }
8758
+ const input = inputs[0];
8759
+ const inChannels = input?.length ?? 0;
8760
+ let code;
8761
+ if (inChannels === 0) {
8762
+ code = this.native.processPreparedSilence(frames);
8763
+ } else if (inChannels > MAX_INPUT_CHANNELS) {
8764
+ this.unsupportedInputBlocks++;
8765
+ code = this.native.processPreparedSilence(frames);
8766
+ } else {
8767
+ for (let ch = 0; ch < inChannels; ch++) {
8768
+ const source = input[ch];
8769
+ const plane = this.inputPlanes[ch];
8770
+ const copied = Math.min(frames, source.length);
8771
+ plane.set(copied === source.length ? source : source.subarray(0, copied));
8772
+ if (copied < frames) {
8773
+ plane.fill(0, copied, frames);
8774
+ }
8775
+ }
8776
+ code = this.native.processPrepared(inChannels, frames);
8777
+ if (code !== 0) {
8778
+ this.unsupportedInputBlocks++;
8779
+ code = this.native.processPreparedSilence(frames);
8780
+ }
8781
+ }
8782
+ if (code === 0) {
8783
+ copyPlanesToOutput(output, this.outputPlanes, frames);
8784
+ } else {
8785
+ for (const channel of output) {
8786
+ channel.fill(0);
8787
+ }
8788
+ }
8789
+ return true;
8790
+ }
8791
+ destroy() {
8792
+ if (this.destroyed) {
8793
+ return;
8794
+ }
8795
+ this.destroyed = true;
8796
+ this.renderer.delete();
8797
+ }
8798
+ acquirePlanes() {
8799
+ this.inputPlanes = [];
8800
+ for (let ch = 0; ch < MAX_INPUT_CHANNELS; ch++) {
8801
+ this.inputPlanes.push(this.native.inputPlane(ch));
8802
+ }
8803
+ this.outputPlanes = [];
8804
+ for (let ch = 0; ch < this.outputChannels; ch++) {
8805
+ this.outputPlanes.push(this.native.outputPlane(ch));
8806
+ }
8807
+ }
8808
+ // Frames past the construction-time capacity are left silent rather than
8809
+ // reallocating on the audio thread.
8810
+ clampFrames(frames) {
8811
+ if (frames <= this.maxBlockSize) {
8812
+ return frames;
8813
+ }
8814
+ if (!_SonarePlaybackWorkletProcessor.warnedBlockOverflow) {
8815
+ _SonarePlaybackWorkletProcessor.warnedBlockOverflow = true;
8816
+ console.warn(
8817
+ `SonarePlaybackWorkletProcessor: requested ${frames} frames exceeds maxBlockSize ${this.maxBlockSize}; clamping.`
8818
+ );
8819
+ }
8820
+ return this.maxBlockSize;
8821
+ }
8822
+ };
8823
+ _SonarePlaybackWorkletProcessor.warnedBlockOverflow = false;
8824
+ var SonarePlaybackWorkletProcessor = _SonarePlaybackWorkletProcessor;
8825
+ function registerSonarePlaybackWorkletProcessor(name = "sonare-playback-processor") {
8826
+ const scope = globalThis;
8827
+ if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
8828
+ throw new Error("AudioWorkletProcessor is not available in this context.");
8829
+ }
8830
+ const Base = scope.AudioWorkletProcessor;
8831
+ class RegisteredSonarePlaybackWorkletProcessor extends Base {
8832
+ constructor(options) {
8833
+ super();
8834
+ const port = this.port;
8835
+ this.bridge = new SonarePlaybackWorkletProcessor(options?.processorOptions ?? {}, port);
8836
+ const onMessage = (event) => {
8837
+ if (isPlaybackMessage(event.data)) {
8838
+ this.bridge.receiveMessage(event.data);
8839
+ }
8840
+ };
8841
+ if (port?.addEventListener) {
8842
+ port.addEventListener("message", onMessage);
8843
+ port.start?.();
8844
+ } else if (port) {
8845
+ port.onmessage = onMessage;
8846
+ }
8847
+ }
8848
+ process(inputs, outputs) {
8849
+ return this.bridge.process(inputs, outputs);
8850
+ }
8851
+ }
8852
+ scope.registerProcessor(name, RegisteredSonarePlaybackWorkletProcessor);
8853
+ }
8854
+ function createSonarePlaybackNode(context, options = {}) {
8855
+ const config = configObject(options.config);
8856
+ const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
8857
+ const processorOptions = {
8858
+ config: JSON.stringify(config),
8859
+ hrtf: options.hrtf,
8860
+ maxBlockSize: options.maxBlockSize,
8861
+ sampleRate: options.sampleRate ?? context.sampleRate
8862
+ };
8863
+ return factory(context, options.processorName ?? "sonare-playback-processor", {
8864
+ numberOfInputs: 1,
8865
+ numberOfOutputs: 1,
8866
+ outputChannelCount: [outputChannelCount(config)],
8867
+ channelCountMode: "max",
8868
+ channelInterpretation: "discrete",
8869
+ processorOptions
8870
+ });
8871
+ }
8872
+
6452
8873
  // src/worklet/voice-changer-processor.ts
6453
8874
  var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChangerWorkletProcessor {
6454
8875
  constructor(options = {}) {
8876
+ // Reused so the mono fan-out never builds an array on the audio thread.
8877
+ this.monoPlane = [new Float32Array(0)];
6455
8878
  this.destroyed = false;
6456
8879
  this.sampleRate = options.sampleRate ?? 48e3;
6457
8880
  this.blockSize = options.blockSize ?? 128;
6458
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 1));
8881
+ this.channelCount = requireChannelCount(options.channelCount, 1);
6459
8882
  this.changer = new RealtimeVoiceChanger(options.preset ?? "neutral-monitor");
6460
8883
  this.changer.prepare(this.sampleRate, this.blockSize, this.channelCount);
6461
8884
  this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
@@ -6508,7 +8931,8 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
6508
8931
  this.monoInput.fill(0, 0, frames2);
6509
8932
  }
6510
8933
  this.changer.processPreparedMono(frames2);
6511
- output[0].set(this.monoOutput.subarray(0, frames2));
8934
+ this.monoPlane[0] = this.monoOutput;
8935
+ copyPlanesToOutput(output, this.monoPlane, frames2);
6512
8936
  return true;
6513
8937
  }
6514
8938
  const frames = this.ensureInterleavedCapacity(requestedFrames, requestedChannels);
@@ -6526,12 +8950,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
6526
8950
  }
6527
8951
  }
6528
8952
  this.changer.processPreparedPlanar(frames);
6529
- for (let ch = 0; ch < channels; ch++) {
6530
- const src = this.planarChannels[ch];
6531
- if (src) {
6532
- output[ch].set(src.subarray(0, frames));
6533
- }
6534
- }
8953
+ copyPlanesToOutput(output, this.planarChannels, frames);
6535
8954
  return true;
6536
8955
  }
6537
8956
  destroy() {
@@ -6641,16 +9060,19 @@ export {
6641
9060
  SonareEngineCommandType,
6642
9061
  SonareEngineTelemetryError,
6643
9062
  SonareEngineTelemetryType,
9063
+ SonarePlaybackWorkletProcessor,
6644
9064
  SonareRealtimeEngineNode,
6645
9065
  SonareRealtimeEngineWorkletProcessor,
6646
9066
  SonareRealtimeVoiceChangerWorkletProcessor,
6647
9067
  SonareWorkletProcessor,
9068
+ StreamingMasteringChain,
6648
9069
  attachOpfsClipStream,
6649
9070
  createSonareClipPageRequestRingBuffer,
6650
9071
  createSonareEngineCommandRingBuffer,
6651
9072
  createSonareEngineTelemetryRingBuffer,
6652
9073
  createSonareExternalMidiRingBuffer,
6653
9074
  createSonareMeterRingBuffer,
9075
+ createSonarePlaybackNode,
6654
9076
  createSonareScopeRingBuffer,
6655
9077
  createSonareSpectrumRingBuffer,
6656
9078
  decodeFrame,
@@ -6668,6 +9090,7 @@ export {
6668
9090
  readSonareMeterRingBuffer,
6669
9091
  readSonareScopeRingBuffer,
6670
9092
  readSonareSpectrumRingBuffer,
9093
+ registerSonarePlaybackWorkletProcessor,
6671
9094
  registerSonareRealtimeEngineWorkletProcessor,
6672
9095
  registerSonareRealtimeVoiceChangerWorkletProcessor,
6673
9096
  registerSonareWorkletProcessor,