@libraz/libsonare 1.7.2 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/NOTICE +178 -0
  2. package/README.md +26 -203
  3. package/dist/_chain_config.d.ts +14 -0
  4. package/dist/_chain_config.d.ts.map +1 -0
  5. package/dist/_effects_common.d.ts +7 -0
  6. package/dist/_effects_common.d.ts.map +1 -0
  7. package/dist/_feature_validation.d.ts +8 -0
  8. package/dist/_feature_validation.d.ts.map +1 -0
  9. package/dist/_fft_options.d.ts +24 -0
  10. package/dist/_fft_options.d.ts.map +1 -0
  11. package/dist/align_take.d.ts +50 -0
  12. package/dist/align_take.d.ts.map +1 -0
  13. package/dist/analysis.d.ts +29 -5840
  14. package/dist/analysis.d.ts.map +1 -0
  15. package/dist/analysis.js +874 -722
  16. package/dist/analysis.js.map +1 -1
  17. package/dist/analysis_helpers.d.ts +9 -0
  18. package/dist/analysis_helpers.d.ts.map +1 -0
  19. package/dist/audio.d.ts +163 -0
  20. package/dist/audio.d.ts.map +1 -0
  21. package/dist/clip_page_streamer.d.ts +133 -0
  22. package/dist/clip_page_streamer.d.ts.map +1 -0
  23. package/dist/codes.d.ts +44 -0
  24. package/dist/codes.d.ts.map +1 -0
  25. package/dist/effects_mastering.d.ts +23 -0
  26. package/dist/effects_mastering.d.ts.map +1 -0
  27. package/dist/effects_note_ops.d.ts +502 -0
  28. package/dist/effects_note_ops.d.ts.map +1 -0
  29. package/dist/effects_percussive.d.ts +185 -0
  30. package/dist/effects_percussive.d.ts.map +1 -0
  31. package/dist/effects_separation.d.ts +65 -0
  32. package/dist/effects_separation.d.ts.map +1 -0
  33. package/dist/effects_spectral.d.ts +28 -0
  34. package/dist/effects_spectral.d.ts.map +1 -0
  35. package/dist/effects_timepitch.d.ts +134 -0
  36. package/dist/effects_timepitch.d.ts.map +1 -0
  37. package/dist/effects_voice_change.d.ts +53 -0
  38. package/dist/effects_voice_change.d.ts.map +1 -0
  39. package/dist/errors.d.ts +51 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/feature_core.d.ts +341 -0
  42. package/dist/feature_core.d.ts.map +1 -0
  43. package/dist/feature_decompose.d.ts +278 -0
  44. package/dist/feature_decompose.d.ts.map +1 -0
  45. package/dist/feature_inverse.d.ts +128 -0
  46. package/dist/feature_inverse.d.ts.map +1 -0
  47. package/dist/feature_loudness.d.ts +66 -0
  48. package/dist/feature_loudness.d.ts.map +1 -0
  49. package/dist/feature_music.d.ts +307 -0
  50. package/dist/feature_music.d.ts.map +1 -0
  51. package/dist/feature_pitch.d.ts +108 -0
  52. package/dist/feature_pitch.d.ts.map +1 -0
  53. package/dist/feature_resample.d.ts +16 -0
  54. package/dist/feature_resample.d.ts.map +1 -0
  55. package/dist/feature_spectral.d.ts +137 -0
  56. package/dist/feature_spectral.d.ts.map +1 -0
  57. package/dist/feature_spectrogram.d.ts +198 -0
  58. package/dist/feature_spectrogram.d.ts.map +1 -0
  59. package/dist/features.d.ts +10 -0
  60. package/dist/features.d.ts.map +1 -0
  61. package/dist/hrtf/default.shrf +0 -0
  62. package/dist/index.d.ts +74 -7431
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +4173 -1621
  65. package/dist/index.js.map +1 -1
  66. package/dist/instrument_types.d.ts +517 -0
  67. package/dist/instrument_types.d.ts.map +1 -0
  68. package/dist/live_audio.d.ts +35 -0
  69. package/dist/live_audio.d.ts.map +1 -0
  70. package/dist/mastering_chain.d.ts +213 -0
  71. package/dist/mastering_chain.d.ts.map +1 -0
  72. package/dist/mastering_core.d.ts +483 -0
  73. package/dist/mastering_core.d.ts.map +1 -0
  74. package/dist/mastering_dynamics.d.ts +80 -0
  75. package/dist/mastering_dynamics.d.ts.map +1 -0
  76. package/dist/metering.d.ts +287 -0
  77. package/dist/metering.d.ts.map +1 -0
  78. package/dist/mixer.d.ts +464 -0
  79. package/dist/mixer.d.ts.map +1 -0
  80. package/dist/mixing_assistant.d.ts +62 -0
  81. package/dist/mixing_assistant.d.ts.map +1 -0
  82. package/dist/mixing_oneshot.d.ts +40 -0
  83. package/dist/mixing_oneshot.d.ts.map +1 -0
  84. package/dist/module_state.d.ts +15 -0
  85. package/dist/module_state.d.ts.map +1 -0
  86. package/dist/opfs_clip_pages.d.ts +28 -0
  87. package/dist/opfs_clip_pages.d.ts.map +1 -0
  88. package/dist/playback_renderer.d.ts +128 -0
  89. package/dist/playback_renderer.d.ts.map +1 -0
  90. package/dist/polyphony.d.ts +202 -0
  91. package/dist/polyphony.d.ts.map +1 -0
  92. package/dist/project.d.ts +8 -0
  93. package/dist/project.d.ts.map +1 -0
  94. package/dist/project_class.d.ts +562 -0
  95. package/dist/project_class.d.ts.map +1 -0
  96. package/dist/project_internal.d.ts +194 -0
  97. package/dist/project_internal.d.ts.map +1 -0
  98. package/dist/project_synth.d.ts +74 -0
  99. package/dist/project_synth.d.ts.map +1 -0
  100. package/dist/project_types.d.ts +654 -0
  101. package/dist/project_types.d.ts.map +1 -0
  102. package/dist/public_types.d.ts +185 -0
  103. package/dist/public_types.d.ts.map +1 -0
  104. package/dist/public_types_acoustic.d.ts +215 -0
  105. package/dist/public_types_acoustic.d.ts.map +1 -0
  106. package/dist/public_types_mastering.d.ts +528 -0
  107. package/dist/public_types_mastering.d.ts.map +1 -0
  108. package/dist/public_types_mixing.d.ts +436 -0
  109. package/dist/public_types_mixing.d.ts.map +1 -0
  110. package/dist/public_types_music.d.ts +619 -0
  111. package/dist/public_types_music.d.ts.map +1 -0
  112. package/dist/public_types_playback.d.ts +165 -0
  113. package/dist/public_types_playback.d.ts.map +1 -0
  114. package/dist/public_types_realtime.d.ts +174 -0
  115. package/dist/public_types_realtime.d.ts.map +1 -0
  116. package/dist/public_types_repair.d.ts +424 -0
  117. package/dist/public_types_repair.d.ts.map +1 -0
  118. package/dist/public_types_spectral.d.ts +697 -0
  119. package/dist/public_types_spectral.d.ts.map +1 -0
  120. package/dist/quick_analysis.d.ts +445 -0
  121. package/dist/quick_analysis.d.ts.map +1 -0
  122. package/dist/realtime_engine.d.ts +893 -0
  123. package/dist/realtime_engine.d.ts.map +1 -0
  124. package/dist/realtime_voice_changer.d.ts +158 -0
  125. package/dist/realtime_voice_changer.d.ts.map +1 -0
  126. package/dist/repair_dereverb.d.ts +187 -0
  127. package/dist/repair_dereverb.d.ts.map +1 -0
  128. package/dist/repair_impulsive.d.ts +186 -0
  129. package/dist/repair_impulsive.d.ts.map +1 -0
  130. package/dist/repair_noise.d.ts +239 -0
  131. package/dist/repair_noise.d.ts.map +1 -0
  132. package/dist/repair_trim.d.ts +123 -0
  133. package/dist/repair_trim.d.ts.map +1 -0
  134. package/dist/sample_bank.d.ts +84 -0
  135. package/dist/sample_bank.d.ts.map +1 -0
  136. package/dist/scale.d.ts +10 -0
  137. package/dist/scale.d.ts.map +1 -0
  138. package/dist/schemas/mixer-scene.schema.json +393 -0
  139. package/dist/schemas/playback-renderer-config.schema.json +392 -0
  140. package/dist/sonare-analysis.d.ts +8 -0
  141. package/dist/sonare-analysis.js +2 -2
  142. package/dist/sonare-analysis.wasm +0 -0
  143. package/dist/sonare.d.ts +3945 -0
  144. package/dist/sonare.js +2 -2
  145. package/dist/sonare.wasm +0 -0
  146. package/dist/stream_analyzer.d.ts +163 -0
  147. package/dist/stream_analyzer.d.ts.map +1 -0
  148. package/dist/stream_types.d.ts +214 -0
  149. package/dist/stream_types.d.ts.map +1 -0
  150. package/dist/streaming_mixing.d.ts +6 -0
  151. package/dist/streaming_mixing.d.ts.map +1 -0
  152. package/dist/streaming_processors.d.ts +340 -0
  153. package/dist/streaming_processors.d.ts.map +1 -0
  154. package/dist/transcribe.d.ts +77 -0
  155. package/dist/transcribe.d.ts.map +1 -0
  156. package/dist/validation.d.ts +140 -0
  157. package/dist/validation.d.ts.map +1 -0
  158. package/dist/web_midi.d.ts +77 -0
  159. package/dist/web_midi.d.ts.map +1 -0
  160. package/dist/worker.d.ts +5 -48
  161. package/dist/worker.d.ts.map +1 -0
  162. package/dist/worker.js +94 -41
  163. package/dist/worker.js.map +1 -1
  164. package/dist/worker_client.d.ts +96 -0
  165. package/dist/worker_client.d.ts.map +1 -0
  166. package/dist/worker_protocol.d.ts +43 -0
  167. package/dist/worker_protocol.d.ts.map +1 -0
  168. package/dist/worklet/audio_types.d.ts +21 -0
  169. package/dist/worklet/audio_types.d.ts.map +1 -0
  170. package/dist/worklet/engine-automation.d.ts +29 -0
  171. package/dist/worklet/engine-automation.d.ts.map +1 -0
  172. package/dist/worklet/engine-capture-facade.d.ts +35 -0
  173. package/dist/worklet/engine-capture-facade.d.ts.map +1 -0
  174. package/dist/worklet/engine-clips.d.ts +23 -0
  175. package/dist/worklet/engine-clips.d.ts.map +1 -0
  176. package/dist/worklet/engine-markers.d.ts +40 -0
  177. package/dist/worklet/engine-markers.d.ts.map +1 -0
  178. package/dist/worklet/engine-mixer-facade.d.ts +164 -0
  179. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
  180. package/dist/worklet/engine-node.d.ts +83 -0
  181. package/dist/worklet/engine-node.d.ts.map +1 -0
  182. package/dist/worklet/engine-offline.d.ts +81 -0
  183. package/dist/worklet/engine-offline.d.ts.map +1 -0
  184. package/dist/worklet/engine-options.d.ts +12 -0
  185. package/dist/worklet/engine-options.d.ts.map +1 -0
  186. package/dist/worklet/engine-parameter-facade.d.ts +106 -0
  187. package/dist/worklet/engine-parameter-facade.d.ts.map +1 -0
  188. package/dist/worklet/engine-processor.d.ts +72 -0
  189. package/dist/worklet/engine-processor.d.ts.map +1 -0
  190. package/dist/worklet/engine-register.d.ts +2 -0
  191. package/dist/worklet/engine-register.d.ts.map +1 -0
  192. package/dist/worklet/engine-strips.d.ts +75 -0
  193. package/dist/worklet/engine-strips.d.ts.map +1 -0
  194. package/dist/worklet/engine-sync.d.ts +39 -0
  195. package/dist/worklet/engine-sync.d.ts.map +1 -0
  196. package/dist/worklet/engine-tempo-facade.d.ts +48 -0
  197. package/dist/worklet/engine-tempo-facade.d.ts.map +1 -0
  198. package/dist/worklet/engine.d.ts +418 -0
  199. package/dist/worklet/engine.d.ts.map +1 -0
  200. package/dist/worklet/guards.d.ts +53 -0
  201. package/dist/worklet/guards.d.ts.map +1 -0
  202. package/dist/worklet/messages.d.ts +710 -0
  203. package/dist/worklet/messages.d.ts.map +1 -0
  204. package/dist/worklet/mixer-processor.d.ts +46 -0
  205. package/dist/worklet/mixer-processor.d.ts.map +1 -0
  206. package/dist/worklet/playback-processor.d.ts +62 -0
  207. package/dist/worklet/playback-processor.d.ts.map +1 -0
  208. package/dist/worklet/protocol.d.ts +331 -0
  209. package/dist/worklet/protocol.d.ts.map +1 -0
  210. package/dist/worklet/voice-changer-processor.d.ts +41 -0
  211. package/dist/worklet/voice-changer-processor.d.ts.map +1 -0
  212. package/dist/worklet.d.ts +16 -2515
  213. package/dist/worklet.d.ts.map +1 -0
  214. package/dist/worklet.js +3200 -541
  215. package/dist/worklet.js.map +1 -1
  216. package/package.json +23 -12
  217. package/src/_effects_common.ts +47 -0
  218. package/src/_feature_validation.ts +34 -0
  219. package/src/_fft_options.ts +39 -0
  220. package/src/align_take.ts +64 -0
  221. package/src/analysis.ts +56 -3
  222. package/src/analysis_helpers.ts +7 -0
  223. package/src/audio.ts +106 -3
  224. package/src/codes.ts +39 -2
  225. package/src/effects_mastering.ts +101 -22
  226. package/src/effects_note_ops.ts +683 -0
  227. package/src/effects_percussive.ts +217 -0
  228. package/src/effects_separation.ts +150 -0
  229. package/src/effects_spectral.ts +60 -0
  230. package/src/effects_timepitch.ts +388 -0
  231. package/src/errors.ts +23 -1
  232. package/src/feature_core.ts +127 -2
  233. package/src/feature_decompose.ts +633 -0
  234. package/src/feature_inverse.ts +454 -0
  235. package/src/feature_loudness.ts +125 -0
  236. package/src/feature_music.ts +107 -14
  237. package/src/feature_pitch.ts +96 -1
  238. package/src/feature_spectral.ts +16 -611
  239. package/src/feature_spectrogram.ts +63 -450
  240. package/src/features.ts +36 -22
  241. package/src/index.ts +288 -30
  242. package/src/instrument_types.ts +645 -0
  243. package/src/live_audio.ts +27 -1
  244. package/src/mastering_chain.ts +184 -0
  245. package/src/mastering_core.ts +441 -32
  246. package/src/mastering_dynamics.ts +22 -11
  247. package/src/metering.ts +67 -24
  248. package/src/mixer.ts +251 -22
  249. package/src/mixing_assistant.ts +138 -0
  250. package/src/mixing_oneshot.ts +10 -5
  251. package/src/module_state.ts +24 -2
  252. package/src/playback_renderer.ts +252 -0
  253. package/src/polyphony.ts +279 -0
  254. package/src/project.ts +61 -24
  255. package/src/project_class.ts +450 -27
  256. package/src/project_internal.ts +149 -42
  257. package/src/project_synth.ts +67 -1
  258. package/src/project_types.ts +271 -271
  259. package/src/public_types.ts +122 -3
  260. package/src/public_types_acoustic.ts +112 -3
  261. package/src/public_types_mastering.ts +275 -73
  262. package/src/public_types_mixing.ts +363 -1
  263. package/src/public_types_music.ts +312 -2
  264. package/src/public_types_playback.ts +196 -0
  265. package/src/public_types_realtime.ts +39 -7
  266. package/src/public_types_repair.ts +446 -0
  267. package/src/public_types_spectral.ts +491 -5
  268. package/src/quick_analysis.ts +203 -26
  269. package/src/realtime_engine.ts +773 -34
  270. package/src/realtime_voice_changer.ts +55 -1
  271. package/src/repair_dereverb.ts +299 -0
  272. package/src/repair_impulsive.ts +395 -0
  273. package/src/repair_noise.ts +425 -0
  274. package/src/repair_trim.ts +226 -0
  275. package/src/sample_bank.ts +113 -0
  276. package/src/sonare.js.d.ts +1158 -30
  277. package/src/stream_analyzer.ts +36 -4
  278. package/src/stream_types.ts +37 -0
  279. package/src/streaming_mixing.ts +1 -1
  280. package/src/streaming_processors.ts +202 -10
  281. package/src/transcribe.ts +89 -0
  282. package/src/validation.ts +285 -11
  283. package/src/web_midi.ts +1 -6
  284. package/src/worker.ts +18 -2
  285. package/src/worklet/audio_types.ts +37 -0
  286. package/src/worklet/engine-mixer-facade.ts +800 -32
  287. package/src/worklet/engine-node.ts +99 -29
  288. package/src/worklet/engine-offline.ts +14 -8
  289. package/src/worklet/engine-parameter-facade.ts +21 -0
  290. package/src/worklet/engine-processor.ts +332 -93
  291. package/src/worklet/engine-register.ts +32 -18
  292. package/src/worklet/engine-strips.ts +275 -9
  293. package/src/worklet/engine-sync.ts +20 -7
  294. package/src/worklet/engine.ts +394 -48
  295. package/src/worklet/guards.ts +195 -44
  296. package/src/worklet/messages.ts +229 -2
  297. package/src/worklet/mixer-processor.ts +117 -48
  298. package/src/worklet/playback-processor.ts +300 -0
  299. package/src/worklet/protocol.ts +82 -11
  300. package/src/worklet/voice-changer-processor.ts +17 -11
  301. package/src/worklet.ts +17 -0
  302. package/src/effects_transform.ts +0 -718
  303. package/src/mastering_repair.ts +0 -273
package/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) {
@@ -544,25 +567,6 @@ function getSonareModule() {
544
567
  return wrappedModule;
545
568
  }
546
569
 
547
- // src/_chain_config.ts
548
- function flattenChainConfig(config) {
549
- const out = {};
550
- const walk = (node, prefix) => {
551
- for (const [key, value] of Object.entries(node)) {
552
- const path = prefix ? `${prefix}.${key}` : key;
553
- if (typeof value === "number" || typeof value === "boolean") {
554
- out[path] = value;
555
- } else if (value !== null && typeof value === "object") {
556
- walk(value, path);
557
- } else if (value !== void 0) {
558
- throw new TypeError(`Mastering override '${path}' must be a number or boolean.`);
559
- }
560
- }
561
- };
562
- walk(config, "");
563
- return out;
564
- }
565
-
566
570
  // src/codes.ts
567
571
  function resolveOrdinalInRange(value, min, max, enumName) {
568
572
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
@@ -598,6 +602,10 @@ var AUTOMATION_CURVE_VALUES = {
598
602
  hold: 2,
599
603
  "s-curve": 3
600
604
  };
605
+ var PROJECT_AUTOMATION_CURVE_VALUES = {
606
+ ...AUTOMATION_CURVE_VALUES,
607
+ scurve: 3
608
+ };
601
609
  var PAN_LAW_VALUES = {
602
610
  const3db: 0,
603
611
  "const-3db": 0,
@@ -623,6 +631,7 @@ var PAN_MODE_VALUES = {
623
631
  };
624
632
  var METER_TAP_VALUES = { preFader: 0, postFader: 1 };
625
633
  var SEND_TIMING_VALUES = { postFader: 0, preFader: 1 };
634
+ var SIDECHAIN_SOURCE_KIND_VALUES = { track: 0, bus: 1 };
626
635
  var TRACK_MONITOR_MODE_VALUES = { off: 0, pfl: 1, afl: 2 };
627
636
  function automationCurveCode(curve) {
628
637
  return resolveEnumOrdinal(curve, AUTOMATION_CURVE_VALUES, "automation curve");
@@ -641,12 +650,276 @@ function meterTapCode(tap) {
641
650
  function sendTimingCode(timing) {
642
651
  return resolveEnumOrdinal(timing, SEND_TIMING_VALUES, "send timing");
643
652
  }
653
+ function sidechainSourceKindCode(kind) {
654
+ return resolveEnumOrdinal(kind, SIDECHAIN_SOURCE_KIND_VALUES, "sidechain source kind");
655
+ }
644
656
  function trackMonitorModeCode(mode) {
645
657
  return resolveEnumOrdinal(mode, TRACK_MONITOR_MODE_VALUES, "track monitor mode");
646
658
  }
647
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
+
648
896
  // src/realtime_engine.ts
649
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
+ }
650
923
  function engineCapabilities() {
651
924
  const abiVersion = getSonareModule().engineAbiVersion();
652
925
  const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === "function";
@@ -662,8 +935,9 @@ function engineCapabilities() {
662
935
  mode: sharedArrayBuffer && atomics ? "sab" : "postMessage"
663
936
  };
664
937
  }
665
- var RealtimeEngine = class {
938
+ var RealtimeEngine = class _RealtimeEngine {
666
939
  constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024, maxChannels = 64) {
940
+ this.released = false;
667
941
  const module2 = getSonareModule();
668
942
  const capabilities = engineCapabilities();
669
943
  if (!capabilities.abiCompatible) {
@@ -679,6 +953,15 @@ var RealtimeEngine = class {
679
953
  maxChannels
680
954
  );
681
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
+ */
682
965
  prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024, maxChannels = 64) {
683
966
  this.native.prepareWithChannels(
684
967
  sampleRate,
@@ -725,9 +1008,13 @@ var RealtimeEngine = class {
725
1008
  * scheduled MIDI clips routed to that destination render through the synth.
726
1009
  * Unknown preset names throw. An object patch's `destinationId` is a JS
727
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.
728
1015
  */
729
1016
  setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
730
- this.native.setSynthInstrument(destinationId, patch);
1017
+ this.native.setSynthInstrument(destinationId, normalizeSynthInstrument(patch));
731
1018
  }
732
1019
  /**
733
1020
  * Load (parse) SoundFont 2 bytes into the engine so SF2 instruments can be
@@ -780,6 +1067,113 @@ var RealtimeEngine = class {
780
1067
  midiCcBindingCount() {
781
1068
  return this.native.midiCcBindingCount();
782
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
+ }
783
1177
  /** Install/replace a live non-destructive MIDI-FX insert for one destination. */
784
1178
  setMidiFx(destinationId, configJson) {
785
1179
  this.native.setMidiFx(destinationId, configJson);
@@ -800,7 +1194,10 @@ var RealtimeEngine = class {
800
1194
  /**
801
1195
  * Route a destination's (track lane's) MIDI to the external output queue
802
1196
  * instead of the internal instrument rack, so the track plays an external
803
- * device. Clearing it restores internal-synth playback.
1197
+ * device. Clearing it restores internal-synth playback. Control-thread only.
1198
+ * The change takes effect at the next processed block; switching a
1199
+ * destination's route first releases its notes and resets its controllers
1200
+ * through the old route, and drops its pending MIDI-FX events. Single writer.
804
1201
  */
805
1202
  setMidiDestinationExternal(destinationId, external) {
806
1203
  this.native.setMidiDestinationExternal(destinationId, external);
@@ -825,6 +1222,12 @@ var RealtimeEngine = class {
825
1222
  * block / animation frame. `maxRecords` caps the number of output events
826
1223
  * returned — the shared unit across every surface. Events past the cap stay
827
1224
  * queued for the next call (lossless); call again to drain the rest.
1225
+ *
1226
+ * One queued record lowers to at most 4 MIDI 1.0 messages (a MIDI 2.0
1227
+ * registered or assignable controller becomes CC 101/100 or 99/98 plus Data
1228
+ * Entry 6/38), so a positive `maxRecords` below 4 could never consume a record
1229
+ * and is rejected with an `InvalidParameter` `SonareError` instead of
1230
+ * returning nothing forever.
828
1231
  */
829
1232
  drainExternalMidi(maxRecords = 1024) {
830
1233
  return this.native.drainExternalMidi(maxRecords);
@@ -837,7 +1240,7 @@ var RealtimeEngine = class {
837
1240
  return this.native.externalMidiScratchDestinationId();
838
1241
  }
839
1242
  externalMidiScratchRenderFrame() {
840
- return this.native.externalMidiScratchRenderFrame();
1243
+ return Number(this.native.externalMidiScratchRenderFrame());
841
1244
  }
842
1245
  externalMidiScratchByteWord() {
843
1246
  return this.native.externalMidiScratchByteWord();
@@ -857,6 +1260,31 @@ var RealtimeEngine = class {
857
1260
  pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
858
1261
  this.native.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
859
1262
  }
1263
+ /**
1264
+ * Push a live MIDI pitch bend to the engine-owned MIDI input source.
1265
+ *
1266
+ * `bend14` is unsigned 14-bit with centre 8192 (0..16383) — the dimension is
1267
+ * not 7-bit, so a value past 16383 is refused rather than narrowed. The input
1268
+ * source must be enabled with {@link setMidiInputSource} first.
1269
+ */
1270
+ pushMidiInputPitchBend(group, channel, bend14, portTimeSamples = 0) {
1271
+ this.native.pushMidiInputPitchBend(group, channel, bend14, portTimeSamples);
1272
+ }
1273
+ /**
1274
+ * Push a live MIDI channel pressure to the engine-owned MIDI input source.
1275
+ * `pressure` is 7-bit (0..127). Under MPE this is the member channel's
1276
+ * per-note pressure.
1277
+ */
1278
+ pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples = 0) {
1279
+ this.native.pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples);
1280
+ }
1281
+ /**
1282
+ * Push a live MIDI polyphonic key pressure to the engine-owned MIDI input
1283
+ * source. `note` and `pressure` are 7-bit (0..127).
1284
+ */
1285
+ pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples = 0) {
1286
+ this.native.pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples);
1287
+ }
860
1288
  pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
861
1289
  this.native.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
862
1290
  }
@@ -872,15 +1300,62 @@ var RealtimeEngine = class {
872
1300
  pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
873
1301
  this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
874
1302
  }
875
- /** Queue one immediate MIDI 1.0 channel-voice UMP word for a destination. */
876
- pushMidiUmp(destinationId, word0, renderFrame = -1) {
877
- this.native.pushMidiUmp(destinationId, word0, renderFrame);
1303
+ /**
1304
+ * Queue an immediate (live) MIDI pitch bend to a MIDI destination. `bend14`
1305
+ * is unsigned 14-bit with centre 8192 (0..16383); `renderFrame` is the frame
1306
+ * to fire at, or -1 for immediate. Mirrors the Node/Python/C-ABI
1307
+ * `pushMidiPitchBend`.
1308
+ */
1309
+ pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame = -1) {
1310
+ this.native.pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame);
1311
+ }
1312
+ /**
1313
+ * Queue an immediate (live) MIDI channel pressure to a MIDI destination.
1314
+ * `pressure` is 7-bit (0..127); `renderFrame` is the frame to fire at, or -1
1315
+ * for immediate. Mirrors the Node/Python/C-ABI `pushMidiChannelPressure`.
1316
+ */
1317
+ pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame = -1) {
1318
+ this.native.pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame);
1319
+ }
1320
+ /**
1321
+ * Queue an immediate (live) MIDI polyphonic key pressure to a MIDI
1322
+ * destination. `note` and `pressure` are 7-bit (0..127); `renderFrame` is the
1323
+ * frame to fire at, or -1 for immediate. Mirrors the Node/Python/C-ABI
1324
+ * `pushMidiPolyPressure`.
1325
+ */
1326
+ pushMidiPolyPressure(destinationId, group, channel, note, pressure, renderFrame = -1) {
1327
+ this.native.pushMidiPolyPressure(destinationId, group, channel, note, pressure, renderFrame);
1328
+ }
1329
+ /**
1330
+ * Queue an immediate (live) raw UMP message to a MIDI destination. `words` is
1331
+ * 1 to 4 words, most significant first, and its length must match the message
1332
+ * type of `words[0]`. MIDI 2.0 channel-voice messages (MT 0x4) arrive at full
1333
+ * width; SysEx7 / data messages (MT 0x3 / 0x5) are refused, use
1334
+ * {@link pushMidiSysex}. Throws when the slot ring or command queue is full
1335
+ * (retry after a process block). `renderFrame` is the render-frame time to
1336
+ * apply, or -1 for immediate. A bare number is accepted as a one-word
1337
+ * message.
1338
+ */
1339
+ pushMidiUmp(destinationId, words, renderFrame = -1) {
1340
+ const list = typeof words === "number" ? [words] : words;
1341
+ this.native.pushMidiUmp(destinationId, assertUmpWords("pushMidiUmp", list), renderFrame);
1342
+ }
1343
+ /**
1344
+ * Push one raw UMP message (1 to 4 words) to the engine-owned MIDI input
1345
+ * source. The message rules match {@link pushMidiUmp}. `portTimeSamples` is
1346
+ * the port timestamp in samples.
1347
+ */
1348
+ pushMidiInputUmp(words, portTimeSamples = 0) {
1349
+ this.native.pushMidiInputUmp(assertUmpWords("pushMidiInputUmp", words), portTimeSamples);
878
1350
  }
879
1351
  /**
880
1352
  * Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
881
1353
  * the full message including the leading 0xF0 and trailing 0xF7 (1..512
882
- * bytes). `renderFrame` is the frame to fire at, or -1 for immediate. Mirrors
883
- * the Node/Python/C-ABI `pushMidiSysex`.
1354
+ * bytes). `renderFrame` is the frame to fire at, or -1 for immediate. Throws
1355
+ * `InvalidParameter` when the destination instrument cannot prepare
1356
+ * the SysEx (retrying cannot help), and `OutOfMemory` when the payload
1357
+ * slots or the command queue are full (retry after a processed block).
1358
+ * Mirrors the Node/Python/C-ABI `pushMidiSysex`.
884
1359
  */
885
1360
  pushMidiSysex(destinationId, data, renderFrame = -1) {
886
1361
  this.native.pushMidiSysex(destinationId, data, renderFrame);
@@ -903,12 +1378,30 @@ var RealtimeEngine = class {
903
1378
  getTransportState() {
904
1379
  return this.native.getTransportState();
905
1380
  }
1381
+ /** Queues an integrated-loudness reset; short-term and momentary windows are retained. */
1382
+ resetMasterLoudnessMeter(renderFrame = -1) {
1383
+ this.native.resetMasterLoudnessMeter(renderFrame);
1384
+ }
1385
+ /** Reads the immutable factory value for a resolved insert parameter id. */
1386
+ insertParameterConstructedValue(paramId) {
1387
+ return this.native.insertParameterConstructedValue(paramId);
1388
+ }
906
1389
  play(renderFrame = -1) {
907
1390
  this.native.play(renderFrame);
908
1391
  }
1392
+ /**
1393
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1394
+ * centred pitch bend on every channel played since the last reset, so
1395
+ * controller values set before a loop region are not restored at the wrap.
1396
+ */
909
1397
  stop(renderFrame = -1) {
910
1398
  this.native.stop(renderFrame);
911
1399
  }
1400
+ /**
1401
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1402
+ * centred pitch bend on every channel played since the last reset, so
1403
+ * controller values set before a loop region are not restored at the wrap.
1404
+ */
912
1405
  seekSample(timelineSample, renderFrame = -1) {
913
1406
  this.native.seekSample(timelineSample, renderFrame);
914
1407
  }
@@ -921,10 +1414,23 @@ var RealtimeEngine = class {
921
1414
  settleParameters() {
922
1415
  this.native.settleParameters();
923
1416
  }
1417
+ /** Snap only insert automation slots after structural replay. */
1418
+ settleInsertParameters() {
1419
+ this.native.settleInsertParameters();
1420
+ }
924
1421
  /** Drains queued commands on an offline/control-only engine immediately. */
925
1422
  flushControlCommands() {
926
1423
  this.native.flushControlCommands();
927
1424
  }
1425
+ /** Applies commands already due on a control-only mirror, retaining future commands. */
1426
+ applyCommandsDueNowPreservingFuture() {
1427
+ this.native.applyCommandsDueNowPreservingFuture();
1428
+ }
1429
+ /**
1430
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1431
+ * centred pitch bend on every channel played since the last reset, so
1432
+ * controller values set before a loop region are not restored at the wrap.
1433
+ */
928
1434
  seekPpq(ppq, renderFrame = -1) {
929
1435
  this.native.seekPpq(ppq, renderFrame);
930
1436
  }
@@ -944,6 +1450,11 @@ var RealtimeEngine = class {
944
1450
  sampleAtPpq(ppq) {
945
1451
  return Number(this.native.sampleAtPpq(ppq));
946
1452
  }
1453
+ /**
1454
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
1455
+ * centred pitch bend on every channel played since the last reset, so
1456
+ * controller values set before a loop region are not restored at the wrap.
1457
+ */
947
1458
  setLoop(startPpq, endPpq, enabled = true) {
948
1459
  this.native.setLoop(startPpq, endPpq, enabled);
949
1460
  }
@@ -1020,22 +1531,27 @@ var RealtimeEngine = class {
1020
1531
  clipCount() {
1021
1532
  return this.native.clipCount();
1022
1533
  }
1534
+ /**
1535
+ * Normalizes each send's pre/post tap point to the integer the native layer
1536
+ * reads (defaults to post-fader when omitted). Shared by track lanes and
1537
+ * buses, which carry the same send shape.
1538
+ */
1539
+ static normalizeSends(sends) {
1540
+ return sends.map((send) => ({
1541
+ ...send,
1542
+ // Post-fader (0) is the default for an omitted sendTiming.
1543
+ sendTiming: send.sendTiming === void 0 ? 0 : sendTimingCode(send.sendTiming)
1544
+ }));
1545
+ }
1023
1546
  setTrackLanes(lanes) {
1024
1547
  this.native.setTrackLanes(
1025
1548
  lanes.map((lane) => {
1026
1549
  if (typeof lane === "number") {
1027
1550
  return { trackId: lane };
1028
1551
  }
1029
- if (!lane.sends) {
1030
- return lane;
1031
- }
1032
1552
  return {
1033
1553
  ...lane,
1034
- sends: lane.sends.map((send) => ({
1035
- ...send,
1036
- // Post-fader (0) is the default for an omitted sendTiming.
1037
- sendTiming: send.sendTiming === void 0 ? 0 : sendTimingCode(send.sendTiming)
1038
- }))
1554
+ sends: lane.sends ? _RealtimeEngine.normalizeSends(lane.sends) : void 0
1039
1555
  };
1040
1556
  })
1041
1557
  );
@@ -1048,7 +1564,26 @@ var RealtimeEngine = class {
1048
1564
  this.native.setLaneSidechain(trackId, insertIndex, sourceTrackId);
1049
1565
  }
1050
1566
  setTrackBuses(buses) {
1051
- this.native.setTrackBuses(buses);
1567
+ this.native.setTrackBuses(
1568
+ Array.isArray(buses) ? buses.map((bus) => ({
1569
+ ...bus,
1570
+ sends: bus.sends ? _RealtimeEngine.normalizeSends(bus.sends) : void 0
1571
+ })) : buses
1572
+ );
1573
+ }
1574
+ /**
1575
+ * Keys one insert of a bus strip from a track lane or another bus
1576
+ * (ducking/sidechainRouter inserts). `sourceId` 0 removes the binding.
1577
+ */
1578
+ setBusSidechain(busId, insertIndex, sourceKind, sourceId) {
1579
+ this.native.setBusSidechain(busId, insertIndex, sidechainSourceKindCode(sourceKind), sourceId);
1580
+ }
1581
+ /**
1582
+ * Keys one insert of the master strip from a track lane or a bus. Same
1583
+ * source rules as {@link setBusSidechain}.
1584
+ */
1585
+ setMasterSidechain(insertIndex, sourceKind, sourceId) {
1586
+ this.native.setMasterSidechain(insertIndex, sidechainSourceKindCode(sourceKind), sourceId);
1052
1587
  }
1053
1588
  setBusStripJson(busId, sceneJson) {
1054
1589
  try {
@@ -1081,6 +1616,17 @@ var RealtimeEngine = class {
1081
1616
  setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass = false) {
1082
1617
  this.native.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
1083
1618
  }
1619
+ /** Bus-strip counterpart of {@link setTrackStripEqBand}. */
1620
+ setBusStripEqBand(busId, bandIndex, band) {
1621
+ this.native.setBusStripEqBandJson(
1622
+ busId,
1623
+ bandIndex,
1624
+ typeof band === "string" ? band : JSON.stringify(band)
1625
+ );
1626
+ }
1627
+ setBusStripEqBandJson(busId, bandIndex, bandJson) {
1628
+ this.native.setBusStripEqBandJson(busId, bandIndex, bandJson);
1629
+ }
1084
1630
  setMasterStripJson(sceneJson) {
1085
1631
  try {
1086
1632
  JSON.parse(sceneJson);
@@ -1104,22 +1650,59 @@ var RealtimeEngine = class {
1104
1650
  }
1105
1651
  /**
1106
1652
  * Changes one track-strip insert parameter in realtime, addressed by the
1107
- * processor's JSON-key parameter name (see {@link masteringInsertParamInfo}).
1108
- * Applied at the next block head via the engine command queue; safe during
1109
- * playback. Throws if the track, insert, or name is unknown, the param is not
1110
- * realtime-safe, or the command queue is full.
1653
+ * processor's JSON-key parameter name — one of the entries
1654
+ * {@link masteringInsertParamInfo} reports with a non-null `id`; a
1655
+ * construction-only entry (`id` null) takes effect only when the insert is
1656
+ * built. Applied at the next block head via the engine command queue; safe
1657
+ * during playback. Throws if the track, insert, or name is unknown, the
1658
+ * param is not realtime-safe, or the command queue is full.
1111
1659
  */
1112
1660
  setTrackStripInsertParamByName(trackId, insertIndex, paramName, value) {
1113
1661
  this.native.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
1114
1662
  }
1663
+ /** Apply a live insert edit on this engine's owning thread without draining its command queue. */
1664
+ applyTrackStripInsertParamByNameNow(trackId, insertIndex, paramName, value) {
1665
+ return this.native.applyTrackStripInsertParamByNameNow(trackId, insertIndex, paramName, value);
1666
+ }
1667
+ /** Restore a retained insert value exactly after a strip scene is replayed. */
1668
+ restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value) {
1669
+ this.native.restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
1670
+ }
1115
1671
  /** Master-strip counterpart of {@link setTrackStripInsertParamByName}. */
1116
1672
  setMasterStripInsertParamByName(insertIndex, paramName, value) {
1117
1673
  this.native.setMasterStripInsertParamByName(insertIndex, paramName, value);
1118
1674
  }
1675
+ applyMasterStripInsertParamByNameNow(insertIndex, paramName, value) {
1676
+ return this.native.applyMasterStripInsertParamByNameNow(insertIndex, paramName, value);
1677
+ }
1678
+ restoreMasterStripInsertParamByName(insertIndex, paramName, value) {
1679
+ this.native.restoreMasterStripInsertParamByName(insertIndex, paramName, value);
1680
+ }
1119
1681
  /** Bus-strip counterpart of {@link setTrackStripInsertParamByName}. */
1120
1682
  setBusStripInsertParamByName(busId, insertIndex, paramName, value) {
1121
1683
  this.native.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
1122
1684
  }
1685
+ applyBusStripInsertParamByNameNow(busId, insertIndex, paramName, value) {
1686
+ return this.native.applyBusStripInsertParamByNameNow(busId, insertIndex, paramName, value);
1687
+ }
1688
+ restoreBusStripInsertParamByName(busId, insertIndex, paramName, value) {
1689
+ this.native.restoreBusStripInsertParamByName(busId, insertIndex, paramName, value);
1690
+ }
1691
+ /**
1692
+ * Forgets the remembered manual insert-parameter values of one track strip
1693
+ * and discards its queued insert edits. Call before {@link setTrackStripJson}
1694
+ * replaces the strip when its old values must not carry over; the setter
1695
+ * never does this itself, since a queued edit may already target the new chain.
1696
+ */
1697
+ clearTrackInsertParameterBases(trackId) {
1698
+ this.native.clearTrackInsertParameterBases(trackId);
1699
+ }
1700
+ clearBusInsertParameterBases(busId) {
1701
+ this.native.clearBusInsertParameterBases(busId);
1702
+ }
1703
+ clearMasterInsertParameterBases() {
1704
+ this.native.clearMasterInsertParameterBases();
1705
+ }
1123
1706
  /** Bus-strip counterpart of {@link setTrackStripInsertBypassed}. */
1124
1707
  setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass = false) {
1125
1708
  this.native.setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass);
@@ -1139,6 +1722,16 @@ var RealtimeEngine = class {
1139
1722
  * stages of the offline mastering chain (`repair.*`, `loudness`, and the
1140
1723
  * match stages) have no insert form and no automation id: they buffer the
1141
1724
  * entire signal by construction and do not run on the realtime path.
1725
+ *
1726
+ * The returned id uses the track's current positional lane selector. When
1727
+ * `setTrackLanes` successfully changes lane order or membership, the engine
1728
+ * remaps already queued and published track automation by track id, but it
1729
+ * cannot update a numeric id retained by the caller. Re-resolve every track
1730
+ * insert id after such a topology change before passing it to
1731
+ * `setAutomationLane`, `setParameter`, or `setParameterSmoothed`. Use
1732
+ * `setTrackStripInsertParamByName` when the operation needs a stable track
1733
+ * identity. Master and bus insert ids are separate and are not invalidated by
1734
+ * track-lane changes.
1142
1735
  */
1143
1736
  resolveTrackInsertAutomationId(trackId, insertIndex, paramName) {
1144
1737
  return this.native.resolveTrackInsertAutomationId(trackId, insertIndex, paramName);
@@ -1149,6 +1742,41 @@ var RealtimeEngine = class {
1149
1742
  resolveBusInsertAutomationId(busId, insertIndex, paramName) {
1150
1743
  return this.native.resolveBusInsertAutomationId(busId, insertIndex, paramName);
1151
1744
  }
1745
+ /**
1746
+ * Resolves a hosted instrument's continuous parameter (by its JSON-key name)
1747
+ * to the reserved automation id usable with `setAutomationLane` /
1748
+ * `setParameter`, so an instrument parameter is driven at audio-block
1749
+ * precision exactly like a strip insert. Returns `-1` when the destination
1750
+ * has no bound instrument, the instrument exposes no automatable parameters,
1751
+ * or the name is unknown.
1752
+ *
1753
+ * For the NativeSynth ({@link setSynthInstrument}) the names are the
1754
+ * continuous {@link SynthPatch} fields: `gain`, `busDrive`, `cutoffHz`,
1755
+ * `resonanceQ`, `drive`, `keyTrack`, `envToCutoffCents`, `velToCutoffCents`,
1756
+ * `ampAttackMs`, `ampDecayMs`, `ampSustain`, `ampReleaseMs`,
1757
+ * `filterAttackMs`, `filterDecayMs`, `filterSustain`, `filterReleaseMs`,
1758
+ * `lfoRateHz`, `lfoToPitchCents`, `lfo2RateHz`, `glideMs`, `bodyMix`,
1759
+ * `stereoSpread`, `detuneCents`, `driftCents`, `pitchOffsetCents`,
1760
+ * `hpCutoffHz`, `sampleHoldHz`, `bitDepth`.
1761
+ *
1762
+ * Structural fields (`preset`, `engineMode`, `waveform`, `filterModel`,
1763
+ * `unison`, `polyphony`, `body`, `modRoutings`) are not automatable and
1764
+ * return `-1`: they resize voice pools or swap DSP topology, which is not
1765
+ * audio-thread safe. Rebind the instrument with a new patch instead.
1766
+ *
1767
+ * `gain`, `busDrive`, `cutoffHz`, `resonanceQ`, `envToCutoffCents`,
1768
+ * `lfoToPitchCents` and `pitchOffsetCents` reach voices that are already
1769
+ * sounding from the next block; the rest are cached into per-voice state at
1770
+ * note-on and take effect from the next note, so a lane that moves one of
1771
+ * them under a held note looks inert until the next one speaks — that is the
1772
+ * behaviour, not a dropped write.
1773
+ *
1774
+ * The id survives an unbind/rebind of the same destination and applies
1775
+ * nothing while that destination is unbound.
1776
+ */
1777
+ resolveInstrumentAutomationId(destinationId, paramName) {
1778
+ return this.native.resolveInstrumentAutomationId(destinationId, paramName);
1779
+ }
1152
1780
  /** Sets a track lane strip's pan position in realtime (glitch-free). */
1153
1781
  setTrackStripPan(trackId, pan) {
1154
1782
  this.native.setTrackStripPan(trackId, pan);
@@ -1166,11 +1794,30 @@ var RealtimeEngine = class {
1166
1794
  this.native.setTrackStripDualPan(trackId, leftPan, rightPan);
1167
1795
  }
1168
1796
  /**
1169
- * Sets a track lane strip's inter-channel alignment delay (whole samples).
1170
- * Adjusts strip latency, so PDC and reported graph latency are refreshed.
1797
+ * Sets a bus strip's output pan position in realtime (glitch-free). Throws
1798
+ * for an unknown bus or one wider than stereo.
1171
1799
  */
1172
- setTrackStripChannelDelaySamples(trackId, delaySamples) {
1173
- this.native.setTrackStripChannelDelaySamples(trackId, delaySamples);
1800
+ setBusStripPan(busId, pan) {
1801
+ this.native.setBusStripPan(busId, pan);
1802
+ }
1803
+ /** Sets a bus strip's pan law in realtime. */
1804
+ setBusStripPanLaw(busId, panLaw) {
1805
+ this.native.setBusStripPanLaw(busId, panLawCode(panLaw));
1806
+ }
1807
+ /** Sets a bus strip's pan mode in realtime. */
1808
+ setBusStripPanMode(busId, panMode) {
1809
+ this.native.setBusStripPanMode(busId, panModeCode(panMode));
1810
+ }
1811
+ /** Sets a bus strip's dual-pan left/right positions in realtime. */
1812
+ setBusStripDualPan(busId, leftPan, rightPan) {
1813
+ this.native.setBusStripDualPan(busId, leftPan, rightPan);
1814
+ }
1815
+ /**
1816
+ * Sets a track lane strip's inter-channel alignment delay (whole samples).
1817
+ * Adjusts strip latency, so PDC and reported graph latency are refreshed.
1818
+ */
1819
+ setTrackStripChannelDelaySamples(trackId, delaySamples) {
1820
+ this.native.setTrackStripChannelDelaySamples(trackId, delaySamples);
1174
1821
  }
1175
1822
  createClipPageProvider(numChannels, numSamples, pageFrames) {
1176
1823
  const id = this.native.createClipPageProvider(numChannels, numSamples, pageFrames);
@@ -1205,6 +1852,52 @@ var RealtimeEngine = class {
1205
1852
  clipPageRequestOverflowCount() {
1206
1853
  return this.native.clipPageRequestOverflowCount();
1207
1854
  }
1855
+ /** Cumulative warp-stretch requests dropped because the native queue was full. */
1856
+ warpStretchOverflowCount() {
1857
+ return this.native.warpStretchOverflowCount();
1858
+ }
1859
+ /**
1860
+ * Sets the number of concurrent time-stretch voices. `voices` must be an
1861
+ * integer in `[0, 64]`; a non-integer, negative, or larger value throws and
1862
+ * leaves the capacity unchanged. Default is 8. Capacity 0 disables
1863
+ * time-stretch, so every warped clip plays resampled instead and none of
1864
+ * that counts toward {@link warpStretchOverflowCount}. A change applied
1865
+ * while the engine is running restarts the splice state of any clip
1866
+ * stretching through a voice at that moment. Control-thread only.
1867
+ */
1868
+ setWarpVoiceCapacity(voices) {
1869
+ this.native.setWarpVoiceCapacity(voices);
1870
+ }
1871
+ /** Reads the current time-stretch voice capacity (default 8). */
1872
+ warpVoiceCapacity() {
1873
+ return this.native.warpVoiceCapacity();
1874
+ }
1875
+ /**
1876
+ * Sets the clip-page look-ahead window in timeline frames.
1877
+ *
1878
+ * The player reports the pages it is *about to* read that are not resident
1879
+ * yet, so a streaming host can service them before the audio thread reaches
1880
+ * them. Without look-ahead a page miss is only reported after the read
1881
+ * already produced silence, which costs one block of silence at every page
1882
+ * boundary the host has not primed — the reason a sliding-window streamer
1883
+ * cannot keep a live playhead fed from miss reports alone.
1884
+ *
1885
+ * Look-ahead requests drain through the same `popClipPageRequest` queue and
1886
+ * are queued *after* the block's genuine misses, so a host that keeps only
1887
+ * the newest request per clip (as {@link ClipPageStreamer} does) tracks the
1888
+ * look-ahead frontier.
1889
+ *
1890
+ * `prepare` defaults this to half a second at the engine's sample rate. `0`
1891
+ * disables the look-ahead. A clip whose pages are all resident produces no
1892
+ * requests at all, with or without look-ahead. Safe to call during playback.
1893
+ */
1894
+ setClipPagePrefetchFrames(frames) {
1895
+ this.native.setClipPagePrefetchFrames(frames);
1896
+ }
1897
+ /** Current clip-page look-ahead window in timeline frames. */
1898
+ clipPagePrefetchFrames() {
1899
+ return this.native.clipPagePrefetchFrames();
1900
+ }
1208
1901
  setCaptureBuffer(numChannels, capacityFrames) {
1209
1902
  this.native.setCaptureBuffer(numChannels, capacityFrames);
1210
1903
  }
@@ -1296,12 +1989,36 @@ var RealtimeEngine = class {
1296
1989
  processWithMonitor(channels) {
1297
1990
  return this.native.processWithMonitor(channels);
1298
1991
  }
1299
- renderOffline(channels, blockSize = 128) {
1300
- return this.native.renderOffline(channels, blockSize);
1992
+ renderOffline(channelsOrRequest, blockSize = 128) {
1993
+ const request = normalizeRenderOfflineRequest(channelsOrRequest, blockSize);
1994
+ return this.native.renderOffline(request.channels, request.blockSize, request.finalize);
1995
+ }
1996
+ /**
1997
+ * End a chunked offline render: release every note the sequencer still holds
1998
+ * and flush the PDC / alignment delay lines. Required after
1999
+ * `renderOffline({ finalize: false })`; the finalizing form does it itself.
2000
+ *
2001
+ * Skipping it leaves every note still sounding at the last chunk held. On an
2002
+ * engine-internal instrument the tail simply never releases; on a destination
2003
+ * marked external ({@link RealtimeEngine.setMidiDestinationExternal}) the
2004
+ * note-ons already left through the external MIDI queue, so the note-offs
2005
+ * emitted here are the only ones the receiving device will get and the notes
2006
+ * otherwise hang outside the engine.
2007
+ */
2008
+ finishOfflineRender() {
2009
+ this.native.finishOfflineRender();
1301
2010
  }
2011
+ /**
2012
+ * Bounce the timeline to an interleaved buffer. `numChannels` above the
2013
+ * prepared channel count throws an `InvalidParameter` `SonareError`.
2014
+ */
1302
2015
  bounceOffline(options) {
1303
2016
  return this.native.bounceOffline(options);
1304
2017
  }
2018
+ /**
2019
+ * Freeze the current graph to audio. `numChannels` above the prepared channel
2020
+ * count throws an `InvalidParameter` `SonareError`.
2021
+ */
1305
2022
  freezeOffline(options) {
1306
2023
  return this.native.freezeOffline(options);
1307
2024
  }
@@ -1341,6 +2058,12 @@ var RealtimeEngine = class {
1341
2058
  meterScratchRenderFrame() {
1342
2059
  return Number(this.native.meterScratchRenderFrame());
1343
2060
  }
2061
+ meterScratchInputPeakDbL() {
2062
+ return this.native.meterScratchInputPeakDbL();
2063
+ }
2064
+ meterScratchInputPeakDbR() {
2065
+ return this.native.meterScratchInputPeakDbR();
2066
+ }
1344
2067
  meterScratchValue(field) {
1345
2068
  return this.native.meterScratchValue(field);
1346
2069
  }
@@ -1350,9 +2073,10 @@ var RealtimeEngine = class {
1350
2073
  /**
1351
2074
  * Drains pending meter telemetry as per-plane (wide) records for a surround
1352
2075
  * target. Use this for a surround mix target; {@link drainMeterTelemetry}
1353
- * stays the stereo fast path. The two share one queue — call only one per
1354
- * target. The live AudioWorklet path owns the queue via the stereo drain, so
1355
- * this wide drain is for an offline (non-worklet) engine instance; per-plane
2076
+ * stays the stereo fast path. The two share one queue and each consumes every
2077
+ * target's records, so an engine uses only one of them. The live AudioWorklet
2078
+ * path owns the queue via the stereo drain, so this wide drain is for an
2079
+ * offline (non-worklet) engine instance; per-plane
1356
2080
  * surround meters are not delivered over the live worklet meter ring.
1357
2081
  */
1358
2082
  drainMeterTelemetryWide(maxRecords = 1024) {
@@ -1395,9 +2119,18 @@ var RealtimeEngine = class {
1395
2119
  scopeScratchPointRight(index) {
1396
2120
  return this.native.scopeScratchPointRight(index);
1397
2121
  }
2122
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
1398
2123
  destroy() {
2124
+ if (this.released) {
2125
+ return;
2126
+ }
2127
+ this.released = true;
1399
2128
  this.native.delete();
1400
2129
  }
2130
+ /** Alias for {@link destroy}, matching embind's own release method name. */
2131
+ delete() {
2132
+ this.destroy();
2133
+ }
1401
2134
  };
1402
2135
  var ClipPageProvider = class {
1403
2136
  constructor(engine, id) {
@@ -1429,12 +2162,20 @@ var ClipPageProvider = class {
1429
2162
  // src/mixer.ts
1430
2163
  var Mixer = class _Mixer {
1431
2164
  constructor(mixer, blockSize) {
2165
+ this.released = false;
1432
2166
  this.mixer = mixer;
1433
2167
  this.blockSize = blockSize;
1434
2168
  }
1435
2169
  /**
1436
2170
  * Build a mixer from a scene JSON string.
1437
2171
  *
2172
+ * A strip's meters are sized when the strip is built, so this is where their
2173
+ * configuration is chosen: an optional `metering` object on the strip
2174
+ * (`enabled` / `lufs` / `truePeak` / `truePeakOversample`) selects it, and
2175
+ * leaving it out keeps the full default (LUFS + true peak at 4x, about 1.4 MB
2176
+ * per strip at 48 kHz). `{"enabled": false}` drops both meters for a strip
2177
+ * whose snapshots are never read.
2178
+ *
1438
2179
  * @param json - Scene JSON (strips, buses, sends, connections, inserts)
1439
2180
  * @param sampleRate - Sample rate in Hz (default: 48000)
1440
2181
  * @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
@@ -1497,50 +2238,113 @@ var Mixer = class _Mixer {
1497
2238
  * after {@link delete}.
1498
2239
  */
1499
2240
  createRealtimeBuffer() {
1500
- const stripCount = this.stripCount();
1501
- let leftInputs = [];
1502
- let rightInputs = [];
2241
+ const leftInputs = [];
2242
+ const rightInputs = [];
1503
2243
  let outLeft = this.mixer.outputLeftView();
1504
2244
  let outRight = this.mixer.outputRightView();
2245
+ let acquiredStripCount = -1;
2246
+ const viewsDetached = () => outLeft.byteLength === 0 || outRight.byteLength === 0;
1505
2247
  const acquire = () => {
1506
- leftInputs = [];
1507
- rightInputs = [];
1508
- for (let index = 0; index < stripCount; index++) {
2248
+ const stripCount = this.stripCount();
2249
+ const detached = viewsDetached();
2250
+ if (detached) {
2251
+ leftInputs.length = 0;
2252
+ rightInputs.length = 0;
2253
+ } else {
2254
+ leftInputs.length = Math.min(leftInputs.length, stripCount);
2255
+ rightInputs.length = Math.min(rightInputs.length, stripCount);
2256
+ }
2257
+ for (let index = leftInputs.length; index < stripCount; index++) {
1509
2258
  leftInputs.push(this.mixer.inputLeftView(index));
2259
+ }
2260
+ for (let index = rightInputs.length; index < stripCount; index++) {
1510
2261
  rightInputs.push(this.mixer.inputRightView(index));
1511
2262
  }
1512
- outLeft = this.mixer.outputLeftView();
1513
- outRight = this.mixer.outputRightView();
2263
+ if (detached) {
2264
+ outLeft = this.mixer.outputLeftView();
2265
+ outRight = this.mixer.outputRightView();
2266
+ }
2267
+ acquiredStripCount = stripCount;
1514
2268
  };
1515
2269
  acquire();
1516
- const reacquireIfDetached = () => {
1517
- if (outLeft.byteLength === 0 || (leftInputs[0]?.byteLength ?? 1) === 0) {
2270
+ const acquireIfNeeded = () => {
2271
+ if (acquiredStripCount !== this.stripCount() || viewsDetached()) {
1518
2272
  acquire();
1519
2273
  }
1520
2274
  };
1521
2275
  return {
1522
2276
  get leftInputs() {
1523
- reacquireIfDetached();
2277
+ acquireIfNeeded();
1524
2278
  return leftInputs;
1525
2279
  },
1526
2280
  get rightInputs() {
1527
- reacquireIfDetached();
2281
+ acquireIfNeeded();
1528
2282
  return rightInputs;
1529
2283
  },
1530
2284
  get outLeft() {
1531
- reacquireIfDetached();
2285
+ acquireIfNeeded();
1532
2286
  return outLeft;
1533
2287
  },
1534
2288
  get outRight() {
1535
- reacquireIfDetached();
2289
+ acquireIfNeeded();
1536
2290
  return outRight;
1537
2291
  },
1538
- process: (numSamples = outLeft.length) => {
1539
- reacquireIfDetached();
1540
- this.mixer.processPreparedStereo(numSamples);
2292
+ process: (numSamples) => {
2293
+ acquireIfNeeded();
2294
+ this.mixer.processPreparedStereo(numSamples ?? outLeft.length);
1541
2295
  }
1542
2296
  };
1543
2297
  }
2298
+ /**
2299
+ * Turn the master-output meter on or off.
2300
+ *
2301
+ * While on, every {@link MixerRealtimeBuffer.process} call meters the stereo
2302
+ * master it just produced, so a caller reads {@link meterSnapshot} instead of
2303
+ * copying the output and measuring it again. `truePeakDb*` is an inter-sample
2304
+ * peak taken after oversampling (ITU-R BS.1770-4 Annex 2 requires at least
2305
+ * 4x), which is a different and higher quantity than the sample peak.
2306
+ *
2307
+ * Enabling resets the meter, so a reading never mixes in audio from a period
2308
+ * when metering was off.
2309
+ *
2310
+ * @param enabled - Whether to meter the master output.
2311
+ * @param truePeakOversample - 0 (= 4x) or a power of two in [1, 16].
2312
+ */
2313
+ configureMeter(enabled, truePeakOversample = 4) {
2314
+ this.mixer.configureMeter(enabled, truePeakOversample);
2315
+ }
2316
+ /**
2317
+ * Latest master-output meter reading, describing the most recently metered
2318
+ * block. All dB fields are finite and floored at -120.
2319
+ *
2320
+ * @throws When the meter has never been enabled.
2321
+ */
2322
+ meterSnapshot() {
2323
+ return this.mixer.meterSnapshot();
2324
+ }
2325
+ /**
2326
+ * Latch the latest meter reading into the mixer's internal scratch so
2327
+ * {@link meterScratchValue} can read it back one number at a time.
2328
+ *
2329
+ * This is the allocation-free form of {@link meterSnapshot}, for an audio
2330
+ * render callback that must not create a JS object per interval. It returns
2331
+ * `false` instead of throwing when the meter has never been enabled.
2332
+ *
2333
+ * @returns Whether a reading was latched.
2334
+ */
2335
+ latchMeterSnapshot() {
2336
+ return this.mixer.latchMeterSnapshot();
2337
+ }
2338
+ /**
2339
+ * Read one field of the snapshot latched by {@link latchMeterSnapshot}.
2340
+ *
2341
+ * @param field - `0` peakDbL, `1` peakDbR, `2` rmsDbL, `3` rmsDbR,
2342
+ * `4` correlation, `5` truePeakDbL, `6` truePeakDbR. Any other index
2343
+ * reads `0`.
2344
+ */
2345
+ meterScratchValue(field) {
2346
+ return this.mixer.meterScratchValue(field);
2347
+ }
1544
2348
  /** Number of strips in the mixer (e.g. strips loaded from the scene). */
1545
2349
  stripCount() {
1546
2350
  return this.mixer.stripCount();
@@ -1578,6 +2382,17 @@ var Mixer = class _Mixer {
1578
2382
  const index = this.mixer.stripById(id);
1579
2383
  return index < 0 ? null : index;
1580
2384
  }
2385
+ /**
2386
+ * Add a channel strip to the mixer topology. `metering` configures the strip's
2387
+ * pre/post taps; omitting it keeps the full default (LUFS + true peak at 4x,
2388
+ * about 1.4 MB per strip at 48 kHz). Marks the routing graph dirty; call
2389
+ * {@link compile} (or {@link processStereo}) to rebuild.
2390
+ *
2391
+ * @throws If the id is already taken, or `truePeakOversample` is outside `[0, 16]`
2392
+ */
2393
+ addStrip(id, metering = {}) {
2394
+ this.mixer.addStrip(id, metering);
2395
+ }
1581
2396
  /**
1582
2397
  * Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
1583
2398
  * `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
@@ -1643,6 +2458,30 @@ var Mixer = class _Mixer {
1643
2458
  setWidth(stripIndex, width) {
1644
2459
  this.mixer.setWidth(stripIndex, width);
1645
2460
  }
2461
+ /**
2462
+ * Snap the strip's input-trim, fader, pan and width smoothers to the values
2463
+ * already set on it, so the next processed block opens at those values
2464
+ * instead of gliding to them over the smoothing window (~5 ms).
2465
+ *
2466
+ * Call it after configuring a strip and before rendering a finite buffer: a
2467
+ * strip is smoothed for a live fader, and an offline render that does not
2468
+ * settle carries that glide as a level and image sweep across the head of
2469
+ * its output. Unlike a reset it clears nothing — automation, meters and
2470
+ * insert state are untouched.
2471
+ *
2472
+ * @param stripIndex - Strip index in `[0, stripCount())`
2473
+ *
2474
+ * @example
2475
+ * ```typescript
2476
+ * mixer.setFaderDb(0, -3);
2477
+ * mixer.setPan(0, 0.3);
2478
+ * mixer.settle(0);
2479
+ * const { left, right } = mixer.processStereo([dryLeft], [dryRight]);
2480
+ * ```
2481
+ */
2482
+ settle(stripIndex) {
2483
+ this.mixer.settle(stripIndex);
2484
+ }
1646
2485
  /** Set the strip's mute state. */
1647
2486
  setMuted(stripIndex, muted) {
1648
2487
  this.mixer.setMuted(stripIndex, muted);
@@ -1686,7 +2525,10 @@ var Mixer = class _Mixer {
1686
2525
  }
1687
2526
  /**
1688
2527
  * Set the strip's surround pan position, used when it feeds a >2-channel bus.
1689
- * Stored on the scene; inert until the surround DSP path applies it.
2528
+ *
2529
+ * Applied when the engine's track mixer renders this strip's lane into a
2530
+ * destination with more than two channels. This stereo-only mixer's own
2531
+ * block entry points ignore it.
1690
2532
  */
1691
2533
  setSurroundPan(stripIndex, pan) {
1692
2534
  this.mixer.setSurroundPan(stripIndex, pan);
@@ -1752,6 +2594,59 @@ var Mixer = class _Mixer {
1752
2594
  busMeter(busId) {
1753
2595
  return this.mixer.busMeter(busId);
1754
2596
  }
2597
+ /**
2598
+ * Number of blocks in which the strip discarded recursive state because a
2599
+ * non-finite value had reached it.
2600
+ *
2601
+ * Advisory telemetry, and the only thing that separates a degraded strip
2602
+ * from a clean one. A discard returns the affected state to its
2603
+ * post-reset value, so the strip recovers in silence and the output stays
2604
+ * finite and in range while carrying samples unrelated to the input;
2605
+ * nothing else reports that this happened.
2606
+ *
2607
+ * The count covers the strip's own state, its EQ, every insert it owns and
2608
+ * both of its meters. None of those is separately addressable here, so a
2609
+ * discard inside one is observable only through this number -- and a
2610
+ * meter that loses its loudness window then reports the floor, which is
2611
+ * exactly what a genuinely silent strip reports, so nothing else
2612
+ * distinguishes the two.
2613
+ *
2614
+ * A meter's own discard lags by one block: it checks its loudness state at
2615
+ * the top of a block, before consuming that block's samples, so the block
2616
+ * that corrupts it is not the block the count moves on -- read this again
2617
+ * after one more block has processed. The EQ and inserts have no such lag;
2618
+ * they discard at the end of their own process, in the same block that
2619
+ * carried the poison.
2620
+ *
2621
+ * Cumulative since the strip was created and never cleared, so two
2622
+ * readings bracket a span of audio. The unit is one processed block, never
2623
+ * a channel, so a stereo block that discards on both channels adds one and
2624
+ * the number does not depend on a dimension the caller did not choose.
2625
+ *
2626
+ * @param stripIndex - Strip index in `[0, stripCount())`
2627
+ */
2628
+ stripNonFiniteDiscardCount(stripIndex) {
2629
+ return this.mixer.stripNonFiniteDiscardCount(stripIndex);
2630
+ }
2631
+ /**
2632
+ * Number of blocks in which a bus discarded recursive state because a
2633
+ * non-finite value had reached it. Same contract as
2634
+ * {@link stripNonFiniteDiscardCount}, for a bus: covers every insert the
2635
+ * bus owns and its meter, neither separately addressable, so a discard
2636
+ * inside one is observable only here. Cumulative across graph recompiles
2637
+ * -- the count lives with the bus, not the compiled node, so an unrelated
2638
+ * edit elsewhere in the mixer does not reset it.
2639
+ *
2640
+ * A bus's DSP record is created by the first {@link compile}. Throws for a
2641
+ * bus that has been declared with {@link addBus} but never compiled --
2642
+ * reading zero there would read as clean, and it is not. Also throws for
2643
+ * an unknown bus id.
2644
+ *
2645
+ * @param busId - Bus id, as passed to {@link addBus} or declared in scene JSON
2646
+ */
2647
+ busNonFiniteDiscardCount(busId) {
2648
+ return this.mixer.busNonFiniteDiscardCount(busId);
2649
+ }
1755
2650
  /**
1756
2651
  * Schedule sample-accurate fader automation on a strip.
1757
2652
  *
@@ -1806,6 +2701,11 @@ var Mixer = class _Mixer {
1806
2701
  /**
1807
2702
  * Read up to `maxPoints` of a strip's most recent goniometer samples
1808
2703
  * (oldest to newest).
2704
+ *
2705
+ * `maxPoints` must be a finite non-negative integer; anything else throws an
2706
+ * `InvalidParameter` error. It is a request rather than an allocation size —
2707
+ * a value beyond the strip's goniometer ring simply returns every point the
2708
+ * ring holds.
1809
2709
  */
1810
2710
  readGoniometerLatest(stripIndex, maxPoints) {
1811
2711
  return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
@@ -1841,8 +2741,12 @@ var Mixer = class _Mixer {
1841
2741
  }
1842
2742
  return this.mixer.drainTailStereo(numSamples);
1843
2743
  }
1844
- /** Release the underlying WASM object. Safe to call only once. */
2744
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
1845
2745
  delete() {
2746
+ if (this.released) {
2747
+ return;
2748
+ }
2749
+ this.released = true;
1846
2750
  this.mixer.delete();
1847
2751
  }
1848
2752
  /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
@@ -1859,10 +2763,19 @@ var RealtimeVoiceChanger = class {
1859
2763
  * explicit {@link prepare} lifecycle for callers that configure later.
1860
2764
  */
1861
2765
  constructor(config = "neutral-monitor", sampleRate, maxBlockSize = 128, channels = 1) {
2766
+ this.released = false;
1862
2767
  const module2 = getSonareModule();
1863
2768
  this.changer = module2.createRealtimeVoiceChanger(config);
1864
2769
  if (sampleRate !== void 0) {
1865
- this.changer.prepare(sampleRate, maxBlockSize, channels);
2770
+ let prepared = false;
2771
+ try {
2772
+ this.changer.prepare(sampleRate, maxBlockSize, channels);
2773
+ prepared = true;
2774
+ } finally {
2775
+ if (!prepared) {
2776
+ this.changer.delete();
2777
+ }
2778
+ }
1866
2779
  }
1867
2780
  }
1868
2781
  prepare(sampleRate, maxBlockSize = 128, channels = 1) {
@@ -1888,6 +2801,34 @@ var RealtimeVoiceChanger = class {
1888
2801
  latencySamples() {
1889
2802
  return this.changer.latencySamples();
1890
2803
  }
2804
+ /**
2805
+ * Channel-blocks in which the chain discarded its own state because a
2806
+ * non-finite value had reached it.
2807
+ *
2808
+ * Advisory telemetry, and the only thing that separates a degraded stream
2809
+ * from a clean one. Every stage of this chain leaves an in-domain finite
2810
+ * value where a non-finite one was — the input scrub and the
2811
+ * inter-sample-peak limiter substitute silence, the sample-domain limiter
2812
+ * folds an infinity onto its ceiling — so the output stays finite, in range
2813
+ * and free of any error while carrying samples unrelated to the input. A
2814
+ * non-zero count is what says the samples in between were not computed from
2815
+ * what you supplied.
2816
+ *
2817
+ * Monotonic for the lifetime of the instance. The unit is one processed
2818
+ * block, never a channel, so a stereo block that discards on both channels
2819
+ * adds one and the number does not depend on a dimension you did not choose.
2820
+ *
2821
+ * @example
2822
+ * ```ts
2823
+ * changer.processInterleaved(block, 2);
2824
+ * if (changer.nonFiniteDiscardCount() > 0) {
2825
+ * // the audio just produced is not a function of `block`
2826
+ * }
2827
+ * ```
2828
+ */
2829
+ nonFiniteDiscardCount() {
2830
+ return this.changer.nonFiniteDiscardCount();
2831
+ }
1891
2832
  /**
1892
2833
  * Monotonically increases whenever {@link prepare} can replace the native
1893
2834
  * scratch buffers. Cached WASM heap views must be reacquired after it changes.
@@ -2055,14 +2996,24 @@ var RealtimeVoiceChanger = class {
2055
2996
  }
2056
2997
  };
2057
2998
  }
2999
+ /** Releases the native handle. Idempotent, as the Node facade is. */
2058
3000
  delete() {
3001
+ if (this.released) {
3002
+ return;
3003
+ }
3004
+ this.released = true;
2059
3005
  this.changer.delete();
2060
3006
  }
3007
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
3008
+ destroy() {
3009
+ this.delete();
3010
+ }
2061
3011
  };
2062
3012
 
2063
3013
  // src/streaming_processors.ts
2064
3014
  var StreamingMasteringChain = class {
2065
3015
  constructor(config) {
3016
+ this.released = false;
2066
3017
  const module2 = getSonareModule();
2067
3018
  const { loudnessStaticGainDb, loudnessStaticGainPeakDb, ...chainConfig } = config;
2068
3019
  this.chain = module2.createStreamingMasteringChain({
@@ -2113,6 +3064,13 @@ var StreamingMasteringChain = class {
2113
3064
  reset() {
2114
3065
  this.chain.reset();
2115
3066
  }
3067
+ /**
3068
+ * Set one realtime-safe full-chain parameter at a serialized block boundary.
3069
+ * Structural, disabled-stage and unknown parameters are rejected.
3070
+ */
3071
+ setParameter(key, value) {
3072
+ this.chain.setParameter(key, value);
3073
+ }
2116
3074
  /** Total reported latency in samples across all active processors. */
2117
3075
  latencySamples() {
2118
3076
  return this.chain.latencySamples();
@@ -2121,10 +3079,75 @@ var StreamingMasteringChain = class {
2121
3079
  stageNames() {
2122
3080
  return this.chain.stageNames();
2123
3081
  }
2124
- /** Release the underlying WASM object. Safe to call only once. */
3082
+ /**
3083
+ * Samples a stage replaced with a finite in-domain one, keeping the output
3084
+ * finite and in range.
3085
+ *
3086
+ * A non-finite sample supplied by the caller is rejected before any stage
3087
+ * runs, so a replacement is always of a value a stage itself produced.
3088
+ *
3089
+ * Only the true-peak limiters replace anything, so with the maximizer's
3090
+ * limiter and the loudness stage both disabled a zero here means no stage was
3091
+ * able to replace anything rather than that nothing needed replacing.
3092
+ *
3093
+ * Cumulative over every block since {@link prepare}, and aggregated over the
3094
+ * stages and channels, so it identifies neither which block nor which stage.
3095
+ * Read it per block and compare against the previous reading to localize one.
3096
+ *
3097
+ * {@link prepare} rebuilds the stages and so clears it; {@link reset} does
3098
+ * not, because it drops processor state without rebuilding.
3099
+ *
3100
+ * @example
3101
+ * ```typescript
3102
+ * chain.processMono(block);
3103
+ * if (chain.nonFiniteSubstitutionCount() > previous) {
3104
+ * // the block just produced is not derived from `block` everywhere
3105
+ * }
3106
+ * ```
3107
+ */
3108
+ nonFiniteSubstitutionCount() {
3109
+ return this.chain.nonFiniteSubstitutionCount();
3110
+ }
3111
+ /**
3112
+ * Processing calls in which a stage discarded its own recursive state
3113
+ * because a non-finite value had reached it.
3114
+ *
3115
+ * The companion to {@link nonFiniteSubstitutionCount}, and not the same
3116
+ * measurement -- a caller who assumes they are will read one and think
3117
+ * they have the other. That one counts SAMPLES a stage replaced and so
3118
+ * sums across stages; a discard is a whole stage returning to its
3119
+ * post-reset value and is counted once per call however many stages did
3120
+ * it. A stage may run more than once per call, which is why this is a
3121
+ * delta over the call and never a sum.
3122
+ *
3123
+ * Non-finite input is rejected before any stage runs, so what a stage
3124
+ * discards is always state it produced itself -- a finite sample large
3125
+ * enough to overflow inside a filter, most often. Unlike the substitution
3126
+ * count every stage can contribute, so a zero here means no stage
3127
+ * discarded rather than that none could.
3128
+ *
3129
+ * Both {@link processMono}/{@link processStereo} and
3130
+ * {@link flushMono}/{@link flushStereo} count, since a flush drives the
3131
+ * same stages. {@link prepare} rebuilds the stages and so clears it (as it
3132
+ * does {@link nonFiniteSubstitutionCount}, so the two counters on one
3133
+ * handle share an epoch); {@link reset} does not, because it drops
3134
+ * processor state without rebuilding.
3135
+ */
3136
+ nonFiniteDiscardCount() {
3137
+ return this.chain.nonFiniteDiscardCount();
3138
+ }
3139
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
2125
3140
  delete() {
3141
+ if (this.released) {
3142
+ return;
3143
+ }
3144
+ this.released = true;
2126
3145
  this.chain.delete();
2127
3146
  }
3147
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
3148
+ destroy() {
3149
+ this.delete();
3150
+ }
2128
3151
  };
2129
3152
 
2130
3153
  // src/index.ts
@@ -2154,14 +3177,16 @@ function isInitialized() {
2154
3177
  }
2155
3178
 
2156
3179
  // src/worklet/engine-sync.ts
2157
- function buildMixerLanes(trackLaneIds, trackSends, trackOutputBus) {
3180
+ function buildMixerLanes(trackLaneIds, trackSends, trackOutputBus, trackSourceChannelLayout) {
2158
3181
  return trackLaneIds.map((trackId) => {
2159
3182
  const sends = trackSends.get(trackId);
2160
3183
  const outputBusId = trackOutputBus.get(trackId);
3184
+ const sourceChannelLayout = trackSourceChannelLayout.get(trackId);
2161
3185
  return {
2162
3186
  trackId,
2163
3187
  ...sends && sends.length > 0 ? { sends: sends.map((send) => ({ ...send })) } : {},
2164
- ...outputBusId !== void 0 ? { outputBusId } : {}
3188
+ ...outputBusId !== void 0 ? { outputBusId } : {},
3189
+ ...sourceChannelLayout !== void 0 ? { sourceChannelLayout } : {}
2165
3190
  };
2166
3191
  });
2167
3192
  }
@@ -2175,11 +3200,11 @@ function buildTempoSync(tempoBpm, timeSignature, tempoSegments, timeSignatureSeg
2175
3200
  };
2176
3201
  }
2177
3202
  function resolveTargetId(target) {
2178
- if (typeof target === "number") {
2179
- return target;
3203
+ const value = typeof target === "number" ? target : target.trim() === "" ? Number.NaN : Number(target);
3204
+ if (!Number.isInteger(value)) {
3205
+ throw new RangeError(`target id must be an integer, got ${JSON.stringify(target)}`);
2180
3206
  }
2181
- const parsed = Number.parseInt(target, 10);
2182
- return Number.isFinite(parsed) ? parsed : 0;
3207
+ return value;
2183
3208
  }
2184
3209
  function resolveParamId(parameters, nodeId, param) {
2185
3210
  if (typeof param === "number") {
@@ -2222,101 +3247,6 @@ function setAutomationLane(ctx, paramId, points) {
2222
3247
  ctx.postSync({ type: "syncAutomation", paramId, points: sorted });
2223
3248
  }
2224
3249
 
2225
- // src/worklet/engine-offline.ts
2226
- function buildCaptureConfig(options, defaultChannels) {
2227
- return {
2228
- bufferFrames: Math.trunc(options.bufferFrames),
2229
- channels: Math.trunc(options.channels ?? defaultChannels),
2230
- source: options.source ?? "output",
2231
- recordOffsetSamples: Math.trunc(options.recordOffsetSamples ?? 0),
2232
- inputMonitor: {
2233
- enabled: Boolean(options.inputMonitor?.enabled),
2234
- gain: options.inputMonitor?.gain ?? 1
2235
- }
2236
- };
2237
- }
2238
- function buildTransportFacade(ctx) {
2239
- return {
2240
- play: (sampleTime = -1) => {
2241
- const ok = ctx.realtimeNode.play(sampleTime);
2242
- if (ok) {
2243
- ctx.setTransportPlaying(true);
2244
- }
2245
- return ok;
2246
- },
2247
- stop: (sampleTime = -1) => {
2248
- const ok = ctx.realtimeNode.stop(sampleTime);
2249
- if (ok) {
2250
- ctx.setTransportPlaying(false);
2251
- ctx.flushPendingInstrumentSync();
2252
- }
2253
- return ok;
2254
- },
2255
- seekPpq: (ppq, sampleTime = -1) => {
2256
- ctx.offlineEngine.seekPpq(ppq, sampleTime);
2257
- const ok = ctx.realtimeNode.seekPpq(ppq, sampleTime);
2258
- ctx.flushOfflineMirror();
2259
- return ok;
2260
- },
2261
- seekSeconds: (seconds, sampleTime = -1) => {
2262
- const timelineSample = Math.max(0, Math.round(seconds * ctx.sampleRate));
2263
- ctx.offlineEngine.seekSample(timelineSample, sampleTime);
2264
- const ok = ctx.realtimeNode.seekSample(timelineSample, sampleTime);
2265
- ctx.flushOfflineMirror();
2266
- return ok;
2267
- },
2268
- setTempo: (bpm) => ctx.setTempo(bpm),
2269
- setTempoSegments: (segments) => ctx.setTempoSegments(segments),
2270
- setLoop: (startPpq, endPpq, enabled = true) => ctx.setLoop(startPpq, endPpq, enabled)
2271
- };
2272
- }
2273
- function normalizeTrackLanes(existing, lanes) {
2274
- const entries = lanes.map((lane) => typeof lane === "number" ? { trackId: lane } : lane);
2275
- const ids = [];
2276
- for (const entry of entries) {
2277
- if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
2278
- throw new Error(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
2279
- }
2280
- ids.push(entry.trackId);
2281
- }
2282
- if (new Set(ids).size !== ids.length) {
2283
- throw new Error("Duplicate track id in mixer lane list");
2284
- }
2285
- for (let index = 0; index < existing.length; index++) {
2286
- if (ids[index] !== existing[index]) {
2287
- throw new Error(
2288
- "Mixer lanes are append-only: keep existing lanes in order and only append new track ids"
2289
- );
2290
- }
2291
- }
2292
- return { entries, ids };
2293
- }
2294
- function resolveMarkerSet(markers, nextMarkerId) {
2295
- const resolved = [];
2296
- const seen = /* @__PURE__ */ new Set();
2297
- let counter = nextMarkerId;
2298
- for (const marker2 of markers) {
2299
- if (!Number.isFinite(marker2.ppq)) {
2300
- throw new Error(`Invalid marker ppq: ${String(marker2.ppq)}`);
2301
- }
2302
- if (marker2.id !== void 0) {
2303
- if (!Number.isInteger(marker2.id) || marker2.id <= 0) {
2304
- throw new Error(`Invalid marker id: ${String(marker2.id)}`);
2305
- }
2306
- if (seen.has(marker2.id)) {
2307
- throw new Error(`Duplicate marker id: ${marker2.id}`);
2308
- }
2309
- }
2310
- const id = marker2.id ?? counter++;
2311
- seen.add(id);
2312
- if (id >= counter) {
2313
- counter = id + 1;
2314
- }
2315
- resolved.push({ id, ppq: marker2.ppq, name: marker2.name ?? "" });
2316
- }
2317
- return { resolved, nextMarkerId: counter };
2318
- }
2319
-
2320
3250
  // src/worklet/protocol.ts
2321
3251
  var ENGINE_MIXER_TARGET_BASE = 1297612800;
2322
3252
  var ENGINE_MIXER_PARAM_FADER_DB = 1;
@@ -2331,10 +3261,12 @@ function engineMixerMasterTarget(paramKind) {
2331
3261
  return ENGINE_MIXER_TARGET_BASE | 255 << 8 | paramKind & 255;
2332
3262
  }
2333
3263
  var SONARE_METER_RING_HEADER_INTS = 4;
2334
- var SONARE_METER_RING_RECORD_FLOATS = 14;
3264
+ var SONARE_METER_RING_RECORD_FLOATS = 16;
2335
3265
  var SONARE_SPECTRUM_RING_HEADER_INTS = 5;
2336
3266
  var SONARE_SCOPE_RING_HEADER_INTS = 6;
2337
3267
  var SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS = 5;
3268
+ var SONARE_SCOPE_DEFAULT_BANDS = 48;
3269
+ var SONARE_SCOPE_DEFAULT_MAX_POINTS = 32;
2338
3270
  var SONARE_FRAME_LANE_BASE = 16777216;
2339
3271
  function encodeFrameLo(frame) {
2340
3272
  const f = Math.max(0, Math.floor(frame));
@@ -2374,6 +3306,7 @@ var SonareEngineCommandType = /* @__PURE__ */ ((SonareEngineCommandType2) => {
2374
3306
  SonareEngineCommandType2[SonareEngineCommandType2["SetMarker"] = 16] = "SetMarker";
2375
3307
  SonareEngineCommandType2[SonareEngineCommandType2["SeekMarker"] = 17] = "SeekMarker";
2376
3308
  SonareEngineCommandType2[SonareEngineCommandType2["SetTrackMonitorMode"] = 26] = "SetTrackMonitorMode";
3309
+ SonareEngineCommandType2[SonareEngineCommandType2["ResetMasterLoudnessMeter"] = 28] = "ResetMasterLoudnessMeter";
2377
3310
  return SonareEngineCommandType2;
2378
3311
  })(SonareEngineCommandType || {});
2379
3312
  var SonareEngineTelemetryType = /* @__PURE__ */ ((SonareEngineTelemetryType2) => {
@@ -2403,11 +3336,10 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
2403
3336
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["MetronomeOverflow"] = 18] = "MetronomeOverflow";
2404
3337
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["InvalidCommand"] = 19] = "InvalidCommand";
2405
3338
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["MaxChannelsExceeded"] = 20] = "MaxChannelsExceeded";
3339
+ SonareEngineTelemetryError2[SonareEngineTelemetryError2["ParameterBaseOverflow"] = 21] = "ParameterBaseOverflow";
2406
3340
  return SonareEngineTelemetryError2;
2407
3341
  })(SonareEngineTelemetryError || {});
2408
- function toDb(value) {
2409
- return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
2410
- }
3342
+ var SONARE_FLOOR_DB = -120;
2411
3343
  function isRecord(value) {
2412
3344
  return typeof value === "object" && value !== null;
2413
3345
  }
@@ -2447,7 +3379,9 @@ function readSonareMeterRingBuffer(ring, readIndex = 0) {
2447
3379
  momentaryLufs: ring.records[offset + 10],
2448
3380
  shortTermLufs: ring.records[offset + 11],
2449
3381
  integratedLufs: ring.records[offset + 12],
2450
- gainReductionDb: ring.records[offset + 13]
3382
+ gainReductionDb: ring.records[offset + 13],
3383
+ inputPeakDbL: ring.records[offset + 14],
3384
+ inputPeakDbR: ring.records[offset + 15]
2451
3385
  });
2452
3386
  }
2453
3387
  return { nextReadIndex: writeIndex, meters };
@@ -2499,13 +3433,13 @@ function readSonareSpectrumRingBuffer(ring, readIndex = 0) {
2499
3433
  function sonareScopeRingRecordFloats(bands, maxPoints) {
2500
3434
  return SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS + bands + 2 * maxPoints;
2501
3435
  }
2502
- function sonareScopeRingBufferByteLength(capacity, bands = 48, maxPoints = 32) {
3436
+ function sonareScopeRingBufferByteLength(capacity, bands = SONARE_SCOPE_DEFAULT_BANDS, maxPoints = SONARE_SCOPE_DEFAULT_MAX_POINTS) {
2503
3437
  const clampedCapacity = Math.max(1, Math.floor(capacity));
2504
3438
  const clampedBands = Math.max(1, Math.floor(bands));
2505
3439
  const clampedPoints = Math.max(0, Math.floor(maxPoints));
2506
3440
  return SONARE_SCOPE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * sonareScopeRingRecordFloats(clampedBands, clampedPoints) * Float32Array.BYTES_PER_ELEMENT;
2507
3441
  }
2508
- function createSonareScopeRingBuffer(capacity = 64, bands = 48, maxPoints = 32) {
3442
+ function createSonareScopeRingBuffer(capacity = 64, bands = SONARE_SCOPE_DEFAULT_BANDS, maxPoints = SONARE_SCOPE_DEFAULT_MAX_POINTS) {
2509
3443
  const clampedCapacity = Math.max(1, Math.floor(capacity));
2510
3444
  const clampedBands = Math.max(1, Math.floor(bands));
2511
3445
  const clampedPoints = Math.max(0, Math.floor(maxPoints));
@@ -2572,8 +3506,14 @@ function scopeRingFromSharedBuffer(sharedBuffer, fallbackCapacity, fallbackBands
2572
3506
  const existingBands = Atomics.load(header, 3);
2573
3507
  const existingMaxPoints = Atomics.load(header, 4);
2574
3508
  const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
2575
- const bands = Math.max(1, Math.floor(existingBands || fallbackBands || 48));
2576
- const maxPoints = Math.max(0, Math.floor(existingMaxPoints || (fallbackMaxPoints ?? 32)));
3509
+ const bands = Math.max(
3510
+ 1,
3511
+ Math.floor(existingBands || fallbackBands || SONARE_SCOPE_DEFAULT_BANDS)
3512
+ );
3513
+ const maxPoints = Math.max(
3514
+ 0,
3515
+ Math.floor(existingMaxPoints || (fallbackMaxPoints ?? SONARE_SCOPE_DEFAULT_MAX_POINTS))
3516
+ );
2577
3517
  const recordFloats = sonareScopeRingRecordFloats(bands, maxPoints);
2578
3518
  const minBytes = sonareScopeRingBufferByteLength(capacity, bands, maxPoints);
2579
3519
  if (sharedBuffer.byteLength < minBytes) {
@@ -2886,6 +3826,16 @@ function clipPageRequestRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
2886
3826
  function recordOffset(index, capacity, recordBytes) {
2887
3827
  return index % capacity * recordBytes;
2888
3828
  }
3829
+ function isUint32Slot(value) {
3830
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 4294967295;
3831
+ }
3832
+ function toUint32Slot(value, fallback, name) {
3833
+ const resolved = value ?? fallback;
3834
+ if (!isUint32Slot(resolved)) {
3835
+ throw new RangeError(`${name} must be an integer within [0, 4294967295]`);
3836
+ }
3837
+ return resolved;
3838
+ }
2889
3839
  function toSafeInteger(value, fallback) {
2890
3840
  const resolved = typeof value === "bigint" ? Number(value) : value;
2891
3841
  if (resolved === void 0) {
@@ -2905,8 +3855,8 @@ function readInt64Words(view, offset) {
2905
3855
  return view.getInt32(offset + 4, true) * 4294967296 + view.getUint32(offset, true);
2906
3856
  }
2907
3857
  function writeEngineCommandRecord(view, offset, command) {
2908
- view.setUint32(offset, command.type, true);
2909
- view.setUint32(offset + 4, command.targetId ?? 0, true);
3858
+ view.setUint32(offset, toUint32Slot(command.type, 0, "type"), true);
3859
+ view.setUint32(offset + 4, toUint32Slot(command.targetId, 0, "targetId"), true);
2910
3860
  writeInt64Words(view, offset + 8, toSafeInteger(command.sampleTime, -1));
2911
3861
  view.setFloat64(offset + 16, command.argFloat ?? 0, true);
2912
3862
  writeInt64Words(view, offset + 24, toSafeInteger(command.argInt, 0));
@@ -2968,34 +3918,349 @@ function meterFromEngine(meter) {
2968
3918
  momentaryLufs: meter.momentaryLufs,
2969
3919
  shortTermLufs: meter.shortTermLufs,
2970
3920
  integratedLufs: meter.integratedLufs,
2971
- gainReductionDb: meter.gainReductionDb
3921
+ gainReductionDb: meter.gainReductionDb,
3922
+ inputPeakDbL: meter.inputPeakDbL,
3923
+ inputPeakDbR: meter.inputPeakDbR
2972
3924
  };
2973
3925
  }
2974
3926
  function magnitudeToDb(value) {
2975
3927
  return value > 1e-12 ? 20 * Math.log10(value) : -120;
2976
3928
  }
2977
3929
 
2978
- // src/worklet/engine-capture-facade.ts
2979
- function configureCapture(ctx, options) {
2980
- const config = buildCaptureConfig(options, ctx.offlineChannelCount);
2981
- ctx.offlineEngine.setCaptureBuffer(config.channels, config.bufferFrames);
2982
- ctx.offlineEngine.setCaptureSource(config.source);
2983
- ctx.offlineEngine.setRecordOffsetSamples(config.recordOffsetSamples);
2984
- ctx.offlineEngine.setInputMonitor(config.inputMonitor.enabled, config.inputMonitor.gain);
2985
- ctx.setCaptureConfig(config);
2986
- ctx.postSync({ type: "syncCapture", ...config });
2987
- }
2988
- function armRecord(ctx, trackId, enabled) {
2989
- if (trackId !== 0) {
2990
- throw new RangeError("Capture is global; armRecord only accepts trackId 0");
2991
- }
2992
- if (enabled && !ctx.getCaptureConfig()) {
2993
- throw new Error("Capture buffer is not configured");
3930
+ // src/worklet/guards.ts
3931
+ function isWorkletMessage(value) {
3932
+ if (!isRecord(value) || typeof value.type !== "string") {
3933
+ return false;
2994
3934
  }
2995
- ctx.offlineEngine.armCapture(enabled);
2996
- return ctx.sendCommand({
2997
- type: 13 /* ArmRecord */,
2998
- targetId: 0,
3935
+ return value.type === "scheduleInsertAutomation" || value.type === "setMeterInterval" || value.type === "destroy";
3936
+ }
3937
+ function isEngineCommandRecord(value) {
3938
+ return isRecord(value) && typeof value.type === "number";
3939
+ }
3940
+ var ENGINE_SYNC_MESSAGE_TYPES = {
3941
+ destroy: true,
3942
+ syncAutomation: true,
3943
+ syncBuiltinInstrument: true,
3944
+ syncBusStripDualPan: true,
3945
+ syncBusStripEqBand: true,
3946
+ syncBusStripInsertBypassed: true,
3947
+ syncBusStripInsertParamByName: true,
3948
+ syncBusStripPan: true,
3949
+ syncBusStripPanLaw: true,
3950
+ syncBusStripPanMode: true,
3951
+ syncCapture: true,
3952
+ syncClearMidiFx: true,
3953
+ syncClearMidiInputSource: true,
3954
+ syncClipPage: true,
3955
+ syncClipPageClear: true,
3956
+ syncClipPageCommit: true,
3957
+ syncClipPageDestroy: true,
3958
+ syncClipPagePrefetchFrames: true,
3959
+ syncClipPageProvider: true,
3960
+ syncClips: true,
3961
+ syncClipsDelta: true,
3962
+ syncExternalMidiClock: true,
3963
+ syncLoadSoundFont: true,
3964
+ syncMarkers: true,
3965
+ syncMasterStripEqBand: true,
3966
+ syncMasterStripInsertBypassed: true,
3967
+ syncMasterStripInsertParamByName: true,
3968
+ syncMetronome: true,
3969
+ syncMidiCc: true,
3970
+ syncMidiCcBinding: true,
3971
+ syncMidiChannelPressure: true,
3972
+ syncMidiClips: true,
3973
+ syncMidiDestinationExternal: true,
3974
+ syncMidiFx: true,
3975
+ syncMidiInputChannelPressure: true,
3976
+ syncMidiInputCc: true,
3977
+ syncMidiInputNoteOff: true,
3978
+ syncMidiInputNoteOn: true,
3979
+ syncMidiInputPitchBend: true,
3980
+ syncMidiInputPolyPressure: true,
3981
+ syncMidiInputSource: true,
3982
+ syncMidiNoteOff: true,
3983
+ syncMidiNoteOn: true,
3984
+ syncMidiPanic: true,
3985
+ syncMidiPitchBend: true,
3986
+ syncMidiPolyPressure: true,
3987
+ syncMidiSysex: true,
3988
+ syncMidiUmp: true,
3989
+ syncMixer: true,
3990
+ syncParameters: true,
3991
+ syncSf2Instrument: true,
3992
+ syncSynthInstrument: true,
3993
+ syncTempo: true,
3994
+ syncTrackStripChannelDelaySamples: true,
3995
+ syncTrackStripDualPan: true,
3996
+ syncTrackStripEqBand: true,
3997
+ syncTrackStripInsertBypassed: true,
3998
+ syncTrackStripInsertParamByName: true,
3999
+ syncTrackStripPan: true,
4000
+ syncTrackStripPanLaw: true,
4001
+ syncTrackStripPanMode: true,
4002
+ syncWarpVoiceCapacity: true
4003
+ };
4004
+ var engineSyncMessageTypes = new Set(Object.keys(ENGINE_SYNC_MESSAGE_TYPES));
4005
+ function isEngineSyncMessage(value) {
4006
+ return isRecord(value) && typeof value.type === "string" && engineSyncMessageTypes.has(value.type);
4007
+ }
4008
+ function isEngineCaptureRequestMessage(value) {
4009
+ return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
4010
+ }
4011
+ function isCaptureStatus(value) {
4012
+ 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);
4013
+ }
4014
+ function isCaptureChannel(value) {
4015
+ return value instanceof Float32Array && typeof ArrayBuffer !== "undefined" && value.buffer instanceof ArrayBuffer;
4016
+ }
4017
+ function isCaptureChannels(value) {
4018
+ return Array.isArray(value) && value.every((channel) => isCaptureChannel(channel));
4019
+ }
4020
+ function hasOwn(value, key) {
4021
+ return Object.prototype.hasOwnProperty.call(value, key);
4022
+ }
4023
+ function engineCaptureResponseRequestId(value) {
4024
+ if (!isRecord(value) || value.type !== "captureResponse" || typeof value.requestId !== "number" || !Number.isSafeInteger(value.requestId)) {
4025
+ return void 0;
4026
+ }
4027
+ return value.requestId;
4028
+ }
4029
+ function isEngineCaptureResponseMessage(value) {
4030
+ const requestId = engineCaptureResponseRequestId(value);
4031
+ if (requestId === void 0 || !isRecord(value)) {
4032
+ return false;
4033
+ }
4034
+ if (value.ok === false) {
4035
+ return typeof value.error === "string" && !hasOwn(value, "status") && !hasOwn(value, "channels");
4036
+ }
4037
+ if (value.ok !== true || hasOwn(value, "error")) {
4038
+ return false;
4039
+ }
4040
+ const hasStatus = hasOwn(value, "status");
4041
+ const hasChannels = hasOwn(value, "channels");
4042
+ if (hasStatus && hasChannels) {
4043
+ return false;
4044
+ }
4045
+ if (hasStatus) {
4046
+ return isCaptureStatus(value.status);
4047
+ }
4048
+ if (hasChannels) {
4049
+ return isCaptureChannels(value.channels);
4050
+ }
4051
+ return true;
4052
+ }
4053
+ function isEngineCaptureResponseForOperation(response, op) {
4054
+ if (!response.ok) {
4055
+ return true;
4056
+ }
4057
+ switch (op) {
4058
+ case "status":
4059
+ return "status" in response;
4060
+ case "read":
4061
+ return "channels" in response;
4062
+ case "reset":
4063
+ return !("status" in response) && !("channels" in response);
4064
+ }
4065
+ }
4066
+ function isEngineTransportRequestMessage(value) {
4067
+ return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
4068
+ }
4069
+ function isEngineTransportResponseMessage(value) {
4070
+ return isRecord(value) && value.type === "transportResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
4071
+ }
4072
+ function isRealtimeVoiceChangerMessage(value) {
4073
+ if (!isRecord(value) || typeof value.type !== "string") {
4074
+ return false;
4075
+ }
4076
+ return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
4077
+ }
4078
+ var isOptionalNumber = (value) => value === void 0 || typeof value === "number";
4079
+ function isPlaybackMessage(value) {
4080
+ if (!isRecord(value) || typeof value.type !== "string") {
4081
+ return false;
4082
+ }
4083
+ switch (value.type) {
4084
+ case "config":
4085
+ return typeof value.config === "string";
4086
+ case "orientation":
4087
+ return typeof value.yaw === "number" && isOptionalNumber(value.pitch) && isOptionalNumber(value.roll);
4088
+ case "reset":
4089
+ case "diagnostics":
4090
+ case "destroy":
4091
+ return true;
4092
+ default:
4093
+ return false;
4094
+ }
4095
+ }
4096
+ function isEngineTelemetryRecord(value) {
4097
+ 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";
4098
+ }
4099
+ function isExternalMidiBatchMessage(value) {
4100
+ return isRecord(value) && value.type === "externalMidi" && Array.isArray(value.events);
4101
+ }
4102
+ function isClipPageRequestMessage(value) {
4103
+ return isRecord(value) && value.type === "clipPageRequest" && Array.isArray(value.requests) && value.requests.every(
4104
+ (request) => isRecord(request) && typeof request.clipId === "number" && typeof request.pageIndex === "number"
4105
+ );
4106
+ }
4107
+ function isMeterSnapshot(value) {
4108
+ 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" && Number.isFinite(value.inputPeakDbL) && Number.isFinite(value.inputPeakDbR) && (typeof value.targetId === "number" || value.targetId === void 0);
4109
+ }
4110
+ var SONARE_SCOPE_MAX_BANDS = 64;
4111
+ var SONARE_SCOPE_MAX_POINTS = 32;
4112
+ function isPlainFloat32Array(value, maxLength, evenLength = false) {
4113
+ if (!(value instanceof Float32Array) || typeof ArrayBuffer === "undefined" || !(value.buffer instanceof ArrayBuffer) || value.length > maxLength || evenLength && value.length % 2 !== 0) {
4114
+ return false;
4115
+ }
4116
+ for (const sample of value) {
4117
+ if (!Number.isFinite(sample)) {
4118
+ return false;
4119
+ }
4120
+ }
4121
+ return true;
4122
+ }
4123
+ function isScopeSnapshot(value) {
4124
+ return isRecord(value) && value.type === "scope" && typeof value.targetId === "number" && Number.isSafeInteger(value.targetId) && value.targetId >= 0 && value.targetId <= 4294967295 && typeof value.frame === "number" && Number.isSafeInteger(value.frame) && value.frame >= 0 && isPlainFloat32Array(value.bands, SONARE_SCOPE_MAX_BANDS) && value.bands.length > 0 && isPlainFloat32Array(value.points, SONARE_SCOPE_MAX_POINTS * 2, true);
4125
+ }
4126
+ function requireIntegerOption(value, fallback, name, minimum) {
4127
+ const resolved = value ?? fallback;
4128
+ if (!Number.isSafeInteger(resolved) || resolved < minimum) {
4129
+ throw new RangeError(`${name} must be an integer of at least ${minimum}`);
4130
+ }
4131
+ return resolved;
4132
+ }
4133
+ function resolveScopeBandCount(value, fallback = SONARE_SCOPE_DEFAULT_BANDS) {
4134
+ return Math.min(requireIntegerOption(value, fallback, "scopeBands", 1), SONARE_SCOPE_MAX_BANDS);
4135
+ }
4136
+ function requireInteger(value, fallback, name) {
4137
+ const resolved = value ?? fallback;
4138
+ if (!Number.isSafeInteger(resolved)) {
4139
+ throw new RangeError(`${name} must be an integer`);
4140
+ }
4141
+ return resolved;
4142
+ }
4143
+ function requireChannelCount(channelCount, fallback) {
4144
+ return requireIntegerOption(channelCount, fallback, "channelCount", 1);
4145
+ }
4146
+
4147
+ // src/worklet/engine-offline.ts
4148
+ function buildCaptureConfig(options, defaultChannels) {
4149
+ return {
4150
+ // bufferFrames has no default; NaN makes the guard refuse an absent value.
4151
+ bufferFrames: requireIntegerOption(options.bufferFrames, Number.NaN, "bufferFrames", 1),
4152
+ channels: requireChannelCount(options.channels, defaultChannels),
4153
+ source: options.source ?? "output",
4154
+ recordOffsetSamples: requireInteger(options.recordOffsetSamples, 0, "recordOffsetSamples"),
4155
+ inputMonitor: {
4156
+ enabled: Boolean(options.inputMonitor?.enabled),
4157
+ gain: options.inputMonitor?.gain ?? 1
4158
+ }
4159
+ };
4160
+ }
4161
+ function buildTransportFacade(ctx) {
4162
+ return {
4163
+ play: (sampleTime = -1) => {
4164
+ const ok = ctx.realtimeNode.play(sampleTime);
4165
+ if (ok) {
4166
+ ctx.setTransportPlaying(true);
4167
+ }
4168
+ return ok;
4169
+ },
4170
+ stop: (sampleTime = -1) => {
4171
+ const ok = ctx.realtimeNode.stop(sampleTime);
4172
+ if (ok) {
4173
+ ctx.setTransportPlaying(false);
4174
+ ctx.flushPendingInstrumentSync();
4175
+ }
4176
+ return ok;
4177
+ },
4178
+ seekPpq: (ppq, sampleTime = -1) => {
4179
+ ctx.offlineEngine.seekPpq(ppq, sampleTime);
4180
+ const ok = ctx.realtimeNode.seekPpq(ppq, sampleTime);
4181
+ ctx.flushOfflineMirror();
4182
+ return ok;
4183
+ },
4184
+ seekSeconds: (seconds, sampleTime = -1) => {
4185
+ const timelineSample = Math.max(0, Math.round(seconds * ctx.sampleRate));
4186
+ ctx.offlineEngine.seekSample(timelineSample, sampleTime);
4187
+ const ok = ctx.realtimeNode.seekSample(timelineSample, sampleTime);
4188
+ ctx.flushOfflineMirror();
4189
+ return ok;
4190
+ },
4191
+ setTempo: (bpm) => ctx.setTempo(bpm),
4192
+ setTempoSegments: (segments) => ctx.setTempoSegments(segments),
4193
+ setLoop: (startPpq, endPpq, enabled = true) => ctx.setLoop(startPpq, endPpq, enabled)
4194
+ };
4195
+ }
4196
+ function normalizeTrackLanes(existing, lanes) {
4197
+ const entries = lanes.map((lane) => typeof lane === "number" ? { trackId: lane } : lane);
4198
+ const ids = [];
4199
+ for (const entry of entries) {
4200
+ if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
4201
+ throw new RangeError(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
4202
+ }
4203
+ ids.push(entry.trackId);
4204
+ }
4205
+ if (new Set(ids).size !== ids.length) {
4206
+ throw new Error("Duplicate track id in mixer lane list");
4207
+ }
4208
+ for (let index = 0; index < existing.length; index++) {
4209
+ if (ids[index] !== existing[index]) {
4210
+ throw new Error(
4211
+ "Mixer lanes are append-only: keep existing lanes in order and only append new track ids"
4212
+ );
4213
+ }
4214
+ }
4215
+ return { entries, ids };
4216
+ }
4217
+ function resolveMarkerSet(markers, nextMarkerId) {
4218
+ const resolved = [];
4219
+ const seen = /* @__PURE__ */ new Set();
4220
+ let counter = nextMarkerId;
4221
+ for (const marker2 of markers) {
4222
+ if (!Number.isFinite(marker2.ppq) || marker2.ppq < 0) {
4223
+ throw new RangeError(`Invalid marker ppq: ${String(marker2.ppq)}`);
4224
+ }
4225
+ if (marker2.id !== void 0) {
4226
+ if (!Number.isInteger(marker2.id) || marker2.id <= 0) {
4227
+ throw new RangeError(`Invalid marker id: ${String(marker2.id)}`);
4228
+ }
4229
+ if (seen.has(marker2.id)) {
4230
+ throw new Error(`Duplicate marker id: ${marker2.id}`);
4231
+ }
4232
+ }
4233
+ const id = marker2.id ?? counter++;
4234
+ seen.add(id);
4235
+ if (id >= counter) {
4236
+ counter = id + 1;
4237
+ }
4238
+ resolved.push({ id, ppq: marker2.ppq, name: marker2.name ?? "" });
4239
+ }
4240
+ return { resolved, nextMarkerId: counter };
4241
+ }
4242
+
4243
+ // src/worklet/engine-capture-facade.ts
4244
+ function configureCapture(ctx, options) {
4245
+ const config = buildCaptureConfig(options, ctx.offlineChannelCount);
4246
+ ctx.offlineEngine.setCaptureBuffer(config.channels, config.bufferFrames);
4247
+ ctx.offlineEngine.setCaptureSource(config.source);
4248
+ ctx.offlineEngine.setRecordOffsetSamples(config.recordOffsetSamples);
4249
+ ctx.offlineEngine.setInputMonitor(config.inputMonitor.enabled, config.inputMonitor.gain);
4250
+ ctx.setCaptureConfig(config);
4251
+ ctx.postSync({ type: "syncCapture", ...config });
4252
+ }
4253
+ function armRecord(ctx, trackId, enabled) {
4254
+ if (trackId !== 0) {
4255
+ throw new RangeError("Capture is global; armRecord only accepts trackId 0");
4256
+ }
4257
+ if (enabled && !ctx.getCaptureConfig()) {
4258
+ throw new Error("Capture buffer is not configured");
4259
+ }
4260
+ ctx.offlineEngine.armCapture(enabled);
4261
+ return ctx.sendCommand({
4262
+ type: 13 /* ArmRecord */,
4263
+ targetId: 0,
2999
4264
  sampleTime: -1,
3000
4265
  argInt: enabled ? 1 : 0
3001
4266
  });
@@ -3167,16 +4432,348 @@ function syncMarkers(ctx) {
3167
4432
  }
3168
4433
 
3169
4434
  // src/worklet/engine-mixer-facade.ts
4435
+ function isSceneObject(value) {
4436
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4437
+ }
4438
+ function hasOwn2(object, key) {
4439
+ return Object.getOwnPropertyDescriptor(object, key) !== void 0;
4440
+ }
4441
+ function canonicalJson(value) {
4442
+ if (Array.isArray(value)) {
4443
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
4444
+ }
4445
+ if (value !== null && typeof value === "object") {
4446
+ const object = value;
4447
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
4448
+ }
4449
+ return JSON.stringify(value);
4450
+ }
4451
+ function aliasedValue(object, primary, legacy) {
4452
+ if (hasOwn2(object, primary)) {
4453
+ return { present: true, value: object[primary] };
4454
+ }
4455
+ if (hasOwn2(object, legacy)) {
4456
+ return { present: true, value: object[legacy] };
4457
+ }
4458
+ return { present: false, value: void 0 };
4459
+ }
4460
+ function insertSignature(insert) {
4461
+ const object = insert !== null && typeof insert === "object" ? insert : {};
4462
+ const slot = object.slot === void 0 ? "pre" : object.slot;
4463
+ const processor = aliasedValue(object, "processor", "processor_name");
4464
+ const params = aliasedValue(object, "params", "params_json");
4465
+ const sidechain = aliasedValue(object, "sidechainKey", "sidechain_key");
4466
+ const processorValue = typeof processor.value === "string" ? processor.value : "";
4467
+ const sidechainValue = typeof sidechain.value === "string" ? sidechain.value : "";
4468
+ let paramsValue;
4469
+ if (!params.present) {
4470
+ paramsValue = ["absent"];
4471
+ } else if (typeof params.value === "string") {
4472
+ if (params.value.length === 0) {
4473
+ paramsValue = ["absent"];
4474
+ } else {
4475
+ try {
4476
+ paramsValue = ["params", canonicalJson(JSON.parse(params.value))];
4477
+ } catch {
4478
+ paramsValue = ["params-invalid", params.value];
4479
+ }
4480
+ }
4481
+ } else {
4482
+ paramsValue = ["params", canonicalJson(params.value)];
4483
+ }
4484
+ return JSON.stringify([slot, processorValue, paramsValue, sidechainValue]);
4485
+ }
4486
+ function decodedInsertSignature(sceneJson, target) {
4487
+ const scene = JSON.parse(sceneJson);
4488
+ const entries = target.kind === "bus" ? scene.buses : scene.strips;
4489
+ const strip = Array.isArray(entries) ? entries.find(isSceneObject) : void 0;
4490
+ const object = isSceneObject(strip) ? strip : {};
4491
+ const inserts = Array.isArray(object.inserts) ? object.inserts.filter(isSceneObject) : [];
4492
+ const ordered = target.kind === "bus" ? inserts : [
4493
+ ...inserts.filter((insert) => insert.slot !== "post"),
4494
+ ...inserts.filter((insert) => insert.slot === "post")
4495
+ ];
4496
+ return JSON.stringify(ordered.map(insertSignature));
4497
+ }
4498
+ function constructionValue(engine, target, override) {
4499
+ let parameterId = -1;
4500
+ switch (target.kind) {
4501
+ case "track":
4502
+ parameterId = engine.resolveTrackInsertAutomationId(
4503
+ target.trackId,
4504
+ override.insertIndex,
4505
+ override.paramName
4506
+ );
4507
+ break;
4508
+ case "bus":
4509
+ parameterId = engine.resolveBusInsertAutomationId(
4510
+ target.busId,
4511
+ override.insertIndex,
4512
+ override.paramName
4513
+ );
4514
+ break;
4515
+ case "master":
4516
+ parameterId = engine.resolveMasterInsertAutomationId(
4517
+ override.insertIndex,
4518
+ override.paramName
4519
+ );
4520
+ break;
4521
+ }
4522
+ if (parameterId < 0) {
4523
+ return void 0;
4524
+ }
4525
+ return engine.insertParameterConstructedValue(parameterId);
4526
+ }
4527
+ function constructionInsertOverrides(engine, target, overrides) {
4528
+ const restored = [];
4529
+ for (const override of overrides.values()) {
4530
+ if (!sameStripTarget(override.target, target)) {
4531
+ continue;
4532
+ }
4533
+ const value = constructionValue(engine, target, override);
4534
+ if (value !== void 0) {
4535
+ restored.push({ ...override, value });
4536
+ }
4537
+ }
4538
+ return restored;
4539
+ }
4540
+ function applyInsertParamOverride(engine, override) {
4541
+ switch (override.target.kind) {
4542
+ case "track":
4543
+ engine.restoreTrackStripInsertParamByName(
4544
+ override.target.trackId,
4545
+ override.insertIndex,
4546
+ override.paramName,
4547
+ override.value
4548
+ );
4549
+ break;
4550
+ case "bus":
4551
+ engine.restoreBusStripInsertParamByName(
4552
+ override.target.busId,
4553
+ override.insertIndex,
4554
+ override.paramName,
4555
+ override.value
4556
+ );
4557
+ break;
4558
+ case "master":
4559
+ engine.restoreMasterStripInsertParamByName(
4560
+ override.insertIndex,
4561
+ override.paramName,
4562
+ override.value
4563
+ );
4564
+ break;
4565
+ }
4566
+ }
4567
+ function applyConstructionInsertOverrides(engine, overrides) {
4568
+ for (const override of overrides) {
4569
+ applyInsertParamOverride(engine, override);
4570
+ }
4571
+ }
4572
+ function clearInsertParameterBases(engine, target) {
4573
+ switch (target.kind) {
4574
+ case "track":
4575
+ engine.clearTrackInsertParameterBases(target.trackId);
4576
+ break;
4577
+ case "bus":
4578
+ engine.clearBusInsertParameterBases(target.busId);
4579
+ break;
4580
+ case "master":
4581
+ engine.clearMasterInsertParameterBases();
4582
+ break;
4583
+ }
4584
+ }
4585
+ function emptyStripJson(target) {
4586
+ switch (target.kind) {
4587
+ case "track":
4588
+ return `{"version":1,"strips":[{"id":"track-${target.trackId}"}],"buses":[],"connections":[]}`;
4589
+ case "bus":
4590
+ return `{"version":1,"strips":[],"buses":[{"id":"bus-${target.busId}"}],"connections":[]}`;
4591
+ case "master":
4592
+ return '{"version":1,"strips":[{"id":"master"}],"buses":[],"connections":[]}';
4593
+ }
4594
+ }
4595
+ function pruneStripSidechains(ctx, target, sceneJson) {
4596
+ const scene = JSON.parse(sceneJson);
4597
+ const entry = target.kind === "bus" ? scene.buses?.[0] : scene.strips?.[0];
4598
+ const insertCount = Array.isArray(entry?.inserts) ? entry.inserts.length : 0;
4599
+ if (target.kind === "track") {
4600
+ for (const [key, binding] of ctx.laneSidechains) {
4601
+ if (binding.trackId === target.trackId && binding.insertIndex >= insertCount) {
4602
+ ctx.laneSidechains.delete(key);
4603
+ }
4604
+ }
4605
+ } else if (target.kind === "bus") {
4606
+ for (const [key, binding] of ctx.busSidechains) {
4607
+ if (binding.busId === target.busId && binding.insertIndex >= insertCount) {
4608
+ ctx.busSidechains.delete(key);
4609
+ }
4610
+ }
4611
+ } else {
4612
+ for (const [key, binding] of ctx.masterSidechains) {
4613
+ if (binding.insertIndex >= insertCount) {
4614
+ ctx.masterSidechains.delete(key);
4615
+ }
4616
+ }
4617
+ }
4618
+ }
4619
+ function hasInsertParamOverrides(overrides, target) {
4620
+ for (const override of overrides.values()) {
4621
+ if (sameStripTarget(override.target, target)) {
4622
+ return true;
4623
+ }
4624
+ }
4625
+ return false;
4626
+ }
4627
+ function applyFullStripJson(engine, target, sceneJson) {
4628
+ const apply = (json) => {
4629
+ switch (target.kind) {
4630
+ case "track":
4631
+ engine.setTrackStripJson(target.trackId, json);
4632
+ break;
4633
+ case "bus":
4634
+ engine.setBusStripJson(target.busId, json);
4635
+ break;
4636
+ case "master":
4637
+ engine.setMasterStripJson(json);
4638
+ break;
4639
+ }
4640
+ };
4641
+ engine.applyCommandsDueNowPreservingFuture();
4642
+ apply(sceneJson);
4643
+ }
4644
+ function insertParamOverrideKey(override) {
4645
+ const targetId = override.target.kind === "track" ? override.target.trackId : override.target.kind === "bus" ? override.target.busId : "master";
4646
+ return JSON.stringify([override.target.kind, targetId, override.insertIndex, override.paramName]);
4647
+ }
4648
+ function clearInsertParamOverrides(overrides, target) {
4649
+ for (const [key, override] of overrides) {
4650
+ if (sameStripTarget(override.target, target)) {
4651
+ overrides.delete(key);
4652
+ }
4653
+ }
4654
+ }
4655
+ function sameStripTarget(left, right) {
4656
+ if (left.kind !== right.kind) {
4657
+ return false;
4658
+ }
4659
+ if (left.kind === "track" && right.kind === "track") {
4660
+ return left.trackId === right.trackId;
4661
+ }
4662
+ if (left.kind === "bus" && right.kind === "bus") {
4663
+ return left.busId === right.busId;
4664
+ }
4665
+ return left.kind === "master" && right.kind === "master";
4666
+ }
4667
+ function flattenInsertParamOverride(override) {
4668
+ switch (override.target.kind) {
4669
+ case "track":
4670
+ return {
4671
+ kind: "track",
4672
+ trackId: override.target.trackId,
4673
+ insertIndex: override.insertIndex,
4674
+ paramName: override.paramName,
4675
+ value: override.value
4676
+ };
4677
+ case "bus":
4678
+ return {
4679
+ kind: "bus",
4680
+ busId: override.target.busId,
4681
+ insertIndex: override.insertIndex,
4682
+ paramName: override.paramName,
4683
+ value: override.value
4684
+ };
4685
+ case "master":
4686
+ return {
4687
+ kind: "master",
4688
+ insertIndex: override.insertIndex,
4689
+ paramName: override.paramName,
4690
+ value: override.value
4691
+ };
4692
+ }
4693
+ }
4694
+ function cachedStripJson(ctx, target) {
4695
+ switch (target.kind) {
4696
+ case "track":
4697
+ return ctx.trackStripJson.get(target.trackId);
4698
+ case "bus":
4699
+ return ctx.busStripJson.get(target.busId);
4700
+ case "master":
4701
+ return ctx.getMasterStripJson();
4702
+ }
4703
+ }
4704
+ function cacheStripJson(ctx, target, sceneJson) {
4705
+ switch (target.kind) {
4706
+ case "track":
4707
+ ctx.trackStripJson.set(target.trackId, sceneJson);
4708
+ return;
4709
+ case "bus":
4710
+ ctx.busStripJson.set(target.busId, sceneJson);
4711
+ return;
4712
+ case "master":
4713
+ ctx.cacheMasterStripJson(sceneJson);
4714
+ return;
4715
+ }
4716
+ }
3170
4717
  function mixerLanes(ctx) {
3171
- return buildMixerLanes(ctx.trackLaneIds, ctx.trackSends, ctx.trackOutputBus);
4718
+ return buildMixerLanes(
4719
+ ctx.trackLaneIds,
4720
+ ctx.trackSends,
4721
+ ctx.trackOutputBus,
4722
+ ctx.trackSourceChannelLayout
4723
+ );
4724
+ }
4725
+ function cloneMixerRouting(ctx) {
4726
+ return {
4727
+ trackLaneIds: [...ctx.trackLaneIds],
4728
+ trackSends: new Map(
4729
+ Array.from(ctx.trackSends, ([trackId, sends]) => [
4730
+ trackId,
4731
+ sends.map((send) => ({ ...send }))
4732
+ ])
4733
+ ),
4734
+ trackOutputBus: new Map(ctx.trackOutputBus),
4735
+ trackSourceChannelLayout: new Map(ctx.trackSourceChannelLayout)
4736
+ };
4737
+ }
4738
+ function commitMixerRouting(ctx, draft) {
4739
+ ctx.trackLaneIds.splice(0, ctx.trackLaneIds.length, ...draft.trackLaneIds);
4740
+ ctx.trackSends.clear();
4741
+ for (const [trackId, sends] of draft.trackSends) {
4742
+ ctx.trackSends.set(
4743
+ trackId,
4744
+ sends.map((send) => ({ ...send }))
4745
+ );
4746
+ }
4747
+ ctx.trackOutputBus.clear();
4748
+ for (const [trackId, busId] of draft.trackOutputBus) {
4749
+ ctx.trackOutputBus.set(trackId, busId);
4750
+ }
4751
+ ctx.trackSourceChannelLayout.clear();
4752
+ for (const [trackId, layout] of draft.trackSourceChannelLayout) {
4753
+ ctx.trackSourceChannelLayout.set(trackId, layout);
4754
+ }
4755
+ }
4756
+ function buildDraftMixerLanes(draft) {
4757
+ return buildMixerLanes(
4758
+ draft.trackLaneIds,
4759
+ draft.trackSends,
4760
+ draft.trackOutputBus,
4761
+ draft.trackSourceChannelLayout
4762
+ );
3172
4763
  }
3173
- function syncMixer(ctx) {
3174
- const lanes = mixerLanes(ctx);
4764
+ function applyMixerRouting(ctx, draft, busesAlreadyApplied) {
4765
+ const lanes = buildDraftMixerLanes(draft);
3175
4766
  const buses = ctx.buses.map((bus) => ({ ...bus }));
3176
- ctx.offlineEngine.setTrackBuses(buses);
4767
+ if (!busesAlreadyApplied) {
4768
+ ctx.offlineEngine.setTrackBuses(buses);
4769
+ }
3177
4770
  if (lanes.length > 0) {
3178
4771
  ctx.offlineEngine.setTrackLanes(lanes);
3179
4772
  }
4773
+ return { lanes, buses };
4774
+ }
4775
+ function postMixerSync(ctx, lanes, buses, insertBaseResets, oneShotInsertParamOverrides) {
4776
+ replayInsertParamOverrides(ctx, lanes, buses);
3180
4777
  const trackStrips = Array.from(ctx.trackStripJson, ([trackId, sceneJson]) => ({
3181
4778
  trackId,
3182
4779
  sceneJson
@@ -3185,6 +4782,10 @@ function syncMixer(ctx) {
3185
4782
  busId,
3186
4783
  sceneJson
3187
4784
  }));
4785
+ const insertParamOverrides = [
4786
+ ...Array.from(ctx.insertParamOverrides.values(), flattenInsertParamOverride),
4787
+ ...oneShotInsertParamOverrides.map(flattenInsertParamOverride)
4788
+ ];
3188
4789
  ctx.postSync({
3189
4790
  type: "syncMixer",
3190
4791
  lanes,
@@ -3192,38 +4793,117 @@ function syncMixer(ctx) {
3192
4793
  trackStrips,
3193
4794
  laneSidechains: Array.from(ctx.laneSidechains.values()),
3194
4795
  busStrips,
3195
- masterStripJson: ctx.getMasterStripJson()
4796
+ masterStripJson: ctx.getMasterStripJson(),
4797
+ busSidechains: Array.from(ctx.busSidechains.values()),
4798
+ masterSidechains: Array.from(ctx.masterSidechains.values()),
4799
+ ...insertParamOverrides.length > 0 ? { insertParamOverrides } : {},
4800
+ ...insertBaseResets.length > 0 ? { insertBaseResets } : {}
3196
4801
  });
3197
4802
  }
4803
+ function syncMixerDraft(ctx, draft, busesAlreadyApplied = false, insertBaseResets = [], oneShotInsertParamOverrides = []) {
4804
+ const { lanes, buses } = applyMixerRouting(ctx, draft, busesAlreadyApplied);
4805
+ commitMixerRouting(ctx, draft);
4806
+ postMixerSync(ctx, lanes, buses, insertBaseResets, oneShotInsertParamOverrides);
4807
+ }
4808
+ function syncMixer(ctx, busesAlreadyApplied = false, insertBaseResets = [], oneShotInsertParamOverrides = []) {
4809
+ syncMixerDraft(
4810
+ ctx,
4811
+ cloneMixerRouting(ctx),
4812
+ busesAlreadyApplied,
4813
+ insertBaseResets,
4814
+ oneShotInsertParamOverrides
4815
+ );
4816
+ }
4817
+ function replayInsertParamOverrides(ctx, lanes = mixerLanes(ctx), buses = ctx.buses) {
4818
+ const activeTrackIds = new Set(lanes.map((lane) => lane.trackId));
4819
+ const activeBusIds = new Set(buses.map((bus) => bus.busId));
4820
+ for (const [key, override] of ctx.insertParamOverrides) {
4821
+ const active = override.target.kind === "track" ? activeTrackIds.has(override.target.trackId) : override.target.kind === "bus" ? activeBusIds.has(override.target.busId) : true;
4822
+ if (!active) {
4823
+ ctx.insertParamOverrides.delete(key);
4824
+ continue;
4825
+ }
4826
+ switch (override.target.kind) {
4827
+ case "track":
4828
+ ctx.offlineEngine.restoreTrackStripInsertParamByName(
4829
+ override.target.trackId,
4830
+ override.insertIndex,
4831
+ override.paramName,
4832
+ override.value
4833
+ );
4834
+ break;
4835
+ case "bus":
4836
+ ctx.offlineEngine.restoreBusStripInsertParamByName(
4837
+ override.target.busId,
4838
+ override.insertIndex,
4839
+ override.paramName,
4840
+ override.value
4841
+ );
4842
+ break;
4843
+ case "master":
4844
+ ctx.offlineEngine.restoreMasterStripInsertParamByName(
4845
+ override.insertIndex,
4846
+ override.paramName,
4847
+ override.value
4848
+ );
4849
+ break;
4850
+ }
4851
+ }
4852
+ }
3198
4853
  function setTrackLanes(ctx, lanes) {
3199
- const { entries, ids } = normalizeTrackLanes(ctx.trackLaneIds, lanes);
4854
+ const draft = cloneMixerRouting(ctx);
4855
+ const { entries, ids } = normalizeTrackLanes(draft.trackLaneIds, lanes);
3200
4856
  for (const entry of entries) {
3201
4857
  if (entry.sends) {
3202
- ctx.trackSends.set(
4858
+ draft.trackSends.set(
3203
4859
  entry.trackId,
3204
4860
  entry.sends.map((send) => ({ ...send }))
3205
4861
  );
3206
4862
  }
3207
4863
  if (entry.outputBusId !== void 0) {
3208
4864
  if (entry.outputBusId === 0) {
3209
- ctx.trackOutputBus.delete(entry.trackId);
4865
+ draft.trackOutputBus.delete(entry.trackId);
3210
4866
  } else {
3211
- ctx.trackOutputBus.set(entry.trackId, entry.outputBusId);
4867
+ draft.trackOutputBus.set(entry.trackId, entry.outputBusId);
3212
4868
  }
3213
4869
  }
4870
+ if (entry.sourceChannelLayout !== void 0) {
4871
+ draft.trackSourceChannelLayout.set(entry.trackId, entry.sourceChannelLayout);
4872
+ }
3214
4873
  }
3215
- ctx.trackLaneIds.splice(0, ctx.trackLaneIds.length, ...ids);
3216
- ctx.syncMixer();
4874
+ draft.trackLaneIds = ids;
4875
+ syncMixerDraft(ctx, draft, true);
4876
+ }
4877
+ function ensureTrackLaneInDraft(draft, target) {
4878
+ const trackId = resolveTargetId(target);
4879
+ if (!Number.isInteger(trackId) || trackId <= 0) {
4880
+ throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
4881
+ }
4882
+ const existing = draft.trackLaneIds.indexOf(trackId);
4883
+ if (existing >= 0) {
4884
+ return existing;
4885
+ }
4886
+ draft.trackLaneIds.push(trackId);
4887
+ return draft.trackLaneIds.length - 1;
4888
+ }
4889
+ function ensureTrackLane(ctx, target) {
4890
+ const draft = cloneMixerRouting(ctx);
4891
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
4892
+ if (draft.trackLaneIds.length !== ctx.trackLaneIds.length) {
4893
+ syncMixerDraft(ctx, draft, true);
4894
+ }
4895
+ return laneIndex;
3217
4896
  }
3218
4897
  function setTrackOutputBus(ctx, target, busId) {
3219
- const laneIndex = ctx.ensureTrackLane(target);
3220
- const trackId = ctx.trackLaneIds[laneIndex];
4898
+ const draft = cloneMixerRouting(ctx);
4899
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
4900
+ const trackId = draft.trackLaneIds[laneIndex];
3221
4901
  if (busId === 0) {
3222
- ctx.trackOutputBus.delete(trackId);
4902
+ draft.trackOutputBus.delete(trackId);
3223
4903
  } else {
3224
- ctx.trackOutputBus.set(trackId, busId);
4904
+ draft.trackOutputBus.set(trackId, busId);
3225
4905
  }
3226
- ctx.syncMixer();
4906
+ syncMixerDraft(ctx, draft, true);
3227
4907
  }
3228
4908
  function setLaneSidechain(ctx, target, insertIndex, sourceTarget) {
3229
4909
  const laneIndex = ctx.ensureTrackLane(target);
@@ -3234,143 +4914,144 @@ function setLaneSidechain(ctx, target, insertIndex, sourceTarget) {
3234
4914
  const sourceIndex = ctx.ensureTrackLane(sourceTarget);
3235
4915
  sourceTrackId = ctx.trackLaneIds[sourceIndex];
3236
4916
  }
4917
+ ctx.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
3237
4918
  if (sourceTrackId === 0) {
3238
4919
  ctx.laneSidechains.delete(key);
3239
4920
  } else {
3240
4921
  ctx.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
3241
4922
  }
3242
- ctx.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
3243
4923
  ctx.postSync({
3244
4924
  type: "syncMixer",
4925
+ sidechainDelta: true,
3245
4926
  lanes: ctx.mixerLanes(),
3246
4927
  laneSidechains: [{ trackId, insertIndex, sourceTrackId }]
3247
4928
  });
3248
4929
  }
3249
- function setSends(ctx, target, sends) {
3250
- const laneIndex = ctx.ensureTrackLane(target);
3251
- const trackId = ctx.trackLaneIds[laneIndex];
3252
- ctx.trackSends.set(
3253
- trackId,
3254
- sends.map((send) => ({ ...send }))
3255
- );
3256
- ctx.syncMixer();
3257
- }
3258
- function setTrackBuses(ctx, buses) {
3259
- ctx.buses.splice(0, ctx.buses.length, ...buses.map((bus) => ({ ...bus })));
3260
- ctx.syncMixer();
3261
- }
3262
- function setBusGain(ctx, busId, db) {
3263
- const busIndex = ctx.ensureBus(busId);
3264
- ctx.buses[busIndex] = { ...ctx.buses[busIndex], busId, gainDb: db };
3265
- ctx.offlineEngine.setTrackBuses(ctx.buses);
3266
- return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
3267
- }
3268
- function setBusStripJson(ctx, busId, sceneJson) {
4930
+ function setBusSidechain(ctx, busId, insertIndex, kind, sourceId) {
4931
+ const sourceKind = sidechainSourceKindCode(kind);
3269
4932
  ctx.ensureBus(busId);
3270
- ctx.offlineEngine.setBusStripJson(busId, sceneJson);
3271
- ctx.busStripJson.set(busId, sceneJson);
3272
- ctx.syncMixer();
3273
- }
3274
-
3275
- // src/worklet/guards.ts
3276
- function isWorkletMessage(value) {
3277
- if (!isRecord(value) || typeof value.type !== "string") {
3278
- return false;
3279
- }
3280
- return value.type === "scheduleInsertAutomation" || value.type === "setMeterInterval" || value.type === "destroy";
3281
- }
3282
- function isEngineCommandRecord(value) {
3283
- return isRecord(value) && typeof value.type === "number";
3284
- }
3285
- function isEngineSyncMessage(value) {
3286
- if (!isRecord(value) || typeof value.type !== "string") {
3287
- return false;
3288
- }
3289
- 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";
3290
- }
3291
- function isEngineCaptureRequestMessage(value) {
3292
- return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
3293
- }
3294
- function isCaptureStatus(value) {
3295
- 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);
3296
- }
3297
- function isCaptureChannel(value) {
3298
- return value instanceof Float32Array && typeof ArrayBuffer !== "undefined" && value.buffer instanceof ArrayBuffer;
3299
- }
3300
- function isCaptureChannels(value) {
3301
- return Array.isArray(value) && value.every((channel) => isCaptureChannel(channel));
3302
- }
3303
- function hasOwn(value, key) {
3304
- return Object.prototype.hasOwnProperty.call(value, key);
3305
- }
3306
- function engineCaptureResponseRequestId(value) {
3307
- if (!isRecord(value) || value.type !== "captureResponse" || typeof value.requestId !== "number" || !Number.isSafeInteger(value.requestId)) {
3308
- return void 0;
4933
+ ensureSidechainSource(ctx, sourceKind, sourceId);
4934
+ ctx.offlineEngine.setBusSidechain(busId, insertIndex, sourceKind, sourceId);
4935
+ const key = `${busId}:${insertIndex}`;
4936
+ if (sourceId === 0) {
4937
+ ctx.busSidechains.delete(key);
4938
+ } else {
4939
+ ctx.busSidechains.set(key, { busId, insertIndex, sourceKind, sourceId });
3309
4940
  }
3310
- return value.requestId;
4941
+ ctx.postSync({
4942
+ type: "syncMixer",
4943
+ sidechainDelta: true,
4944
+ lanes: ctx.mixerLanes(),
4945
+ busSidechains: [{ busId, insertIndex, sourceKind, sourceId }]
4946
+ });
3311
4947
  }
3312
- function isEngineCaptureResponseMessage(value) {
3313
- const requestId = engineCaptureResponseRequestId(value);
3314
- if (requestId === void 0 || !isRecord(value)) {
3315
- return false;
3316
- }
3317
- if (value.ok === false) {
3318
- return typeof value.error === "string" && !hasOwn(value, "status") && !hasOwn(value, "channels");
3319
- }
3320
- if (value.ok !== true || hasOwn(value, "error")) {
3321
- return false;
3322
- }
3323
- const hasStatus = hasOwn(value, "status");
3324
- const hasChannels = hasOwn(value, "channels");
3325
- if (hasStatus && hasChannels) {
3326
- return false;
3327
- }
3328
- if (hasStatus) {
3329
- return isCaptureStatus(value.status);
3330
- }
3331
- if (hasChannels) {
3332
- return isCaptureChannels(value.channels);
4948
+ function setMasterSidechain(ctx, insertIndex, kind, sourceId) {
4949
+ const sourceKind = sidechainSourceKindCode(kind);
4950
+ ensureSidechainSource(ctx, sourceKind, sourceId);
4951
+ ctx.offlineEngine.setMasterSidechain(insertIndex, sourceKind, sourceId);
4952
+ if (sourceId === 0) {
4953
+ ctx.masterSidechains.delete(insertIndex);
4954
+ } else {
4955
+ ctx.masterSidechains.set(insertIndex, { insertIndex, sourceKind, sourceId });
3333
4956
  }
3334
- return true;
4957
+ ctx.postSync({
4958
+ type: "syncMixer",
4959
+ sidechainDelta: true,
4960
+ lanes: ctx.mixerLanes(),
4961
+ masterSidechains: [{ insertIndex, sourceKind, sourceId }]
4962
+ });
3335
4963
  }
3336
- function isEngineCaptureResponseForOperation(response, op) {
3337
- if (!response.ok) {
3338
- return true;
4964
+ function ensureSidechainSource(ctx, sourceKind, sourceId) {
4965
+ if (sourceId === 0) {
4966
+ return;
3339
4967
  }
3340
- switch (op) {
3341
- case "status":
3342
- return "status" in response;
3343
- case "read":
3344
- return "channels" in response;
3345
- case "reset":
3346
- return !("status" in response) && !("channels" in response);
4968
+ if (sourceKind === 1) {
4969
+ ctx.ensureBus(sourceId);
4970
+ } else {
4971
+ ctx.ensureTrackLane(sourceId);
3347
4972
  }
3348
4973
  }
3349
- function isEngineTransportRequestMessage(value) {
3350
- return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
3351
- }
3352
- function isEngineTransportResponseMessage(value) {
3353
- return isRecord(value) && value.type === "transportResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
4974
+ function setSends(ctx, target, sends) {
4975
+ const draft = cloneMixerRouting(ctx);
4976
+ const laneIndex = ensureTrackLaneInDraft(draft, target);
4977
+ const trackId = draft.trackLaneIds[laneIndex];
4978
+ draft.trackSends.set(
4979
+ trackId,
4980
+ sends.map((send) => ({ ...send }))
4981
+ );
4982
+ syncMixerDraft(ctx, draft, true);
3354
4983
  }
3355
- function isRealtimeVoiceChangerMessage(value) {
3356
- if (!isRecord(value) || typeof value.type !== "string") {
3357
- return false;
4984
+ function setTrackBuses(ctx, buses) {
4985
+ ctx.offlineEngine.setTrackBuses(buses);
4986
+ const retainedBusIds = new Set(buses.map((bus) => bus.busId));
4987
+ for (const bus of ctx.buses) {
4988
+ if (!retainedBusIds.has(bus.busId)) {
4989
+ ctx.clearInsertAutomationLanes({ kind: "bus", busId: bus.busId }, true);
4990
+ }
3358
4991
  }
3359
- return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
4992
+ ctx.buses.splice(0, ctx.buses.length, ...buses.map((bus) => ({ ...bus })));
4993
+ const activeBusIds = new Set(ctx.buses.map((bus) => bus.busId));
4994
+ for (const busId of ctx.busStripJson.keys()) {
4995
+ if (!activeBusIds.has(busId)) {
4996
+ ctx.busStripJson.delete(busId);
4997
+ }
4998
+ }
4999
+ for (const [key, binding] of ctx.busSidechains) {
5000
+ if (!activeBusIds.has(binding.busId) || binding.sourceKind === 1 && !activeBusIds.has(binding.sourceId)) {
5001
+ ctx.busSidechains.delete(key);
5002
+ }
5003
+ }
5004
+ for (const [key, binding] of ctx.masterSidechains) {
5005
+ if (binding.sourceKind === 1 && !activeBusIds.has(binding.sourceId)) {
5006
+ ctx.masterSidechains.delete(key);
5007
+ }
5008
+ }
5009
+ syncMixer(ctx, true);
3360
5010
  }
3361
- function isEngineTelemetryRecord(value) {
3362
- 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";
5011
+ function setBusGain(ctx, busId, db) {
5012
+ const busIndex = ctx.ensureBus(busId);
5013
+ ctx.buses[busIndex] = { ...ctx.buses[busIndex], busId, gainDb: db };
5014
+ return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
3363
5015
  }
3364
- function isExternalMidiBatchMessage(value) {
3365
- return isRecord(value) && value.type === "externalMidi" && Array.isArray(value.events);
5016
+ function replaceStripScene(ctx, target, previousJson, sceneJson) {
5017
+ const chainChanged = previousJson !== void 0 && decodedInsertSignature(previousJson, target) !== decodedInsertSignature(sceneJson, target);
5018
+ const hasOverrides = hasInsertParamOverrides(ctx.insertParamOverrides, target);
5019
+ applyFullStripJson(ctx.offlineEngine, target, sceneJson);
5020
+ if (!chainChanged && !hasOverrides) {
5021
+ return { insertBaseResets: [], constructionOverrides: [] };
5022
+ }
5023
+ const constructionOverrides = chainChanged ? [] : constructionInsertOverrides(ctx.offlineEngine, target, ctx.insertParamOverrides);
5024
+ ctx.clearInsertAutomationLanes(target);
5025
+ clearInsertParameterBases(ctx.offlineEngine, target);
5026
+ applyConstructionInsertOverrides(ctx.offlineEngine, constructionOverrides);
5027
+ clearInsertParamOverrides(ctx.insertParamOverrides, target);
5028
+ return { insertBaseResets: [target], constructionOverrides };
3366
5029
  }
3367
- function isClipPageRequestMessage(value) {
3368
- return isRecord(value) && value.type === "clipPageRequest" && Array.isArray(value.requests) && value.requests.every(
3369
- (request) => isRecord(request) && typeof request.clipId === "number" && typeof request.pageIndex === "number"
5030
+ function setBusStripJson(ctx, busId, sceneJson) {
5031
+ const target = { kind: "bus", busId };
5032
+ decodedInsertSignature(sceneJson, target);
5033
+ ctx.ensureBus(busId);
5034
+ const { insertBaseResets, constructionOverrides } = replaceStripScene(
5035
+ ctx,
5036
+ target,
5037
+ ctx.busStripJson.get(busId),
5038
+ sceneJson
3370
5039
  );
3371
- }
3372
- function isMeterSnapshot(value) {
3373
- 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);
5040
+ pruneStripSidechains(ctx, target, sceneJson);
5041
+ ctx.busStripJson.set(busId, sceneJson);
5042
+ ctx.syncMixer(insertBaseResets, constructionOverrides);
5043
+ }
5044
+ function setMasterStripJson(ctx, sceneJson) {
5045
+ const target = { kind: "master" };
5046
+ const { insertBaseResets, constructionOverrides } = replaceStripScene(
5047
+ ctx,
5048
+ target,
5049
+ ctx.getMasterStripJson(),
5050
+ sceneJson
5051
+ );
5052
+ pruneStripSidechains(ctx, target, sceneJson);
5053
+ ctx.cacheMasterStripJson(sceneJson);
5054
+ ctx.syncMixer(insertBaseResets, constructionOverrides);
3374
5055
  }
3375
5056
 
3376
5057
  // src/worklet/engine-node.ts
@@ -3387,15 +5068,17 @@ function isTrackMonitorMode(value) {
3387
5068
  const mode = typeof value === "bigint" ? Number(value) : value;
3388
5069
  return typeof mode === "number" && Number.isSafeInteger(mode) && mode >= 0 && mode <= 2;
3389
5070
  }
3390
- function isTrackMonitorLaneIndex(value) {
3391
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 4294967295;
3392
- }
3393
5071
  function isValidCommandRecord(command) {
3394
5072
  const type = Number(command.type);
3395
- if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */ && type !== 26 /* SetTrackMonitorMode */) {
5073
+ if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */ && type !== 26 /* SetTrackMonitorMode */ && type !== 28 /* ResetMasterLoudnessMeter */) {
3396
5074
  return false;
3397
5075
  }
3398
- 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));
5076
+ return (
5077
+ // The record writer refuses a targetId outside the slot by throwing, and
5078
+ // this path answers false instead, so it must reject the same domain here.
5079
+ (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.
5080
+ (type !== 26 /* SetTrackMonitorMode */ || isUint32Slot(command.targetId) && isTrackMonitorMode(command.argInt))
5081
+ );
3399
5082
  }
3400
5083
  var AUDIO_WORKLET_RENDER_QUANTUM = 128;
3401
5084
  function workletBlockSize(blockSize) {
@@ -3440,6 +5123,9 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3440
5123
  this.resolveReady();
3441
5124
  }
3442
5125
  this.node.port.onmessage = (event) => {
5126
+ if (this.destroyed) {
5127
+ return;
5128
+ }
3443
5129
  const captureRequestId = engineCaptureResponseRequestId(event.data);
3444
5130
  if (captureRequestId !== void 0) {
3445
5131
  const pending = this.captureRequests.get(captureRequestId);
@@ -3469,6 +5155,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3469
5155
  this.emitTelemetry(event.data);
3470
5156
  } else if (isMeterSnapshot(event.data)) {
3471
5157
  this.emitMeter(event.data);
5158
+ } else if (isScopeSnapshot(event.data)) {
5159
+ this.emitScope(event.data);
3472
5160
  } else if (isExternalMidiBatchMessage(event.data)) {
3473
5161
  this.emitMidiOut(event.data.events);
3474
5162
  } else if (isClipPageRequestMessage(event.data)) {
@@ -3521,19 +5209,71 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3521
5209
  "SharedArrayBuffer mode requested but SharedArrayBuffer/Atomics are unavailable."
3522
5210
  );
3523
5211
  }
3524
- const commandRing = mode === "sab" ? createSonareEngineCommandRingBuffer(options.commandRingCapacity ?? 128) : void 0;
3525
- const telemetryRing = mode === "sab" ? createSonareEngineTelemetryRingBuffer(options.telemetryRingCapacity ?? 128) : void 0;
3526
- const meterRing = mode === "sab" ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128) : void 0;
3527
- const scopeIntervalFrames = Math.max(0, Math.floor(options.scopeIntervalFrames ?? 0));
3528
- const scopeRing = mode === "sab" && scopeIntervalFrames > 0 ? createSonareScopeRingBuffer(options.scopeRingCapacity ?? 64, options.scopeBands ?? 48) : void 0;
3529
- const clipPageRequestRing = mode === "sab" ? createSonareClipPageRequestRingBuffer(options.clipPageRequestRingCapacity ?? 128) : void 0;
3530
- const externalMidiRing = mode === "sab" ? createSonareExternalMidiRingBuffer(options.externalMidiRingCapacity ?? 256) : void 0;
3531
- const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
5212
+ const meterIntervalFrames = requireIntegerOption(
5213
+ options.meterIntervalFrames,
5214
+ 2048,
5215
+ "meterIntervalFrames",
5216
+ 0
5217
+ );
5218
+ const commandRingCapacity = requireIntegerOption(
5219
+ options.commandRingCapacity,
5220
+ 128,
5221
+ "commandRingCapacity",
5222
+ 1
5223
+ );
5224
+ if (commandRingCapacity > 65536) {
5225
+ throw new RangeError("commandRingCapacity must be at most 65536");
5226
+ }
5227
+ const telemetryRingCapacity = requireIntegerOption(
5228
+ options.telemetryRingCapacity,
5229
+ 128,
5230
+ "telemetryRingCapacity",
5231
+ 1
5232
+ );
5233
+ const meterRingCapacity = requireIntegerOption(
5234
+ options.meterRingCapacity,
5235
+ 128,
5236
+ "meterRingCapacity",
5237
+ 1
5238
+ );
5239
+ const scopeRingCapacity = requireIntegerOption(
5240
+ options.scopeRingCapacity,
5241
+ 64,
5242
+ "scopeRingCapacity",
5243
+ 1
5244
+ );
5245
+ const scopeBands = resolveScopeBandCount(options.scopeBands);
5246
+ const clipPageRequestRingCapacity = requireIntegerOption(
5247
+ options.clipPageRequestRingCapacity,
5248
+ 128,
5249
+ "clipPageRequestRingCapacity",
5250
+ 1
5251
+ );
5252
+ const externalMidiRingCapacity = requireIntegerOption(
5253
+ options.externalMidiRingCapacity,
5254
+ 256,
5255
+ "externalMidiRingCapacity",
5256
+ 1
5257
+ );
5258
+ const commandRing = mode === "sab" ? createSonareEngineCommandRingBuffer(commandRingCapacity) : void 0;
5259
+ const telemetryRing = mode === "sab" ? createSonareEngineTelemetryRingBuffer(telemetryRingCapacity) : void 0;
5260
+ const meterRing = mode === "sab" ? createSonareMeterRingBuffer(meterRingCapacity) : void 0;
5261
+ const scopeIntervalFrames = requireIntegerOption(
5262
+ options.scopeIntervalFrames,
5263
+ 0,
5264
+ "scopeIntervalFrames",
5265
+ 0
5266
+ );
5267
+ const scopeRing = mode === "sab" && scopeIntervalFrames > 0 ? createSonareScopeRingBuffer(scopeRingCapacity, scopeBands) : void 0;
5268
+ const clipPageRequestRing = mode === "sab" ? createSonareClipPageRequestRingBuffer(clipPageRequestRingCapacity) : void 0;
5269
+ const externalMidiRing = mode === "sab" ? createSonareExternalMidiRingBuffer(externalMidiRingCapacity) : void 0;
5270
+ const channelCount = requireChannelCount(options.channelCount, 2);
3532
5271
  const cueOutput = options.cueOutput === true;
3533
5272
  const processorOptions = {
3534
5273
  sampleRate: options.sampleRate ?? context.sampleRate,
3535
5274
  blockSize,
3536
5275
  channelCount,
5276
+ meterIntervalFrames,
3537
5277
  cueOutput,
3538
5278
  commandSharedBuffer: commandRing?.sharedBuffer,
3539
5279
  commandRingCapacity: commandRing?.capacity,
@@ -3543,8 +5283,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3543
5283
  meterRingCapacity: meterRing?.capacity,
3544
5284
  scopeSharedBuffer: scopeRing?.sharedBuffer,
3545
5285
  scopeRingCapacity: scopeRing?.capacity,
3546
- scopeBands: scopeRing?.bands,
3547
- scopeIntervalFrames: scopeRing ? scopeIntervalFrames : void 0,
5286
+ scopeBands: scopeIntervalFrames > 0 ? scopeRing?.bands ?? scopeBands : void 0,
5287
+ scopeIntervalFrames: scopeIntervalFrames > 0 ? scopeIntervalFrames : void 0,
3548
5288
  clipPageRequestSharedBuffer: clipPageRequestRing?.sharedBuffer,
3549
5289
  clipPageRequestRingCapacity: clipPageRequestRing?.capacity,
3550
5290
  externalMidiSharedBuffer: externalMidiRing?.sharedBuffer,
@@ -3593,6 +5333,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3593
5333
  play(sampleTime = -1) {
3594
5334
  return this.sendCommand({ type: 2 /* TransportPlay */, sampleTime });
3595
5335
  }
5336
+ /** Queues a reset of the master's integrated-loudness accumulator. */
5337
+ resetMasterLoudnessMeter(sampleTime = -1) {
5338
+ return this.sendCommand({ type: 28 /* ResetMasterLoudnessMeter */, sampleTime });
5339
+ }
3596
5340
  stop(sampleTime = -1) {
3597
5341
  return this.sendCommand({ type: 3 /* TransportStop */, sampleTime });
3598
5342
  }
@@ -3805,6 +5549,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3805
5549
  this.scopeListeners.clear();
3806
5550
  this.midiOutListeners.clear();
3807
5551
  this.clipPageRequestListeners.clear();
5552
+ this.syncErrorListeners.clear();
5553
+ this.node.port.onmessage = null;
3808
5554
  }
3809
5555
  emitTelemetry(telemetry) {
3810
5556
  for (const listener of this.telemetryListeners) {
@@ -3944,6 +5690,9 @@ function resolveBusInsertAutomationId(ctx, busId, insertIndex, paramName) {
3944
5690
  ctx.ensureBus(busId);
3945
5691
  return ctx.offlineEngine.resolveBusInsertAutomationId(busId, insertIndex, paramName);
3946
5692
  }
5693
+ function resolveInstrumentAutomationId(ctx, destinationId, paramName) {
5694
+ return ctx.offlineEngine.resolveInstrumentAutomationId(destinationId, paramName);
5695
+ }
3947
5696
  function automationLaneCount(ctx) {
3948
5697
  return ctx.offlineEngine.automationLaneCount();
3949
5698
  }
@@ -3968,14 +5717,39 @@ function clearParameters(ctx) {
3968
5717
  function trackIdFor(ctx, target) {
3969
5718
  return ctx.trackLaneIds[ctx.ensureTrackLane(target)];
3970
5719
  }
5720
+ function mergeStripJson(ctx, target, update) {
5721
+ const scene = JSON.parse(ctx.readStripJson(target) ?? emptyStripJson(target));
5722
+ update(target.kind === "bus" ? scene.buses[0] : scene.strips[0]);
5723
+ ctx.writeStripJson(target, JSON.stringify(scene));
5724
+ }
5725
+ function mergeEqBand(ctx, target, bandIndex, bandJson) {
5726
+ mergeStripJson(ctx, target, (entry) => {
5727
+ const eq = entry.eq ?? { enabled: true, bands: [] };
5728
+ const bands = eq.bands ?? [];
5729
+ while (bands.length < bandIndex) {
5730
+ bands.push({});
5731
+ }
5732
+ bands[bandIndex] = JSON.parse(bandJson);
5733
+ entry.eq = { ...eq, bands };
5734
+ });
5735
+ }
5736
+ function setInsertParamByName(ctx, target, insertIndex, paramName, value, applyNative, message) {
5737
+ applyNative();
5738
+ const override = { target, insertIndex, paramName, value };
5739
+ ctx.insertParamOverrides.set(insertParamOverrideKey(override), override);
5740
+ ctx.postSync(message);
5741
+ }
3971
5742
  function setTrackStripJson(ctx, trackId, sceneJson, trackStripJson) {
3972
- ctx.offlineEngine.setTrackStripJson(trackId, sceneJson);
5743
+ const target = { kind: "track", trackId };
5744
+ const result = replaceStripScene(ctx, target, trackStripJson.get(trackId), sceneJson);
3973
5745
  trackStripJson.set(trackId, sceneJson);
5746
+ return result;
3974
5747
  }
3975
5748
  function setTrackStripEqBand(ctx, target, bandIndex, band) {
3976
5749
  const trackId = trackIdFor(ctx, target);
3977
5750
  const bandJson = typeof band === "string" ? band : JSON.stringify(band);
3978
5751
  ctx.offlineEngine.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
5752
+ mergeEqBand(ctx, { kind: "track", trackId }, bandIndex, bandJson);
3979
5753
  ctx.postSync({ type: "syncTrackStripEqBand", trackId, bandIndex, bandJson });
3980
5754
  }
3981
5755
  function setTrackStripInsertBypassed(ctx, target, insertIndex, bypassed, resetOnBypass) {
@@ -3991,39 +5765,68 @@ function setTrackStripInsertBypassed(ctx, target, insertIndex, bypassed, resetOn
3991
5765
  }
3992
5766
  function setTrackStripInsertParamByName(ctx, target, insertIndex, paramName, value) {
3993
5767
  const trackId = trackIdFor(ctx, target);
3994
- ctx.offlineEngine.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
3995
- ctx.postSync({ type: "syncTrackStripInsertParamByName", trackId, insertIndex, paramName, value });
5768
+ setInsertParamByName(
5769
+ ctx,
5770
+ { kind: "track", trackId },
5771
+ insertIndex,
5772
+ paramName,
5773
+ value,
5774
+ () => ctx.offlineEngine.restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value),
5775
+ { type: "syncTrackStripInsertParamByName", trackId, insertIndex, paramName, value }
5776
+ );
3996
5777
  }
3997
5778
  function setTrackStripPan(ctx, target, pan) {
3998
5779
  const trackId = trackIdFor(ctx, target);
3999
5780
  ctx.offlineEngine.setTrackStripPan(trackId, pan);
5781
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5782
+ entry.pan = pan;
5783
+ });
4000
5784
  ctx.postSync({ type: "syncTrackStripPan", trackId, pan });
4001
5785
  }
4002
5786
  function setTrackStripPanLaw(ctx, target, panLaw) {
4003
5787
  const trackId = trackIdFor(ctx, target);
4004
5788
  const code = panLawCode(panLaw);
4005
5789
  ctx.offlineEngine.setTrackStripPanLaw(trackId, code);
5790
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5791
+ entry.panLaw = code;
5792
+ });
4006
5793
  ctx.postSync({ type: "syncTrackStripPanLaw", trackId, panLaw: code });
4007
5794
  }
4008
5795
  function setTrackStripPanMode(ctx, target, panMode) {
4009
5796
  const trackId = trackIdFor(ctx, target);
4010
5797
  const code = panModeCode(panMode);
4011
5798
  ctx.offlineEngine.setTrackStripPanMode(trackId, code);
5799
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5800
+ entry.panMode = code;
5801
+ });
4012
5802
  ctx.postSync({ type: "syncTrackStripPanMode", trackId, panMode: code });
4013
5803
  }
4014
5804
  function setTrackStripDualPan(ctx, target, leftPan, rightPan) {
4015
5805
  const trackId = trackIdFor(ctx, target);
4016
5806
  ctx.offlineEngine.setTrackStripDualPan(trackId, leftPan, rightPan);
5807
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5808
+ entry.dualPanLeft = leftPan;
5809
+ entry.dualPanRight = rightPan;
5810
+ });
4017
5811
  ctx.postSync({ type: "syncTrackStripDualPan", trackId, leftPan, rightPan });
4018
5812
  }
4019
5813
  function setTrackStripChannelDelaySamples(ctx, target, delaySamples) {
4020
5814
  const trackId = trackIdFor(ctx, target);
4021
5815
  ctx.offlineEngine.setTrackStripChannelDelaySamples(trackId, delaySamples);
5816
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5817
+ entry.channelDelaySamples = delaySamples;
5818
+ });
4022
5819
  ctx.postSync({ type: "syncTrackStripChannelDelaySamples", trackId, delaySamples });
4023
5820
  }
5821
+ function cacheMasterStripScalar(ctx, field, value) {
5822
+ mergeStripJson(ctx, { kind: "master" }, (entry) => {
5823
+ entry[field] = value;
5824
+ });
5825
+ }
4024
5826
  function setMasterStripEqBand(ctx, bandIndex, band) {
4025
5827
  const bandJson = typeof band === "string" ? band : JSON.stringify(band);
4026
5828
  ctx.offlineEngine.setMasterStripEqBandJson(bandIndex, bandJson);
5829
+ mergeEqBand(ctx, { kind: "master" }, bandIndex, bandJson);
4027
5830
  ctx.postSync({ type: "syncMasterStripEqBand", bandIndex, bandJson });
4028
5831
  }
4029
5832
  function setMasterStripInsertBypassed(ctx, insertIndex, bypassed, resetOnBypass) {
@@ -4031,12 +5834,67 @@ function setMasterStripInsertBypassed(ctx, insertIndex, bypassed, resetOnBypass)
4031
5834
  ctx.postSync({ type: "syncMasterStripInsertBypassed", insertIndex, bypassed, resetOnBypass });
4032
5835
  }
4033
5836
  function setMasterStripInsertParamByName(ctx, insertIndex, paramName, value) {
4034
- ctx.offlineEngine.setMasterStripInsertParamByName(insertIndex, paramName, value);
4035
- ctx.postSync({ type: "syncMasterStripInsertParamByName", insertIndex, paramName, value });
5837
+ setInsertParamByName(
5838
+ ctx,
5839
+ { kind: "master" },
5840
+ insertIndex,
5841
+ paramName,
5842
+ value,
5843
+ () => ctx.offlineEngine.restoreMasterStripInsertParamByName(insertIndex, paramName, value),
5844
+ { type: "syncMasterStripInsertParamByName", insertIndex, paramName, value }
5845
+ );
4036
5846
  }
4037
5847
  function setBusStripInsertParamByName(ctx, busId, insertIndex, paramName, value) {
4038
- ctx.offlineEngine.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
4039
- ctx.postSync({ type: "syncBusStripInsertParamByName", busId, insertIndex, paramName, value });
5848
+ setInsertParamByName(
5849
+ ctx,
5850
+ { kind: "bus", busId },
5851
+ insertIndex,
5852
+ paramName,
5853
+ value,
5854
+ () => ctx.offlineEngine.restoreBusStripInsertParamByName(busId, insertIndex, paramName, value),
5855
+ { type: "syncBusStripInsertParamByName", busId, insertIndex, paramName, value }
5856
+ );
5857
+ }
5858
+ function setBusStripInsertBypassed(ctx, busId, insertIndex, bypassed, resetOnBypass) {
5859
+ ctx.offlineEngine.setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass);
5860
+ ctx.postSync({ type: "syncBusStripInsertBypassed", busId, insertIndex, bypassed, resetOnBypass });
5861
+ }
5862
+ function setBusStripEqBand(ctx, busId, bandIndex, band) {
5863
+ const bandJson = typeof band === "string" ? band : JSON.stringify(band);
5864
+ ctx.offlineEngine.setBusStripEqBandJson(busId, bandIndex, bandJson);
5865
+ mergeEqBand(ctx, { kind: "bus", busId }, bandIndex, bandJson);
5866
+ ctx.postSync({ type: "syncBusStripEqBand", busId, bandIndex, bandJson });
5867
+ }
5868
+ function setBusStripPan(ctx, busId, pan) {
5869
+ ctx.offlineEngine.setBusStripPan(busId, pan);
5870
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5871
+ entry.pan = pan;
5872
+ });
5873
+ ctx.postSync({ type: "syncBusStripPan", busId, pan });
5874
+ }
5875
+ function setBusStripPanLaw(ctx, busId, panLaw) {
5876
+ const code = panLawCode(panLaw);
5877
+ ctx.offlineEngine.setBusStripPanLaw(busId, code);
5878
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5879
+ entry.panLaw = code;
5880
+ });
5881
+ ctx.postSync({ type: "syncBusStripPanLaw", busId, panLaw: code });
5882
+ }
5883
+ function setBusStripPanMode(ctx, busId, panMode) {
5884
+ const code = panModeCode(panMode);
5885
+ ctx.offlineEngine.setBusStripPanMode(busId, code);
5886
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5887
+ entry.panMode = code;
5888
+ });
5889
+ ctx.postSync({ type: "syncBusStripPanMode", busId, panMode: code });
5890
+ }
5891
+ function setBusStripDualPan(ctx, busId, leftPan, rightPan) {
5892
+ ctx.offlineEngine.setBusStripDualPan(busId, leftPan, rightPan);
5893
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5894
+ entry.dualPanLeft = leftPan;
5895
+ entry.dualPanRight = rightPan;
5896
+ });
5897
+ ctx.postSync({ type: "syncBusStripDualPan", busId, leftPan, rightPan });
4040
5898
  }
4041
5899
  function pushMidiNoteOn(ctx, trackId, group, channel, note, velocity, renderFrame) {
4042
5900
  const destinationId = ctx.resolveTargetId(trackId);
@@ -4077,9 +5935,55 @@ function pushMidiCc(ctx, trackId, group, channel, controller, value, renderFrame
4077
5935
  renderFrame
4078
5936
  });
4079
5937
  }
5938
+ function pushMidiPitchBend(ctx, trackId, group, channel, bend14, renderFrame) {
5939
+ const destinationId = ctx.resolveTargetId(trackId);
5940
+ ctx.offlineEngine.pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame);
5941
+ ctx.postSync({
5942
+ type: "syncMidiPitchBend",
5943
+ destinationId,
5944
+ group,
5945
+ channel,
5946
+ data0: bend14,
5947
+ data1: 0,
5948
+ renderFrame
5949
+ });
5950
+ }
5951
+ function pushMidiChannelPressure(ctx, trackId, group, channel, pressure, renderFrame) {
5952
+ const destinationId = ctx.resolveTargetId(trackId);
5953
+ ctx.offlineEngine.pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame);
5954
+ ctx.postSync({
5955
+ type: "syncMidiChannelPressure",
5956
+ destinationId,
5957
+ group,
5958
+ channel,
5959
+ data0: pressure,
5960
+ data1: 0,
5961
+ renderFrame
5962
+ });
5963
+ }
5964
+ function pushMidiPolyPressure(ctx, trackId, group, channel, note, pressure, renderFrame) {
5965
+ const destinationId = ctx.resolveTargetId(trackId);
5966
+ ctx.offlineEngine.pushMidiPolyPressure(
5967
+ destinationId,
5968
+ group,
5969
+ channel,
5970
+ note,
5971
+ pressure,
5972
+ renderFrame
5973
+ );
5974
+ ctx.postSync({
5975
+ type: "syncMidiPolyPressure",
5976
+ destinationId,
5977
+ group,
5978
+ channel,
5979
+ data0: note,
5980
+ data1: pressure,
5981
+ renderFrame
5982
+ });
5983
+ }
4080
5984
  function pushMidiUmp(ctx, trackId, word0, renderFrame) {
4081
5985
  const destinationId = ctx.resolveTargetId(trackId);
4082
- ctx.offlineEngine.pushMidiUmp(destinationId, word0, renderFrame);
5986
+ ctx.offlineEngine.pushMidiUmp(destinationId, [word0], renderFrame);
4083
5987
  ctx.postSync({ type: "syncMidiUmp", destinationId, word0, renderFrame });
4084
5988
  }
4085
5989
  function setBuiltinInstrument(ctx, trackId, config) {
@@ -4205,16 +6109,21 @@ function transferableAudioBuffers(channels) {
4205
6109
  var SonareEngine = class _SonareEngine {
4206
6110
  constructor(context, realtimeNode, offlineEngine, sampleRate, offlineBlockSize, offlineChannelCount) {
4207
6111
  this.automationLanes = /* @__PURE__ */ new Map();
6112
+ this.insertAutomationIdsByTarget = /* @__PURE__ */ new Map();
4208
6113
  this.clips = /* @__PURE__ */ new Map();
4209
6114
  this.midiClips = /* @__PURE__ */ new Map();
4210
6115
  this.markers = /* @__PURE__ */ new Map();
4211
6116
  this.trackLaneIds = [];
4212
6117
  this.trackSends = /* @__PURE__ */ new Map();
4213
6118
  this.trackOutputBus = /* @__PURE__ */ new Map();
6119
+ this.trackSourceChannelLayout = /* @__PURE__ */ new Map();
4214
6120
  this.laneSidechains = /* @__PURE__ */ new Map();
6121
+ this.busSidechains = /* @__PURE__ */ new Map();
6122
+ this.masterSidechains = /* @__PURE__ */ new Map();
4215
6123
  this.buses = [];
4216
6124
  this.trackStripJson = /* @__PURE__ */ new Map();
4217
6125
  this.busStripJson = /* @__PURE__ */ new Map();
6126
+ this.insertParamOverrides = /* @__PURE__ */ new Map();
4218
6127
  this.tempoBpm = 120;
4219
6128
  this.timeSignature = { numerator: 4, denominator: 4 };
4220
6129
  this.tempoSegments = [{ startPpq: 0, bpm: 120 }];
@@ -4309,6 +6218,11 @@ var SonareEngine = class _SonareEngine {
4309
6218
  setTimeSignatureSegments(segments) {
4310
6219
  setTimeSignatureSegments(this.tempoContext, segments);
4311
6220
  }
6221
+ /**
6222
+ * A loop wrap, seek or stop sends note-offs plus CC64=0, CC121, CC123 and a
6223
+ * centred pitch bend on every channel played since the last reset, so
6224
+ * controller values set before a loop region are not restored at the wrap.
6225
+ */
4312
6226
  setLoop(startPpq, endPpq, enabled = true) {
4313
6227
  return setLoop(this.tempoContext, startPpq, endPpq, enabled);
4314
6228
  }
@@ -4367,18 +6281,32 @@ var SonareEngine = class _SonareEngine {
4367
6281
  * mixer lane first (like automationParamId) so the offline engine resolves the
4368
6282
  * same strip selector the realtime engine uses.
4369
6283
  *
6284
+ * The returned id uses the track's current positional lane selector. When
6285
+ * setTrackLanes successfully changes lane order or membership, the engine
6286
+ * remaps already queued and published track automation by track id, but it
6287
+ * cannot update a numeric id retained by the caller. Re-resolve every track
6288
+ * insert id after such a topology change before passing it to
6289
+ * setAutomationLane. Use setTrackStripInsertParamByName when the operation
6290
+ * needs a stable track identity. Master and bus insert ids are separate and
6291
+ * are not invalidated by track-lane changes.
6292
+ *
4370
6293
  * @param target Track id (declares a mixer lane on first use).
4371
6294
  * @param insertIndex Index into the strip's combined insert sequence.
4372
6295
  * @param paramName Processor JSON-key parameter name.
4373
6296
  * @returns Reserved insert-automation id, or -1 when strip/insert/key unknown.
4374
6297
  */
4375
6298
  resolveTrackInsertAutomationId(target, insertIndex, paramName) {
4376
- return resolveTrackInsertAutomationId(
6299
+ const id = resolveTrackInsertAutomationId(
4377
6300
  this.parameterContext,
4378
6301
  target,
4379
6302
  insertIndex,
4380
6303
  paramName
4381
6304
  );
6305
+ if (id >= 0) {
6306
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
6307
+ this.rememberInsertAutomationId({ kind: "track", trackId }, id);
6308
+ }
6309
+ return id;
4382
6310
  }
4383
6311
  /**
4384
6312
  * Resolves a master-strip insert parameter to its reserved insert-automation
@@ -4389,7 +6317,15 @@ var SonareEngine = class _SonareEngine {
4389
6317
  * @returns Reserved insert-automation id, or -1 when insert/key unknown.
4390
6318
  */
4391
6319
  resolveMasterInsertAutomationId(insertIndex, paramName) {
4392
- return resolveMasterInsertAutomationId(this.parameterContext, insertIndex, paramName);
6320
+ const id = resolveMasterInsertAutomationId(
6321
+ this.parameterContext,
6322
+ insertIndex,
6323
+ paramName
6324
+ );
6325
+ if (id >= 0) {
6326
+ this.rememberInsertAutomationId({ kind: "master" }, id);
6327
+ }
6328
+ return id;
4393
6329
  }
4394
6330
  /**
4395
6331
  * Resolves a bus-strip insert parameter to its reserved insert-automation id.
@@ -4402,12 +6338,29 @@ var SonareEngine = class _SonareEngine {
4402
6338
  * @returns Reserved insert-automation id, or -1 when bus/insert/key unknown.
4403
6339
  */
4404
6340
  resolveBusInsertAutomationId(busId, insertIndex, paramName) {
4405
- return resolveBusInsertAutomationId(
6341
+ const id = resolveBusInsertAutomationId(
4406
6342
  this.parameterContext,
4407
6343
  busId,
4408
6344
  insertIndex,
4409
6345
  paramName
4410
6346
  );
6347
+ if (id >= 0) {
6348
+ this.rememberInsertAutomationId({ kind: "bus", busId }, id);
6349
+ }
6350
+ return id;
6351
+ }
6352
+ /**
6353
+ * Resolves a hosted instrument's continuous parameter to its reserved
6354
+ * instrument-automation id, so an instrument parameter follows a breakpoint
6355
+ * lane at audio-block precision like a strip insert. Bind the instrument
6356
+ * (`setSynthInstrument`) before resolving.
6357
+ *
6358
+ * @param destinationId MIDI destination the instrument is bound to.
6359
+ * @param paramName Instrument JSON-key parameter name (e.g. `cutoffHz`).
6360
+ * @returns Reserved instrument-automation id, or -1 when destination/key unknown.
6361
+ */
6362
+ resolveInstrumentAutomationId(destinationId, paramName) {
6363
+ return resolveInstrumentAutomationId(this.parameterContext, destinationId, paramName);
4411
6364
  }
4412
6365
  /**
4413
6366
  * Returns the number of automation lanes installed on the engine, including
@@ -4437,19 +6390,36 @@ var SonareEngine = class _SonareEngine {
4437
6390
  return setTrackMonitorMode(this.parameterContext, target, mode, renderFrame);
4438
6391
  }
4439
6392
  setStripGain(target, db) {
4440
- return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
6393
+ const sent = this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
6394
+ if (sent && target === "master") {
6395
+ cacheMasterStripScalar(this.stripContext, "faderDb", db);
6396
+ }
6397
+ return sent;
4441
6398
  }
4442
6399
  setStripPan(target, pan) {
4443
- return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_PAN), pan);
6400
+ const sent = this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_PAN), pan);
6401
+ if (sent && target === "master") {
6402
+ cacheMasterStripScalar(this.stripContext, "pan", pan);
6403
+ }
6404
+ return sent;
6405
+ }
6406
+ /** Resets the master's integrated loudness accumulator in both engine mirrors. */
6407
+ resetMasterLoudnessMeter(renderFrame = -1) {
6408
+ this.offlineEngine.resetMasterLoudnessMeter(renderFrame);
6409
+ return this.sendMirroredCommand({
6410
+ type: 28 /* ResetMasterLoudnessMeter */,
6411
+ sampleTime: renderFrame
6412
+ });
4444
6413
  }
4445
6414
  /**
4446
6415
  * Declares the mixer track lanes in an explicit order.
4447
6416
  *
4448
- * Lane indices are append-only: once a track id occupies a lane, its index
4449
- * stays fixed for the engine's lifetime. The given list must therefore start
4450
- * with the already-declared lane ids in their current order and may only
4451
- * append new track ids after them. Entries carrying `sends` replace that
4452
- * track's send list; entries without `sends` leave existing sends untouched.
6417
+ * A successful call may reorder existing track ids, remove track ids, or add
6418
+ * new ones. Existing queued and published track automation follows the track
6419
+ * id, while numeric track-insert automation ids retained by the caller do
6420
+ * not; re-resolve those ids after every topology change. Entries carrying
6421
+ * `sends` replace that track's send list; entries without `sends` leave
6422
+ * existing sends untouched.
4453
6423
  *
4454
6424
  * @param lanes Track ids or lane descriptors in the desired lane order.
4455
6425
  */
@@ -4477,14 +6447,35 @@ var SonareEngine = class _SonareEngine {
4477
6447
  setTrackBuses(buses) {
4478
6448
  setTrackBuses(this.mixerContext, buses);
4479
6449
  }
6450
+ /**
6451
+ * Keys one insert of a bus strip from a track lane or another bus
6452
+ * (ducking/sidechainRouter inserts). `sourceId` 0 removes the binding.
6453
+ */
6454
+ setBusSidechain(busId, insertIndex, sourceKind, sourceId) {
6455
+ setBusSidechain(this.mixerContext, busId, insertIndex, sourceKind, sourceId);
6456
+ }
6457
+ /**
6458
+ * Keys one insert of the master strip from a track lane or a bus. Same
6459
+ * source rules as {@link setBusSidechain}.
6460
+ */
6461
+ setMasterSidechain(insertIndex, sourceKind, sourceId) {
6462
+ setMasterSidechain(this.mixerContext, insertIndex, sourceKind, sourceId);
6463
+ }
4480
6464
  setBusGain(busId, db) {
4481
6465
  return setBusGain(this.mixerContext, busId, db);
4482
6466
  }
4483
6467
  setTrackStripJson(target, sceneJson) {
6468
+ decodedInsertSignature(sceneJson, { kind: "track", trackId: 0 });
4484
6469
  const laneIndex = this.ensureTrackLane(target);
4485
6470
  const trackId = this.trackLaneIds[laneIndex];
4486
- setTrackStripJson(this.stripContext, trackId, sceneJson, this.trackStripJson);
4487
- this.syncMixer();
6471
+ const { insertBaseResets, constructionOverrides } = setTrackStripJson(
6472
+ this.stripContext,
6473
+ trackId,
6474
+ sceneJson,
6475
+ this.trackStripJson
6476
+ );
6477
+ pruneStripSidechains(this.mixerContext, { kind: "track", trackId }, sceneJson);
6478
+ this.syncMixer(insertBaseResets, constructionOverrides);
4488
6479
  }
4489
6480
  setTrackStripEqBand(target, bandIndex, band) {
4490
6481
  setTrackStripEqBand(this.stripContext, target, bandIndex, band);
@@ -4541,9 +6532,7 @@ var SonareEngine = class _SonareEngine {
4541
6532
  setBusStripJson(this.mixerContext, busId, sceneJson);
4542
6533
  }
4543
6534
  setMasterStripJson(sceneJson) {
4544
- this.offlineEngine.setMasterStripJson(sceneJson);
4545
- this.masterStripJson = sceneJson;
4546
- this.syncMixer();
6535
+ setMasterStripJson(this.mixerContext, sceneJson);
4547
6536
  }
4548
6537
  setMasterStripEqBand(bandIndex, band) {
4549
6538
  setMasterStripEqBand(this.stripContext, bandIndex, band);
@@ -4558,6 +6547,47 @@ var SonareEngine = class _SonareEngine {
4558
6547
  this.ensureBus(busId);
4559
6548
  setBusStripInsertParamByName(this.stripContext, busId, insertIndex, paramName, value);
4560
6549
  }
6550
+ /**
6551
+ * Bus-strip counterpart of {@link setStripInsertBypassed}. A bus is addressed
6552
+ * by its numeric id, which the `target` of the track/master form cannot
6553
+ * express, so it is its own method exactly like
6554
+ * {@link setBusStripInsertParamByName}. Bypassing keeps the insert's internal
6555
+ * state (reverb tail, compressor envelope) unless `resetOnBypass` is set;
6556
+ * re-posting the bus scene JSON, the only workaround before this existed,
6557
+ * rebuilds the chain and loses it.
6558
+ */
6559
+ setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass = false) {
6560
+ this.ensureBus(busId);
6561
+ setBusStripInsertBypassed(
6562
+ this.stripContext,
6563
+ busId,
6564
+ insertIndex,
6565
+ bypassed,
6566
+ resetOnBypass
6567
+ );
6568
+ }
6569
+ /** Bus-strip counterpart of {@link setTrackStripEqBand}. */
6570
+ setBusStripEqBand(busId, bandIndex, band) {
6571
+ this.ensureBus(busId);
6572
+ setBusStripEqBand(this.stripContext, busId, bandIndex, band);
6573
+ }
6574
+ /** Bus-strip counterpart of {@link setTrackStripPan}; refused on a surround bus. */
6575
+ setBusStripPan(busId, pan) {
6576
+ this.ensureBus(busId);
6577
+ setBusStripPan(this.stripContext, busId, pan);
6578
+ }
6579
+ setBusStripPanLaw(busId, panLaw) {
6580
+ this.ensureBus(busId);
6581
+ setBusStripPanLaw(this.stripContext, busId, panLaw);
6582
+ }
6583
+ setBusStripPanMode(busId, panMode) {
6584
+ this.ensureBus(busId);
6585
+ setBusStripPanMode(this.stripContext, busId, panMode);
6586
+ }
6587
+ setBusStripDualPan(busId, leftPan, rightPan) {
6588
+ this.ensureBus(busId);
6589
+ setBusStripDualPan(this.stripContext, busId, leftPan, rightPan);
6590
+ }
4561
6591
  setStripInsertParamByName(target, insertIndex, paramName, value) {
4562
6592
  if (target === "master") {
4563
6593
  this.setMasterStripInsertParamByName(insertIndex, paramName, value);
@@ -4638,6 +6668,49 @@ var SonareEngine = class _SonareEngine {
4638
6668
  }
4639
6669
  return { binding, provider: binding.provider };
4640
6670
  }
6671
+ /**
6672
+ * Sets the clip-page look-ahead window in timeline frames on both the worklet
6673
+ * engine and this thread's offline engine.
6674
+ *
6675
+ * The audio thread reports the pages it is *about to* read that are not
6676
+ * resident yet, so the sliding-window streamer can service them before the
6677
+ * playhead reaches them. Without look-ahead a page miss is only reported
6678
+ * after the read already produced silence, which costs one block of silence
6679
+ * at every page boundary that was not primed.
6680
+ *
6681
+ * Defaults to half a second at the engine's sample rate. `0` disables it.
6682
+ * Safe to call during playback.
6683
+ */
6684
+ setClipPagePrefetchFrames(frames) {
6685
+ if (this.destroyed) {
6686
+ throw new Error("SonareEngine is destroyed.");
6687
+ }
6688
+ if (!Number.isFinite(frames) || frames < 0) {
6689
+ throw new Error("clip page prefetch frames must be a finite value >= 0.");
6690
+ }
6691
+ this.offlineEngine.setClipPagePrefetchFrames(frames);
6692
+ this.postSync({ type: "syncClipPagePrefetchFrames", frames });
6693
+ }
6694
+ /**
6695
+ * Sets the number of concurrent time-stretch voices on both this thread's
6696
+ * offline engine and the live worklet engine. `voices` must be an integer
6697
+ * in `[0, 64]`; a non-integer, negative, or larger value throws and leaves
6698
+ * the capacity unchanged on both engines. Default is 8. Capacity 0 disables
6699
+ * time-stretch, so every warped clip plays resampled instead. A change
6700
+ * applied while the engine is running restarts the splice state of any clip
6701
+ * stretching through a voice at that moment.
6702
+ */
6703
+ setWarpVoiceCapacity(voices) {
6704
+ if (this.destroyed) {
6705
+ throw new Error("SonareEngine is destroyed.");
6706
+ }
6707
+ this.offlineEngine.setWarpVoiceCapacity(voices);
6708
+ this.postSync({ type: "syncWarpVoiceCapacity", voices });
6709
+ }
6710
+ /** Reads the current time-stretch voice capacity (default 8). */
6711
+ warpVoiceCapacity() {
6712
+ return this.offlineEngine.warpVoiceCapacity();
6713
+ }
4641
6714
  addClip(trackId, buffer, startPpq, opts = {}) {
4642
6715
  return addClip(this.clipContext, trackId, buffer, startPpq, opts);
4643
6716
  }
@@ -4662,7 +6735,10 @@ var SonareEngine = class _SonareEngine {
4662
6735
  /**
4663
6736
  * Route a track's MIDI to the external output (drained via {@link onMidiOut})
4664
6737
  * instead of an internal instrument, so the track plays an external device.
4665
- * Pass `external=false` to restore internal-synth playback.
6738
+ * Pass `external=false` to restore internal-synth playback. The change takes
6739
+ * effect at the next processed block; switching a destination's route first
6740
+ * releases its notes and resets its controllers through the old route, and
6741
+ * drops its pending MIDI-FX events.
4666
6742
  */
4667
6743
  setMidiDestinationExternal(trackId, external) {
4668
6744
  setMidiDestinationExternal(this.stripContext, trackId, external);
@@ -4699,6 +6775,30 @@ var SonareEngine = class _SonareEngine {
4699
6775
  pushMidiCc(trackId, group, channel, controller, value, renderFrame = -1) {
4700
6776
  pushMidiCc(this.stripContext, trackId, group, channel, controller, value, renderFrame);
4701
6777
  }
6778
+ pushMidiPitchBend(trackId, group, channel, bend14, renderFrame = -1) {
6779
+ pushMidiPitchBend(this.stripContext, trackId, group, channel, bend14, renderFrame);
6780
+ }
6781
+ pushMidiChannelPressure(trackId, group, channel, pressure, renderFrame = -1) {
6782
+ pushMidiChannelPressure(
6783
+ this.stripContext,
6784
+ trackId,
6785
+ group,
6786
+ channel,
6787
+ pressure,
6788
+ renderFrame
6789
+ );
6790
+ }
6791
+ pushMidiPolyPressure(trackId, group, channel, note, pressure, renderFrame = -1) {
6792
+ pushMidiPolyPressure(
6793
+ this.stripContext,
6794
+ trackId,
6795
+ group,
6796
+ channel,
6797
+ note,
6798
+ pressure,
6799
+ renderFrame
6800
+ );
6801
+ }
4702
6802
  pushMidiUmp(trackId, word0, renderFrame = -1) {
4703
6803
  pushMidiUmp(this.stripContext, trackId, word0, renderFrame);
4704
6804
  }
@@ -4752,6 +6852,39 @@ var SonareEngine = class _SonareEngine {
4752
6852
  portTimeSamples
4753
6853
  });
4754
6854
  }
6855
+ pushMidiInputPitchBend(group, channel, bend14, portTimeSamples = 0) {
6856
+ this.offlineEngine.pushMidiInputPitchBend(group, channel, bend14, portTimeSamples);
6857
+ this.postSync({
6858
+ type: "syncMidiInputPitchBend",
6859
+ group,
6860
+ channel,
6861
+ data0: bend14,
6862
+ data1: 0,
6863
+ portTimeSamples
6864
+ });
6865
+ }
6866
+ pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples = 0) {
6867
+ this.offlineEngine.pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples);
6868
+ this.postSync({
6869
+ type: "syncMidiInputChannelPressure",
6870
+ group,
6871
+ channel,
6872
+ data0: pressure,
6873
+ data1: 0,
6874
+ portTimeSamples
6875
+ });
6876
+ }
6877
+ pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples = 0) {
6878
+ this.offlineEngine.pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples);
6879
+ this.postSync({
6880
+ type: "syncMidiInputPolyPressure",
6881
+ group,
6882
+ channel,
6883
+ data0: note,
6884
+ data1: pressure,
6885
+ portTimeSamples
6886
+ });
6887
+ }
4755
6888
  pushMidiPanic(renderFrame = -1) {
4756
6889
  this.offlineEngine.pushMidiPanic(renderFrame);
4757
6890
  this.postSync({ type: "syncMidiPanic", renderFrame });
@@ -4949,8 +7082,32 @@ var SonareEngine = class _SonareEngine {
4949
7082
  mixerLanes() {
4950
7083
  return mixerLanes(this.mixerContext);
4951
7084
  }
4952
- syncMixer() {
4953
- syncMixer(this.mixerContext);
7085
+ syncMixer(insertBaseResets = [], oneShotInsertParamOverrides = []) {
7086
+ syncMixer(this.mixerContext, false, insertBaseResets, oneShotInsertParamOverrides);
7087
+ }
7088
+ insertAutomationTargetKey(target) {
7089
+ return target.kind === "track" ? `track:${target.trackId}` : target.kind === "bus" ? `bus:${target.busId}` : "master";
7090
+ }
7091
+ rememberInsertAutomationId(target, id) {
7092
+ const key = this.insertAutomationTargetKey(target);
7093
+ const ids = this.insertAutomationIdsByTarget.get(key) ?? /* @__PURE__ */ new Set();
7094
+ ids.add(id);
7095
+ this.insertAutomationIdsByTarget.set(key, ids);
7096
+ }
7097
+ clearInsertAutomationLanes(target, forgetResolvedIds = false) {
7098
+ const key = this.insertAutomationTargetKey(target);
7099
+ const ids = this.insertAutomationIdsByTarget.get(key);
7100
+ if (!ids) {
7101
+ return;
7102
+ }
7103
+ for (const id of ids) {
7104
+ if (this.automationLanes.has(id)) {
7105
+ this.setAutomationLane(id, []);
7106
+ }
7107
+ }
7108
+ if (forgetResolvedIds) {
7109
+ this.insertAutomationIdsByTarget.delete(key);
7110
+ }
4954
7111
  }
4955
7112
  postInstrumentSync(message) {
4956
7113
  if (this.destroyed) {
@@ -4978,21 +7135,16 @@ var SonareEngine = class _SonareEngine {
4978
7135
  if (this.destroyed) {
4979
7136
  return;
4980
7137
  }
4981
- try {
4982
- if (transfer && transfer.length > 0) {
4983
- this.realtimeNode.node.port.postMessage(message, transfer);
4984
- } else {
4985
- this.realtimeNode.node.port.postMessage(message);
4986
- }
4987
- } finally {
4988
- this.flushOfflineMirror();
7138
+ if (transfer && transfer.length > 0) {
7139
+ this.realtimeNode.node.port.postMessage(message, transfer);
7140
+ } else {
7141
+ this.realtimeNode.node.port.postMessage(message);
4989
7142
  }
4990
7143
  }
4991
- // The offline engine is a control-thread mirror. It has no render loop to
4992
- // drain its command ring, so drain it after each control operation. Never
4993
- // call this from the worklet/audio path: it is intentionally control-only.
7144
+ // The control-only mirror applies due commands immediately while preserving
7145
+ // scheduled ones for its next render block.
4994
7146
  flushOfflineMirror() {
4995
- this.offlineEngine.flushControlCommands();
7147
+ this.offlineEngine.applyCommandsDueNowPreservingFuture();
4996
7148
  }
4997
7149
  sendMirroredCommand(command) {
4998
7150
  const accepted = this.realtimeNode.sendCommand(command);
@@ -5009,17 +7161,26 @@ var SonareEngine = class _SonareEngine {
5009
7161
  trackLaneIds: this.trackLaneIds,
5010
7162
  trackSends: this.trackSends,
5011
7163
  trackOutputBus: this.trackOutputBus,
7164
+ trackSourceChannelLayout: this.trackSourceChannelLayout,
5012
7165
  laneSidechains: this.laneSidechains,
7166
+ busSidechains: this.busSidechains,
7167
+ masterSidechains: this.masterSidechains,
5013
7168
  buses: this.buses,
5014
7169
  trackStripJson: this.trackStripJson,
5015
7170
  busStripJson: this.busStripJson,
7171
+ insertParamOverrides: this.insertParamOverrides,
7172
+ flushOfflineMirror: () => this.flushOfflineMirror(),
5016
7173
  postSync: (message) => this.postSync(message),
5017
7174
  ensureTrackLane: (target) => this.ensureTrackLane(target),
5018
7175
  ensureBus: (busId) => this.ensureBus(busId),
5019
7176
  mixerLanes: () => this.mixerLanes(),
5020
- syncMixer: () => this.syncMixer(),
7177
+ syncMixer: (insertBaseResets, oneShotInsertParamOverrides) => this.syncMixer(insertBaseResets, oneShotInsertParamOverrides),
7178
+ clearInsertAutomationLanes: (target, forgetResolvedIds) => this.clearInsertAutomationLanes(target, forgetResolvedIds),
5021
7179
  sendSmoothedParam: (paramId, value) => this.sendSmoothedParam(paramId, value),
5022
- getMasterStripJson: () => this.masterStripJson
7180
+ getMasterStripJson: () => this.masterStripJson,
7181
+ cacheMasterStripJson: (sceneJson) => {
7182
+ this.masterStripJson = sceneJson;
7183
+ }
5023
7184
  };
5024
7185
  }
5025
7186
  // Collaborator surface handed to the strip/pan/EQ/insert/MIDI free functions
@@ -5032,7 +7193,11 @@ var SonareEngine = class _SonareEngine {
5032
7193
  postSync: (message) => this.postSync(message),
5033
7194
  postInstrumentSync: (message) => this.postInstrumentSync(message),
5034
7195
  ensureTrackLane: (target) => this.ensureTrackLane(target),
5035
- resolveTargetId: (target) => this.resolveTargetId(target)
7196
+ resolveTargetId: (target) => this.resolveTargetId(target),
7197
+ readStripJson: (target) => cachedStripJson(this.mixerContext, target),
7198
+ writeStripJson: (target, sceneJson) => cacheStripJson(this.mixerContext, target, sceneJson),
7199
+ insertParamOverrides: this.insertParamOverrides,
7200
+ clearInsertAutomationLanes: (target) => this.clearInsertAutomationLanes(target)
5036
7201
  };
5037
7202
  }
5038
7203
  // Collaborator surface handed to the automation-lane free functions so they
@@ -5172,33 +7337,39 @@ var SonareEngine = class _SonareEngine {
5172
7337
  return resolveTargetId(target);
5173
7338
  }
5174
7339
  ensureTrackLane(target) {
5175
- const trackId = this.resolveTargetId(target);
5176
- if (!Number.isInteger(trackId) || trackId <= 0) {
5177
- throw new Error(`Invalid track id for mixer lane: ${String(target)}`);
5178
- }
5179
- const existing = this.trackLaneIds.indexOf(trackId);
5180
- if (existing >= 0) {
5181
- return existing;
5182
- }
5183
- this.trackLaneIds.push(trackId);
5184
- this.syncMixer();
5185
- return this.trackLaneIds.length - 1;
7340
+ return ensureTrackLane(this.mixerContext, target);
5186
7341
  }
5187
7342
  ensureBus(busId) {
5188
- const resolved = Math.trunc(busId);
5189
- if (!Number.isInteger(resolved) || resolved <= 0) {
5190
- throw new Error(`Invalid bus id for mixer bus: ${String(busId)}`);
7343
+ if (!Number.isInteger(busId) || busId <= 0) {
7344
+ throw new RangeError(`Invalid bus id for mixer bus: ${String(busId)}`);
5191
7345
  }
5192
- const existing = this.buses.findIndex((bus) => bus.busId === resolved);
7346
+ const existing = this.buses.findIndex((bus) => bus.busId === busId);
5193
7347
  if (existing >= 0) {
5194
7348
  return existing;
5195
7349
  }
5196
- this.buses.push({ busId: resolved });
7350
+ this.buses.push({ busId });
5197
7351
  this.syncMixer();
5198
7352
  return this.buses.length - 1;
5199
7353
  }
5200
7354
  };
5201
7355
 
7356
+ // src/worklet/audio_types.ts
7357
+ function copyPlanesToOutput(output, planes, frames) {
7358
+ const monoFanOut = planes.length === 1;
7359
+ for (let ch = 0; ch < output.length; ch++) {
7360
+ const target = output[ch];
7361
+ const source = monoFanOut ? planes[0] : planes[ch];
7362
+ let copied = 0;
7363
+ if (source) {
7364
+ copied = Math.min(target.length, frames, source.length);
7365
+ target.set(source.subarray(0, copied));
7366
+ }
7367
+ if (copied < target.length) {
7368
+ target.fill(0, copied);
7369
+ }
7370
+ }
7371
+ }
7372
+
5202
7373
  // src/worklet/messages.ts
5203
7374
  var DEFAULT_METRONOME_CONFIG = {
5204
7375
  beatGain: 0.35,
@@ -5214,26 +7385,12 @@ function resolveMetronomeConfig(config) {
5214
7385
  clickSeconds: config.clickSeconds ?? DEFAULT_METRONOME_CONFIG.clickSeconds
5215
7386
  };
5216
7387
  if (!Number.isFinite(resolved.beatGain) || resolved.beatGain < 0 || !Number.isFinite(resolved.accentGain) || resolved.accentGain < 0 || !Number.isInteger(resolved.clickSamples) || resolved.clickSamples < 0 || !Number.isFinite(resolved.clickSeconds) || resolved.clickSeconds < 0 || resolved.clickSamples > 384e3 || config.clickSeconds !== void 0 && (!Number.isFinite(config.clickSeconds) || config.clickSeconds < 0 || config.clickSeconds > 1)) {
5217
- throw new RangeError("invalid metronome gains or click length");
5218
- }
5219
- return resolved;
5220
- }
5221
-
5222
- // src/worklet/engine-processor.ts
5223
- function copyPlanesToOutput(output, planes, frames) {
5224
- for (let ch = 0; ch < output.length; ch++) {
5225
- const target = output[ch];
5226
- const source = planes[ch] ?? planes[0];
5227
- if (source) {
5228
- target.set(source.subarray(0, Math.min(target.length, frames)));
5229
- if (target.length > frames) {
5230
- target.fill(0, frames);
5231
- }
5232
- } else {
5233
- target.fill(0);
5234
- }
7388
+ throw new RangeError("invalid metronome gains or click length");
5235
7389
  }
7390
+ return resolved;
5236
7391
  }
7392
+
7393
+ // src/worklet/engine-processor.ts
5237
7394
  function captureTransferList(channels) {
5238
7395
  const transfers = [];
5239
7396
  const seen = /* @__PURE__ */ new Set();
@@ -5273,9 +7430,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5273
7430
  this.clipPageRequestOverflowReported = 0;
5274
7431
  this.sampleRate = options.sampleRate ?? 48e3;
5275
7432
  this.blockSize = options.blockSize ?? 128;
5276
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
7433
+ this.channelCount = requireChannelCount(options.channelCount, 2);
5277
7434
  this.transport = transport;
5278
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
7435
+ this.meterIntervalFrames = requireIntegerOption(
7436
+ options.meterIntervalFrames,
7437
+ 2048,
7438
+ "meterIntervalFrames",
7439
+ 0
7440
+ );
5279
7441
  this.commandRing = options.commandSharedBuffer ? this.commandRingFromSharedBuffer(options.commandSharedBuffer, options.commandRingCapacity) : void 0;
5280
7442
  this.telemetryRing = options.telemetrySharedBuffer ? this.telemetryRingFromSharedBuffer(
5281
7443
  options.telemetrySharedBuffer,
@@ -5287,6 +7449,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5287
7449
  options.scopeRingCapacity,
5288
7450
  options.scopeBands
5289
7451
  ) : void 0;
7452
+ this.scopeIntervalFrames = requireIntegerOption(
7453
+ options.scopeIntervalFrames,
7454
+ this.scopeRing ? this.blockSize : 0,
7455
+ "scopeIntervalFrames",
7456
+ 0
7457
+ );
7458
+ this.scopeBands = resolveScopeBandCount(options.scopeBands);
7459
+ this.scopeEnabled = this.scopeIntervalFrames > 0 && (this.scopeRing !== void 0 || typeof this.transport?.postMessage === "function");
5290
7460
  this.clipPageRequestRing = options.clipPageRequestSharedBuffer ? clipPageRequestRingFromSharedBuffer(
5291
7461
  options.clipPageRequestSharedBuffer,
5292
7462
  options.clipPageRequestRingCapacity
@@ -5295,10 +7465,13 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5295
7465
  options.externalMidiSharedBuffer,
5296
7466
  options.externalMidiRingCapacity
5297
7467
  ) : void 0;
7468
+ if (this.commandRing && this.commandRing.capacity > 65536) {
7469
+ throw new RangeError("commandRingCapacity must be at most 65536");
7470
+ }
5298
7471
  this.engine = new RealtimeEngine(
5299
7472
  this.sampleRate,
5300
7473
  this.blockSize,
5301
- 1024,
7474
+ Math.max(1024, this.commandRing?.capacity ?? 0),
5302
7475
  1024,
5303
7476
  this.channelCount
5304
7477
  );
@@ -5315,9 +7488,11 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5315
7488
  this.monitorBuffers[ch] = this.engine.getMonitorChannelBuffer(ch, this.blockSize);
5316
7489
  }
5317
7490
  }
5318
- if (this.scopeRing) {
5319
- const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
5320
- this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
7491
+ if (this.scopeEnabled) {
7492
+ this.engine.configureScopeTelemetry(
7493
+ this.scopeIntervalFrames,
7494
+ Math.min(this.scopeRing?.bands ?? this.scopeBands, SONARE_SCOPE_MAX_BANDS)
7495
+ );
5321
7496
  }
5322
7497
  }
5323
7498
  process(inputs, outputs) {
@@ -5480,6 +7655,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5480
7655
  }
5481
7656
  break;
5482
7657
  }
7658
+ case "syncClipPagePrefetchFrames": {
7659
+ this.engine.setClipPagePrefetchFrames(message.frames);
7660
+ break;
7661
+ }
7662
+ case "syncWarpVoiceCapacity": {
7663
+ this.engine.setWarpVoiceCapacity(message.voices);
7664
+ break;
7665
+ }
5483
7666
  case "syncClipPageCommit": {
5484
7667
  const providerId = this.pagedClipProviders.get(message.clipId);
5485
7668
  const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
@@ -5538,7 +7721,38 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5538
7721
  );
5539
7722
  }
5540
7723
  break;
5541
- case "syncMixer":
7724
+ case "syncMixer": {
7725
+ if (message.sidechainDelta === true) {
7726
+ for (const binding of message.laneSidechains ?? []) {
7727
+ this.engine.setLaneSidechain(
7728
+ binding.trackId,
7729
+ binding.insertIndex,
7730
+ binding.sourceTrackId
7731
+ );
7732
+ }
7733
+ for (const binding of message.busSidechains ?? []) {
7734
+ this.engine.setBusSidechain(
7735
+ binding.busId,
7736
+ binding.insertIndex,
7737
+ binding.sourceKind,
7738
+ binding.sourceId
7739
+ );
7740
+ }
7741
+ for (const binding of message.masterSidechains ?? []) {
7742
+ this.engine.setMasterSidechain(
7743
+ binding.insertIndex,
7744
+ binding.sourceKind,
7745
+ binding.sourceId
7746
+ );
7747
+ }
7748
+ break;
7749
+ }
7750
+ if (this.commandRing) {
7751
+ const write = Atomics.load(this.commandRing.header, 0);
7752
+ const read = Atomics.load(this.commandRing.header, 1);
7753
+ this.drainCommands(Math.min(this.commandRing.capacity, Math.max(0, write - read)));
7754
+ }
7755
+ this.engine.applyCommandsDueNowPreservingFuture();
5542
7756
  if (message.buses) {
5543
7757
  this.engine.setTrackBuses(message.buses);
5544
7758
  }
@@ -5552,10 +7766,62 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5552
7766
  if (message.masterStripJson) {
5553
7767
  this.engine.setMasterStripJson(message.masterStripJson);
5554
7768
  }
7769
+ for (const target of message.insertBaseResets ?? []) {
7770
+ switch (target.kind) {
7771
+ case "track":
7772
+ this.engine.clearTrackInsertParameterBases(target.trackId);
7773
+ break;
7774
+ case "bus":
7775
+ this.engine.clearBusInsertParameterBases(target.busId);
7776
+ break;
7777
+ case "master":
7778
+ this.engine.clearMasterInsertParameterBases();
7779
+ break;
7780
+ }
7781
+ }
5555
7782
  for (const binding of message.laneSidechains ?? []) {
5556
7783
  this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
5557
7784
  }
7785
+ for (const binding of message.busSidechains ?? []) {
7786
+ this.engine.setBusSidechain(
7787
+ binding.busId,
7788
+ binding.insertIndex,
7789
+ binding.sourceKind,
7790
+ binding.sourceId
7791
+ );
7792
+ }
7793
+ for (const binding of message.masterSidechains ?? []) {
7794
+ this.engine.setMasterSidechain(binding.insertIndex, binding.sourceKind, binding.sourceId);
7795
+ }
7796
+ for (const override of message.insertParamOverrides ?? []) {
7797
+ switch (override.kind) {
7798
+ case "track":
7799
+ this.engine.restoreTrackStripInsertParamByName(
7800
+ override.trackId,
7801
+ override.insertIndex,
7802
+ override.paramName,
7803
+ override.value
7804
+ );
7805
+ break;
7806
+ case "bus":
7807
+ this.engine.restoreBusStripInsertParamByName(
7808
+ override.busId,
7809
+ override.insertIndex,
7810
+ override.paramName,
7811
+ override.value
7812
+ );
7813
+ break;
7814
+ case "master":
7815
+ this.engine.restoreMasterStripInsertParamByName(
7816
+ override.insertIndex,
7817
+ override.paramName,
7818
+ override.value
7819
+ );
7820
+ break;
7821
+ }
7822
+ }
5558
7823
  break;
7824
+ }
5559
7825
  case "syncCapture":
5560
7826
  this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
5561
7827
  this.engine.setCaptureSource(message.source);
@@ -5568,6 +7834,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5568
7834
  case "syncMasterStripEqBand":
5569
7835
  this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
5570
7836
  break;
7837
+ case "syncBusStripEqBand":
7838
+ this.engine.setBusStripEqBandJson(message.busId, message.bandIndex, message.bandJson);
7839
+ break;
5571
7840
  case "syncTrackStripInsertBypassed":
5572
7841
  this.engine.setTrackStripInsertBypassed(
5573
7842
  message.trackId,
@@ -5584,26 +7853,57 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5584
7853
  );
5585
7854
  break;
5586
7855
  case "syncTrackStripInsertParamByName":
5587
- this.engine.setTrackStripInsertParamByName(
7856
+ this.engine.applyCommandsDueNowPreservingFuture();
7857
+ if (!this.engine.applyTrackStripInsertParamByNameNow(
5588
7858
  message.trackId,
5589
7859
  message.insertIndex,
5590
7860
  message.paramName,
5591
7861
  message.value
5592
- );
7862
+ )) {
7863
+ this.engine.restoreTrackStripInsertParamByName(
7864
+ message.trackId,
7865
+ message.insertIndex,
7866
+ message.paramName,
7867
+ message.value
7868
+ );
7869
+ }
5593
7870
  break;
5594
7871
  case "syncMasterStripInsertParamByName":
5595
- this.engine.setMasterStripInsertParamByName(
7872
+ this.engine.applyCommandsDueNowPreservingFuture();
7873
+ if (!this.engine.applyMasterStripInsertParamByNameNow(
5596
7874
  message.insertIndex,
5597
7875
  message.paramName,
5598
7876
  message.value
5599
- );
7877
+ )) {
7878
+ this.engine.restoreMasterStripInsertParamByName(
7879
+ message.insertIndex,
7880
+ message.paramName,
7881
+ message.value
7882
+ );
7883
+ }
5600
7884
  break;
5601
7885
  case "syncBusStripInsertParamByName":
5602
- this.engine.setBusStripInsertParamByName(
7886
+ this.engine.applyCommandsDueNowPreservingFuture();
7887
+ if (!this.engine.applyBusStripInsertParamByNameNow(
5603
7888
  message.busId,
5604
7889
  message.insertIndex,
5605
7890
  message.paramName,
5606
7891
  message.value
7892
+ )) {
7893
+ this.engine.restoreBusStripInsertParamByName(
7894
+ message.busId,
7895
+ message.insertIndex,
7896
+ message.paramName,
7897
+ message.value
7898
+ );
7899
+ }
7900
+ break;
7901
+ case "syncBusStripInsertBypassed":
7902
+ this.engine.setBusStripInsertBypassed(
7903
+ message.busId,
7904
+ message.insertIndex,
7905
+ message.bypassed,
7906
+ message.resetOnBypass
5607
7907
  );
5608
7908
  break;
5609
7909
  case "syncTrackStripPan":
@@ -5618,6 +7918,18 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5618
7918
  case "syncTrackStripDualPan":
5619
7919
  this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
5620
7920
  break;
7921
+ case "syncBusStripPan":
7922
+ this.engine.setBusStripPan(message.busId, message.pan);
7923
+ break;
7924
+ case "syncBusStripPanLaw":
7925
+ this.engine.setBusStripPanLaw(message.busId, message.panLaw);
7926
+ break;
7927
+ case "syncBusStripPanMode":
7928
+ this.engine.setBusStripPanMode(message.busId, message.panMode);
7929
+ break;
7930
+ case "syncBusStripDualPan":
7931
+ this.engine.setBusStripDualPan(message.busId, message.leftPan, message.rightPan);
7932
+ break;
5621
7933
  case "syncTrackStripChannelDelaySamples":
5622
7934
  this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
5623
7935
  break;
@@ -5669,8 +7981,36 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5669
7981
  message.renderFrame
5670
7982
  );
5671
7983
  break;
7984
+ case "syncMidiPitchBend":
7985
+ this.engine.pushMidiPitchBend(
7986
+ message.destinationId,
7987
+ message.group,
7988
+ message.channel,
7989
+ message.data0,
7990
+ message.renderFrame
7991
+ );
7992
+ break;
7993
+ case "syncMidiChannelPressure":
7994
+ this.engine.pushMidiChannelPressure(
7995
+ message.destinationId,
7996
+ message.group,
7997
+ message.channel,
7998
+ message.data0,
7999
+ message.renderFrame
8000
+ );
8001
+ break;
8002
+ case "syncMidiPolyPressure":
8003
+ this.engine.pushMidiPolyPressure(
8004
+ message.destinationId,
8005
+ message.group,
8006
+ message.channel,
8007
+ message.data0,
8008
+ message.data1,
8009
+ message.renderFrame
8010
+ );
8011
+ break;
5672
8012
  case "syncMidiUmp":
5673
- this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
8013
+ this.engine.pushMidiUmp(message.destinationId, [message.word0], message.renderFrame);
5674
8014
  break;
5675
8015
  case "syncMidiSysex":
5676
8016
  this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
@@ -5723,6 +8063,31 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5723
8063
  message.portTimeSamples
5724
8064
  );
5725
8065
  break;
8066
+ case "syncMidiInputPitchBend":
8067
+ this.engine.pushMidiInputPitchBend(
8068
+ message.group,
8069
+ message.channel,
8070
+ message.data0,
8071
+ message.portTimeSamples
8072
+ );
8073
+ break;
8074
+ case "syncMidiInputChannelPressure":
8075
+ this.engine.pushMidiInputChannelPressure(
8076
+ message.group,
8077
+ message.channel,
8078
+ message.data0,
8079
+ message.portTimeSamples
8080
+ );
8081
+ break;
8082
+ case "syncMidiInputPolyPressure":
8083
+ this.engine.pushMidiInputPolyPressure(
8084
+ message.group,
8085
+ message.channel,
8086
+ message.data0,
8087
+ message.data1,
8088
+ message.portTimeSamples
8089
+ );
8090
+ break;
5726
8091
  }
5727
8092
  }
5728
8093
  receiveCaptureRequest(message) {
@@ -5803,11 +8168,11 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5803
8168
  this.closed = true;
5804
8169
  }
5805
8170
  }
5806
- drainCommands() {
8171
+ drainCommands(limit = 64) {
5807
8172
  if (!this.commandRing) {
5808
8173
  return;
5809
8174
  }
5810
- for (let i = 0; i < 64; i++) {
8175
+ for (let i = 0; i < limit; i++) {
5811
8176
  const command = popSonareEngineCommandRingBuffer(this.commandRing);
5812
8177
  if (!command) {
5813
8178
  return;
@@ -5909,6 +8274,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5909
8274
  this.engine.setTrackMonitorMode(laneIndex, mode, sampleTime);
5910
8275
  break;
5911
8276
  }
8277
+ case 28 /* ResetMasterLoudnessMeter */:
8278
+ this.engine.resetMasterLoudnessMeter(sampleTime);
8279
+ break;
5912
8280
  default:
5913
8281
  this.publishTelemetryRecord({
5914
8282
  type: 1 /* Error */,
@@ -5994,10 +8362,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5994
8362
  if (meter.frame !== this.lastMeterFrame) {
5995
8363
  this.lastMeterFrame = meter.frame;
5996
8364
  }
5997
- if (this.meterRing) {
5998
- this.writeMeterRing(meter);
8365
+ if (this.transport?.onMeter) {
8366
+ this.transport.onMeter(meter);
5999
8367
  } else {
6000
- this.transport?.onMeter?.(meter);
6001
8368
  this.transport?.postMessage?.(meter);
6002
8369
  }
6003
8370
  }
@@ -6012,42 +8379,69 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
6012
8379
  for (let field = 0; field < 11; field++) {
6013
8380
  ring.records[offset + 3 + field] = this.engine.meterScratchValue(field);
6014
8381
  }
8382
+ ring.records[offset + 14] = this.engine.meterScratchInputPeakDbL();
8383
+ ring.records[offset + 15] = this.engine.meterScratchInputPeakDbR();
6015
8384
  Atomics.store(ring.header, 0, writeIndex + 1);
6016
8385
  }
6017
- writeMeterRing(meter) {
6018
- const ring = this.meterRing;
6019
- if (!ring) {
6020
- return;
6021
- }
6022
- const writeIndex = Atomics.load(ring.header, 0);
6023
- const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
6024
- ring.records[offset] = encodeFrameLo(meter.frame);
6025
- ring.records[offset + 1] = encodeFrameHi(meter.frame);
6026
- ring.records[offset + 2] = meter.targetId;
6027
- ring.records[offset + 3] = meter.peakDbL;
6028
- ring.records[offset + 4] = meter.peakDbR;
6029
- ring.records[offset + 5] = meter.rmsDbL;
6030
- ring.records[offset + 6] = meter.rmsDbR;
6031
- ring.records[offset + 7] = meter.correlation;
6032
- ring.records[offset + 8] = meter.truePeakDbL;
6033
- ring.records[offset + 9] = meter.truePeakDbR;
6034
- ring.records[offset + 10] = meter.momentaryLufs;
6035
- ring.records[offset + 11] = meter.shortTermLufs;
6036
- ring.records[offset + 12] = meter.integratedLufs;
6037
- ring.records[offset + 13] = meter.gainReductionDb;
6038
- Atomics.store(ring.header, 0, writeIndex + 1);
6039
- }
8386
+ // A snapshot-object ring writer used to live here as well. It was
8387
+ // unreachable: publishMeters returns after the writeMeterScratch loop
8388
+ // whenever a ring exists, so the ring is never fed from a materialised
8389
+ // snapshot. writeMeterScratch above is the only ring writer, and it keeps the
8390
+ // render callback allocation-free by reading the engine's scratch registers
8391
+ // directly.
8392
+ //
8393
+ // writeIndex is a free-running monotonic counter, so an overflow guard would
8394
+ // fire on essentially every write past the first `capacity` records and store
8395
+ // an ever-growing value, not a dropped-record count. Readers already detect
8396
+ // silent overrun via firstReadable = max(readIndex, writeIndex - capacity),
8397
+ // so header slot 3 is left at its initial 0.
6040
8398
  // Drains the engine's scope producer (FFT spectrum + goniometer points) into
6041
- // the lock-free SAB scope ring. No allocation on the render path: records are
6042
- // written field-by-field into the ring.
8399
+ // the lock-free SAB scope ring or the bounded postMessage fallback. The SAB
8400
+ // path remains allocation-free; postMessage materialises only records that
8401
+ // are ready to publish.
6043
8402
  publishScope() {
6044
- const ring = this.scopeRing;
6045
- if (!ring) {
8403
+ if (!this.scopeEnabled) {
6046
8404
  return;
6047
8405
  }
8406
+ const ring = this.scopeRing;
8407
+ const transport = this.transport;
6048
8408
  for (let count = 0; count < 64 && this.engine.popScopeTelemetryToScratch(); count++) {
6049
- this.writeScopeScratch(ring);
8409
+ if (ring) {
8410
+ this.writeScopeScratch(ring);
8411
+ } else {
8412
+ this.postScopeScratch(transport);
8413
+ }
8414
+ }
8415
+ }
8416
+ postScopeScratch(transport) {
8417
+ if (!transport?.postMessage) {
8418
+ return;
8419
+ }
8420
+ const bandCount = Math.min(
8421
+ SONARE_SCOPE_MAX_BANDS,
8422
+ Math.max(0, Math.trunc(Number(this.engine.scopeScratchBandCount())))
8423
+ );
8424
+ const pointCount = Math.min(
8425
+ SONARE_SCOPE_MAX_POINTS,
8426
+ Math.max(0, Math.trunc(Number(this.engine.scopeScratchPointCount())))
8427
+ );
8428
+ const bands = new Float32Array(bandCount);
8429
+ for (let i = 0; i < bandCount; i++) {
8430
+ bands[i] = this.engine.scopeScratchBand(i);
8431
+ }
8432
+ const points = new Float32Array(pointCount * 2);
8433
+ for (let i = 0; i < pointCount; i++) {
8434
+ points[2 * i] = this.engine.scopeScratchPointLeft(i);
8435
+ points[2 * i + 1] = this.engine.scopeScratchPointRight(i);
6050
8436
  }
8437
+ const scope = {
8438
+ type: "scope",
8439
+ targetId: this.engine.scopeScratchTargetId(),
8440
+ frame: Number(this.engine.scopeScratchRenderFrame()),
8441
+ bands,
8442
+ points
8443
+ };
8444
+ transport.postMessage(scope, [bands.buffer, points.buffer]);
6051
8445
  }
6052
8446
  writeScopeScratch(ring) {
6053
8447
  const writeIndex = Atomics.load(ring.header, 0);
@@ -6244,15 +8638,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
6244
8638
  }
6245
8639
  return;
6246
8640
  }
6247
- if (isEngineCommandRecord(event.data)) {
6248
- this.bridge.receiveCommand(event.data);
6249
- } else if (isEngineSyncMessage(event.data)) {
6250
- this.bridge.receiveSync(event.data);
6251
- } else if (isEngineCaptureRequestMessage(event.data)) {
6252
- this.bridge.receiveCaptureRequest(event.data);
6253
- } else if (isEngineTransportRequestMessage(event.data)) {
6254
- this.bridge.receiveTransportRequest(event.data);
6255
- }
8641
+ this.routeMessage(event.data);
6256
8642
  };
6257
8643
  if (port?.addEventListener) {
6258
8644
  port.addEventListener("message", onMessage);
@@ -6271,18 +8657,35 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
6271
8657
  }
6272
8658
  return true;
6273
8659
  }
8660
+ /**
8661
+ * Single dispatch point for the port, shared with the buffered replay so
8662
+ * the two cannot recognize different message sets.
8663
+ */
8664
+ routeMessage(data) {
8665
+ const bridge = this.bridge;
8666
+ if (!bridge) {
8667
+ return;
8668
+ }
8669
+ if (isEngineCommandRecord(data)) {
8670
+ bridge.receiveCommand(data);
8671
+ } else if (isEngineSyncMessage(data)) {
8672
+ bridge.receiveSync(data);
8673
+ } else if (isEngineCaptureRequestMessage(data)) {
8674
+ bridge.receiveCaptureRequest(data);
8675
+ } else if (isEngineTransportRequestMessage(data)) {
8676
+ bridge.receiveTransportRequest(data);
8677
+ } else if (isRecord(data) && typeof data.type === "string" && data.type.startsWith("sync")) {
8678
+ this.port?.postMessage?.({
8679
+ type: "syncError",
8680
+ syncType: data.type,
8681
+ message: `Unrecognized worklet sync message: ${data.type}`
8682
+ });
8683
+ }
8684
+ }
6274
8685
  replayPendingMessages() {
6275
8686
  const messages = this.pendingMessages.splice(0);
6276
8687
  for (const data of messages) {
6277
- if (isEngineCommandRecord(data)) {
6278
- this.bridge?.receiveCommand(data);
6279
- } else if (isEngineSyncMessage(data)) {
6280
- this.bridge?.receiveSync(data);
6281
- } else if (isEngineCaptureRequestMessage(data)) {
6282
- this.bridge?.receiveCaptureRequest(data);
6283
- } else if (isEngineTransportRequestMessage(data)) {
6284
- this.bridge?.receiveTransportRequest(data);
6285
- }
8688
+ this.routeMessage(data);
6286
8689
  }
6287
8690
  }
6288
8691
  async initializeEmbind(options, port) {
@@ -6335,15 +8738,50 @@ var SonareWorkletProcessor = class {
6335
8738
  this.processedFrames = 0;
6336
8739
  this.lastMeterFrame = 0;
6337
8740
  this.lastSpectrumFrame = 0;
8741
+ /**
8742
+ * Reused meter record, so a publish writes fields instead of allocating one.
8743
+ *
8744
+ * `targetId` is always the master and the four LUFS / gain-reduction fields
8745
+ * are always unavailable here — the mixer worklet does not run the
8746
+ * K-weighting filters or the engine's pre-trim input tap — so finite floor
8747
+ * values are set once rather than per interval.
8748
+ */
8749
+ this.meterScratch = {
8750
+ type: "meter",
8751
+ targetId: 0,
8752
+ frame: 0,
8753
+ peakDbL: 0,
8754
+ peakDbR: 0,
8755
+ rmsDbL: 0,
8756
+ rmsDbR: 0,
8757
+ correlation: 0,
8758
+ truePeakDbL: 0,
8759
+ truePeakDbR: 0,
8760
+ momentaryLufs: Number.NaN,
8761
+ shortTermLufs: Number.NaN,
8762
+ integratedLufs: Number.NaN,
8763
+ gainReductionDb: Number.NaN,
8764
+ inputPeakDbL: SONARE_FLOOR_DB,
8765
+ inputPeakDbR: SONARE_FLOOR_DB
8766
+ };
6338
8767
  if (!options.sceneJson) {
6339
8768
  throw new Error("sceneJson is required.");
6340
8769
  }
6341
8770
  this.sampleRate = options.sampleRate ?? 48e3;
6342
8771
  this.blockSize = options.blockSize ?? 128;
6343
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
6344
- this.spectrumIntervalFrames = Math.max(0, Math.floor(options.spectrumIntervalFrames ?? 0));
8772
+ this.meterIntervalFrames = requireIntegerOption(
8773
+ options.meterIntervalFrames,
8774
+ 2048,
8775
+ "meterIntervalFrames",
8776
+ 0
8777
+ );
8778
+ this.spectrumIntervalFrames = requireIntegerOption(
8779
+ options.spectrumIntervalFrames,
8780
+ 0,
8781
+ "spectrumIntervalFrames",
8782
+ 0
8783
+ );
6345
8784
  this.transport = transport;
6346
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
6347
8785
  this.meterRing = options.meterSharedBuffer ? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity) : void 0;
6348
8786
  this.spectrumRing = options.spectrumSharedBuffer ? spectrumRingFromSharedBuffer(
6349
8787
  options.spectrumSharedBuffer,
@@ -6360,6 +8798,7 @@ var SonareWorkletProcessor = class {
6360
8798
  throw new Error("stripCount must match the scene strip count.");
6361
8799
  }
6362
8800
  this.realtime = this.mixer.createRealtimeBuffer();
8801
+ this.mixer.configureMeter(this.meterIntervalFrames > 0, 4);
6363
8802
  }
6364
8803
  process(inputs, outputs) {
6365
8804
  if (this.closed) {
@@ -6406,10 +8845,7 @@ var SonareWorkletProcessor = class {
6406
8845
  }
6407
8846
  }
6408
8847
  this.processedFrames += usable;
6409
- this.publishMeter(
6410
- this.realtime.outLeft.subarray(0, usable),
6411
- this.realtime.outRight.subarray(0, usable)
6412
- );
8848
+ this.publishMeter();
6413
8849
  this.publishSpectrum(
6414
8850
  this.realtime.outLeft.subarray(0, usable),
6415
8851
  this.realtime.outRight.subarray(0, usable)
@@ -6425,7 +8861,11 @@ var SonareWorkletProcessor = class {
6425
8861
  return;
6426
8862
  }
6427
8863
  if (message.type === "setMeterInterval") {
6428
- this.meterIntervalFrames = Math.max(0, Math.floor(message.frames));
8864
+ const frames = Math.max(0, Math.floor(message.frames));
8865
+ if (frames > 0 !== this.meterIntervalFrames > 0) {
8866
+ this.mixer.configureMeter(frames > 0, 4);
8867
+ }
8868
+ this.meterIntervalFrames = frames;
6429
8869
  return;
6430
8870
  }
6431
8871
  if (message.type === "scheduleInsertAutomation") {
@@ -6445,58 +8885,54 @@ var SonareWorkletProcessor = class {
6445
8885
  this.closed = true;
6446
8886
  }
6447
8887
  }
6448
- publishMeter(left, right) {
6449
- if (!this.transport || this.meterIntervalFrames <= 0) {
8888
+ publishMeter() {
8889
+ if (!this.transport && !this.meterRing || this.meterIntervalFrames <= 0) {
6450
8890
  return;
6451
8891
  }
6452
8892
  if (this.processedFrames - this.lastMeterFrame < this.meterIntervalFrames) {
6453
8893
  return;
6454
8894
  }
6455
8895
  this.lastMeterFrame = this.processedFrames;
6456
- let peakL = 0;
6457
- let peakR = 0;
6458
- let sumL = 0;
6459
- let sumR = 0;
6460
- let sumLR = 0;
6461
- for (let i = 0; i < left.length; i++) {
6462
- const l = left[i] ?? 0;
6463
- const r = right[i] ?? 0;
6464
- const absL = Math.abs(l);
6465
- const absR = Math.abs(r);
6466
- if (absL > peakL) {
6467
- peakL = absL;
6468
- }
6469
- if (absR > peakR) {
6470
- peakR = absR;
6471
- }
6472
- sumL += l * l;
6473
- sumR += r * r;
6474
- sumLR += l * r;
8896
+ if (!this.mixer.latchMeterSnapshot()) {
8897
+ return;
8898
+ }
8899
+ if (this.meterRing) {
8900
+ const meter2 = this.meterScratch;
8901
+ meter2.frame = this.processedFrames;
8902
+ meter2.peakDbL = this.mixer.meterScratchValue(0);
8903
+ meter2.peakDbR = this.mixer.meterScratchValue(1);
8904
+ meter2.rmsDbL = this.mixer.meterScratchValue(2);
8905
+ meter2.rmsDbR = this.mixer.meterScratchValue(3);
8906
+ meter2.correlation = this.mixer.meterScratchValue(4);
8907
+ meter2.truePeakDbL = this.mixer.meterScratchValue(5);
8908
+ meter2.truePeakDbR = this.mixer.meterScratchValue(6);
8909
+ this.writeMeterRing(meter2);
8910
+ return;
6475
8911
  }
6476
- const rmsL = Math.sqrt(sumL / Math.max(1, left.length));
6477
- const rmsR = Math.sqrt(sumR / Math.max(1, right.length));
6478
- const denominator = Math.sqrt(sumL * sumR);
6479
8912
  const meter = {
6480
8913
  type: "meter",
6481
8914
  targetId: 0,
6482
8915
  frame: this.processedFrames,
6483
- peakDbL: toDb(peakL),
6484
- peakDbR: toDb(peakR),
6485
- rmsDbL: toDb(rmsL),
6486
- rmsDbR: toDb(rmsR),
6487
- correlation: denominator > 0 ? sumLR / denominator : 0,
6488
- truePeakDbL: toDb(peakL),
6489
- truePeakDbR: toDb(peakR),
8916
+ peakDbL: this.mixer.meterScratchValue(0),
8917
+ peakDbR: this.mixer.meterScratchValue(1),
8918
+ rmsDbL: this.mixer.meterScratchValue(2),
8919
+ rmsDbR: this.mixer.meterScratchValue(3),
8920
+ correlation: this.mixer.meterScratchValue(4),
8921
+ truePeakDbL: this.mixer.meterScratchValue(5),
8922
+ truePeakDbR: this.mixer.meterScratchValue(6),
8923
+ // Declared unavailable rather than floored: the mixer worklet does not run
8924
+ // the K-weighting filters, and a floor value would read as silence.
6490
8925
  momentaryLufs: Number.NaN,
6491
8926
  shortTermLufs: Number.NaN,
6492
8927
  integratedLufs: Number.NaN,
6493
- gainReductionDb: Number.NaN
8928
+ gainReductionDb: Number.NaN,
8929
+ inputPeakDbL: SONARE_FLOOR_DB,
8930
+ inputPeakDbR: SONARE_FLOOR_DB
6494
8931
  };
6495
- this.transport.onMeter?.(meter);
6496
- if (this.meterRing) {
6497
- this.writeMeterRing(meter);
8932
+ if (this.transport?.onMeter) {
8933
+ this.transport.onMeter(meter);
6498
8934
  } else {
6499
- this.transport.postMessage?.(meter);
8935
+ this.transport?.postMessage?.(meter);
6500
8936
  }
6501
8937
  }
6502
8938
  writeMeterRing(meter) {
@@ -6520,6 +8956,8 @@ var SonareWorkletProcessor = class {
6520
8956
  ring.records[offset + 11] = meter.shortTermLufs;
6521
8957
  ring.records[offset + 12] = meter.integratedLufs;
6522
8958
  ring.records[offset + 13] = meter.gainReductionDb;
8959
+ ring.records[offset + 14] = meter.inputPeakDbL;
8960
+ ring.records[offset + 15] = meter.inputPeakDbR;
6523
8961
  Atomics.store(ring.header, 0, writeIndex + 1);
6524
8962
  }
6525
8963
  publishSpectrum(left, right) {
@@ -6540,8 +8978,11 @@ var SonareWorkletProcessor = class {
6540
8978
  frame: this.processedFrames,
6541
8979
  bands: new Float32Array(this.spectrumBands)
6542
8980
  };
6543
- this.transport?.onSpectrum?.(spectrum);
6544
- this.transport?.postMessage?.(spectrum);
8981
+ if (this.transport?.onSpectrum) {
8982
+ this.transport.onSpectrum(spectrum);
8983
+ } else {
8984
+ this.transport?.postMessage?.(spectrum);
8985
+ }
6545
8986
  }
6546
8987
  computeSpectrum(left, right) {
6547
8988
  const n = Math.max(1, Math.min(left.length, right.length));
@@ -6569,7 +9010,7 @@ var SonareWorkletProcessor = class {
6569
9010
  return;
6570
9011
  }
6571
9012
  const writeIndex = Atomics.load(ring.header, 0);
6572
- const offset = writeIndex % ring.capacity * ring.recordFloats;
9013
+ const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
6573
9014
  ring.records[offset] = encodeFrameLo(frame);
6574
9015
  ring.records[offset + 1] = encodeFrameHi(frame);
6575
9016
  ring.records[offset + 2] = bands.length;
@@ -6609,13 +9050,232 @@ function registerSonareWorkletProcessor(name = "sonare-worklet-processor") {
6609
9050
  scope.registerProcessor(name, RegisteredSonareWorkletProcessor);
6610
9051
  }
6611
9052
 
9053
+ // src/worklet/playback-processor.ts
9054
+ var MAX_INPUT_CHANNELS = 8;
9055
+ var SPEAKER_LAYOUT_CHANNELS = { stereo: 2, "5.1": 6, "7.1": 8 };
9056
+ function configObject(config) {
9057
+ if (config === void 0) {
9058
+ return {};
9059
+ }
9060
+ return typeof config === "string" ? JSON.parse(config) : config;
9061
+ }
9062
+ function outputChannelCount(config) {
9063
+ if (config.target?.kind !== "speakers") {
9064
+ return 2;
9065
+ }
9066
+ const count = SPEAKER_LAYOUT_CHANNELS[config.target.layout ?? ""];
9067
+ if (count === void 0) {
9068
+ throw new RangeError('target.layout must be "stereo", "5.1" or "7.1" for a speakers target');
9069
+ }
9070
+ return count;
9071
+ }
9072
+ var _SonarePlaybackWorkletProcessor = class _SonarePlaybackWorkletProcessor {
9073
+ constructor(options = {}, port) {
9074
+ this.inputPlanes = [];
9075
+ this.outputPlanes = [];
9076
+ this.unsupportedInputBlocks = 0;
9077
+ this.destroyed = false;
9078
+ this.port = port;
9079
+ this.maxBlockSize = requireIntegerOption(options.maxBlockSize, 128, "maxBlockSize", 1);
9080
+ const scopeRate = globalThis.sampleRate;
9081
+ const sampleRate = options.sampleRate ?? (typeof scopeRate === "number" ? scopeRate : 48e3);
9082
+ const hrtf = options.hrtf ? HrtfSet.fromBytes(new Uint8Array(options.hrtf)) : void 0;
9083
+ try {
9084
+ this.renderer = new PlaybackRenderer({
9085
+ config: options.config ?? {},
9086
+ hrtf,
9087
+ sampleRate,
9088
+ maxBlockSize: this.maxBlockSize
9089
+ });
9090
+ } finally {
9091
+ hrtf?.delete();
9092
+ }
9093
+ this.native = this.renderer.native;
9094
+ this.outputChannels = this.native.outputChannels();
9095
+ this.acquirePlanes();
9096
+ }
9097
+ /**
9098
+ * Handles a control-plane message. AudioWorklet port handlers run on the
9099
+ * rendering thread between `process()` calls, which is what makes `reset`
9100
+ * safe here. A refused message is answered with an `error` message.
9101
+ */
9102
+ receiveMessage(message) {
9103
+ if (this.destroyed) {
9104
+ return;
9105
+ }
9106
+ try {
9107
+ switch (message.type) {
9108
+ case "config":
9109
+ this.renderer.setConfig(message.config);
9110
+ break;
9111
+ case "orientation":
9112
+ this.renderer.setHeadOrientation(message.yaw, message.pitch ?? 0, message.roll ?? 0);
9113
+ break;
9114
+ case "reset":
9115
+ this.renderer.reset();
9116
+ break;
9117
+ case "diagnostics":
9118
+ this.port?.postMessage?.({
9119
+ type: "diagnostics",
9120
+ diagnostics: this.diagnostics()
9121
+ });
9122
+ break;
9123
+ case "destroy":
9124
+ this.destroy();
9125
+ break;
9126
+ }
9127
+ } catch (error) {
9128
+ this.port?.postMessage?.({
9129
+ type: "error",
9130
+ request: message.type,
9131
+ message: error instanceof Error ? error.message : String(error)
9132
+ });
9133
+ }
9134
+ }
9135
+ /** The renderer's diagnostics plus `unsupported_input_blocks`, as a plain object. */
9136
+ diagnostics() {
9137
+ return {
9138
+ ...this.renderer.diagnostics(),
9139
+ unsupported_input_blocks: this.unsupportedInputBlocks
9140
+ };
9141
+ }
9142
+ process(inputs, outputs) {
9143
+ if (this.destroyed) {
9144
+ return false;
9145
+ }
9146
+ const output = outputs[0];
9147
+ const requested = output?.[0]?.length ?? 0;
9148
+ if (!output || requested === 0) {
9149
+ return true;
9150
+ }
9151
+ const frames = this.clampFrames(requested);
9152
+ if (this.inputPlanes[0]?.byteLength === 0 || this.outputPlanes[0]?.byteLength === 0) {
9153
+ this.acquirePlanes();
9154
+ }
9155
+ const input = inputs[0];
9156
+ const inChannels = input?.length ?? 0;
9157
+ let code;
9158
+ if (inChannels === 0) {
9159
+ code = this.native.processPreparedSilence(frames);
9160
+ } else if (inChannels > MAX_INPUT_CHANNELS) {
9161
+ this.unsupportedInputBlocks++;
9162
+ code = this.native.processPreparedSilence(frames);
9163
+ } else {
9164
+ for (let ch = 0; ch < inChannels; ch++) {
9165
+ const source = input[ch];
9166
+ const plane = this.inputPlanes[ch];
9167
+ const copied = Math.min(frames, source.length);
9168
+ plane.set(copied === source.length ? source : source.subarray(0, copied));
9169
+ if (copied < frames) {
9170
+ plane.fill(0, copied, frames);
9171
+ }
9172
+ }
9173
+ code = this.native.processPrepared(inChannels, frames);
9174
+ if (code !== 0) {
9175
+ this.unsupportedInputBlocks++;
9176
+ code = this.native.processPreparedSilence(frames);
9177
+ }
9178
+ }
9179
+ if (code === 0) {
9180
+ copyPlanesToOutput(output, this.outputPlanes, frames);
9181
+ } else {
9182
+ for (const channel of output) {
9183
+ channel.fill(0);
9184
+ }
9185
+ }
9186
+ return true;
9187
+ }
9188
+ destroy() {
9189
+ if (this.destroyed) {
9190
+ return;
9191
+ }
9192
+ this.destroyed = true;
9193
+ this.renderer.delete();
9194
+ }
9195
+ acquirePlanes() {
9196
+ this.inputPlanes = [];
9197
+ for (let ch = 0; ch < MAX_INPUT_CHANNELS; ch++) {
9198
+ this.inputPlanes.push(this.native.inputPlane(ch));
9199
+ }
9200
+ this.outputPlanes = [];
9201
+ for (let ch = 0; ch < this.outputChannels; ch++) {
9202
+ this.outputPlanes.push(this.native.outputPlane(ch));
9203
+ }
9204
+ }
9205
+ // Frames past the construction-time capacity are left silent rather than
9206
+ // reallocating on the audio thread.
9207
+ clampFrames(frames) {
9208
+ if (frames <= this.maxBlockSize) {
9209
+ return frames;
9210
+ }
9211
+ if (!_SonarePlaybackWorkletProcessor.warnedBlockOverflow) {
9212
+ _SonarePlaybackWorkletProcessor.warnedBlockOverflow = true;
9213
+ console.warn(
9214
+ `SonarePlaybackWorkletProcessor: requested ${frames} frames exceeds maxBlockSize ${this.maxBlockSize}; clamping.`
9215
+ );
9216
+ }
9217
+ return this.maxBlockSize;
9218
+ }
9219
+ };
9220
+ _SonarePlaybackWorkletProcessor.warnedBlockOverflow = false;
9221
+ var SonarePlaybackWorkletProcessor = _SonarePlaybackWorkletProcessor;
9222
+ function registerSonarePlaybackWorkletProcessor(name = "sonare-playback-processor") {
9223
+ const scope = globalThis;
9224
+ if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
9225
+ throw new Error("AudioWorkletProcessor is not available in this context.");
9226
+ }
9227
+ const Base = scope.AudioWorkletProcessor;
9228
+ class RegisteredSonarePlaybackWorkletProcessor extends Base {
9229
+ constructor(options) {
9230
+ super();
9231
+ const port = this.port;
9232
+ this.bridge = new SonarePlaybackWorkletProcessor(options?.processorOptions ?? {}, port);
9233
+ const onMessage = (event) => {
9234
+ if (isPlaybackMessage(event.data)) {
9235
+ this.bridge.receiveMessage(event.data);
9236
+ }
9237
+ };
9238
+ if (port?.addEventListener) {
9239
+ port.addEventListener("message", onMessage);
9240
+ port.start?.();
9241
+ } else if (port) {
9242
+ port.onmessage = onMessage;
9243
+ }
9244
+ }
9245
+ process(inputs, outputs) {
9246
+ return this.bridge.process(inputs, outputs);
9247
+ }
9248
+ }
9249
+ scope.registerProcessor(name, RegisteredSonarePlaybackWorkletProcessor);
9250
+ }
9251
+ function createSonarePlaybackNode(context, options = {}) {
9252
+ const config = configObject(options.config);
9253
+ const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
9254
+ const processorOptions = {
9255
+ config: JSON.stringify(config),
9256
+ hrtf: options.hrtf,
9257
+ maxBlockSize: options.maxBlockSize,
9258
+ sampleRate: options.sampleRate ?? context.sampleRate
9259
+ };
9260
+ return factory(context, options.processorName ?? "sonare-playback-processor", {
9261
+ numberOfInputs: 1,
9262
+ numberOfOutputs: 1,
9263
+ outputChannelCount: [outputChannelCount(config)],
9264
+ channelCountMode: "max",
9265
+ channelInterpretation: "discrete",
9266
+ processorOptions
9267
+ });
9268
+ }
9269
+
6612
9270
  // src/worklet/voice-changer-processor.ts
6613
9271
  var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChangerWorkletProcessor {
6614
9272
  constructor(options = {}) {
9273
+ // Reused so the mono fan-out never builds an array on the audio thread.
9274
+ this.monoPlane = [new Float32Array(0)];
6615
9275
  this.destroyed = false;
6616
9276
  this.sampleRate = options.sampleRate ?? 48e3;
6617
9277
  this.blockSize = options.blockSize ?? 128;
6618
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 1));
9278
+ this.channelCount = requireChannelCount(options.channelCount, 1);
6619
9279
  this.changer = new RealtimeVoiceChanger(options.preset ?? "neutral-monitor");
6620
9280
  this.changer.prepare(this.sampleRate, this.blockSize, this.channelCount);
6621
9281
  this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
@@ -6668,7 +9328,8 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
6668
9328
  this.monoInput.fill(0, 0, frames2);
6669
9329
  }
6670
9330
  this.changer.processPreparedMono(frames2);
6671
- output[0].set(this.monoOutput.subarray(0, frames2));
9331
+ this.monoPlane[0] = this.monoOutput;
9332
+ copyPlanesToOutput(output, this.monoPlane, frames2);
6672
9333
  return true;
6673
9334
  }
6674
9335
  const frames = this.ensureInterleavedCapacity(requestedFrames, requestedChannels);
@@ -6686,12 +9347,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
6686
9347
  }
6687
9348
  }
6688
9349
  this.changer.processPreparedPlanar(frames);
6689
- for (let ch = 0; ch < channels; ch++) {
6690
- const src = this.planarChannels[ch];
6691
- if (src) {
6692
- output[ch].set(src.subarray(0, frames));
6693
- }
6694
- }
9350
+ copyPlanesToOutput(output, this.planarChannels, frames);
6695
9351
  return true;
6696
9352
  }
6697
9353
  destroy() {
@@ -6801,6 +9457,7 @@ export {
6801
9457
  SonareEngineCommandType,
6802
9458
  SonareEngineTelemetryError,
6803
9459
  SonareEngineTelemetryType,
9460
+ SonarePlaybackWorkletProcessor,
6804
9461
  SonareRealtimeEngineNode,
6805
9462
  SonareRealtimeEngineWorkletProcessor,
6806
9463
  SonareRealtimeVoiceChangerWorkletProcessor,
@@ -6812,6 +9469,7 @@ export {
6812
9469
  createSonareEngineTelemetryRingBuffer,
6813
9470
  createSonareExternalMidiRingBuffer,
6814
9471
  createSonareMeterRingBuffer,
9472
+ createSonarePlaybackNode,
6815
9473
  createSonareScopeRingBuffer,
6816
9474
  createSonareSpectrumRingBuffer,
6817
9475
  decodeFrame,
@@ -6829,6 +9487,7 @@ export {
6829
9487
  readSonareMeterRingBuffer,
6830
9488
  readSonareScopeRingBuffer,
6831
9489
  readSonareSpectrumRingBuffer,
9490
+ registerSonarePlaybackWorkletProcessor,
6832
9491
  registerSonareRealtimeEngineWorkletProcessor,
6833
9492
  registerSonareRealtimeVoiceChangerWorkletProcessor,
6834
9493
  registerSonareWorkletProcessor,