@libraz/libsonare 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/NOTICE +178 -0
  2. package/README.md +26 -203
  3. package/dist/_chain_config.d.ts +14 -0
  4. package/dist/_chain_config.d.ts.map +1 -0
  5. package/dist/_effects_common.d.ts +6 -0
  6. package/dist/_effects_common.d.ts.map +1 -0
  7. package/dist/_feature_validation.d.ts +8 -0
  8. package/dist/_feature_validation.d.ts.map +1 -0
  9. package/dist/_fft_options.d.ts +24 -0
  10. package/dist/_fft_options.d.ts.map +1 -0
  11. package/dist/align_take.d.ts +50 -0
  12. package/dist/align_take.d.ts.map +1 -0
  13. package/dist/analysis.d.ts +29 -5840
  14. package/dist/analysis.d.ts.map +1 -0
  15. package/dist/analysis.js +873 -722
  16. package/dist/analysis.js.map +1 -1
  17. package/dist/analysis_helpers.d.ts +9 -0
  18. package/dist/analysis_helpers.d.ts.map +1 -0
  19. package/dist/audio.d.ts +163 -0
  20. package/dist/audio.d.ts.map +1 -0
  21. package/dist/clip_page_streamer.d.ts +133 -0
  22. package/dist/clip_page_streamer.d.ts.map +1 -0
  23. package/dist/codes.d.ts +44 -0
  24. package/dist/codes.d.ts.map +1 -0
  25. package/dist/effects_mastering.d.ts +23 -0
  26. package/dist/effects_mastering.d.ts.map +1 -0
  27. package/dist/effects_note_ops.d.ts +477 -0
  28. package/dist/effects_note_ops.d.ts.map +1 -0
  29. package/dist/effects_percussive.d.ts +185 -0
  30. package/dist/effects_percussive.d.ts.map +1 -0
  31. package/dist/effects_separation.d.ts +65 -0
  32. package/dist/effects_separation.d.ts.map +1 -0
  33. package/dist/effects_spectral.d.ts +28 -0
  34. package/dist/effects_spectral.d.ts.map +1 -0
  35. package/dist/effects_timepitch.d.ts +129 -0
  36. package/dist/effects_timepitch.d.ts.map +1 -0
  37. package/dist/effects_voice_change.d.ts +53 -0
  38. package/dist/effects_voice_change.d.ts.map +1 -0
  39. package/dist/errors.d.ts +51 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/feature_core.d.ts +341 -0
  42. package/dist/feature_core.d.ts.map +1 -0
  43. package/dist/feature_decompose.d.ts +278 -0
  44. package/dist/feature_decompose.d.ts.map +1 -0
  45. package/dist/feature_inverse.d.ts +128 -0
  46. package/dist/feature_inverse.d.ts.map +1 -0
  47. package/dist/feature_loudness.d.ts +66 -0
  48. package/dist/feature_loudness.d.ts.map +1 -0
  49. package/dist/feature_music.d.ts +307 -0
  50. package/dist/feature_music.d.ts.map +1 -0
  51. package/dist/feature_pitch.d.ts +108 -0
  52. package/dist/feature_pitch.d.ts.map +1 -0
  53. package/dist/feature_resample.d.ts +16 -0
  54. package/dist/feature_resample.d.ts.map +1 -0
  55. package/dist/feature_spectral.d.ts +137 -0
  56. package/dist/feature_spectral.d.ts.map +1 -0
  57. package/dist/feature_spectrogram.d.ts +198 -0
  58. package/dist/feature_spectrogram.d.ts.map +1 -0
  59. package/dist/features.d.ts +10 -0
  60. package/dist/features.d.ts.map +1 -0
  61. package/dist/hrtf/default.shrf +0 -0
  62. package/dist/index.d.ts +74 -7431
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +3758 -1362
  65. package/dist/index.js.map +1 -1
  66. package/dist/instrument_types.d.ts +517 -0
  67. package/dist/instrument_types.d.ts.map +1 -0
  68. package/dist/live_audio.d.ts +35 -0
  69. package/dist/live_audio.d.ts.map +1 -0
  70. package/dist/mastering_chain.d.ts +213 -0
  71. package/dist/mastering_chain.d.ts.map +1 -0
  72. package/dist/mastering_core.d.ts +457 -0
  73. package/dist/mastering_core.d.ts.map +1 -0
  74. package/dist/mastering_dynamics.d.ts +80 -0
  75. package/dist/mastering_dynamics.d.ts.map +1 -0
  76. package/dist/metering.d.ts +287 -0
  77. package/dist/metering.d.ts.map +1 -0
  78. package/dist/mixer.d.ts +464 -0
  79. package/dist/mixer.d.ts.map +1 -0
  80. package/dist/mixing_assistant.d.ts +62 -0
  81. package/dist/mixing_assistant.d.ts.map +1 -0
  82. package/dist/mixing_oneshot.d.ts +40 -0
  83. package/dist/mixing_oneshot.d.ts.map +1 -0
  84. package/dist/module_state.d.ts +15 -0
  85. package/dist/module_state.d.ts.map +1 -0
  86. package/dist/opfs_clip_pages.d.ts +28 -0
  87. package/dist/opfs_clip_pages.d.ts.map +1 -0
  88. package/dist/playback_renderer.d.ts +128 -0
  89. package/dist/playback_renderer.d.ts.map +1 -0
  90. package/dist/polyphony.d.ts +202 -0
  91. package/dist/polyphony.d.ts.map +1 -0
  92. package/dist/project.d.ts +8 -0
  93. package/dist/project.d.ts.map +1 -0
  94. package/dist/project_class.d.ts +562 -0
  95. package/dist/project_class.d.ts.map +1 -0
  96. package/dist/project_internal.d.ts +194 -0
  97. package/dist/project_internal.d.ts.map +1 -0
  98. package/dist/project_synth.d.ts +74 -0
  99. package/dist/project_synth.d.ts.map +1 -0
  100. package/dist/project_types.d.ts +652 -0
  101. package/dist/project_types.d.ts.map +1 -0
  102. package/dist/public_types.d.ts +185 -0
  103. package/dist/public_types.d.ts.map +1 -0
  104. package/dist/public_types_acoustic.d.ts +215 -0
  105. package/dist/public_types_acoustic.d.ts.map +1 -0
  106. package/dist/public_types_mastering.d.ts +510 -0
  107. package/dist/public_types_mastering.d.ts.map +1 -0
  108. package/dist/public_types_mixing.d.ts +436 -0
  109. package/dist/public_types_mixing.d.ts.map +1 -0
  110. package/dist/public_types_music.d.ts +619 -0
  111. package/dist/public_types_music.d.ts.map +1 -0
  112. package/dist/public_types_playback.d.ts +164 -0
  113. package/dist/public_types_playback.d.ts.map +1 -0
  114. package/dist/public_types_realtime.d.ts +174 -0
  115. package/dist/public_types_realtime.d.ts.map +1 -0
  116. package/dist/public_types_repair.d.ts +424 -0
  117. package/dist/public_types_repair.d.ts.map +1 -0
  118. package/dist/public_types_spectral.d.ts +697 -0
  119. package/dist/public_types_spectral.d.ts.map +1 -0
  120. package/dist/quick_analysis.d.ts +445 -0
  121. package/dist/quick_analysis.d.ts.map +1 -0
  122. package/dist/realtime_engine.d.ts +850 -0
  123. package/dist/realtime_engine.d.ts.map +1 -0
  124. package/dist/realtime_voice_changer.d.ts +158 -0
  125. package/dist/realtime_voice_changer.d.ts.map +1 -0
  126. package/dist/repair_dereverb.d.ts +187 -0
  127. package/dist/repair_dereverb.d.ts.map +1 -0
  128. package/dist/repair_impulsive.d.ts +186 -0
  129. package/dist/repair_impulsive.d.ts.map +1 -0
  130. package/dist/repair_noise.d.ts +239 -0
  131. package/dist/repair_noise.d.ts.map +1 -0
  132. package/dist/repair_trim.d.ts +123 -0
  133. package/dist/repair_trim.d.ts.map +1 -0
  134. package/dist/sample_bank.d.ts +84 -0
  135. package/dist/sample_bank.d.ts.map +1 -0
  136. package/dist/scale.d.ts +10 -0
  137. package/dist/scale.d.ts.map +1 -0
  138. package/dist/schemas/mixer-scene.schema.json +393 -0
  139. package/dist/schemas/playback-renderer-config.schema.json +392 -0
  140. package/dist/sonare-analysis.d.ts +8 -0
  141. package/dist/sonare-analysis.js +2 -2
  142. package/dist/sonare-analysis.wasm +0 -0
  143. package/dist/sonare.d.ts +3909 -0
  144. package/dist/sonare.js +2 -2
  145. package/dist/sonare.wasm +0 -0
  146. package/dist/stream_analyzer.d.ts +163 -0
  147. package/dist/stream_analyzer.d.ts.map +1 -0
  148. package/dist/stream_types.d.ts +214 -0
  149. package/dist/stream_types.d.ts.map +1 -0
  150. package/dist/streaming_mixing.d.ts +6 -0
  151. package/dist/streaming_mixing.d.ts.map +1 -0
  152. package/dist/streaming_processors.d.ts +335 -0
  153. package/dist/streaming_processors.d.ts.map +1 -0
  154. package/dist/transcribe.d.ts +77 -0
  155. package/dist/transcribe.d.ts.map +1 -0
  156. package/dist/validation.d.ts +140 -0
  157. package/dist/validation.d.ts.map +1 -0
  158. package/dist/web_midi.d.ts +77 -0
  159. package/dist/web_midi.d.ts.map +1 -0
  160. package/dist/worker.d.ts +5 -48
  161. package/dist/worker.d.ts.map +1 -0
  162. package/dist/worker.js +94 -41
  163. package/dist/worker.js.map +1 -1
  164. package/dist/worker_client.d.ts +96 -0
  165. package/dist/worker_client.d.ts.map +1 -0
  166. package/dist/worker_protocol.d.ts +43 -0
  167. package/dist/worker_protocol.d.ts.map +1 -0
  168. package/dist/worklet/audio_types.d.ts +21 -0
  169. package/dist/worklet/audio_types.d.ts.map +1 -0
  170. package/dist/worklet/engine-automation.d.ts +29 -0
  171. package/dist/worklet/engine-automation.d.ts.map +1 -0
  172. package/dist/worklet/engine-capture-facade.d.ts +35 -0
  173. package/dist/worklet/engine-capture-facade.d.ts.map +1 -0
  174. package/dist/worklet/engine-clips.d.ts +23 -0
  175. package/dist/worklet/engine-clips.d.ts.map +1 -0
  176. package/dist/worklet/engine-markers.d.ts +40 -0
  177. package/dist/worklet/engine-markers.d.ts.map +1 -0
  178. package/dist/worklet/engine-mixer-facade.d.ts +128 -0
  179. package/dist/worklet/engine-mixer-facade.d.ts.map +1 -0
  180. package/dist/worklet/engine-node.d.ts +81 -0
  181. package/dist/worklet/engine-node.d.ts.map +1 -0
  182. package/dist/worklet/engine-offline.d.ts +81 -0
  183. package/dist/worklet/engine-offline.d.ts.map +1 -0
  184. package/dist/worklet/engine-options.d.ts +12 -0
  185. package/dist/worklet/engine-options.d.ts.map +1 -0
  186. package/dist/worklet/engine-parameter-facade.d.ts +106 -0
  187. package/dist/worklet/engine-parameter-facade.d.ts.map +1 -0
  188. package/dist/worklet/engine-processor.d.ts +68 -0
  189. package/dist/worklet/engine-processor.d.ts.map +1 -0
  190. package/dist/worklet/engine-register.d.ts +2 -0
  191. package/dist/worklet/engine-register.d.ts.map +1 -0
  192. package/dist/worklet/engine-strips.d.ts +73 -0
  193. package/dist/worklet/engine-strips.d.ts.map +1 -0
  194. package/dist/worklet/engine-sync.d.ts +37 -0
  195. package/dist/worklet/engine-sync.d.ts.map +1 -0
  196. package/dist/worklet/engine-tempo-facade.d.ts +48 -0
  197. package/dist/worklet/engine-tempo-facade.d.ts.map +1 -0
  198. package/dist/worklet/engine.d.ts +397 -0
  199. package/dist/worklet/engine.d.ts.map +1 -0
  200. package/dist/worklet/guards.d.ts +47 -0
  201. package/dist/worklet/guards.d.ts.map +1 -0
  202. package/dist/worklet/messages.d.ts +710 -0
  203. package/dist/worklet/messages.d.ts.map +1 -0
  204. package/dist/worklet/mixer-processor.d.ts +46 -0
  205. package/dist/worklet/mixer-processor.d.ts.map +1 -0
  206. package/dist/worklet/playback-processor.d.ts +62 -0
  207. package/dist/worklet/playback-processor.d.ts.map +1 -0
  208. package/dist/worklet/protocol.d.ts +323 -0
  209. package/dist/worklet/protocol.d.ts.map +1 -0
  210. package/dist/worklet/voice-changer-processor.d.ts +41 -0
  211. package/dist/worklet/voice-changer-processor.d.ts.map +1 -0
  212. package/dist/worklet.d.ts +16 -2515
  213. package/dist/worklet.d.ts.map +1 -0
  214. package/dist/worklet.js +2763 -501
  215. package/dist/worklet.js.map +1 -1
  216. package/package.json +23 -12
  217. package/src/_effects_common.ts +17 -0
  218. package/src/_feature_validation.ts +34 -0
  219. package/src/_fft_options.ts +39 -0
  220. package/src/align_take.ts +64 -0
  221. package/src/analysis.ts +56 -3
  222. package/src/analysis_helpers.ts +7 -0
  223. package/src/audio.ts +106 -3
  224. package/src/codes.ts +39 -2
  225. package/src/effects_mastering.ts +97 -22
  226. package/src/effects_note_ops.ts +635 -0
  227. package/src/effects_percussive.ts +217 -0
  228. package/src/effects_separation.ts +150 -0
  229. package/src/effects_spectral.ts +60 -0
  230. package/src/effects_timepitch.ts +377 -0
  231. package/src/errors.ts +23 -1
  232. package/src/feature_core.ts +127 -2
  233. package/src/feature_decompose.ts +633 -0
  234. package/src/feature_inverse.ts +454 -0
  235. package/src/feature_loudness.ts +125 -0
  236. package/src/feature_music.ts +107 -14
  237. package/src/feature_pitch.ts +96 -1
  238. package/src/feature_spectral.ts +16 -611
  239. package/src/feature_spectrogram.ts +63 -450
  240. package/src/features.ts +36 -22
  241. package/src/index.ts +282 -30
  242. package/src/instrument_types.ts +645 -0
  243. package/src/live_audio.ts +27 -1
  244. package/src/mastering_chain.ts +184 -0
  245. package/src/mastering_core.ts +346 -32
  246. package/src/mastering_dynamics.ts +22 -11
  247. package/src/metering.ts +67 -24
  248. package/src/mixer.ts +212 -3
  249. package/src/mixing_assistant.ts +138 -0
  250. package/src/mixing_oneshot.ts +10 -5
  251. package/src/module_state.ts +24 -2
  252. package/src/playback_renderer.ts +252 -0
  253. package/src/polyphony.ts +279 -0
  254. package/src/project.ts +61 -24
  255. package/src/project_class.ts +450 -27
  256. package/src/project_internal.ts +149 -42
  257. package/src/project_synth.ts +67 -1
  258. package/src/project_types.ts +268 -270
  259. package/src/public_types.ts +122 -3
  260. package/src/public_types_acoustic.ts +112 -3
  261. package/src/public_types_mastering.ts +254 -73
  262. package/src/public_types_mixing.ts +363 -1
  263. package/src/public_types_music.ts +312 -2
  264. package/src/public_types_playback.ts +195 -0
  265. package/src/public_types_realtime.ts +39 -7
  266. package/src/public_types_repair.ts +446 -0
  267. package/src/public_types_spectral.ts +487 -1
  268. package/src/quick_analysis.ts +203 -26
  269. package/src/realtime_engine.ts +711 -28
  270. package/src/realtime_voice_changer.ts +55 -1
  271. package/src/repair_dereverb.ts +299 -0
  272. package/src/repair_impulsive.ts +395 -0
  273. package/src/repair_noise.ts +425 -0
  274. package/src/repair_trim.ts +226 -0
  275. package/src/sample_bank.ts +113 -0
  276. package/src/sonare.js.d.ts +1122 -30
  277. package/src/stream_analyzer.ts +36 -4
  278. package/src/stream_types.ts +37 -0
  279. package/src/streaming_mixing.ts +1 -1
  280. package/src/streaming_processors.ts +194 -10
  281. package/src/transcribe.ts +89 -0
  282. package/src/validation.ts +285 -11
  283. package/src/web_midi.ts +1 -6
  284. package/src/worker.ts +18 -2
  285. package/src/worklet/audio_types.ts +37 -0
  286. package/src/worklet/engine-mixer-facade.ts +444 -10
  287. package/src/worklet/engine-node.ts +78 -26
  288. package/src/worklet/engine-offline.ts +14 -8
  289. package/src/worklet/engine-parameter-facade.ts +21 -0
  290. package/src/worklet/engine-processor.ts +272 -83
  291. package/src/worklet/engine-register.ts +32 -18
  292. package/src/worklet/engine-strips.ts +280 -9
  293. package/src/worklet/engine-sync.ts +14 -6
  294. package/src/worklet/engine.ts +365 -31
  295. package/src/worklet/guards.ts +140 -44
  296. package/src/worklet/messages.ts +227 -2
  297. package/src/worklet/mixer-processor.ts +109 -47
  298. package/src/worklet/playback-processor.ts +300 -0
  299. package/src/worklet/protocol.ts +53 -3
  300. package/src/worklet/voice-changer-processor.ts +17 -11
  301. package/src/worklet.ts +17 -0
  302. package/src/effects_transform.ts +0 -718
  303. package/src/mastering_repair.ts +0 -273
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);
@@ -825,6 +1219,12 @@ var RealtimeEngine = class {
825
1219
  * block / animation frame. `maxRecords` caps the number of output events
826
1220
  * returned — the shared unit across every surface. Events past the cap stay
827
1221
  * queued for the next call (lossless); call again to drain the rest.
1222
+ *
1223
+ * One queued record lowers to at most 4 MIDI 1.0 messages (a MIDI 2.0
1224
+ * registered or assignable controller becomes CC 101/100 or 99/98 plus Data
1225
+ * Entry 6/38), so a positive `maxRecords` below 4 could never consume a record
1226
+ * and is rejected with an `InvalidParameter` `SonareError` instead of
1227
+ * returning nothing forever.
828
1228
  */
829
1229
  drainExternalMidi(maxRecords = 1024) {
830
1230
  return this.native.drainExternalMidi(maxRecords);
@@ -837,7 +1237,7 @@ var RealtimeEngine = class {
837
1237
  return this.native.externalMidiScratchDestinationId();
838
1238
  }
839
1239
  externalMidiScratchRenderFrame() {
840
- return this.native.externalMidiScratchRenderFrame();
1240
+ return Number(this.native.externalMidiScratchRenderFrame());
841
1241
  }
842
1242
  externalMidiScratchByteWord() {
843
1243
  return this.native.externalMidiScratchByteWord();
@@ -857,6 +1257,31 @@ var RealtimeEngine = class {
857
1257
  pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
858
1258
  this.native.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
859
1259
  }
1260
+ /**
1261
+ * Push a live MIDI pitch bend to the engine-owned MIDI input source.
1262
+ *
1263
+ * `bend14` is unsigned 14-bit with centre 8192 (0..16383) — the dimension is
1264
+ * not 7-bit, so a value past 16383 is refused rather than narrowed. The input
1265
+ * source must be enabled with {@link setMidiInputSource} first.
1266
+ */
1267
+ pushMidiInputPitchBend(group, channel, bend14, portTimeSamples = 0) {
1268
+ this.native.pushMidiInputPitchBend(group, channel, bend14, portTimeSamples);
1269
+ }
1270
+ /**
1271
+ * Push a live MIDI channel pressure to the engine-owned MIDI input source.
1272
+ * `pressure` is 7-bit (0..127). Under MPE this is the member channel's
1273
+ * per-note pressure.
1274
+ */
1275
+ pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples = 0) {
1276
+ this.native.pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples);
1277
+ }
1278
+ /**
1279
+ * Push a live MIDI polyphonic key pressure to the engine-owned MIDI input
1280
+ * source. `note` and `pressure` are 7-bit (0..127).
1281
+ */
1282
+ pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples = 0) {
1283
+ this.native.pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples);
1284
+ }
860
1285
  pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
861
1286
  this.native.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
862
1287
  }
@@ -872,9 +1297,53 @@ var RealtimeEngine = class {
872
1297
  pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
873
1298
  this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
874
1299
  }
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);
1300
+ /**
1301
+ * Queue an immediate (live) MIDI pitch bend to a MIDI destination. `bend14`
1302
+ * is unsigned 14-bit with centre 8192 (0..16383); `renderFrame` is the frame
1303
+ * to fire at, or -1 for immediate. Mirrors the Node/Python/C-ABI
1304
+ * `pushMidiPitchBend`.
1305
+ */
1306
+ pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame = -1) {
1307
+ this.native.pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame);
1308
+ }
1309
+ /**
1310
+ * Queue an immediate (live) MIDI channel pressure to a MIDI destination.
1311
+ * `pressure` is 7-bit (0..127); `renderFrame` is the frame to fire at, or -1
1312
+ * for immediate. Mirrors the Node/Python/C-ABI `pushMidiChannelPressure`.
1313
+ */
1314
+ pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame = -1) {
1315
+ this.native.pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame);
1316
+ }
1317
+ /**
1318
+ * Queue an immediate (live) MIDI polyphonic key pressure to a MIDI
1319
+ * destination. `note` and `pressure` are 7-bit (0..127); `renderFrame` is the
1320
+ * frame to fire at, or -1 for immediate. Mirrors the Node/Python/C-ABI
1321
+ * `pushMidiPolyPressure`.
1322
+ */
1323
+ pushMidiPolyPressure(destinationId, group, channel, note, pressure, renderFrame = -1) {
1324
+ this.native.pushMidiPolyPressure(destinationId, group, channel, note, pressure, renderFrame);
1325
+ }
1326
+ /**
1327
+ * Queue an immediate (live) raw UMP message to a MIDI destination. `words` is
1328
+ * 1 to 4 words, most significant first, and its length must match the message
1329
+ * type of `words[0]`. MIDI 2.0 channel-voice messages (MT 0x4) arrive at full
1330
+ * width; SysEx7 / data messages (MT 0x3 / 0x5) are refused, use
1331
+ * {@link pushMidiSysex}. Throws when the slot ring or command queue is full
1332
+ * (retry after a process block). `renderFrame` is the render-frame time to
1333
+ * apply, or -1 for immediate. A bare number is accepted as a one-word
1334
+ * message.
1335
+ */
1336
+ pushMidiUmp(destinationId, words, renderFrame = -1) {
1337
+ const list = typeof words === "number" ? [words] : words;
1338
+ this.native.pushMidiUmp(destinationId, assertUmpWords("pushMidiUmp", list), renderFrame);
1339
+ }
1340
+ /**
1341
+ * Push one raw UMP message (1 to 4 words) to the engine-owned MIDI input
1342
+ * source. The message rules match {@link pushMidiUmp}. `portTimeSamples` is
1343
+ * the port timestamp in samples.
1344
+ */
1345
+ pushMidiInputUmp(words, portTimeSamples = 0) {
1346
+ this.native.pushMidiInputUmp(assertUmpWords("pushMidiInputUmp", words), portTimeSamples);
878
1347
  }
879
1348
  /**
880
1349
  * Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
@@ -921,10 +1390,18 @@ var RealtimeEngine = class {
921
1390
  settleParameters() {
922
1391
  this.native.settleParameters();
923
1392
  }
1393
+ /** Snap only insert automation slots after structural replay. */
1394
+ settleInsertParameters() {
1395
+ this.native.settleInsertParameters();
1396
+ }
924
1397
  /** Drains queued commands on an offline/control-only engine immediately. */
925
1398
  flushControlCommands() {
926
1399
  this.native.flushControlCommands();
927
1400
  }
1401
+ /** Applies commands already due on a control-only mirror, retaining future commands. */
1402
+ applyCommandsDueNowPreservingFuture() {
1403
+ this.native.applyCommandsDueNowPreservingFuture();
1404
+ }
928
1405
  seekPpq(ppq, renderFrame = -1) {
929
1406
  this.native.seekPpq(ppq, renderFrame);
930
1407
  }
@@ -1020,6 +1497,18 @@ var RealtimeEngine = class {
1020
1497
  clipCount() {
1021
1498
  return this.native.clipCount();
1022
1499
  }
1500
+ /**
1501
+ * Normalizes each send's pre/post tap point to the integer the native layer
1502
+ * reads (defaults to post-fader when omitted). Shared by track lanes and
1503
+ * buses, which carry the same send shape.
1504
+ */
1505
+ static normalizeSends(sends) {
1506
+ return sends.map((send) => ({
1507
+ ...send,
1508
+ // Post-fader (0) is the default for an omitted sendTiming.
1509
+ sendTiming: send.sendTiming === void 0 ? 0 : sendTimingCode(send.sendTiming)
1510
+ }));
1511
+ }
1023
1512
  setTrackLanes(lanes) {
1024
1513
  this.native.setTrackLanes(
1025
1514
  lanes.map((lane) => {
@@ -1029,14 +1518,7 @@ var RealtimeEngine = class {
1029
1518
  if (!lane.sends) {
1030
1519
  return lane;
1031
1520
  }
1032
- return {
1033
- ...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
- }))
1039
- };
1521
+ return { ...lane, sends: _RealtimeEngine.normalizeSends(lane.sends) };
1040
1522
  })
1041
1523
  );
1042
1524
  }
@@ -1048,7 +1530,25 @@ var RealtimeEngine = class {
1048
1530
  this.native.setLaneSidechain(trackId, insertIndex, sourceTrackId);
1049
1531
  }
1050
1532
  setTrackBuses(buses) {
1051
- this.native.setTrackBuses(buses);
1533
+ this.native.setTrackBuses(
1534
+ Array.isArray(buses) ? buses.map(
1535
+ (bus) => bus.sends ? { ...bus, sends: _RealtimeEngine.normalizeSends(bus.sends) } : bus
1536
+ ) : buses
1537
+ );
1538
+ }
1539
+ /**
1540
+ * Keys one insert of a bus strip from a track lane or another bus
1541
+ * (ducking/sidechainRouter inserts). `sourceId` 0 removes the binding.
1542
+ */
1543
+ setBusSidechain(busId, insertIndex, sourceKind, sourceId) {
1544
+ this.native.setBusSidechain(busId, insertIndex, sidechainSourceKindCode(sourceKind), sourceId);
1545
+ }
1546
+ /**
1547
+ * Keys one insert of the master strip from a track lane or a bus. Same
1548
+ * source rules as {@link setBusSidechain}.
1549
+ */
1550
+ setMasterSidechain(insertIndex, sourceKind, sourceId) {
1551
+ this.native.setMasterSidechain(insertIndex, sidechainSourceKindCode(sourceKind), sourceId);
1052
1552
  }
1053
1553
  setBusStripJson(busId, sceneJson) {
1054
1554
  try {
@@ -1081,6 +1581,17 @@ var RealtimeEngine = class {
1081
1581
  setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass = false) {
1082
1582
  this.native.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
1083
1583
  }
1584
+ /** Bus-strip counterpart of {@link setTrackStripEqBand}. */
1585
+ setBusStripEqBand(busId, bandIndex, band) {
1586
+ this.native.setBusStripEqBandJson(
1587
+ busId,
1588
+ bandIndex,
1589
+ typeof band === "string" ? band : JSON.stringify(band)
1590
+ );
1591
+ }
1592
+ setBusStripEqBandJson(busId, bandIndex, bandJson) {
1593
+ this.native.setBusStripEqBandJson(busId, bandIndex, bandJson);
1594
+ }
1084
1595
  setMasterStripJson(sceneJson) {
1085
1596
  try {
1086
1597
  JSON.parse(sceneJson);
@@ -1104,22 +1615,59 @@ var RealtimeEngine = class {
1104
1615
  }
1105
1616
  /**
1106
1617
  * 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.
1618
+ * processor's JSON-key parameter name — one of the entries
1619
+ * {@link masteringInsertParamInfo} reports with a non-null `id`; a
1620
+ * construction-only entry (`id` null) takes effect only when the insert is
1621
+ * built. Applied at the next block head via the engine command queue; safe
1622
+ * during playback. Throws if the track, insert, or name is unknown, the
1623
+ * param is not realtime-safe, or the command queue is full.
1111
1624
  */
1112
1625
  setTrackStripInsertParamByName(trackId, insertIndex, paramName, value) {
1113
1626
  this.native.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
1114
1627
  }
1628
+ /** Apply a live insert edit on this engine's owning thread without draining its command queue. */
1629
+ applyTrackStripInsertParamByNameNow(trackId, insertIndex, paramName, value) {
1630
+ return this.native.applyTrackStripInsertParamByNameNow(trackId, insertIndex, paramName, value);
1631
+ }
1632
+ /** Restore a retained insert value exactly after a strip scene is replayed. */
1633
+ restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value) {
1634
+ this.native.restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
1635
+ }
1115
1636
  /** Master-strip counterpart of {@link setTrackStripInsertParamByName}. */
1116
1637
  setMasterStripInsertParamByName(insertIndex, paramName, value) {
1117
1638
  this.native.setMasterStripInsertParamByName(insertIndex, paramName, value);
1118
1639
  }
1640
+ applyMasterStripInsertParamByNameNow(insertIndex, paramName, value) {
1641
+ return this.native.applyMasterStripInsertParamByNameNow(insertIndex, paramName, value);
1642
+ }
1643
+ restoreMasterStripInsertParamByName(insertIndex, paramName, value) {
1644
+ this.native.restoreMasterStripInsertParamByName(insertIndex, paramName, value);
1645
+ }
1119
1646
  /** Bus-strip counterpart of {@link setTrackStripInsertParamByName}. */
1120
1647
  setBusStripInsertParamByName(busId, insertIndex, paramName, value) {
1121
1648
  this.native.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
1122
1649
  }
1650
+ applyBusStripInsertParamByNameNow(busId, insertIndex, paramName, value) {
1651
+ return this.native.applyBusStripInsertParamByNameNow(busId, insertIndex, paramName, value);
1652
+ }
1653
+ restoreBusStripInsertParamByName(busId, insertIndex, paramName, value) {
1654
+ this.native.restoreBusStripInsertParamByName(busId, insertIndex, paramName, value);
1655
+ }
1656
+ /**
1657
+ * Forgets the remembered manual insert-parameter values of one track strip
1658
+ * and discards its queued insert edits. Call before {@link setTrackStripJson}
1659
+ * replaces the strip when its old values must not carry over; the setter
1660
+ * never does this itself, since a queued edit may already target the new chain.
1661
+ */
1662
+ clearTrackInsertParameterBases(trackId) {
1663
+ this.native.clearTrackInsertParameterBases(trackId);
1664
+ }
1665
+ clearBusInsertParameterBases(busId) {
1666
+ this.native.clearBusInsertParameterBases(busId);
1667
+ }
1668
+ clearMasterInsertParameterBases() {
1669
+ this.native.clearMasterInsertParameterBases();
1670
+ }
1123
1671
  /** Bus-strip counterpart of {@link setTrackStripInsertBypassed}. */
1124
1672
  setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass = false) {
1125
1673
  this.native.setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass);
@@ -1149,6 +1697,41 @@ var RealtimeEngine = class {
1149
1697
  resolveBusInsertAutomationId(busId, insertIndex, paramName) {
1150
1698
  return this.native.resolveBusInsertAutomationId(busId, insertIndex, paramName);
1151
1699
  }
1700
+ /**
1701
+ * Resolves a hosted instrument's continuous parameter (by its JSON-key name)
1702
+ * to the reserved automation id usable with `setAutomationLane` /
1703
+ * `setParameter`, so an instrument parameter is driven at audio-block
1704
+ * precision exactly like a strip insert. Returns `-1` when the destination
1705
+ * has no bound instrument, the instrument exposes no automatable parameters,
1706
+ * or the name is unknown.
1707
+ *
1708
+ * For the NativeSynth ({@link setSynthInstrument}) the names are the
1709
+ * continuous {@link SynthPatch} fields: `gain`, `busDrive`, `cutoffHz`,
1710
+ * `resonanceQ`, `drive`, `keyTrack`, `envToCutoffCents`, `velToCutoffCents`,
1711
+ * `ampAttackMs`, `ampDecayMs`, `ampSustain`, `ampReleaseMs`,
1712
+ * `filterAttackMs`, `filterDecayMs`, `filterSustain`, `filterReleaseMs`,
1713
+ * `lfoRateHz`, `lfoToPitchCents`, `lfo2RateHz`, `glideMs`, `bodyMix`,
1714
+ * `stereoSpread`, `detuneCents`, `driftCents`, `pitchOffsetCents`,
1715
+ * `hpCutoffHz`, `sampleHoldHz`, `bitDepth`.
1716
+ *
1717
+ * Structural fields (`preset`, `engineMode`, `waveform`, `filterModel`,
1718
+ * `unison`, `polyphony`, `body`, `modRoutings`) are not automatable and
1719
+ * return `-1`: they resize voice pools or swap DSP topology, which is not
1720
+ * audio-thread safe. Rebind the instrument with a new patch instead.
1721
+ *
1722
+ * `gain`, `busDrive`, `cutoffHz`, `resonanceQ`, `envToCutoffCents`,
1723
+ * `lfoToPitchCents` and `pitchOffsetCents` reach voices that are already
1724
+ * sounding from the next block; the rest are cached into per-voice state at
1725
+ * note-on and take effect from the next note, so a lane that moves one of
1726
+ * them under a held note looks inert until the next one speaks — that is the
1727
+ * behaviour, not a dropped write.
1728
+ *
1729
+ * The id survives an unbind/rebind of the same destination and applies
1730
+ * nothing while that destination is unbound.
1731
+ */
1732
+ resolveInstrumentAutomationId(destinationId, paramName) {
1733
+ return this.native.resolveInstrumentAutomationId(destinationId, paramName);
1734
+ }
1152
1735
  /** Sets a track lane strip's pan position in realtime (glitch-free). */
1153
1736
  setTrackStripPan(trackId, pan) {
1154
1737
  this.native.setTrackStripPan(trackId, pan);
@@ -1165,6 +1748,25 @@ var RealtimeEngine = class {
1165
1748
  setTrackStripDualPan(trackId, leftPan, rightPan) {
1166
1749
  this.native.setTrackStripDualPan(trackId, leftPan, rightPan);
1167
1750
  }
1751
+ /**
1752
+ * Sets a bus strip's output pan position in realtime (glitch-free). Throws
1753
+ * for an unknown bus or one wider than stereo.
1754
+ */
1755
+ setBusStripPan(busId, pan) {
1756
+ this.native.setBusStripPan(busId, pan);
1757
+ }
1758
+ /** Sets a bus strip's pan law in realtime. */
1759
+ setBusStripPanLaw(busId, panLaw) {
1760
+ this.native.setBusStripPanLaw(busId, panLawCode(panLaw));
1761
+ }
1762
+ /** Sets a bus strip's pan mode in realtime. */
1763
+ setBusStripPanMode(busId, panMode) {
1764
+ this.native.setBusStripPanMode(busId, panModeCode(panMode));
1765
+ }
1766
+ /** Sets a bus strip's dual-pan left/right positions in realtime. */
1767
+ setBusStripDualPan(busId, leftPan, rightPan) {
1768
+ this.native.setBusStripDualPan(busId, leftPan, rightPan);
1769
+ }
1168
1770
  /**
1169
1771
  * Sets a track lane strip's inter-channel alignment delay (whole samples).
1170
1772
  * Adjusts strip latency, so PDC and reported graph latency are refreshed.
@@ -1205,16 +1807,62 @@ var RealtimeEngine = class {
1205
1807
  clipPageRequestOverflowCount() {
1206
1808
  return this.native.clipPageRequestOverflowCount();
1207
1809
  }
1208
- setCaptureBuffer(numChannels, capacityFrames) {
1209
- this.native.setCaptureBuffer(numChannels, capacityFrames);
1810
+ /** Cumulative warp-stretch requests dropped because the native queue was full. */
1811
+ warpStretchOverflowCount() {
1812
+ return this.native.warpStretchOverflowCount();
1210
1813
  }
1211
- armCapture(armed = true) {
1212
- this.native.armCapture(armed);
1814
+ /**
1815
+ * Sets the number of concurrent time-stretch voices. `voices` must be an
1816
+ * integer in `[0, 64]`; a non-integer, negative, or larger value throws and
1817
+ * leaves the capacity unchanged. Default is 8. Capacity 0 disables
1818
+ * time-stretch, so every warped clip plays resampled instead and none of
1819
+ * that counts toward {@link warpStretchOverflowCount}. A change applied
1820
+ * while the engine is running restarts the splice state of any clip
1821
+ * stretching through a voice at that moment. Control-thread only.
1822
+ */
1823
+ setWarpVoiceCapacity(voices) {
1824
+ this.native.setWarpVoiceCapacity(voices);
1213
1825
  }
1214
- setCapturePunch(startSample, endSample, enabled = true) {
1215
- this.native.setCapturePunch(startSample, endSample, enabled);
1826
+ /** Reads the current time-stretch voice capacity (default 8). */
1827
+ warpVoiceCapacity() {
1828
+ return this.native.warpVoiceCapacity();
1216
1829
  }
1217
- setCaptureSource(source) {
1830
+ /**
1831
+ * Sets the clip-page look-ahead window in timeline frames.
1832
+ *
1833
+ * The player reports the pages it is *about to* read that are not resident
1834
+ * yet, so a streaming host can service them before the audio thread reaches
1835
+ * them. Without look-ahead a page miss is only reported after the read
1836
+ * already produced silence, which costs one block of silence at every page
1837
+ * boundary the host has not primed — the reason a sliding-window streamer
1838
+ * cannot keep a live playhead fed from miss reports alone.
1839
+ *
1840
+ * Look-ahead requests drain through the same `popClipPageRequest` queue and
1841
+ * are queued *after* the block's genuine misses, so a host that keeps only
1842
+ * the newest request per clip (as {@link ClipPageStreamer} does) tracks the
1843
+ * look-ahead frontier.
1844
+ *
1845
+ * `prepare` defaults this to half a second at the engine's sample rate. `0`
1846
+ * disables the look-ahead. A clip whose pages are all resident produces no
1847
+ * requests at all, with or without look-ahead. Safe to call during playback.
1848
+ */
1849
+ setClipPagePrefetchFrames(frames) {
1850
+ this.native.setClipPagePrefetchFrames(frames);
1851
+ }
1852
+ /** Current clip-page look-ahead window in timeline frames. */
1853
+ clipPagePrefetchFrames() {
1854
+ return this.native.clipPagePrefetchFrames();
1855
+ }
1856
+ setCaptureBuffer(numChannels, capacityFrames) {
1857
+ this.native.setCaptureBuffer(numChannels, capacityFrames);
1858
+ }
1859
+ armCapture(armed = true) {
1860
+ this.native.armCapture(armed);
1861
+ }
1862
+ setCapturePunch(startSample, endSample, enabled = true) {
1863
+ this.native.setCapturePunch(startSample, endSample, enabled);
1864
+ }
1865
+ setCaptureSource(source) {
1218
1866
  this.native.setCaptureSource(source);
1219
1867
  }
1220
1868
  /** Positive values delay capture relative to the punch window. */
@@ -1296,12 +1944,36 @@ var RealtimeEngine = class {
1296
1944
  processWithMonitor(channels) {
1297
1945
  return this.native.processWithMonitor(channels);
1298
1946
  }
1299
- renderOffline(channels, blockSize = 128) {
1300
- return this.native.renderOffline(channels, blockSize);
1947
+ renderOffline(channelsOrRequest, blockSize = 128) {
1948
+ const request = normalizeRenderOfflineRequest(channelsOrRequest, blockSize);
1949
+ return this.native.renderOffline(request.channels, request.blockSize, request.finalize);
1950
+ }
1951
+ /**
1952
+ * End a chunked offline render: release every note the sequencer still holds
1953
+ * and flush the PDC / alignment delay lines. Required after
1954
+ * `renderOffline({ finalize: false })`; the finalizing form does it itself.
1955
+ *
1956
+ * Skipping it leaves every note still sounding at the last chunk held. On an
1957
+ * engine-internal instrument the tail simply never releases; on a destination
1958
+ * marked external ({@link RealtimeEngine.setMidiDestinationExternal}) the
1959
+ * note-ons already left through the external MIDI queue, so the note-offs
1960
+ * emitted here are the only ones the receiving device will get and the notes
1961
+ * otherwise hang outside the engine.
1962
+ */
1963
+ finishOfflineRender() {
1964
+ this.native.finishOfflineRender();
1301
1965
  }
1966
+ /**
1967
+ * Bounce the timeline to an interleaved buffer. `numChannels` above the
1968
+ * prepared channel count throws an `InvalidParameter` `SonareError`.
1969
+ */
1302
1970
  bounceOffline(options) {
1303
1971
  return this.native.bounceOffline(options);
1304
1972
  }
1973
+ /**
1974
+ * Freeze the current graph to audio. `numChannels` above the prepared channel
1975
+ * count throws an `InvalidParameter` `SonareError`.
1976
+ */
1305
1977
  freezeOffline(options) {
1306
1978
  return this.native.freezeOffline(options);
1307
1979
  }
@@ -1395,9 +2067,18 @@ var RealtimeEngine = class {
1395
2067
  scopeScratchPointRight(index) {
1396
2068
  return this.native.scopeScratchPointRight(index);
1397
2069
  }
2070
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
1398
2071
  destroy() {
2072
+ if (this.released) {
2073
+ return;
2074
+ }
2075
+ this.released = true;
1399
2076
  this.native.delete();
1400
2077
  }
2078
+ /** Alias for {@link destroy}, matching embind's own release method name. */
2079
+ delete() {
2080
+ this.destroy();
2081
+ }
1401
2082
  };
1402
2083
  var ClipPageProvider = class {
1403
2084
  constructor(engine, id) {
@@ -1429,12 +2110,20 @@ var ClipPageProvider = class {
1429
2110
  // src/mixer.ts
1430
2111
  var Mixer = class _Mixer {
1431
2112
  constructor(mixer, blockSize) {
2113
+ this.released = false;
1432
2114
  this.mixer = mixer;
1433
2115
  this.blockSize = blockSize;
1434
2116
  }
1435
2117
  /**
1436
2118
  * Build a mixer from a scene JSON string.
1437
2119
  *
2120
+ * A strip's meters are sized when the strip is built, so this is where their
2121
+ * configuration is chosen: an optional `metering` object on the strip
2122
+ * (`enabled` / `lufs` / `truePeak` / `truePeakOversample`) selects it, and
2123
+ * leaving it out keeps the full default (LUFS + true peak at 4x, about 1.4 MB
2124
+ * per strip at 48 kHz). `{"enabled": false}` drops both meters for a strip
2125
+ * whose snapshots are never read.
2126
+ *
1438
2127
  * @param json - Scene JSON (strips, buses, sends, connections, inserts)
1439
2128
  * @param sampleRate - Sample rate in Hz (default: 48000)
1440
2129
  * @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
@@ -1541,6 +2230,56 @@ var Mixer = class _Mixer {
1541
2230
  }
1542
2231
  };
1543
2232
  }
2233
+ /**
2234
+ * Turn the master-output meter on or off.
2235
+ *
2236
+ * While on, every {@link MixerRealtimeBuffer.process} call meters the stereo
2237
+ * master it just produced, so a caller reads {@link meterSnapshot} instead of
2238
+ * copying the output and measuring it again. `truePeakDb*` is an inter-sample
2239
+ * peak taken after oversampling (ITU-R BS.1770-4 Annex 2 requires at least
2240
+ * 4x), which is a different and higher quantity than the sample peak.
2241
+ *
2242
+ * Enabling resets the meter, so a reading never mixes in audio from a period
2243
+ * when metering was off.
2244
+ *
2245
+ * @param enabled - Whether to meter the master output.
2246
+ * @param truePeakOversample - 0 (= 4x) or a power of two in [1, 16].
2247
+ */
2248
+ configureMeter(enabled, truePeakOversample = 4) {
2249
+ this.mixer.configureMeter(enabled, truePeakOversample);
2250
+ }
2251
+ /**
2252
+ * Latest master-output meter reading, describing the most recently metered
2253
+ * block. All dB fields are finite and floored at -120.
2254
+ *
2255
+ * @throws When the meter has never been enabled.
2256
+ */
2257
+ meterSnapshot() {
2258
+ return this.mixer.meterSnapshot();
2259
+ }
2260
+ /**
2261
+ * Latch the latest meter reading into the mixer's internal scratch so
2262
+ * {@link meterScratchValue} can read it back one number at a time.
2263
+ *
2264
+ * This is the allocation-free form of {@link meterSnapshot}, for an audio
2265
+ * render callback that must not create a JS object per interval. It returns
2266
+ * `false` instead of throwing when the meter has never been enabled.
2267
+ *
2268
+ * @returns Whether a reading was latched.
2269
+ */
2270
+ latchMeterSnapshot() {
2271
+ return this.mixer.latchMeterSnapshot();
2272
+ }
2273
+ /**
2274
+ * Read one field of the snapshot latched by {@link latchMeterSnapshot}.
2275
+ *
2276
+ * @param field - `0` peakDbL, `1` peakDbR, `2` rmsDbL, `3` rmsDbR,
2277
+ * `4` correlation, `5` truePeakDbL, `6` truePeakDbR. Any other index
2278
+ * reads `0`.
2279
+ */
2280
+ meterScratchValue(field) {
2281
+ return this.mixer.meterScratchValue(field);
2282
+ }
1544
2283
  /** Number of strips in the mixer (e.g. strips loaded from the scene). */
1545
2284
  stripCount() {
1546
2285
  return this.mixer.stripCount();
@@ -1578,6 +2317,17 @@ var Mixer = class _Mixer {
1578
2317
  const index = this.mixer.stripById(id);
1579
2318
  return index < 0 ? null : index;
1580
2319
  }
2320
+ /**
2321
+ * Add a channel strip to the mixer topology. `metering` configures the strip's
2322
+ * pre/post taps; omitting it keeps the full default (LUFS + true peak at 4x,
2323
+ * about 1.4 MB per strip at 48 kHz). Marks the routing graph dirty; call
2324
+ * {@link compile} (or {@link processStereo}) to rebuild.
2325
+ *
2326
+ * @throws If the id is already taken, or `truePeakOversample` is outside `[0, 16]`
2327
+ */
2328
+ addStrip(id, metering = {}) {
2329
+ this.mixer.addStrip(id, metering);
2330
+ }
1581
2331
  /**
1582
2332
  * Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
1583
2333
  * `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
@@ -1643,6 +2393,30 @@ var Mixer = class _Mixer {
1643
2393
  setWidth(stripIndex, width) {
1644
2394
  this.mixer.setWidth(stripIndex, width);
1645
2395
  }
2396
+ /**
2397
+ * Snap the strip's input-trim, fader, pan and width smoothers to the values
2398
+ * already set on it, so the next processed block opens at those values
2399
+ * instead of gliding to them over the smoothing window (~5 ms).
2400
+ *
2401
+ * Call it after configuring a strip and before rendering a finite buffer: a
2402
+ * strip is smoothed for a live fader, and an offline render that does not
2403
+ * settle carries that glide as a level and image sweep across the head of
2404
+ * its output. Unlike a reset it clears nothing — automation, meters and
2405
+ * insert state are untouched.
2406
+ *
2407
+ * @param stripIndex - Strip index in `[0, stripCount())`
2408
+ *
2409
+ * @example
2410
+ * ```typescript
2411
+ * mixer.setFaderDb(0, -3);
2412
+ * mixer.setPan(0, 0.3);
2413
+ * mixer.settle(0);
2414
+ * const { left, right } = mixer.processStereo([dryLeft], [dryRight]);
2415
+ * ```
2416
+ */
2417
+ settle(stripIndex) {
2418
+ this.mixer.settle(stripIndex);
2419
+ }
1646
2420
  /** Set the strip's mute state. */
1647
2421
  setMuted(stripIndex, muted) {
1648
2422
  this.mixer.setMuted(stripIndex, muted);
@@ -1686,7 +2460,10 @@ var Mixer = class _Mixer {
1686
2460
  }
1687
2461
  /**
1688
2462
  * 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.
2463
+ *
2464
+ * Applied when the engine's track mixer renders this strip's lane into a
2465
+ * destination with more than two channels. This stereo-only mixer's own
2466
+ * block entry points ignore it.
1690
2467
  */
1691
2468
  setSurroundPan(stripIndex, pan) {
1692
2469
  this.mixer.setSurroundPan(stripIndex, pan);
@@ -1752,6 +2529,59 @@ var Mixer = class _Mixer {
1752
2529
  busMeter(busId) {
1753
2530
  return this.mixer.busMeter(busId);
1754
2531
  }
2532
+ /**
2533
+ * Number of blocks in which the strip discarded recursive state because a
2534
+ * non-finite value had reached it.
2535
+ *
2536
+ * Advisory telemetry, and the only thing that separates a degraded strip
2537
+ * from a clean one. A discard returns the affected state to its
2538
+ * post-reset value, so the strip recovers in silence and the output stays
2539
+ * finite and in range while carrying samples unrelated to the input;
2540
+ * nothing else reports that this happened.
2541
+ *
2542
+ * The count covers the strip's own state, its EQ, every insert it owns and
2543
+ * both of its meters. None of those is separately addressable here, so a
2544
+ * discard inside one is observable only through this number -- and a
2545
+ * meter that loses its loudness window then reports the floor, which is
2546
+ * exactly what a genuinely silent strip reports, so nothing else
2547
+ * distinguishes the two.
2548
+ *
2549
+ * A meter's own discard lags by one block: it checks its loudness state at
2550
+ * the top of a block, before consuming that block's samples, so the block
2551
+ * that corrupts it is not the block the count moves on -- read this again
2552
+ * after one more block has processed. The EQ and inserts have no such lag;
2553
+ * they discard at the end of their own process, in the same block that
2554
+ * carried the poison.
2555
+ *
2556
+ * Cumulative since the strip was created and never cleared, so two
2557
+ * readings bracket a span of audio. The unit is one processed block, never
2558
+ * a channel, so a stereo block that discards on both channels adds one and
2559
+ * the number does not depend on a dimension the caller did not choose.
2560
+ *
2561
+ * @param stripIndex - Strip index in `[0, stripCount())`
2562
+ */
2563
+ stripNonFiniteDiscardCount(stripIndex) {
2564
+ return this.mixer.stripNonFiniteDiscardCount(stripIndex);
2565
+ }
2566
+ /**
2567
+ * Number of blocks in which a bus discarded recursive state because a
2568
+ * non-finite value had reached it. Same contract as
2569
+ * {@link stripNonFiniteDiscardCount}, for a bus: covers every insert the
2570
+ * bus owns and its meter, neither separately addressable, so a discard
2571
+ * inside one is observable only here. Cumulative across graph recompiles
2572
+ * -- the count lives with the bus, not the compiled node, so an unrelated
2573
+ * edit elsewhere in the mixer does not reset it.
2574
+ *
2575
+ * A bus's DSP record is created by the first {@link compile}. Throws for a
2576
+ * bus that has been declared with {@link addBus} but never compiled --
2577
+ * reading zero there would read as clean, and it is not. Also throws for
2578
+ * an unknown bus id.
2579
+ *
2580
+ * @param busId - Bus id, as passed to {@link addBus} or declared in scene JSON
2581
+ */
2582
+ busNonFiniteDiscardCount(busId) {
2583
+ return this.mixer.busNonFiniteDiscardCount(busId);
2584
+ }
1755
2585
  /**
1756
2586
  * Schedule sample-accurate fader automation on a strip.
1757
2587
  *
@@ -1806,6 +2636,11 @@ var Mixer = class _Mixer {
1806
2636
  /**
1807
2637
  * Read up to `maxPoints` of a strip's most recent goniometer samples
1808
2638
  * (oldest to newest).
2639
+ *
2640
+ * `maxPoints` must be a finite non-negative integer; anything else throws an
2641
+ * `InvalidParameter` error. It is a request rather than an allocation size —
2642
+ * a value beyond the strip's goniometer ring simply returns every point the
2643
+ * ring holds.
1809
2644
  */
1810
2645
  readGoniometerLatest(stripIndex, maxPoints) {
1811
2646
  return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
@@ -1841,8 +2676,12 @@ var Mixer = class _Mixer {
1841
2676
  }
1842
2677
  return this.mixer.drainTailStereo(numSamples);
1843
2678
  }
1844
- /** Release the underlying WASM object. Safe to call only once. */
2679
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
1845
2680
  delete() {
2681
+ if (this.released) {
2682
+ return;
2683
+ }
2684
+ this.released = true;
1846
2685
  this.mixer.delete();
1847
2686
  }
1848
2687
  /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
@@ -1859,10 +2698,19 @@ var RealtimeVoiceChanger = class {
1859
2698
  * explicit {@link prepare} lifecycle for callers that configure later.
1860
2699
  */
1861
2700
  constructor(config = "neutral-monitor", sampleRate, maxBlockSize = 128, channels = 1) {
2701
+ this.released = false;
1862
2702
  const module2 = getSonareModule();
1863
2703
  this.changer = module2.createRealtimeVoiceChanger(config);
1864
2704
  if (sampleRate !== void 0) {
1865
- this.changer.prepare(sampleRate, maxBlockSize, channels);
2705
+ let prepared = false;
2706
+ try {
2707
+ this.changer.prepare(sampleRate, maxBlockSize, channels);
2708
+ prepared = true;
2709
+ } finally {
2710
+ if (!prepared) {
2711
+ this.changer.delete();
2712
+ }
2713
+ }
1866
2714
  }
1867
2715
  }
1868
2716
  prepare(sampleRate, maxBlockSize = 128, channels = 1) {
@@ -1888,6 +2736,34 @@ var RealtimeVoiceChanger = class {
1888
2736
  latencySamples() {
1889
2737
  return this.changer.latencySamples();
1890
2738
  }
2739
+ /**
2740
+ * Channel-blocks in which the chain discarded its own state because a
2741
+ * non-finite value had reached it.
2742
+ *
2743
+ * Advisory telemetry, and the only thing that separates a degraded stream
2744
+ * from a clean one. Every stage of this chain leaves an in-domain finite
2745
+ * value where a non-finite one was — the input scrub and the
2746
+ * inter-sample-peak limiter substitute silence, the sample-domain limiter
2747
+ * folds an infinity onto its ceiling — so the output stays finite, in range
2748
+ * and free of any error while carrying samples unrelated to the input. A
2749
+ * non-zero count is what says the samples in between were not computed from
2750
+ * what you supplied.
2751
+ *
2752
+ * Monotonic for the lifetime of the instance. The unit is one processed
2753
+ * block, never a channel, so a stereo block that discards on both channels
2754
+ * adds one and the number does not depend on a dimension you did not choose.
2755
+ *
2756
+ * @example
2757
+ * ```ts
2758
+ * changer.processInterleaved(block, 2);
2759
+ * if (changer.nonFiniteDiscardCount() > 0) {
2760
+ * // the audio just produced is not a function of `block`
2761
+ * }
2762
+ * ```
2763
+ */
2764
+ nonFiniteDiscardCount() {
2765
+ return this.changer.nonFiniteDiscardCount();
2766
+ }
1891
2767
  /**
1892
2768
  * Monotonically increases whenever {@link prepare} can replace the native
1893
2769
  * scratch buffers. Cached WASM heap views must be reacquired after it changes.
@@ -2055,14 +2931,24 @@ var RealtimeVoiceChanger = class {
2055
2931
  }
2056
2932
  };
2057
2933
  }
2934
+ /** Releases the native handle. Idempotent, as the Node facade is. */
2058
2935
  delete() {
2936
+ if (this.released) {
2937
+ return;
2938
+ }
2939
+ this.released = true;
2059
2940
  this.changer.delete();
2060
2941
  }
2942
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
2943
+ destroy() {
2944
+ this.delete();
2945
+ }
2061
2946
  };
2062
2947
 
2063
2948
  // src/streaming_processors.ts
2064
2949
  var StreamingMasteringChain = class {
2065
2950
  constructor(config) {
2951
+ this.released = false;
2066
2952
  const module2 = getSonareModule();
2067
2953
  const { loudnessStaticGainDb, loudnessStaticGainPeakDb, ...chainConfig } = config;
2068
2954
  this.chain = module2.createStreamingMasteringChain({
@@ -2121,10 +3007,75 @@ var StreamingMasteringChain = class {
2121
3007
  stageNames() {
2122
3008
  return this.chain.stageNames();
2123
3009
  }
2124
- /** Release the underlying WASM object. Safe to call only once. */
3010
+ /**
3011
+ * Samples a stage replaced with a finite in-domain one, keeping the output
3012
+ * finite and in range.
3013
+ *
3014
+ * A non-finite sample supplied by the caller is rejected before any stage
3015
+ * runs, so a replacement is always of a value a stage itself produced.
3016
+ *
3017
+ * Only the true-peak limiters replace anything, so with the maximizer's
3018
+ * limiter and the loudness stage both disabled a zero here means no stage was
3019
+ * able to replace anything rather than that nothing needed replacing.
3020
+ *
3021
+ * Cumulative over every block since {@link prepare}, and aggregated over the
3022
+ * stages and channels, so it identifies neither which block nor which stage.
3023
+ * Read it per block and compare against the previous reading to localize one.
3024
+ *
3025
+ * {@link prepare} rebuilds the stages and so clears it; {@link reset} does
3026
+ * not, because it drops processor state without rebuilding.
3027
+ *
3028
+ * @example
3029
+ * ```typescript
3030
+ * chain.processMono(block);
3031
+ * if (chain.nonFiniteSubstitutionCount() > previous) {
3032
+ * // the block just produced is not derived from `block` everywhere
3033
+ * }
3034
+ * ```
3035
+ */
3036
+ nonFiniteSubstitutionCount() {
3037
+ return this.chain.nonFiniteSubstitutionCount();
3038
+ }
3039
+ /**
3040
+ * Processing calls in which a stage discarded its own recursive state
3041
+ * because a non-finite value had reached it.
3042
+ *
3043
+ * The companion to {@link nonFiniteSubstitutionCount}, and not the same
3044
+ * measurement -- a caller who assumes they are will read one and think
3045
+ * they have the other. That one counts SAMPLES a stage replaced and so
3046
+ * sums across stages; a discard is a whole stage returning to its
3047
+ * post-reset value and is counted once per call however many stages did
3048
+ * it. A stage may run more than once per call, which is why this is a
3049
+ * delta over the call and never a sum.
3050
+ *
3051
+ * Non-finite input is rejected before any stage runs, so what a stage
3052
+ * discards is always state it produced itself -- a finite sample large
3053
+ * enough to overflow inside a filter, most often. Unlike the substitution
3054
+ * count every stage can contribute, so a zero here means no stage
3055
+ * discarded rather than that none could.
3056
+ *
3057
+ * Both {@link processMono}/{@link processStereo} and
3058
+ * {@link flushMono}/{@link flushStereo} count, since a flush drives the
3059
+ * same stages. {@link prepare} rebuilds the stages and so clears it (as it
3060
+ * does {@link nonFiniteSubstitutionCount}, so the two counters on one
3061
+ * handle share an epoch); {@link reset} does not, because it drops
3062
+ * processor state without rebuilding.
3063
+ */
3064
+ nonFiniteDiscardCount() {
3065
+ return this.chain.nonFiniteDiscardCount();
3066
+ }
3067
+ /** Release the underlying WASM object. Idempotent, as the Node facade is. */
2125
3068
  delete() {
3069
+ if (this.released) {
3070
+ return;
3071
+ }
3072
+ this.released = true;
2126
3073
  this.chain.delete();
2127
3074
  }
3075
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
3076
+ destroy() {
3077
+ this.delete();
3078
+ }
2128
3079
  };
2129
3080
 
2130
3081
  // src/index.ts
@@ -2175,11 +3126,11 @@ function buildTempoSync(tempoBpm, timeSignature, tempoSegments, timeSignatureSeg
2175
3126
  };
2176
3127
  }
2177
3128
  function resolveTargetId(target) {
2178
- if (typeof target === "number") {
2179
- return target;
3129
+ const value = typeof target === "number" ? target : target.trim() === "" ? Number.NaN : Number(target);
3130
+ if (!Number.isInteger(value)) {
3131
+ throw new RangeError(`target id must be an integer, got ${JSON.stringify(target)}`);
2180
3132
  }
2181
- const parsed = Number.parseInt(target, 10);
2182
- return Number.isFinite(parsed) ? parsed : 0;
3133
+ return value;
2183
3134
  }
2184
3135
  function resolveParamId(parameters, nodeId, param) {
2185
3136
  if (typeof param === "number") {
@@ -2222,101 +3173,6 @@ function setAutomationLane(ctx, paramId, points) {
2222
3173
  ctx.postSync({ type: "syncAutomation", paramId, points: sorted });
2223
3174
  }
2224
3175
 
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
3176
  // src/worklet/protocol.ts
2321
3177
  var ENGINE_MIXER_TARGET_BASE = 1297612800;
2322
3178
  var ENGINE_MIXER_PARAM_FADER_DB = 1;
@@ -2403,11 +3259,9 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
2403
3259
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["MetronomeOverflow"] = 18] = "MetronomeOverflow";
2404
3260
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["InvalidCommand"] = 19] = "InvalidCommand";
2405
3261
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["MaxChannelsExceeded"] = 20] = "MaxChannelsExceeded";
3262
+ SonareEngineTelemetryError2[SonareEngineTelemetryError2["ParameterBaseOverflow"] = 21] = "ParameterBaseOverflow";
2406
3263
  return SonareEngineTelemetryError2;
2407
3264
  })(SonareEngineTelemetryError || {});
2408
- function toDb(value) {
2409
- return value > 0 ? 20 * Math.log10(value) : Number.NEGATIVE_INFINITY;
2410
- }
2411
3265
  function isRecord(value) {
2412
3266
  return typeof value === "object" && value !== null;
2413
3267
  }
@@ -2886,6 +3740,16 @@ function clipPageRequestRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
2886
3740
  function recordOffset(index, capacity, recordBytes) {
2887
3741
  return index % capacity * recordBytes;
2888
3742
  }
3743
+ function isUint32Slot(value) {
3744
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 4294967295;
3745
+ }
3746
+ function toUint32Slot(value, fallback, name) {
3747
+ const resolved = value ?? fallback;
3748
+ if (!isUint32Slot(resolved)) {
3749
+ throw new RangeError(`${name} must be an integer within [0, 4294967295]`);
3750
+ }
3751
+ return resolved;
3752
+ }
2889
3753
  function toSafeInteger(value, fallback) {
2890
3754
  const resolved = typeof value === "bigint" ? Number(value) : value;
2891
3755
  if (resolved === void 0) {
@@ -2905,8 +3769,8 @@ function readInt64Words(view, offset) {
2905
3769
  return view.getInt32(offset + 4, true) * 4294967296 + view.getUint32(offset, true);
2906
3770
  }
2907
3771
  function writeEngineCommandRecord(view, offset, command) {
2908
- view.setUint32(offset, command.type, true);
2909
- view.setUint32(offset + 4, command.targetId ?? 0, true);
3772
+ view.setUint32(offset, toUint32Slot(command.type, 0, "type"), true);
3773
+ view.setUint32(offset + 4, toUint32Slot(command.targetId, 0, "targetId"), true);
2910
3774
  writeInt64Words(view, offset + 8, toSafeInteger(command.sampleTime, -1));
2911
3775
  view.setFloat64(offset + 16, command.argFloat ?? 0, true);
2912
3776
  writeInt64Words(view, offset + 24, toSafeInteger(command.argInt, 0));
@@ -2975,47 +3839,341 @@ function magnitudeToDb(value) {
2975
3839
  return value > 1e-12 ? 20 * Math.log10(value) : -120;
2976
3840
  }
2977
3841
 
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");
3842
+ // src/worklet/guards.ts
3843
+ function isWorkletMessage(value) {
3844
+ if (!isRecord(value) || typeof value.type !== "string") {
3845
+ return false;
2994
3846
  }
2995
- ctx.offlineEngine.armCapture(enabled);
2996
- return ctx.sendCommand({
2997
- type: 13 /* ArmRecord */,
2998
- targetId: 0,
2999
- sampleTime: -1,
3000
- argInt: enabled ? 1 : 0
3001
- });
3847
+ return value.type === "scheduleInsertAutomation" || value.type === "setMeterInterval" || value.type === "destroy";
3002
3848
  }
3003
- function punch(ctx, inPpq, outPpq) {
3004
- const inSample = ctx.offlineEngine.sampleAtPpq(inPpq);
3005
- const outSample = ctx.offlineEngine.sampleAtPpq(outPpq);
3006
- ctx.offlineEngine.setCapturePunch(inSample, outSample, true);
3007
- return ctx.sendCommand({
3008
- type: 14 /* Punch */,
3009
- sampleTime: -1,
3010
- argInt: inSample,
3011
- argFloat: outSample
3012
- });
3849
+ function isEngineCommandRecord(value) {
3850
+ return isRecord(value) && typeof value.type === "number";
3013
3851
  }
3014
- function captureStatus(ctx) {
3015
- return ctx.realtimeNode.requestCaptureStatus();
3852
+ var ENGINE_SYNC_MESSAGE_TYPES = {
3853
+ destroy: true,
3854
+ syncAutomation: true,
3855
+ syncBuiltinInstrument: true,
3856
+ syncBusStripDualPan: true,
3857
+ syncBusStripEqBand: true,
3858
+ syncBusStripInsertBypassed: true,
3859
+ syncBusStripInsertParamByName: true,
3860
+ syncBusStripPan: true,
3861
+ syncBusStripPanLaw: true,
3862
+ syncBusStripPanMode: true,
3863
+ syncCapture: true,
3864
+ syncClearMidiFx: true,
3865
+ syncClearMidiInputSource: true,
3866
+ syncClipPage: true,
3867
+ syncClipPageClear: true,
3868
+ syncClipPageCommit: true,
3869
+ syncClipPageDestroy: true,
3870
+ syncClipPagePrefetchFrames: true,
3871
+ syncClipPageProvider: true,
3872
+ syncClips: true,
3873
+ syncClipsDelta: true,
3874
+ syncExternalMidiClock: true,
3875
+ syncLoadSoundFont: true,
3876
+ syncMarkers: true,
3877
+ syncMasterStripEqBand: true,
3878
+ syncMasterStripInsertBypassed: true,
3879
+ syncMasterStripInsertParamByName: true,
3880
+ syncMetronome: true,
3881
+ syncMidiCc: true,
3882
+ syncMidiCcBinding: true,
3883
+ syncMidiChannelPressure: true,
3884
+ syncMidiClips: true,
3885
+ syncMidiDestinationExternal: true,
3886
+ syncMidiFx: true,
3887
+ syncMidiInputChannelPressure: true,
3888
+ syncMidiInputCc: true,
3889
+ syncMidiInputNoteOff: true,
3890
+ syncMidiInputNoteOn: true,
3891
+ syncMidiInputPitchBend: true,
3892
+ syncMidiInputPolyPressure: true,
3893
+ syncMidiInputSource: true,
3894
+ syncMidiNoteOff: true,
3895
+ syncMidiNoteOn: true,
3896
+ syncMidiPanic: true,
3897
+ syncMidiPitchBend: true,
3898
+ syncMidiPolyPressure: true,
3899
+ syncMidiSysex: true,
3900
+ syncMidiUmp: true,
3901
+ syncMixer: true,
3902
+ syncParameters: true,
3903
+ syncSf2Instrument: true,
3904
+ syncSynthInstrument: true,
3905
+ syncTempo: true,
3906
+ syncTrackStripChannelDelaySamples: true,
3907
+ syncTrackStripDualPan: true,
3908
+ syncTrackStripEqBand: true,
3909
+ syncTrackStripInsertBypassed: true,
3910
+ syncTrackStripInsertParamByName: true,
3911
+ syncTrackStripPan: true,
3912
+ syncTrackStripPanLaw: true,
3913
+ syncTrackStripPanMode: true,
3914
+ syncWarpVoiceCapacity: true
3915
+ };
3916
+ var engineSyncMessageTypes = new Set(Object.keys(ENGINE_SYNC_MESSAGE_TYPES));
3917
+ function isEngineSyncMessage(value) {
3918
+ return isRecord(value) && typeof value.type === "string" && engineSyncMessageTypes.has(value.type);
3016
3919
  }
3017
- function capturedAudio(ctx) {
3018
- return ctx.realtimeNode.requestCapturedAudio();
3920
+ function isEngineCaptureRequestMessage(value) {
3921
+ return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
3922
+ }
3923
+ function isCaptureStatus(value) {
3924
+ return isRecord(value) && typeof value.capturedFrames === "number" && Number.isSafeInteger(value.capturedFrames) && value.capturedFrames >= 0 && typeof value.overflowCount === "number" && Number.isSafeInteger(value.overflowCount) && value.overflowCount >= 0 && typeof value.armed === "boolean" && typeof value.punchEnabled === "boolean" && (value.source === "input" || value.source === "output") && typeof value.recordOffsetSamples === "number" && Number.isSafeInteger(value.recordOffsetSamples);
3925
+ }
3926
+ function isCaptureChannel(value) {
3927
+ return value instanceof Float32Array && typeof ArrayBuffer !== "undefined" && value.buffer instanceof ArrayBuffer;
3928
+ }
3929
+ function isCaptureChannels(value) {
3930
+ return Array.isArray(value) && value.every((channel) => isCaptureChannel(channel));
3931
+ }
3932
+ function hasOwn(value, key) {
3933
+ return Object.prototype.hasOwnProperty.call(value, key);
3934
+ }
3935
+ function engineCaptureResponseRequestId(value) {
3936
+ if (!isRecord(value) || value.type !== "captureResponse" || typeof value.requestId !== "number" || !Number.isSafeInteger(value.requestId)) {
3937
+ return void 0;
3938
+ }
3939
+ return value.requestId;
3940
+ }
3941
+ function isEngineCaptureResponseMessage(value) {
3942
+ const requestId = engineCaptureResponseRequestId(value);
3943
+ if (requestId === void 0 || !isRecord(value)) {
3944
+ return false;
3945
+ }
3946
+ if (value.ok === false) {
3947
+ return typeof value.error === "string" && !hasOwn(value, "status") && !hasOwn(value, "channels");
3948
+ }
3949
+ if (value.ok !== true || hasOwn(value, "error")) {
3950
+ return false;
3951
+ }
3952
+ const hasStatus = hasOwn(value, "status");
3953
+ const hasChannels = hasOwn(value, "channels");
3954
+ if (hasStatus && hasChannels) {
3955
+ return false;
3956
+ }
3957
+ if (hasStatus) {
3958
+ return isCaptureStatus(value.status);
3959
+ }
3960
+ if (hasChannels) {
3961
+ return isCaptureChannels(value.channels);
3962
+ }
3963
+ return true;
3964
+ }
3965
+ function isEngineCaptureResponseForOperation(response, op) {
3966
+ if (!response.ok) {
3967
+ return true;
3968
+ }
3969
+ switch (op) {
3970
+ case "status":
3971
+ return "status" in response;
3972
+ case "read":
3973
+ return "channels" in response;
3974
+ case "reset":
3975
+ return !("status" in response) && !("channels" in response);
3976
+ }
3977
+ }
3978
+ function isEngineTransportRequestMessage(value) {
3979
+ return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
3980
+ }
3981
+ function isEngineTransportResponseMessage(value) {
3982
+ return isRecord(value) && value.type === "transportResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
3983
+ }
3984
+ function isRealtimeVoiceChangerMessage(value) {
3985
+ if (!isRecord(value) || typeof value.type !== "string") {
3986
+ return false;
3987
+ }
3988
+ return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
3989
+ }
3990
+ var isOptionalNumber = (value) => value === void 0 || typeof value === "number";
3991
+ function isPlaybackMessage(value) {
3992
+ if (!isRecord(value) || typeof value.type !== "string") {
3993
+ return false;
3994
+ }
3995
+ switch (value.type) {
3996
+ case "config":
3997
+ return typeof value.config === "string";
3998
+ case "orientation":
3999
+ return typeof value.yaw === "number" && isOptionalNumber(value.pitch) && isOptionalNumber(value.roll);
4000
+ case "reset":
4001
+ case "diagnostics":
4002
+ case "destroy":
4003
+ return true;
4004
+ default:
4005
+ return false;
4006
+ }
4007
+ }
4008
+ function isEngineTelemetryRecord(value) {
4009
+ return isRecord(value) && typeof value.type === "number" && typeof value.error === "number" && typeof value.renderFrame === "number" && typeof value.timelineSample === "number" && typeof value.audibleTimelineSample === "number" && typeof value.graphLatencySamplesQ8 === "number" && typeof value.value === "number";
4010
+ }
4011
+ function isExternalMidiBatchMessage(value) {
4012
+ return isRecord(value) && value.type === "externalMidi" && Array.isArray(value.events);
4013
+ }
4014
+ function isClipPageRequestMessage(value) {
4015
+ return isRecord(value) && value.type === "clipPageRequest" && Array.isArray(value.requests) && value.requests.every(
4016
+ (request) => isRecord(request) && typeof request.clipId === "number" && typeof request.pageIndex === "number"
4017
+ );
4018
+ }
4019
+ function isMeterSnapshot(value) {
4020
+ return isRecord(value) && value.type === "meter" && typeof value.frame === "number" && typeof value.peakDbL === "number" && typeof value.peakDbR === "number" && typeof value.rmsDbL === "number" && typeof value.rmsDbR === "number" && typeof value.correlation === "number" && (typeof value.targetId === "number" || value.targetId === void 0);
4021
+ }
4022
+ function requireIntegerOption(value, fallback, name, minimum) {
4023
+ const resolved = value ?? fallback;
4024
+ if (!Number.isSafeInteger(resolved) || resolved < minimum) {
4025
+ throw new RangeError(`${name} must be an integer of at least ${minimum}`);
4026
+ }
4027
+ return resolved;
4028
+ }
4029
+ function requireInteger(value, fallback, name) {
4030
+ const resolved = value ?? fallback;
4031
+ if (!Number.isSafeInteger(resolved)) {
4032
+ throw new RangeError(`${name} must be an integer`);
4033
+ }
4034
+ return resolved;
4035
+ }
4036
+ function requireChannelCount(channelCount, fallback) {
4037
+ return requireIntegerOption(channelCount, fallback, "channelCount", 1);
4038
+ }
4039
+
4040
+ // src/worklet/engine-offline.ts
4041
+ function buildCaptureConfig(options, defaultChannels) {
4042
+ return {
4043
+ // bufferFrames has no default; NaN makes the guard refuse an absent value.
4044
+ bufferFrames: requireIntegerOption(options.bufferFrames, Number.NaN, "bufferFrames", 1),
4045
+ channels: requireChannelCount(options.channels, defaultChannels),
4046
+ source: options.source ?? "output",
4047
+ recordOffsetSamples: requireInteger(options.recordOffsetSamples, 0, "recordOffsetSamples"),
4048
+ inputMonitor: {
4049
+ enabled: Boolean(options.inputMonitor?.enabled),
4050
+ gain: options.inputMonitor?.gain ?? 1
4051
+ }
4052
+ };
4053
+ }
4054
+ function buildTransportFacade(ctx) {
4055
+ return {
4056
+ play: (sampleTime = -1) => {
4057
+ const ok = ctx.realtimeNode.play(sampleTime);
4058
+ if (ok) {
4059
+ ctx.setTransportPlaying(true);
4060
+ }
4061
+ return ok;
4062
+ },
4063
+ stop: (sampleTime = -1) => {
4064
+ const ok = ctx.realtimeNode.stop(sampleTime);
4065
+ if (ok) {
4066
+ ctx.setTransportPlaying(false);
4067
+ ctx.flushPendingInstrumentSync();
4068
+ }
4069
+ return ok;
4070
+ },
4071
+ seekPpq: (ppq, sampleTime = -1) => {
4072
+ ctx.offlineEngine.seekPpq(ppq, sampleTime);
4073
+ const ok = ctx.realtimeNode.seekPpq(ppq, sampleTime);
4074
+ ctx.flushOfflineMirror();
4075
+ return ok;
4076
+ },
4077
+ seekSeconds: (seconds, sampleTime = -1) => {
4078
+ const timelineSample = Math.max(0, Math.round(seconds * ctx.sampleRate));
4079
+ ctx.offlineEngine.seekSample(timelineSample, sampleTime);
4080
+ const ok = ctx.realtimeNode.seekSample(timelineSample, sampleTime);
4081
+ ctx.flushOfflineMirror();
4082
+ return ok;
4083
+ },
4084
+ setTempo: (bpm) => ctx.setTempo(bpm),
4085
+ setTempoSegments: (segments) => ctx.setTempoSegments(segments),
4086
+ setLoop: (startPpq, endPpq, enabled = true) => ctx.setLoop(startPpq, endPpq, enabled)
4087
+ };
4088
+ }
4089
+ function normalizeTrackLanes(existing, lanes) {
4090
+ const entries = lanes.map((lane) => typeof lane === "number" ? { trackId: lane } : lane);
4091
+ const ids = [];
4092
+ for (const entry of entries) {
4093
+ if (!Number.isInteger(entry.trackId) || entry.trackId <= 0) {
4094
+ throw new RangeError(`Invalid track id for mixer lane: ${String(entry.trackId)}`);
4095
+ }
4096
+ ids.push(entry.trackId);
4097
+ }
4098
+ if (new Set(ids).size !== ids.length) {
4099
+ throw new Error("Duplicate track id in mixer lane list");
4100
+ }
4101
+ for (let index = 0; index < existing.length; index++) {
4102
+ if (ids[index] !== existing[index]) {
4103
+ throw new Error(
4104
+ "Mixer lanes are append-only: keep existing lanes in order and only append new track ids"
4105
+ );
4106
+ }
4107
+ }
4108
+ return { entries, ids };
4109
+ }
4110
+ function resolveMarkerSet(markers, nextMarkerId) {
4111
+ const resolved = [];
4112
+ const seen = /* @__PURE__ */ new Set();
4113
+ let counter = nextMarkerId;
4114
+ for (const marker2 of markers) {
4115
+ if (!Number.isFinite(marker2.ppq) || marker2.ppq < 0) {
4116
+ throw new RangeError(`Invalid marker ppq: ${String(marker2.ppq)}`);
4117
+ }
4118
+ if (marker2.id !== void 0) {
4119
+ if (!Number.isInteger(marker2.id) || marker2.id <= 0) {
4120
+ throw new RangeError(`Invalid marker id: ${String(marker2.id)}`);
4121
+ }
4122
+ if (seen.has(marker2.id)) {
4123
+ throw new Error(`Duplicate marker id: ${marker2.id}`);
4124
+ }
4125
+ }
4126
+ const id = marker2.id ?? counter++;
4127
+ seen.add(id);
4128
+ if (id >= counter) {
4129
+ counter = id + 1;
4130
+ }
4131
+ resolved.push({ id, ppq: marker2.ppq, name: marker2.name ?? "" });
4132
+ }
4133
+ return { resolved, nextMarkerId: counter };
4134
+ }
4135
+
4136
+ // src/worklet/engine-capture-facade.ts
4137
+ function configureCapture(ctx, options) {
4138
+ const config = buildCaptureConfig(options, ctx.offlineChannelCount);
4139
+ ctx.offlineEngine.setCaptureBuffer(config.channels, config.bufferFrames);
4140
+ ctx.offlineEngine.setCaptureSource(config.source);
4141
+ ctx.offlineEngine.setRecordOffsetSamples(config.recordOffsetSamples);
4142
+ ctx.offlineEngine.setInputMonitor(config.inputMonitor.enabled, config.inputMonitor.gain);
4143
+ ctx.setCaptureConfig(config);
4144
+ ctx.postSync({ type: "syncCapture", ...config });
4145
+ }
4146
+ function armRecord(ctx, trackId, enabled) {
4147
+ if (trackId !== 0) {
4148
+ throw new RangeError("Capture is global; armRecord only accepts trackId 0");
4149
+ }
4150
+ if (enabled && !ctx.getCaptureConfig()) {
4151
+ throw new Error("Capture buffer is not configured");
4152
+ }
4153
+ ctx.offlineEngine.armCapture(enabled);
4154
+ return ctx.sendCommand({
4155
+ type: 13 /* ArmRecord */,
4156
+ targetId: 0,
4157
+ sampleTime: -1,
4158
+ argInt: enabled ? 1 : 0
4159
+ });
4160
+ }
4161
+ function punch(ctx, inPpq, outPpq) {
4162
+ const inSample = ctx.offlineEngine.sampleAtPpq(inPpq);
4163
+ const outSample = ctx.offlineEngine.sampleAtPpq(outPpq);
4164
+ ctx.offlineEngine.setCapturePunch(inSample, outSample, true);
4165
+ return ctx.sendCommand({
4166
+ type: 14 /* Punch */,
4167
+ sampleTime: -1,
4168
+ argInt: inSample,
4169
+ argFloat: outSample
4170
+ });
4171
+ }
4172
+ function captureStatus(ctx) {
4173
+ return ctx.realtimeNode.requestCaptureStatus();
4174
+ }
4175
+ function capturedAudio(ctx) {
4176
+ return ctx.realtimeNode.requestCapturedAudio();
3019
4177
  }
3020
4178
  async function resetCapture(ctx) {
3021
4179
  ctx.offlineEngine.resetCapture();
@@ -3167,16 +4325,191 @@ function syncMarkers(ctx) {
3167
4325
  }
3168
4326
 
3169
4327
  // src/worklet/engine-mixer-facade.ts
4328
+ function emptyStripJson(target) {
4329
+ switch (target.kind) {
4330
+ case "track":
4331
+ return `{"version":1,"strips":[{"id":"track-${target.trackId}"}],"buses":[],"connections":[]}`;
4332
+ case "bus":
4333
+ return `{"version":1,"strips":[],"buses":[{"id":"bus-${target.busId}"}],"connections":[]}`;
4334
+ case "master":
4335
+ return '{"version":1,"strips":[{"id":"master"}],"buses":[],"connections":[]}';
4336
+ }
4337
+ }
4338
+ function pruneStripSidechains(ctx, target, sceneJson) {
4339
+ const scene = JSON.parse(sceneJson);
4340
+ const entry = target.kind === "bus" ? scene.buses?.[0] : scene.strips?.[0];
4341
+ const insertCount = Array.isArray(entry?.inserts) ? entry.inserts.length : 0;
4342
+ if (target.kind === "track") {
4343
+ for (const [key, binding] of ctx.laneSidechains) {
4344
+ if (binding.trackId === target.trackId && binding.insertIndex >= insertCount) {
4345
+ ctx.laneSidechains.delete(key);
4346
+ }
4347
+ }
4348
+ } else if (target.kind === "bus") {
4349
+ for (const [key, binding] of ctx.busSidechains) {
4350
+ if (binding.busId === target.busId && binding.insertIndex >= insertCount) {
4351
+ ctx.busSidechains.delete(key);
4352
+ }
4353
+ }
4354
+ } else {
4355
+ for (const [key, binding] of ctx.masterSidechains) {
4356
+ if (binding.insertIndex >= insertCount) {
4357
+ ctx.masterSidechains.delete(key);
4358
+ }
4359
+ }
4360
+ }
4361
+ }
4362
+ function hasInsertParamOverrides(overrides, target) {
4363
+ for (const override of overrides.values()) {
4364
+ if (sameStripTarget(override.target, target)) {
4365
+ return true;
4366
+ }
4367
+ }
4368
+ return false;
4369
+ }
4370
+ function applyFullStripJson(engine, target, sceneJson, resetInserts) {
4371
+ const apply = (json) => {
4372
+ switch (target.kind) {
4373
+ case "track":
4374
+ engine.setTrackStripJson(target.trackId, json);
4375
+ break;
4376
+ case "bus":
4377
+ engine.setBusStripJson(target.busId, json);
4378
+ break;
4379
+ case "master":
4380
+ engine.setMasterStripJson(json);
4381
+ break;
4382
+ }
4383
+ };
4384
+ engine.applyCommandsDueNowPreservingFuture();
4385
+ apply(sceneJson);
4386
+ if (resetInserts) {
4387
+ switch (target.kind) {
4388
+ case "track":
4389
+ engine.clearTrackInsertParameterBases(target.trackId);
4390
+ break;
4391
+ case "bus":
4392
+ engine.clearBusInsertParameterBases(target.busId);
4393
+ break;
4394
+ case "master":
4395
+ engine.clearMasterInsertParameterBases();
4396
+ break;
4397
+ }
4398
+ apply(emptyStripJson(target));
4399
+ apply(sceneJson);
4400
+ }
4401
+ }
4402
+ function insertParamOverrideKey(override) {
4403
+ const targetId = override.target.kind === "track" ? override.target.trackId : override.target.kind === "bus" ? override.target.busId : "master";
4404
+ return JSON.stringify([override.target.kind, targetId, override.insertIndex, override.paramName]);
4405
+ }
4406
+ function clearInsertParamOverrides(overrides, target) {
4407
+ for (const [key, override] of overrides) {
4408
+ if (sameStripTarget(override.target, target)) {
4409
+ overrides.delete(key);
4410
+ }
4411
+ }
4412
+ }
4413
+ function sameStripTarget(left, right) {
4414
+ if (left.kind !== right.kind) {
4415
+ return false;
4416
+ }
4417
+ if (left.kind === "track" && right.kind === "track") {
4418
+ return left.trackId === right.trackId;
4419
+ }
4420
+ if (left.kind === "bus" && right.kind === "bus") {
4421
+ return left.busId === right.busId;
4422
+ }
4423
+ return left.kind === "master" && right.kind === "master";
4424
+ }
4425
+ function flattenInsertParamOverride(override) {
4426
+ switch (override.target.kind) {
4427
+ case "track":
4428
+ return {
4429
+ kind: "track",
4430
+ trackId: override.target.trackId,
4431
+ insertIndex: override.insertIndex,
4432
+ paramName: override.paramName,
4433
+ value: override.value
4434
+ };
4435
+ case "bus":
4436
+ return {
4437
+ kind: "bus",
4438
+ busId: override.target.busId,
4439
+ insertIndex: override.insertIndex,
4440
+ paramName: override.paramName,
4441
+ value: override.value
4442
+ };
4443
+ case "master":
4444
+ return {
4445
+ kind: "master",
4446
+ insertIndex: override.insertIndex,
4447
+ paramName: override.paramName,
4448
+ value: override.value
4449
+ };
4450
+ }
4451
+ }
4452
+ function cachedStripJson(ctx, target) {
4453
+ switch (target.kind) {
4454
+ case "track":
4455
+ return ctx.trackStripJson.get(target.trackId);
4456
+ case "bus":
4457
+ return ctx.busStripJson.get(target.busId);
4458
+ case "master":
4459
+ return ctx.getMasterStripJson();
4460
+ }
4461
+ }
4462
+ function cacheStripJson(ctx, target, sceneJson) {
4463
+ switch (target.kind) {
4464
+ case "track":
4465
+ ctx.trackStripJson.set(target.trackId, sceneJson);
4466
+ return;
4467
+ case "bus":
4468
+ ctx.busStripJson.set(target.busId, sceneJson);
4469
+ return;
4470
+ case "master":
4471
+ ctx.cacheMasterStripJson(sceneJson);
4472
+ return;
4473
+ }
4474
+ }
3170
4475
  function mixerLanes(ctx) {
3171
4476
  return buildMixerLanes(ctx.trackLaneIds, ctx.trackSends, ctx.trackOutputBus);
3172
4477
  }
3173
- function syncMixer(ctx) {
4478
+ function syncMixer(ctx, busesAlreadyApplied = false, forceInsertResets = []) {
3174
4479
  const lanes = mixerLanes(ctx);
3175
4480
  const buses = ctx.buses.map((bus) => ({ ...bus }));
3176
- ctx.offlineEngine.setTrackBuses(buses);
4481
+ if (!busesAlreadyApplied) {
4482
+ ctx.offlineEngine.setTrackBuses(buses);
4483
+ }
3177
4484
  if (lanes.length > 0) {
4485
+ ctx.offlineEngine.settleInsertParameters();
3178
4486
  ctx.offlineEngine.setTrackLanes(lanes);
3179
4487
  }
4488
+ if (forceInsertResets.length > 0) {
4489
+ for (const binding of ctx.laneSidechains.values()) {
4490
+ ctx.offlineEngine.setLaneSidechain(
4491
+ binding.trackId,
4492
+ binding.insertIndex,
4493
+ binding.sourceTrackId
4494
+ );
4495
+ }
4496
+ for (const binding of ctx.busSidechains.values()) {
4497
+ ctx.offlineEngine.setBusSidechain(
4498
+ binding.busId,
4499
+ binding.insertIndex,
4500
+ binding.sourceKind,
4501
+ binding.sourceId
4502
+ );
4503
+ }
4504
+ for (const binding of ctx.masterSidechains.values()) {
4505
+ ctx.offlineEngine.setMasterSidechain(
4506
+ binding.insertIndex,
4507
+ binding.sourceKind,
4508
+ binding.sourceId
4509
+ );
4510
+ }
4511
+ }
4512
+ replayInsertParamOverrides(ctx, lanes, buses);
3180
4513
  const trackStrips = Array.from(ctx.trackStripJson, ([trackId, sceneJson]) => ({
3181
4514
  trackId,
3182
4515
  sceneJson
@@ -3185,6 +4518,10 @@ function syncMixer(ctx) {
3185
4518
  busId,
3186
4519
  sceneJson
3187
4520
  }));
4521
+ const insertParamOverrides = Array.from(
4522
+ ctx.insertParamOverrides.values(),
4523
+ flattenInsertParamOverride
4524
+ );
3188
4525
  ctx.postSync({
3189
4526
  type: "syncMixer",
3190
4527
  lanes,
@@ -3192,9 +4529,49 @@ function syncMixer(ctx) {
3192
4529
  trackStrips,
3193
4530
  laneSidechains: Array.from(ctx.laneSidechains.values()),
3194
4531
  busStrips,
3195
- masterStripJson: ctx.getMasterStripJson()
4532
+ masterStripJson: ctx.getMasterStripJson(),
4533
+ busSidechains: Array.from(ctx.busSidechains.values()),
4534
+ masterSidechains: Array.from(ctx.masterSidechains.values()),
4535
+ ...insertParamOverrides.length > 0 ? { insertParamOverrides } : {},
4536
+ ...forceInsertResets.length > 0 ? { forceInsertResets } : {}
3196
4537
  });
3197
4538
  }
4539
+ function replayInsertParamOverrides(ctx, lanes = mixerLanes(ctx), buses = ctx.buses) {
4540
+ const activeTrackIds = new Set(lanes.map((lane) => lane.trackId));
4541
+ const activeBusIds = new Set(buses.map((bus) => bus.busId));
4542
+ for (const [key, override] of ctx.insertParamOverrides) {
4543
+ const active = override.target.kind === "track" ? activeTrackIds.has(override.target.trackId) : override.target.kind === "bus" ? activeBusIds.has(override.target.busId) : true;
4544
+ if (!active) {
4545
+ ctx.insertParamOverrides.delete(key);
4546
+ continue;
4547
+ }
4548
+ switch (override.target.kind) {
4549
+ case "track":
4550
+ ctx.offlineEngine.restoreTrackStripInsertParamByName(
4551
+ override.target.trackId,
4552
+ override.insertIndex,
4553
+ override.paramName,
4554
+ override.value
4555
+ );
4556
+ break;
4557
+ case "bus":
4558
+ ctx.offlineEngine.restoreBusStripInsertParamByName(
4559
+ override.target.busId,
4560
+ override.insertIndex,
4561
+ override.paramName,
4562
+ override.value
4563
+ );
4564
+ break;
4565
+ case "master":
4566
+ ctx.offlineEngine.restoreMasterStripInsertParamByName(
4567
+ override.insertIndex,
4568
+ override.paramName,
4569
+ override.value
4570
+ );
4571
+ break;
4572
+ }
4573
+ }
4574
+ }
3198
4575
  function setTrackLanes(ctx, lanes) {
3199
4576
  const { entries, ids } = normalizeTrackLanes(ctx.trackLaneIds, lanes);
3200
4577
  for (const entry of entries) {
@@ -3234,143 +4611,116 @@ function setLaneSidechain(ctx, target, insertIndex, sourceTarget) {
3234
4611
  const sourceIndex = ctx.ensureTrackLane(sourceTarget);
3235
4612
  sourceTrackId = ctx.trackLaneIds[sourceIndex];
3236
4613
  }
3237
- if (sourceTrackId === 0) {
3238
- ctx.laneSidechains.delete(key);
3239
- } else {
3240
- ctx.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
3241
- }
3242
4614
  ctx.offlineEngine.setLaneSidechain(trackId, insertIndex, sourceTrackId);
3243
- ctx.postSync({
3244
- type: "syncMixer",
3245
- lanes: ctx.mixerLanes(),
3246
- laneSidechains: [{ trackId, insertIndex, sourceTrackId }]
3247
- });
3248
- }
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) {
3269
- 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;
3309
- }
3310
- return value.requestId;
3311
- }
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);
3333
- }
3334
- return true;
3335
- }
3336
- function isEngineCaptureResponseForOperation(response, op) {
3337
- if (!response.ok) {
3338
- return true;
3339
- }
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);
4615
+ if (sourceTrackId === 0) {
4616
+ ctx.laneSidechains.delete(key);
4617
+ } else {
4618
+ ctx.laneSidechains.set(key, { trackId, insertIndex, sourceTrackId });
3347
4619
  }
4620
+ ctx.postSync({
4621
+ type: "syncMixer",
4622
+ sidechainDelta: true,
4623
+ lanes: ctx.mixerLanes(),
4624
+ laneSidechains: [{ trackId, insertIndex, sourceTrackId }]
4625
+ });
3348
4626
  }
3349
- function isEngineTransportRequestMessage(value) {
3350
- return isRecord(value) && value.type === "transportRequest" && typeof value.requestId === "number" && value.op === "state";
4627
+ function setBusSidechain(ctx, busId, insertIndex, kind, sourceId) {
4628
+ const sourceKind = sidechainSourceKindCode(kind);
4629
+ ctx.ensureBus(busId);
4630
+ ensureSidechainSource(ctx, sourceKind, sourceId);
4631
+ ctx.offlineEngine.setBusSidechain(busId, insertIndex, sourceKind, sourceId);
4632
+ const key = `${busId}:${insertIndex}`;
4633
+ if (sourceId === 0) {
4634
+ ctx.busSidechains.delete(key);
4635
+ } else {
4636
+ ctx.busSidechains.set(key, { busId, insertIndex, sourceKind, sourceId });
4637
+ }
4638
+ ctx.postSync({
4639
+ type: "syncMixer",
4640
+ sidechainDelta: true,
4641
+ lanes: ctx.mixerLanes(),
4642
+ busSidechains: [{ busId, insertIndex, sourceKind, sourceId }]
4643
+ });
3351
4644
  }
3352
- function isEngineTransportResponseMessage(value) {
3353
- return isRecord(value) && value.type === "transportResponse" && typeof value.requestId === "number" && typeof value.ok === "boolean";
4645
+ function setMasterSidechain(ctx, insertIndex, kind, sourceId) {
4646
+ const sourceKind = sidechainSourceKindCode(kind);
4647
+ ensureSidechainSource(ctx, sourceKind, sourceId);
4648
+ ctx.offlineEngine.setMasterSidechain(insertIndex, sourceKind, sourceId);
4649
+ if (sourceId === 0) {
4650
+ ctx.masterSidechains.delete(insertIndex);
4651
+ } else {
4652
+ ctx.masterSidechains.set(insertIndex, { insertIndex, sourceKind, sourceId });
4653
+ }
4654
+ ctx.postSync({
4655
+ type: "syncMixer",
4656
+ sidechainDelta: true,
4657
+ lanes: ctx.mixerLanes(),
4658
+ masterSidechains: [{ insertIndex, sourceKind, sourceId }]
4659
+ });
3354
4660
  }
3355
- function isRealtimeVoiceChangerMessage(value) {
3356
- if (!isRecord(value) || typeof value.type !== "string") {
3357
- return false;
4661
+ function ensureSidechainSource(ctx, sourceKind, sourceId) {
4662
+ if (sourceId === 0) {
4663
+ return;
4664
+ }
4665
+ if (sourceKind === 1) {
4666
+ ctx.ensureBus(sourceId);
4667
+ } else {
4668
+ ctx.ensureTrackLane(sourceId);
3358
4669
  }
3359
- return value.type === "setConfig" || value.type === "reset" || value.type === "destroy";
3360
4670
  }
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";
4671
+ function setSends(ctx, target, sends) {
4672
+ const laneIndex = ctx.ensureTrackLane(target);
4673
+ const trackId = ctx.trackLaneIds[laneIndex];
4674
+ ctx.trackSends.set(
4675
+ trackId,
4676
+ sends.map((send) => ({ ...send }))
4677
+ );
4678
+ ctx.syncMixer();
3363
4679
  }
3364
- function isExternalMidiBatchMessage(value) {
3365
- return isRecord(value) && value.type === "externalMidi" && Array.isArray(value.events);
4680
+ function setTrackBuses(ctx, buses) {
4681
+ ctx.offlineEngine.setTrackBuses(buses);
4682
+ const retainedBusIds = new Set(buses.map((bus) => bus.busId));
4683
+ for (const bus of ctx.buses) {
4684
+ if (!retainedBusIds.has(bus.busId)) {
4685
+ ctx.clearInsertAutomationLanes({ kind: "bus", busId: bus.busId }, true);
4686
+ }
4687
+ }
4688
+ ctx.buses.splice(0, ctx.buses.length, ...buses.map((bus) => ({ ...bus })));
4689
+ const activeBusIds = new Set(ctx.buses.map((bus) => bus.busId));
4690
+ for (const busId of ctx.busStripJson.keys()) {
4691
+ if (!activeBusIds.has(busId)) {
4692
+ ctx.busStripJson.delete(busId);
4693
+ }
4694
+ }
4695
+ for (const [key, binding] of ctx.busSidechains) {
4696
+ if (!activeBusIds.has(binding.busId) || binding.sourceKind === 1 && !activeBusIds.has(binding.sourceId)) {
4697
+ ctx.busSidechains.delete(key);
4698
+ }
4699
+ }
4700
+ for (const [key, binding] of ctx.masterSidechains) {
4701
+ if (binding.sourceKind === 1 && !activeBusIds.has(binding.sourceId)) {
4702
+ ctx.masterSidechains.delete(key);
4703
+ }
4704
+ }
4705
+ syncMixer(ctx, true);
3366
4706
  }
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"
3370
- );
4707
+ function setBusGain(ctx, busId, db) {
4708
+ const busIndex = ctx.ensureBus(busId);
4709
+ ctx.buses[busIndex] = { ...ctx.buses[busIndex], busId, gainDb: db };
4710
+ return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
3371
4711
  }
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);
4712
+ function setBusStripJson(ctx, busId, sceneJson) {
4713
+ ctx.ensureBus(busId);
4714
+ const target = { kind: "bus", busId };
4715
+ const resetInserts = ctx.busStripJson.has(busId) && ctx.busStripJson.get(busId) !== sceneJson || hasInsertParamOverrides(ctx.insertParamOverrides, target);
4716
+ applyFullStripJson(ctx.offlineEngine, target, sceneJson, resetInserts);
4717
+ if (resetInserts) {
4718
+ ctx.clearInsertAutomationLanes(target);
4719
+ }
4720
+ pruneStripSidechains(ctx, target, sceneJson);
4721
+ ctx.busStripJson.set(busId, sceneJson);
4722
+ clearInsertParamOverrides(ctx.insertParamOverrides, target);
4723
+ ctx.syncMixer(resetInserts ? [target] : []);
3374
4724
  }
3375
4725
 
3376
4726
  // src/worklet/engine-node.ts
@@ -3387,15 +4737,17 @@ function isTrackMonitorMode(value) {
3387
4737
  const mode = typeof value === "bigint" ? Number(value) : value;
3388
4738
  return typeof mode === "number" && Number.isSafeInteger(mode) && mode >= 0 && mode <= 2;
3389
4739
  }
3390
- function isTrackMonitorLaneIndex(value) {
3391
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 4294967295;
3392
- }
3393
4740
  function isValidCommandRecord(command) {
3394
4741
  const type = Number(command.type);
3395
4742
  if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */ && type !== 26 /* SetTrackMonitorMode */) {
3396
4743
  return false;
3397
4744
  }
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));
4745
+ return (
4746
+ // The record writer refuses a targetId outside the slot by throwing, and
4747
+ // this path answers false instead, so it must reject the same domain here.
4748
+ (command.targetId === void 0 || isUint32Slot(command.targetId)) && (command.argFloat === void 0 || Number.isFinite(command.argFloat)) && isFiniteInteger(command.argInt) && isFiniteInteger(command.sampleTime) && // A track-monitor command cannot infer a lane: its target is required.
4749
+ (type !== 26 /* SetTrackMonitorMode */ || isUint32Slot(command.targetId) && isTrackMonitorMode(command.argInt))
4750
+ );
3399
4751
  }
3400
4752
  var AUDIO_WORKLET_RENDER_QUANTUM = 128;
3401
4753
  function workletBlockSize(blockSize) {
@@ -3440,6 +4792,9 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3440
4792
  this.resolveReady();
3441
4793
  }
3442
4794
  this.node.port.onmessage = (event) => {
4795
+ if (this.destroyed) {
4796
+ return;
4797
+ }
3443
4798
  const captureRequestId = engineCaptureResponseRequestId(event.data);
3444
4799
  if (captureRequestId !== void 0) {
3445
4800
  const pending = this.captureRequests.get(captureRequestId);
@@ -3521,14 +4876,59 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3521
4876
  "SharedArrayBuffer mode requested but SharedArrayBuffer/Atomics are unavailable."
3522
4877
  );
3523
4878
  }
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));
4879
+ const commandRingCapacity = requireIntegerOption(
4880
+ options.commandRingCapacity,
4881
+ 128,
4882
+ "commandRingCapacity",
4883
+ 1
4884
+ );
4885
+ if (commandRingCapacity > 65536) {
4886
+ throw new RangeError("commandRingCapacity must be at most 65536");
4887
+ }
4888
+ const telemetryRingCapacity = requireIntegerOption(
4889
+ options.telemetryRingCapacity,
4890
+ 128,
4891
+ "telemetryRingCapacity",
4892
+ 1
4893
+ );
4894
+ const meterRingCapacity = requireIntegerOption(
4895
+ options.meterRingCapacity,
4896
+ 128,
4897
+ "meterRingCapacity",
4898
+ 1
4899
+ );
4900
+ const scopeRingCapacity = requireIntegerOption(
4901
+ options.scopeRingCapacity,
4902
+ 64,
4903
+ "scopeRingCapacity",
4904
+ 1
4905
+ );
4906
+ const scopeBands = requireIntegerOption(options.scopeBands, 48, "scopeBands", 1);
4907
+ const clipPageRequestRingCapacity = requireIntegerOption(
4908
+ options.clipPageRequestRingCapacity,
4909
+ 128,
4910
+ "clipPageRequestRingCapacity",
4911
+ 1
4912
+ );
4913
+ const externalMidiRingCapacity = requireIntegerOption(
4914
+ options.externalMidiRingCapacity,
4915
+ 256,
4916
+ "externalMidiRingCapacity",
4917
+ 1
4918
+ );
4919
+ const commandRing = mode === "sab" ? createSonareEngineCommandRingBuffer(commandRingCapacity) : void 0;
4920
+ const telemetryRing = mode === "sab" ? createSonareEngineTelemetryRingBuffer(telemetryRingCapacity) : void 0;
4921
+ const meterRing = mode === "sab" ? createSonareMeterRingBuffer(meterRingCapacity) : void 0;
4922
+ const scopeIntervalFrames = requireIntegerOption(
4923
+ options.scopeIntervalFrames,
4924
+ 0,
4925
+ "scopeIntervalFrames",
4926
+ 0
4927
+ );
4928
+ const scopeRing = mode === "sab" && scopeIntervalFrames > 0 ? createSonareScopeRingBuffer(scopeRingCapacity, scopeBands) : void 0;
4929
+ const clipPageRequestRing = mode === "sab" ? createSonareClipPageRequestRingBuffer(clipPageRequestRingCapacity) : void 0;
4930
+ const externalMidiRing = mode === "sab" ? createSonareExternalMidiRingBuffer(externalMidiRingCapacity) : void 0;
4931
+ const channelCount = requireChannelCount(options.channelCount, 2);
3532
4932
  const cueOutput = options.cueOutput === true;
3533
4933
  const processorOptions = {
3534
4934
  sampleRate: options.sampleRate ?? context.sampleRate,
@@ -3805,6 +5205,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
3805
5205
  this.scopeListeners.clear();
3806
5206
  this.midiOutListeners.clear();
3807
5207
  this.clipPageRequestListeners.clear();
5208
+ this.syncErrorListeners.clear();
5209
+ this.node.port.onmessage = null;
3808
5210
  }
3809
5211
  emitTelemetry(telemetry) {
3810
5212
  for (const listener of this.telemetryListeners) {
@@ -3944,6 +5346,9 @@ function resolveBusInsertAutomationId(ctx, busId, insertIndex, paramName) {
3944
5346
  ctx.ensureBus(busId);
3945
5347
  return ctx.offlineEngine.resolveBusInsertAutomationId(busId, insertIndex, paramName);
3946
5348
  }
5349
+ function resolveInstrumentAutomationId(ctx, destinationId, paramName) {
5350
+ return ctx.offlineEngine.resolveInstrumentAutomationId(destinationId, paramName);
5351
+ }
3947
5352
  function automationLaneCount(ctx) {
3948
5353
  return ctx.offlineEngine.automationLaneCount();
3949
5354
  }
@@ -3968,14 +5373,41 @@ function clearParameters(ctx) {
3968
5373
  function trackIdFor(ctx, target) {
3969
5374
  return ctx.trackLaneIds[ctx.ensureTrackLane(target)];
3970
5375
  }
5376
+ function mergeStripJson(ctx, target, update) {
5377
+ const scene = JSON.parse(ctx.readStripJson(target) ?? emptyStripJson(target));
5378
+ update(target.kind === "bus" ? scene.buses[0] : scene.strips[0]);
5379
+ ctx.writeStripJson(target, JSON.stringify(scene));
5380
+ }
5381
+ function mergeEqBand(ctx, target, bandIndex, bandJson) {
5382
+ mergeStripJson(ctx, target, (entry) => {
5383
+ const eq = entry.eq ?? { enabled: true, bands: [] };
5384
+ const bands = eq.bands ?? [];
5385
+ while (bands.length < bandIndex) {
5386
+ bands.push({});
5387
+ }
5388
+ bands[bandIndex] = JSON.parse(bandJson);
5389
+ entry.eq = { ...eq, bands };
5390
+ });
5391
+ }
5392
+ function setInsertParamByName(ctx, target, insertIndex, paramName, value, applyNative, message) {
5393
+ applyNative();
5394
+ const override = { target, insertIndex, paramName, value };
5395
+ ctx.insertParamOverrides.set(insertParamOverrideKey(override), override);
5396
+ ctx.postSync(message);
5397
+ }
3971
5398
  function setTrackStripJson(ctx, trackId, sceneJson, trackStripJson) {
3972
- ctx.offlineEngine.setTrackStripJson(trackId, sceneJson);
5399
+ const target = { kind: "track", trackId };
5400
+ const resetInserts = trackStripJson.has(trackId) && trackStripJson.get(trackId) !== sceneJson || hasInsertParamOverrides(ctx.insertParamOverrides, target);
5401
+ applyFullStripJson(ctx.offlineEngine, target, sceneJson, resetInserts);
5402
+ clearInsertParamOverrides(ctx.insertParamOverrides, target);
3973
5403
  trackStripJson.set(trackId, sceneJson);
5404
+ return { resetInserts };
3974
5405
  }
3975
5406
  function setTrackStripEqBand(ctx, target, bandIndex, band) {
3976
5407
  const trackId = trackIdFor(ctx, target);
3977
5408
  const bandJson = typeof band === "string" ? band : JSON.stringify(band);
3978
5409
  ctx.offlineEngine.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
5410
+ mergeEqBand(ctx, { kind: "track", trackId }, bandIndex, bandJson);
3979
5411
  ctx.postSync({ type: "syncTrackStripEqBand", trackId, bandIndex, bandJson });
3980
5412
  }
3981
5413
  function setTrackStripInsertBypassed(ctx, target, insertIndex, bypassed, resetOnBypass) {
@@ -3991,39 +5423,68 @@ function setTrackStripInsertBypassed(ctx, target, insertIndex, bypassed, resetOn
3991
5423
  }
3992
5424
  function setTrackStripInsertParamByName(ctx, target, insertIndex, paramName, value) {
3993
5425
  const trackId = trackIdFor(ctx, target);
3994
- ctx.offlineEngine.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
3995
- ctx.postSync({ type: "syncTrackStripInsertParamByName", trackId, insertIndex, paramName, value });
5426
+ setInsertParamByName(
5427
+ ctx,
5428
+ { kind: "track", trackId },
5429
+ insertIndex,
5430
+ paramName,
5431
+ value,
5432
+ () => ctx.offlineEngine.restoreTrackStripInsertParamByName(trackId, insertIndex, paramName, value),
5433
+ { type: "syncTrackStripInsertParamByName", trackId, insertIndex, paramName, value }
5434
+ );
3996
5435
  }
3997
5436
  function setTrackStripPan(ctx, target, pan) {
3998
5437
  const trackId = trackIdFor(ctx, target);
3999
5438
  ctx.offlineEngine.setTrackStripPan(trackId, pan);
5439
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5440
+ entry.pan = pan;
5441
+ });
4000
5442
  ctx.postSync({ type: "syncTrackStripPan", trackId, pan });
4001
5443
  }
4002
5444
  function setTrackStripPanLaw(ctx, target, panLaw) {
4003
5445
  const trackId = trackIdFor(ctx, target);
4004
5446
  const code = panLawCode(panLaw);
4005
5447
  ctx.offlineEngine.setTrackStripPanLaw(trackId, code);
5448
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5449
+ entry.panLaw = code;
5450
+ });
4006
5451
  ctx.postSync({ type: "syncTrackStripPanLaw", trackId, panLaw: code });
4007
5452
  }
4008
5453
  function setTrackStripPanMode(ctx, target, panMode) {
4009
5454
  const trackId = trackIdFor(ctx, target);
4010
5455
  const code = panModeCode(panMode);
4011
5456
  ctx.offlineEngine.setTrackStripPanMode(trackId, code);
5457
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5458
+ entry.panMode = code;
5459
+ });
4012
5460
  ctx.postSync({ type: "syncTrackStripPanMode", trackId, panMode: code });
4013
5461
  }
4014
5462
  function setTrackStripDualPan(ctx, target, leftPan, rightPan) {
4015
5463
  const trackId = trackIdFor(ctx, target);
4016
5464
  ctx.offlineEngine.setTrackStripDualPan(trackId, leftPan, rightPan);
5465
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5466
+ entry.dualPanLeft = leftPan;
5467
+ entry.dualPanRight = rightPan;
5468
+ });
4017
5469
  ctx.postSync({ type: "syncTrackStripDualPan", trackId, leftPan, rightPan });
4018
5470
  }
4019
5471
  function setTrackStripChannelDelaySamples(ctx, target, delaySamples) {
4020
5472
  const trackId = trackIdFor(ctx, target);
4021
5473
  ctx.offlineEngine.setTrackStripChannelDelaySamples(trackId, delaySamples);
5474
+ mergeStripJson(ctx, { kind: "track", trackId }, (entry) => {
5475
+ entry.channelDelaySamples = delaySamples;
5476
+ });
4022
5477
  ctx.postSync({ type: "syncTrackStripChannelDelaySamples", trackId, delaySamples });
4023
5478
  }
5479
+ function cacheMasterStripScalar(ctx, field, value) {
5480
+ mergeStripJson(ctx, { kind: "master" }, (entry) => {
5481
+ entry[field] = value;
5482
+ });
5483
+ }
4024
5484
  function setMasterStripEqBand(ctx, bandIndex, band) {
4025
5485
  const bandJson = typeof band === "string" ? band : JSON.stringify(band);
4026
5486
  ctx.offlineEngine.setMasterStripEqBandJson(bandIndex, bandJson);
5487
+ mergeEqBand(ctx, { kind: "master" }, bandIndex, bandJson);
4027
5488
  ctx.postSync({ type: "syncMasterStripEqBand", bandIndex, bandJson });
4028
5489
  }
4029
5490
  function setMasterStripInsertBypassed(ctx, insertIndex, bypassed, resetOnBypass) {
@@ -4031,12 +5492,67 @@ function setMasterStripInsertBypassed(ctx, insertIndex, bypassed, resetOnBypass)
4031
5492
  ctx.postSync({ type: "syncMasterStripInsertBypassed", insertIndex, bypassed, resetOnBypass });
4032
5493
  }
4033
5494
  function setMasterStripInsertParamByName(ctx, insertIndex, paramName, value) {
4034
- ctx.offlineEngine.setMasterStripInsertParamByName(insertIndex, paramName, value);
4035
- ctx.postSync({ type: "syncMasterStripInsertParamByName", insertIndex, paramName, value });
5495
+ setInsertParamByName(
5496
+ ctx,
5497
+ { kind: "master" },
5498
+ insertIndex,
5499
+ paramName,
5500
+ value,
5501
+ () => ctx.offlineEngine.restoreMasterStripInsertParamByName(insertIndex, paramName, value),
5502
+ { type: "syncMasterStripInsertParamByName", insertIndex, paramName, value }
5503
+ );
4036
5504
  }
4037
5505
  function setBusStripInsertParamByName(ctx, busId, insertIndex, paramName, value) {
4038
- ctx.offlineEngine.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
4039
- ctx.postSync({ type: "syncBusStripInsertParamByName", busId, insertIndex, paramName, value });
5506
+ setInsertParamByName(
5507
+ ctx,
5508
+ { kind: "bus", busId },
5509
+ insertIndex,
5510
+ paramName,
5511
+ value,
5512
+ () => ctx.offlineEngine.restoreBusStripInsertParamByName(busId, insertIndex, paramName, value),
5513
+ { type: "syncBusStripInsertParamByName", busId, insertIndex, paramName, value }
5514
+ );
5515
+ }
5516
+ function setBusStripInsertBypassed(ctx, busId, insertIndex, bypassed, resetOnBypass) {
5517
+ ctx.offlineEngine.setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass);
5518
+ ctx.postSync({ type: "syncBusStripInsertBypassed", busId, insertIndex, bypassed, resetOnBypass });
5519
+ }
5520
+ function setBusStripEqBand(ctx, busId, bandIndex, band) {
5521
+ const bandJson = typeof band === "string" ? band : JSON.stringify(band);
5522
+ ctx.offlineEngine.setBusStripEqBandJson(busId, bandIndex, bandJson);
5523
+ mergeEqBand(ctx, { kind: "bus", busId }, bandIndex, bandJson);
5524
+ ctx.postSync({ type: "syncBusStripEqBand", busId, bandIndex, bandJson });
5525
+ }
5526
+ function setBusStripPan(ctx, busId, pan) {
5527
+ ctx.offlineEngine.setBusStripPan(busId, pan);
5528
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5529
+ entry.pan = pan;
5530
+ });
5531
+ ctx.postSync({ type: "syncBusStripPan", busId, pan });
5532
+ }
5533
+ function setBusStripPanLaw(ctx, busId, panLaw) {
5534
+ const code = panLawCode(panLaw);
5535
+ ctx.offlineEngine.setBusStripPanLaw(busId, code);
5536
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5537
+ entry.panLaw = code;
5538
+ });
5539
+ ctx.postSync({ type: "syncBusStripPanLaw", busId, panLaw: code });
5540
+ }
5541
+ function setBusStripPanMode(ctx, busId, panMode) {
5542
+ const code = panModeCode(panMode);
5543
+ ctx.offlineEngine.setBusStripPanMode(busId, code);
5544
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5545
+ entry.panMode = code;
5546
+ });
5547
+ ctx.postSync({ type: "syncBusStripPanMode", busId, panMode: code });
5548
+ }
5549
+ function setBusStripDualPan(ctx, busId, leftPan, rightPan) {
5550
+ ctx.offlineEngine.setBusStripDualPan(busId, leftPan, rightPan);
5551
+ mergeStripJson(ctx, { kind: "bus", busId }, (entry) => {
5552
+ entry.dualPanLeft = leftPan;
5553
+ entry.dualPanRight = rightPan;
5554
+ });
5555
+ ctx.postSync({ type: "syncBusStripDualPan", busId, leftPan, rightPan });
4040
5556
  }
4041
5557
  function pushMidiNoteOn(ctx, trackId, group, channel, note, velocity, renderFrame) {
4042
5558
  const destinationId = ctx.resolveTargetId(trackId);
@@ -4077,9 +5593,55 @@ function pushMidiCc(ctx, trackId, group, channel, controller, value, renderFrame
4077
5593
  renderFrame
4078
5594
  });
4079
5595
  }
5596
+ function pushMidiPitchBend(ctx, trackId, group, channel, bend14, renderFrame) {
5597
+ const destinationId = ctx.resolveTargetId(trackId);
5598
+ ctx.offlineEngine.pushMidiPitchBend(destinationId, group, channel, bend14, renderFrame);
5599
+ ctx.postSync({
5600
+ type: "syncMidiPitchBend",
5601
+ destinationId,
5602
+ group,
5603
+ channel,
5604
+ data0: bend14,
5605
+ data1: 0,
5606
+ renderFrame
5607
+ });
5608
+ }
5609
+ function pushMidiChannelPressure(ctx, trackId, group, channel, pressure, renderFrame) {
5610
+ const destinationId = ctx.resolveTargetId(trackId);
5611
+ ctx.offlineEngine.pushMidiChannelPressure(destinationId, group, channel, pressure, renderFrame);
5612
+ ctx.postSync({
5613
+ type: "syncMidiChannelPressure",
5614
+ destinationId,
5615
+ group,
5616
+ channel,
5617
+ data0: pressure,
5618
+ data1: 0,
5619
+ renderFrame
5620
+ });
5621
+ }
5622
+ function pushMidiPolyPressure(ctx, trackId, group, channel, note, pressure, renderFrame) {
5623
+ const destinationId = ctx.resolveTargetId(trackId);
5624
+ ctx.offlineEngine.pushMidiPolyPressure(
5625
+ destinationId,
5626
+ group,
5627
+ channel,
5628
+ note,
5629
+ pressure,
5630
+ renderFrame
5631
+ );
5632
+ ctx.postSync({
5633
+ type: "syncMidiPolyPressure",
5634
+ destinationId,
5635
+ group,
5636
+ channel,
5637
+ data0: note,
5638
+ data1: pressure,
5639
+ renderFrame
5640
+ });
5641
+ }
4080
5642
  function pushMidiUmp(ctx, trackId, word0, renderFrame) {
4081
5643
  const destinationId = ctx.resolveTargetId(trackId);
4082
- ctx.offlineEngine.pushMidiUmp(destinationId, word0, renderFrame);
5644
+ ctx.offlineEngine.pushMidiUmp(destinationId, [word0], renderFrame);
4083
5645
  ctx.postSync({ type: "syncMidiUmp", destinationId, word0, renderFrame });
4084
5646
  }
4085
5647
  function setBuiltinInstrument(ctx, trackId, config) {
@@ -4205,6 +5767,7 @@ function transferableAudioBuffers(channels) {
4205
5767
  var SonareEngine = class _SonareEngine {
4206
5768
  constructor(context, realtimeNode, offlineEngine, sampleRate, offlineBlockSize, offlineChannelCount) {
4207
5769
  this.automationLanes = /* @__PURE__ */ new Map();
5770
+ this.insertAutomationIdsByTarget = /* @__PURE__ */ new Map();
4208
5771
  this.clips = /* @__PURE__ */ new Map();
4209
5772
  this.midiClips = /* @__PURE__ */ new Map();
4210
5773
  this.markers = /* @__PURE__ */ new Map();
@@ -4212,9 +5775,12 @@ var SonareEngine = class _SonareEngine {
4212
5775
  this.trackSends = /* @__PURE__ */ new Map();
4213
5776
  this.trackOutputBus = /* @__PURE__ */ new Map();
4214
5777
  this.laneSidechains = /* @__PURE__ */ new Map();
5778
+ this.busSidechains = /* @__PURE__ */ new Map();
5779
+ this.masterSidechains = /* @__PURE__ */ new Map();
4215
5780
  this.buses = [];
4216
5781
  this.trackStripJson = /* @__PURE__ */ new Map();
4217
5782
  this.busStripJson = /* @__PURE__ */ new Map();
5783
+ this.insertParamOverrides = /* @__PURE__ */ new Map();
4218
5784
  this.tempoBpm = 120;
4219
5785
  this.timeSignature = { numerator: 4, denominator: 4 };
4220
5786
  this.tempoSegments = [{ startPpq: 0, bpm: 120 }];
@@ -4373,12 +5939,17 @@ var SonareEngine = class _SonareEngine {
4373
5939
  * @returns Reserved insert-automation id, or -1 when strip/insert/key unknown.
4374
5940
  */
4375
5941
  resolveTrackInsertAutomationId(target, insertIndex, paramName) {
4376
- return resolveTrackInsertAutomationId(
5942
+ const id = resolveTrackInsertAutomationId(
4377
5943
  this.parameterContext,
4378
5944
  target,
4379
5945
  insertIndex,
4380
5946
  paramName
4381
5947
  );
5948
+ if (id >= 0) {
5949
+ const trackId = this.trackLaneIds[this.ensureTrackLane(target)];
5950
+ this.rememberInsertAutomationId({ kind: "track", trackId }, id);
5951
+ }
5952
+ return id;
4382
5953
  }
4383
5954
  /**
4384
5955
  * Resolves a master-strip insert parameter to its reserved insert-automation
@@ -4389,7 +5960,15 @@ var SonareEngine = class _SonareEngine {
4389
5960
  * @returns Reserved insert-automation id, or -1 when insert/key unknown.
4390
5961
  */
4391
5962
  resolveMasterInsertAutomationId(insertIndex, paramName) {
4392
- return resolveMasterInsertAutomationId(this.parameterContext, insertIndex, paramName);
5963
+ const id = resolveMasterInsertAutomationId(
5964
+ this.parameterContext,
5965
+ insertIndex,
5966
+ paramName
5967
+ );
5968
+ if (id >= 0) {
5969
+ this.rememberInsertAutomationId({ kind: "master" }, id);
5970
+ }
5971
+ return id;
4393
5972
  }
4394
5973
  /**
4395
5974
  * Resolves a bus-strip insert parameter to its reserved insert-automation id.
@@ -4402,12 +5981,29 @@ var SonareEngine = class _SonareEngine {
4402
5981
  * @returns Reserved insert-automation id, or -1 when bus/insert/key unknown.
4403
5982
  */
4404
5983
  resolveBusInsertAutomationId(busId, insertIndex, paramName) {
4405
- return resolveBusInsertAutomationId(
5984
+ const id = resolveBusInsertAutomationId(
4406
5985
  this.parameterContext,
4407
5986
  busId,
4408
5987
  insertIndex,
4409
5988
  paramName
4410
5989
  );
5990
+ if (id >= 0) {
5991
+ this.rememberInsertAutomationId({ kind: "bus", busId }, id);
5992
+ }
5993
+ return id;
5994
+ }
5995
+ /**
5996
+ * Resolves a hosted instrument's continuous parameter to its reserved
5997
+ * instrument-automation id, so an instrument parameter follows a breakpoint
5998
+ * lane at audio-block precision like a strip insert. Bind the instrument
5999
+ * (`setSynthInstrument`) before resolving.
6000
+ *
6001
+ * @param destinationId MIDI destination the instrument is bound to.
6002
+ * @param paramName Instrument JSON-key parameter name (e.g. `cutoffHz`).
6003
+ * @returns Reserved instrument-automation id, or -1 when destination/key unknown.
6004
+ */
6005
+ resolveInstrumentAutomationId(destinationId, paramName) {
6006
+ return resolveInstrumentAutomationId(this.parameterContext, destinationId, paramName);
4411
6007
  }
4412
6008
  /**
4413
6009
  * Returns the number of automation lanes installed on the engine, including
@@ -4437,10 +6033,18 @@ var SonareEngine = class _SonareEngine {
4437
6033
  return setTrackMonitorMode(this.parameterContext, target, mode, renderFrame);
4438
6034
  }
4439
6035
  setStripGain(target, db) {
4440
- return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
6036
+ const sent = this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_FADER_DB), db);
6037
+ if (sent && target === "master") {
6038
+ cacheMasterStripScalar(this.stripContext, "faderDb", db);
6039
+ }
6040
+ return sent;
4441
6041
  }
4442
6042
  setStripPan(target, pan) {
4443
- return this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_PAN), pan);
6043
+ const sent = this.sendSmoothedParam(this.stripParamId(target, ENGINE_MIXER_PARAM_PAN), pan);
6044
+ if (sent && target === "master") {
6045
+ cacheMasterStripScalar(this.stripContext, "pan", pan);
6046
+ }
6047
+ return sent;
4444
6048
  }
4445
6049
  /**
4446
6050
  * Declares the mixer track lanes in an explicit order.
@@ -4477,14 +6081,37 @@ var SonareEngine = class _SonareEngine {
4477
6081
  setTrackBuses(buses) {
4478
6082
  setTrackBuses(this.mixerContext, buses);
4479
6083
  }
6084
+ /**
6085
+ * Keys one insert of a bus strip from a track lane or another bus
6086
+ * (ducking/sidechainRouter inserts). `sourceId` 0 removes the binding.
6087
+ */
6088
+ setBusSidechain(busId, insertIndex, sourceKind, sourceId) {
6089
+ setBusSidechain(this.mixerContext, busId, insertIndex, sourceKind, sourceId);
6090
+ }
6091
+ /**
6092
+ * Keys one insert of the master strip from a track lane or a bus. Same
6093
+ * source rules as {@link setBusSidechain}.
6094
+ */
6095
+ setMasterSidechain(insertIndex, sourceKind, sourceId) {
6096
+ setMasterSidechain(this.mixerContext, insertIndex, sourceKind, sourceId);
6097
+ }
4480
6098
  setBusGain(busId, db) {
4481
6099
  return setBusGain(this.mixerContext, busId, db);
4482
6100
  }
4483
6101
  setTrackStripJson(target, sceneJson) {
4484
6102
  const laneIndex = this.ensureTrackLane(target);
4485
6103
  const trackId = this.trackLaneIds[laneIndex];
4486
- setTrackStripJson(this.stripContext, trackId, sceneJson, this.trackStripJson);
4487
- this.syncMixer();
6104
+ const { resetInserts } = setTrackStripJson(
6105
+ this.stripContext,
6106
+ trackId,
6107
+ sceneJson,
6108
+ this.trackStripJson
6109
+ );
6110
+ if (resetInserts) {
6111
+ this.clearInsertAutomationLanes({ kind: "track", trackId });
6112
+ }
6113
+ pruneStripSidechains(this.mixerContext, { kind: "track", trackId }, sceneJson);
6114
+ this.syncMixer(resetInserts ? [{ kind: "track", trackId }] : []);
4488
6115
  }
4489
6116
  setTrackStripEqBand(target, bandIndex, band) {
4490
6117
  setTrackStripEqBand(this.stripContext, target, bandIndex, band);
@@ -4541,9 +6168,16 @@ var SonareEngine = class _SonareEngine {
4541
6168
  setBusStripJson(this.mixerContext, busId, sceneJson);
4542
6169
  }
4543
6170
  setMasterStripJson(sceneJson) {
4544
- this.offlineEngine.setMasterStripJson(sceneJson);
6171
+ const target = { kind: "master" };
6172
+ const resetInserts = this.masterStripJson !== void 0 && this.masterStripJson !== sceneJson || hasInsertParamOverrides(this.insertParamOverrides, target);
6173
+ applyFullStripJson(this.offlineEngine, target, sceneJson, resetInserts);
6174
+ if (resetInserts) {
6175
+ this.clearInsertAutomationLanes(target);
6176
+ }
6177
+ pruneStripSidechains(this.mixerContext, target, sceneJson);
6178
+ clearInsertParamOverrides(this.insertParamOverrides, target);
4545
6179
  this.masterStripJson = sceneJson;
4546
- this.syncMixer();
6180
+ this.syncMixer(resetInserts ? [target] : []);
4547
6181
  }
4548
6182
  setMasterStripEqBand(bandIndex, band) {
4549
6183
  setMasterStripEqBand(this.stripContext, bandIndex, band);
@@ -4558,6 +6192,47 @@ var SonareEngine = class _SonareEngine {
4558
6192
  this.ensureBus(busId);
4559
6193
  setBusStripInsertParamByName(this.stripContext, busId, insertIndex, paramName, value);
4560
6194
  }
6195
+ /**
6196
+ * Bus-strip counterpart of {@link setStripInsertBypassed}. A bus is addressed
6197
+ * by its numeric id, which the `target` of the track/master form cannot
6198
+ * express, so it is its own method exactly like
6199
+ * {@link setBusStripInsertParamByName}. Bypassing keeps the insert's internal
6200
+ * state (reverb tail, compressor envelope) unless `resetOnBypass` is set;
6201
+ * re-posting the bus scene JSON, the only workaround before this existed,
6202
+ * rebuilds the chain and loses it.
6203
+ */
6204
+ setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass = false) {
6205
+ this.ensureBus(busId);
6206
+ setBusStripInsertBypassed(
6207
+ this.stripContext,
6208
+ busId,
6209
+ insertIndex,
6210
+ bypassed,
6211
+ resetOnBypass
6212
+ );
6213
+ }
6214
+ /** Bus-strip counterpart of {@link setTrackStripEqBand}. */
6215
+ setBusStripEqBand(busId, bandIndex, band) {
6216
+ this.ensureBus(busId);
6217
+ setBusStripEqBand(this.stripContext, busId, bandIndex, band);
6218
+ }
6219
+ /** Bus-strip counterpart of {@link setTrackStripPan}; refused on a surround bus. */
6220
+ setBusStripPan(busId, pan) {
6221
+ this.ensureBus(busId);
6222
+ setBusStripPan(this.stripContext, busId, pan);
6223
+ }
6224
+ setBusStripPanLaw(busId, panLaw) {
6225
+ this.ensureBus(busId);
6226
+ setBusStripPanLaw(this.stripContext, busId, panLaw);
6227
+ }
6228
+ setBusStripPanMode(busId, panMode) {
6229
+ this.ensureBus(busId);
6230
+ setBusStripPanMode(this.stripContext, busId, panMode);
6231
+ }
6232
+ setBusStripDualPan(busId, leftPan, rightPan) {
6233
+ this.ensureBus(busId);
6234
+ setBusStripDualPan(this.stripContext, busId, leftPan, rightPan);
6235
+ }
4561
6236
  setStripInsertParamByName(target, insertIndex, paramName, value) {
4562
6237
  if (target === "master") {
4563
6238
  this.setMasterStripInsertParamByName(insertIndex, paramName, value);
@@ -4638,6 +6313,49 @@ var SonareEngine = class _SonareEngine {
4638
6313
  }
4639
6314
  return { binding, provider: binding.provider };
4640
6315
  }
6316
+ /**
6317
+ * Sets the clip-page look-ahead window in timeline frames on both the worklet
6318
+ * engine and this thread's offline engine.
6319
+ *
6320
+ * The audio thread reports the pages it is *about to* read that are not
6321
+ * resident yet, so the sliding-window streamer can service them before the
6322
+ * playhead reaches them. Without look-ahead a page miss is only reported
6323
+ * after the read already produced silence, which costs one block of silence
6324
+ * at every page boundary that was not primed.
6325
+ *
6326
+ * Defaults to half a second at the engine's sample rate. `0` disables it.
6327
+ * Safe to call during playback.
6328
+ */
6329
+ setClipPagePrefetchFrames(frames) {
6330
+ if (this.destroyed) {
6331
+ throw new Error("SonareEngine is destroyed.");
6332
+ }
6333
+ if (!Number.isFinite(frames) || frames < 0) {
6334
+ throw new Error("clip page prefetch frames must be a finite value >= 0.");
6335
+ }
6336
+ this.offlineEngine.setClipPagePrefetchFrames(frames);
6337
+ this.postSync({ type: "syncClipPagePrefetchFrames", frames });
6338
+ }
6339
+ /**
6340
+ * Sets the number of concurrent time-stretch voices on both this thread's
6341
+ * offline engine and the live worklet engine. `voices` must be an integer
6342
+ * in `[0, 64]`; a non-integer, negative, or larger value throws and leaves
6343
+ * the capacity unchanged on both engines. Default is 8. Capacity 0 disables
6344
+ * time-stretch, so every warped clip plays resampled instead. A change
6345
+ * applied while the engine is running restarts the splice state of any clip
6346
+ * stretching through a voice at that moment.
6347
+ */
6348
+ setWarpVoiceCapacity(voices) {
6349
+ if (this.destroyed) {
6350
+ throw new Error("SonareEngine is destroyed.");
6351
+ }
6352
+ this.offlineEngine.setWarpVoiceCapacity(voices);
6353
+ this.postSync({ type: "syncWarpVoiceCapacity", voices });
6354
+ }
6355
+ /** Reads the current time-stretch voice capacity (default 8). */
6356
+ warpVoiceCapacity() {
6357
+ return this.offlineEngine.warpVoiceCapacity();
6358
+ }
4641
6359
  addClip(trackId, buffer, startPpq, opts = {}) {
4642
6360
  return addClip(this.clipContext, trackId, buffer, startPpq, opts);
4643
6361
  }
@@ -4699,6 +6417,30 @@ var SonareEngine = class _SonareEngine {
4699
6417
  pushMidiCc(trackId, group, channel, controller, value, renderFrame = -1) {
4700
6418
  pushMidiCc(this.stripContext, trackId, group, channel, controller, value, renderFrame);
4701
6419
  }
6420
+ pushMidiPitchBend(trackId, group, channel, bend14, renderFrame = -1) {
6421
+ pushMidiPitchBend(this.stripContext, trackId, group, channel, bend14, renderFrame);
6422
+ }
6423
+ pushMidiChannelPressure(trackId, group, channel, pressure, renderFrame = -1) {
6424
+ pushMidiChannelPressure(
6425
+ this.stripContext,
6426
+ trackId,
6427
+ group,
6428
+ channel,
6429
+ pressure,
6430
+ renderFrame
6431
+ );
6432
+ }
6433
+ pushMidiPolyPressure(trackId, group, channel, note, pressure, renderFrame = -1) {
6434
+ pushMidiPolyPressure(
6435
+ this.stripContext,
6436
+ trackId,
6437
+ group,
6438
+ channel,
6439
+ note,
6440
+ pressure,
6441
+ renderFrame
6442
+ );
6443
+ }
4702
6444
  pushMidiUmp(trackId, word0, renderFrame = -1) {
4703
6445
  pushMidiUmp(this.stripContext, trackId, word0, renderFrame);
4704
6446
  }
@@ -4752,6 +6494,39 @@ var SonareEngine = class _SonareEngine {
4752
6494
  portTimeSamples
4753
6495
  });
4754
6496
  }
6497
+ pushMidiInputPitchBend(group, channel, bend14, portTimeSamples = 0) {
6498
+ this.offlineEngine.pushMidiInputPitchBend(group, channel, bend14, portTimeSamples);
6499
+ this.postSync({
6500
+ type: "syncMidiInputPitchBend",
6501
+ group,
6502
+ channel,
6503
+ data0: bend14,
6504
+ data1: 0,
6505
+ portTimeSamples
6506
+ });
6507
+ }
6508
+ pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples = 0) {
6509
+ this.offlineEngine.pushMidiInputChannelPressure(group, channel, pressure, portTimeSamples);
6510
+ this.postSync({
6511
+ type: "syncMidiInputChannelPressure",
6512
+ group,
6513
+ channel,
6514
+ data0: pressure,
6515
+ data1: 0,
6516
+ portTimeSamples
6517
+ });
6518
+ }
6519
+ pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples = 0) {
6520
+ this.offlineEngine.pushMidiInputPolyPressure(group, channel, note, pressure, portTimeSamples);
6521
+ this.postSync({
6522
+ type: "syncMidiInputPolyPressure",
6523
+ group,
6524
+ channel,
6525
+ data0: note,
6526
+ data1: pressure,
6527
+ portTimeSamples
6528
+ });
6529
+ }
4755
6530
  pushMidiPanic(renderFrame = -1) {
4756
6531
  this.offlineEngine.pushMidiPanic(renderFrame);
4757
6532
  this.postSync({ type: "syncMidiPanic", renderFrame });
@@ -4949,8 +6724,32 @@ var SonareEngine = class _SonareEngine {
4949
6724
  mixerLanes() {
4950
6725
  return mixerLanes(this.mixerContext);
4951
6726
  }
4952
- syncMixer() {
4953
- syncMixer(this.mixerContext);
6727
+ syncMixer(forceInsertResets = []) {
6728
+ syncMixer(this.mixerContext, false, forceInsertResets);
6729
+ }
6730
+ insertAutomationTargetKey(target) {
6731
+ return target.kind === "track" ? `track:${target.trackId}` : target.kind === "bus" ? `bus:${target.busId}` : "master";
6732
+ }
6733
+ rememberInsertAutomationId(target, id) {
6734
+ const key = this.insertAutomationTargetKey(target);
6735
+ const ids = this.insertAutomationIdsByTarget.get(key) ?? /* @__PURE__ */ new Set();
6736
+ ids.add(id);
6737
+ this.insertAutomationIdsByTarget.set(key, ids);
6738
+ }
6739
+ clearInsertAutomationLanes(target, forgetResolvedIds = false) {
6740
+ const key = this.insertAutomationTargetKey(target);
6741
+ const ids = this.insertAutomationIdsByTarget.get(key);
6742
+ if (!ids) {
6743
+ return;
6744
+ }
6745
+ for (const id of ids) {
6746
+ if (this.automationLanes.has(id)) {
6747
+ this.setAutomationLane(id, []);
6748
+ }
6749
+ }
6750
+ if (forgetResolvedIds) {
6751
+ this.insertAutomationIdsByTarget.delete(key);
6752
+ }
4954
6753
  }
4955
6754
  postInstrumentSync(message) {
4956
6755
  if (this.destroyed) {
@@ -4975,24 +6774,19 @@ var SonareEngine = class _SonareEngine {
4975
6774
  // Sync messages use a string `type` so the worklet's message handler routes
4976
6775
  // them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
4977
6776
  postSync(message, transfer) {
4978
- if (this.destroyed) {
4979
- return;
4980
- }
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();
6777
+ if (this.destroyed) {
6778
+ return;
6779
+ }
6780
+ if (transfer && transfer.length > 0) {
6781
+ this.realtimeNode.node.port.postMessage(message, transfer);
6782
+ } else {
6783
+ this.realtimeNode.node.port.postMessage(message);
4989
6784
  }
4990
6785
  }
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.
6786
+ // The control-only mirror applies due commands immediately while preserving
6787
+ // scheduled ones for its next render block.
4994
6788
  flushOfflineMirror() {
4995
- this.offlineEngine.flushControlCommands();
6789
+ this.offlineEngine.applyCommandsDueNowPreservingFuture();
4996
6790
  }
4997
6791
  sendMirroredCommand(command) {
4998
6792
  const accepted = this.realtimeNode.sendCommand(command);
@@ -5010,16 +6804,24 @@ var SonareEngine = class _SonareEngine {
5010
6804
  trackSends: this.trackSends,
5011
6805
  trackOutputBus: this.trackOutputBus,
5012
6806
  laneSidechains: this.laneSidechains,
6807
+ busSidechains: this.busSidechains,
6808
+ masterSidechains: this.masterSidechains,
5013
6809
  buses: this.buses,
5014
6810
  trackStripJson: this.trackStripJson,
5015
6811
  busStripJson: this.busStripJson,
6812
+ insertParamOverrides: this.insertParamOverrides,
6813
+ flushOfflineMirror: () => this.flushOfflineMirror(),
5016
6814
  postSync: (message) => this.postSync(message),
5017
6815
  ensureTrackLane: (target) => this.ensureTrackLane(target),
5018
6816
  ensureBus: (busId) => this.ensureBus(busId),
5019
6817
  mixerLanes: () => this.mixerLanes(),
5020
- syncMixer: () => this.syncMixer(),
6818
+ syncMixer: (forceInsertResets) => this.syncMixer(forceInsertResets),
6819
+ clearInsertAutomationLanes: (target, forgetResolvedIds) => this.clearInsertAutomationLanes(target, forgetResolvedIds),
5021
6820
  sendSmoothedParam: (paramId, value) => this.sendSmoothedParam(paramId, value),
5022
- getMasterStripJson: () => this.masterStripJson
6821
+ getMasterStripJson: () => this.masterStripJson,
6822
+ cacheMasterStripJson: (sceneJson) => {
6823
+ this.masterStripJson = sceneJson;
6824
+ }
5023
6825
  };
5024
6826
  }
5025
6827
  // Collaborator surface handed to the strip/pan/EQ/insert/MIDI free functions
@@ -5032,7 +6834,10 @@ var SonareEngine = class _SonareEngine {
5032
6834
  postSync: (message) => this.postSync(message),
5033
6835
  postInstrumentSync: (message) => this.postInstrumentSync(message),
5034
6836
  ensureTrackLane: (target) => this.ensureTrackLane(target),
5035
- resolveTargetId: (target) => this.resolveTargetId(target)
6837
+ resolveTargetId: (target) => this.resolveTargetId(target),
6838
+ readStripJson: (target) => cachedStripJson(this.mixerContext, target),
6839
+ writeStripJson: (target, sceneJson) => cacheStripJson(this.mixerContext, target, sceneJson),
6840
+ insertParamOverrides: this.insertParamOverrides
5036
6841
  };
5037
6842
  }
5038
6843
  // Collaborator surface handed to the automation-lane free functions so they
@@ -5174,7 +6979,7 @@ var SonareEngine = class _SonareEngine {
5174
6979
  ensureTrackLane(target) {
5175
6980
  const trackId = this.resolveTargetId(target);
5176
6981
  if (!Number.isInteger(trackId) || trackId <= 0) {
5177
- throw new Error(`Invalid track id for mixer lane: ${String(target)}`);
6982
+ throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
5178
6983
  }
5179
6984
  const existing = this.trackLaneIds.indexOf(trackId);
5180
6985
  if (existing >= 0) {
@@ -5185,20 +6990,36 @@ var SonareEngine = class _SonareEngine {
5185
6990
  return this.trackLaneIds.length - 1;
5186
6991
  }
5187
6992
  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)}`);
6993
+ if (!Number.isInteger(busId) || busId <= 0) {
6994
+ throw new RangeError(`Invalid bus id for mixer bus: ${String(busId)}`);
5191
6995
  }
5192
- const existing = this.buses.findIndex((bus) => bus.busId === resolved);
6996
+ const existing = this.buses.findIndex((bus) => bus.busId === busId);
5193
6997
  if (existing >= 0) {
5194
6998
  return existing;
5195
6999
  }
5196
- this.buses.push({ busId: resolved });
7000
+ this.buses.push({ busId });
5197
7001
  this.syncMixer();
5198
7002
  return this.buses.length - 1;
5199
7003
  }
5200
7004
  };
5201
7005
 
7006
+ // src/worklet/audio_types.ts
7007
+ function copyPlanesToOutput(output, planes, frames) {
7008
+ const monoFanOut = planes.length === 1;
7009
+ for (let ch = 0; ch < output.length; ch++) {
7010
+ const target = output[ch];
7011
+ const source = monoFanOut ? planes[0] : planes[ch];
7012
+ let copied = 0;
7013
+ if (source) {
7014
+ copied = Math.min(target.length, frames, source.length);
7015
+ target.set(source.subarray(0, copied));
7016
+ }
7017
+ if (copied < target.length) {
7018
+ target.fill(0, copied);
7019
+ }
7020
+ }
7021
+ }
7022
+
5202
7023
  // src/worklet/messages.ts
5203
7024
  var DEFAULT_METRONOME_CONFIG = {
5204
7025
  beatGain: 0.35,
@@ -5220,20 +7041,6 @@ function resolveMetronomeConfig(config) {
5220
7041
  }
5221
7042
 
5222
7043
  // 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
- }
5235
- }
5236
- }
5237
7044
  function captureTransferList(channels) {
5238
7045
  const transfers = [];
5239
7046
  const seen = /* @__PURE__ */ new Set();
@@ -5273,9 +7080,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5273
7080
  this.clipPageRequestOverflowReported = 0;
5274
7081
  this.sampleRate = options.sampleRate ?? 48e3;
5275
7082
  this.blockSize = options.blockSize ?? 128;
5276
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
7083
+ this.channelCount = requireChannelCount(options.channelCount, 2);
5277
7084
  this.transport = transport;
5278
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
7085
+ this.meterIntervalFrames = requireIntegerOption(
7086
+ options.meterIntervalFrames,
7087
+ 2048,
7088
+ "meterIntervalFrames",
7089
+ 0
7090
+ );
5279
7091
  this.commandRing = options.commandSharedBuffer ? this.commandRingFromSharedBuffer(options.commandSharedBuffer, options.commandRingCapacity) : void 0;
5280
7092
  this.telemetryRing = options.telemetrySharedBuffer ? this.telemetryRingFromSharedBuffer(
5281
7093
  options.telemetrySharedBuffer,
@@ -5295,10 +7107,13 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5295
7107
  options.externalMidiSharedBuffer,
5296
7108
  options.externalMidiRingCapacity
5297
7109
  ) : void 0;
7110
+ if (this.commandRing && this.commandRing.capacity > 65536) {
7111
+ throw new RangeError("commandRingCapacity must be at most 65536");
7112
+ }
5298
7113
  this.engine = new RealtimeEngine(
5299
7114
  this.sampleRate,
5300
7115
  this.blockSize,
5301
- 1024,
7116
+ Math.max(1024, this.commandRing?.capacity ?? 0),
5302
7117
  1024,
5303
7118
  this.channelCount
5304
7119
  );
@@ -5316,7 +7131,12 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5316
7131
  }
5317
7132
  }
5318
7133
  if (this.scopeRing) {
5319
- const interval = Math.max(1, Math.floor(options.scopeIntervalFrames ?? this.blockSize));
7134
+ const interval = requireIntegerOption(
7135
+ options.scopeIntervalFrames,
7136
+ this.blockSize,
7137
+ "scopeIntervalFrames",
7138
+ 0
7139
+ );
5320
7140
  this.engine.configureScopeTelemetry(interval, this.scopeRing.bands);
5321
7141
  }
5322
7142
  }
@@ -5480,6 +7300,14 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5480
7300
  }
5481
7301
  break;
5482
7302
  }
7303
+ case "syncClipPagePrefetchFrames": {
7304
+ this.engine.setClipPagePrefetchFrames(message.frames);
7305
+ break;
7306
+ }
7307
+ case "syncWarpVoiceCapacity": {
7308
+ this.engine.setWarpVoiceCapacity(message.voices);
7309
+ break;
7310
+ }
5483
7311
  case "syncClipPageCommit": {
5484
7312
  const providerId = this.pagedClipProviders.get(message.clipId);
5485
7313
  const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
@@ -5538,11 +7366,60 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5538
7366
  );
5539
7367
  }
5540
7368
  break;
5541
- case "syncMixer":
7369
+ case "syncMixer": {
7370
+ if (message.sidechainDelta === true) {
7371
+ for (const binding of message.laneSidechains ?? []) {
7372
+ this.engine.setLaneSidechain(
7373
+ binding.trackId,
7374
+ binding.insertIndex,
7375
+ binding.sourceTrackId
7376
+ );
7377
+ }
7378
+ for (const binding of message.busSidechains ?? []) {
7379
+ this.engine.setBusSidechain(
7380
+ binding.busId,
7381
+ binding.insertIndex,
7382
+ binding.sourceKind,
7383
+ binding.sourceId
7384
+ );
7385
+ }
7386
+ for (const binding of message.masterSidechains ?? []) {
7387
+ this.engine.setMasterSidechain(
7388
+ binding.insertIndex,
7389
+ binding.sourceKind,
7390
+ binding.sourceId
7391
+ );
7392
+ }
7393
+ break;
7394
+ }
7395
+ if (this.commandRing) {
7396
+ const write = Atomics.load(this.commandRing.header, 0);
7397
+ const read = Atomics.load(this.commandRing.header, 1);
7398
+ this.drainCommands(Math.min(this.commandRing.capacity, Math.max(0, write - read)));
7399
+ }
7400
+ this.engine.applyCommandsDueNowPreservingFuture();
7401
+ this.engine.settleInsertParameters();
5542
7402
  if (message.buses) {
5543
7403
  this.engine.setTrackBuses(message.buses);
5544
7404
  }
5545
7405
  this.engine.setTrackLanes(message.lanes);
7406
+ for (const target of message.forceInsertResets ?? []) {
7407
+ const emptyJson = emptyStripJson(target);
7408
+ switch (target.kind) {
7409
+ case "track":
7410
+ this.engine.clearTrackInsertParameterBases(target.trackId);
7411
+ this.engine.setTrackStripJson(target.trackId, emptyJson);
7412
+ break;
7413
+ case "bus":
7414
+ this.engine.clearBusInsertParameterBases(target.busId);
7415
+ this.engine.setBusStripJson(target.busId, emptyJson);
7416
+ break;
7417
+ case "master":
7418
+ this.engine.clearMasterInsertParameterBases();
7419
+ this.engine.setMasterStripJson(emptyJson);
7420
+ break;
7421
+ }
7422
+ }
5546
7423
  for (const strip of message.trackStrips ?? []) {
5547
7424
  this.engine.setTrackStripJson(strip.trackId, strip.sceneJson);
5548
7425
  }
@@ -5555,7 +7432,46 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5555
7432
  for (const binding of message.laneSidechains ?? []) {
5556
7433
  this.engine.setLaneSidechain(binding.trackId, binding.insertIndex, binding.sourceTrackId);
5557
7434
  }
7435
+ for (const binding of message.busSidechains ?? []) {
7436
+ this.engine.setBusSidechain(
7437
+ binding.busId,
7438
+ binding.insertIndex,
7439
+ binding.sourceKind,
7440
+ binding.sourceId
7441
+ );
7442
+ }
7443
+ for (const binding of message.masterSidechains ?? []) {
7444
+ this.engine.setMasterSidechain(binding.insertIndex, binding.sourceKind, binding.sourceId);
7445
+ }
7446
+ for (const override of message.insertParamOverrides ?? []) {
7447
+ switch (override.kind) {
7448
+ case "track":
7449
+ this.engine.restoreTrackStripInsertParamByName(
7450
+ override.trackId,
7451
+ override.insertIndex,
7452
+ override.paramName,
7453
+ override.value
7454
+ );
7455
+ break;
7456
+ case "bus":
7457
+ this.engine.restoreBusStripInsertParamByName(
7458
+ override.busId,
7459
+ override.insertIndex,
7460
+ override.paramName,
7461
+ override.value
7462
+ );
7463
+ break;
7464
+ case "master":
7465
+ this.engine.restoreMasterStripInsertParamByName(
7466
+ override.insertIndex,
7467
+ override.paramName,
7468
+ override.value
7469
+ );
7470
+ break;
7471
+ }
7472
+ }
5558
7473
  break;
7474
+ }
5559
7475
  case "syncCapture":
5560
7476
  this.engine.setCaptureBuffer(message.channels, message.bufferFrames);
5561
7477
  this.engine.setCaptureSource(message.source);
@@ -5568,6 +7484,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5568
7484
  case "syncMasterStripEqBand":
5569
7485
  this.engine.setMasterStripEqBandJson(message.bandIndex, message.bandJson);
5570
7486
  break;
7487
+ case "syncBusStripEqBand":
7488
+ this.engine.setBusStripEqBandJson(message.busId, message.bandIndex, message.bandJson);
7489
+ break;
5571
7490
  case "syncTrackStripInsertBypassed":
5572
7491
  this.engine.setTrackStripInsertBypassed(
5573
7492
  message.trackId,
@@ -5584,26 +7503,57 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5584
7503
  );
5585
7504
  break;
5586
7505
  case "syncTrackStripInsertParamByName":
5587
- this.engine.setTrackStripInsertParamByName(
7506
+ this.engine.applyCommandsDueNowPreservingFuture();
7507
+ if (!this.engine.applyTrackStripInsertParamByNameNow(
5588
7508
  message.trackId,
5589
7509
  message.insertIndex,
5590
7510
  message.paramName,
5591
7511
  message.value
5592
- );
7512
+ )) {
7513
+ this.engine.restoreTrackStripInsertParamByName(
7514
+ message.trackId,
7515
+ message.insertIndex,
7516
+ message.paramName,
7517
+ message.value
7518
+ );
7519
+ }
5593
7520
  break;
5594
7521
  case "syncMasterStripInsertParamByName":
5595
- this.engine.setMasterStripInsertParamByName(
7522
+ this.engine.applyCommandsDueNowPreservingFuture();
7523
+ if (!this.engine.applyMasterStripInsertParamByNameNow(
5596
7524
  message.insertIndex,
5597
7525
  message.paramName,
5598
7526
  message.value
5599
- );
7527
+ )) {
7528
+ this.engine.restoreMasterStripInsertParamByName(
7529
+ message.insertIndex,
7530
+ message.paramName,
7531
+ message.value
7532
+ );
7533
+ }
5600
7534
  break;
5601
7535
  case "syncBusStripInsertParamByName":
5602
- this.engine.setBusStripInsertParamByName(
7536
+ this.engine.applyCommandsDueNowPreservingFuture();
7537
+ if (!this.engine.applyBusStripInsertParamByNameNow(
5603
7538
  message.busId,
5604
7539
  message.insertIndex,
5605
7540
  message.paramName,
5606
7541
  message.value
7542
+ )) {
7543
+ this.engine.restoreBusStripInsertParamByName(
7544
+ message.busId,
7545
+ message.insertIndex,
7546
+ message.paramName,
7547
+ message.value
7548
+ );
7549
+ }
7550
+ break;
7551
+ case "syncBusStripInsertBypassed":
7552
+ this.engine.setBusStripInsertBypassed(
7553
+ message.busId,
7554
+ message.insertIndex,
7555
+ message.bypassed,
7556
+ message.resetOnBypass
5607
7557
  );
5608
7558
  break;
5609
7559
  case "syncTrackStripPan":
@@ -5618,6 +7568,18 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5618
7568
  case "syncTrackStripDualPan":
5619
7569
  this.engine.setTrackStripDualPan(message.trackId, message.leftPan, message.rightPan);
5620
7570
  break;
7571
+ case "syncBusStripPan":
7572
+ this.engine.setBusStripPan(message.busId, message.pan);
7573
+ break;
7574
+ case "syncBusStripPanLaw":
7575
+ this.engine.setBusStripPanLaw(message.busId, message.panLaw);
7576
+ break;
7577
+ case "syncBusStripPanMode":
7578
+ this.engine.setBusStripPanMode(message.busId, message.panMode);
7579
+ break;
7580
+ case "syncBusStripDualPan":
7581
+ this.engine.setBusStripDualPan(message.busId, message.leftPan, message.rightPan);
7582
+ break;
5621
7583
  case "syncTrackStripChannelDelaySamples":
5622
7584
  this.engine.setTrackStripChannelDelaySamples(message.trackId, message.delaySamples);
5623
7585
  break;
@@ -5669,8 +7631,36 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5669
7631
  message.renderFrame
5670
7632
  );
5671
7633
  break;
7634
+ case "syncMidiPitchBend":
7635
+ this.engine.pushMidiPitchBend(
7636
+ message.destinationId,
7637
+ message.group,
7638
+ message.channel,
7639
+ message.data0,
7640
+ message.renderFrame
7641
+ );
7642
+ break;
7643
+ case "syncMidiChannelPressure":
7644
+ this.engine.pushMidiChannelPressure(
7645
+ message.destinationId,
7646
+ message.group,
7647
+ message.channel,
7648
+ message.data0,
7649
+ message.renderFrame
7650
+ );
7651
+ break;
7652
+ case "syncMidiPolyPressure":
7653
+ this.engine.pushMidiPolyPressure(
7654
+ message.destinationId,
7655
+ message.group,
7656
+ message.channel,
7657
+ message.data0,
7658
+ message.data1,
7659
+ message.renderFrame
7660
+ );
7661
+ break;
5672
7662
  case "syncMidiUmp":
5673
- this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
7663
+ this.engine.pushMidiUmp(message.destinationId, [message.word0], message.renderFrame);
5674
7664
  break;
5675
7665
  case "syncMidiSysex":
5676
7666
  this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
@@ -5723,6 +7713,31 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5723
7713
  message.portTimeSamples
5724
7714
  );
5725
7715
  break;
7716
+ case "syncMidiInputPitchBend":
7717
+ this.engine.pushMidiInputPitchBend(
7718
+ message.group,
7719
+ message.channel,
7720
+ message.data0,
7721
+ message.portTimeSamples
7722
+ );
7723
+ break;
7724
+ case "syncMidiInputChannelPressure":
7725
+ this.engine.pushMidiInputChannelPressure(
7726
+ message.group,
7727
+ message.channel,
7728
+ message.data0,
7729
+ message.portTimeSamples
7730
+ );
7731
+ break;
7732
+ case "syncMidiInputPolyPressure":
7733
+ this.engine.pushMidiInputPolyPressure(
7734
+ message.group,
7735
+ message.channel,
7736
+ message.data0,
7737
+ message.data1,
7738
+ message.portTimeSamples
7739
+ );
7740
+ break;
5726
7741
  }
5727
7742
  }
5728
7743
  receiveCaptureRequest(message) {
@@ -5803,11 +7818,11 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5803
7818
  this.closed = true;
5804
7819
  }
5805
7820
  }
5806
- drainCommands() {
7821
+ drainCommands(limit = 64) {
5807
7822
  if (!this.commandRing) {
5808
7823
  return;
5809
7824
  }
5810
- for (let i = 0; i < 64; i++) {
7825
+ for (let i = 0; i < limit; i++) {
5811
7826
  const command = popSonareEngineCommandRingBuffer(this.commandRing);
5812
7827
  if (!command) {
5813
7828
  return;
@@ -5994,10 +8009,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
5994
8009
  if (meter.frame !== this.lastMeterFrame) {
5995
8010
  this.lastMeterFrame = meter.frame;
5996
8011
  }
5997
- if (this.meterRing) {
5998
- this.writeMeterRing(meter);
8012
+ if (this.transport?.onMeter) {
8013
+ this.transport.onMeter(meter);
5999
8014
  } else {
6000
- this.transport?.onMeter?.(meter);
6001
8015
  this.transport?.postMessage?.(meter);
6002
8016
  }
6003
8017
  }
@@ -6014,29 +8028,18 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
6014
8028
  }
6015
8029
  Atomics.store(ring.header, 0, writeIndex + 1);
6016
8030
  }
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
- }
8031
+ // A snapshot-object ring writer used to live here as well. It was
8032
+ // unreachable: publishMeters returns after the writeMeterScratch loop
8033
+ // whenever a ring exists, so the ring is never fed from a materialised
8034
+ // snapshot. writeMeterScratch above is the only ring writer, and it keeps the
8035
+ // render callback allocation-free by reading the engine's scratch registers
8036
+ // directly.
8037
+ //
8038
+ // writeIndex is a free-running monotonic counter, so an overflow guard would
8039
+ // fire on essentially every write past the first `capacity` records and store
8040
+ // an ever-growing value, not a dropped-record count. Readers already detect
8041
+ // silent overrun via firstReadable = max(readIndex, writeIndex - capacity),
8042
+ // so header slot 3 is left at its initial 0.
6040
8043
  // Drains the engine's scope producer (FFT spectrum + goniometer points) into
6041
8044
  // the lock-free SAB scope ring. No allocation on the render path: records are
6042
8045
  // written field-by-field into the ring.
@@ -6244,15 +8247,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
6244
8247
  }
6245
8248
  return;
6246
8249
  }
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
- }
8250
+ this.routeMessage(event.data);
6256
8251
  };
6257
8252
  if (port?.addEventListener) {
6258
8253
  port.addEventListener("message", onMessage);
@@ -6271,18 +8266,35 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
6271
8266
  }
6272
8267
  return true;
6273
8268
  }
8269
+ /**
8270
+ * Single dispatch point for the port, shared with the buffered replay so
8271
+ * the two cannot recognize different message sets.
8272
+ */
8273
+ routeMessage(data) {
8274
+ const bridge = this.bridge;
8275
+ if (!bridge) {
8276
+ return;
8277
+ }
8278
+ if (isEngineCommandRecord(data)) {
8279
+ bridge.receiveCommand(data);
8280
+ } else if (isEngineSyncMessage(data)) {
8281
+ bridge.receiveSync(data);
8282
+ } else if (isEngineCaptureRequestMessage(data)) {
8283
+ bridge.receiveCaptureRequest(data);
8284
+ } else if (isEngineTransportRequestMessage(data)) {
8285
+ bridge.receiveTransportRequest(data);
8286
+ } else if (isRecord(data) && typeof data.type === "string" && data.type.startsWith("sync")) {
8287
+ this.port?.postMessage?.({
8288
+ type: "syncError",
8289
+ syncType: data.type,
8290
+ message: `Unrecognized worklet sync message: ${data.type}`
8291
+ });
8292
+ }
8293
+ }
6274
8294
  replayPendingMessages() {
6275
8295
  const messages = this.pendingMessages.splice(0);
6276
8296
  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
- }
8297
+ this.routeMessage(data);
6286
8298
  }
6287
8299
  }
6288
8300
  async initializeEmbind(options, port) {
@@ -6335,15 +8347,48 @@ var SonareWorkletProcessor = class {
6335
8347
  this.processedFrames = 0;
6336
8348
  this.lastMeterFrame = 0;
6337
8349
  this.lastSpectrumFrame = 0;
8350
+ /**
8351
+ * Reused meter record, so a publish writes fields instead of allocating one.
8352
+ *
8353
+ * `targetId` is always the master and the four LUFS / gain-reduction fields
8354
+ * are always unavailable here — the mixer worklet does not run the
8355
+ * K-weighting filters, and a floor value would read as silence — so both are
8356
+ * set once rather than per interval.
8357
+ */
8358
+ this.meterScratch = {
8359
+ type: "meter",
8360
+ targetId: 0,
8361
+ frame: 0,
8362
+ peakDbL: 0,
8363
+ peakDbR: 0,
8364
+ rmsDbL: 0,
8365
+ rmsDbR: 0,
8366
+ correlation: 0,
8367
+ truePeakDbL: 0,
8368
+ truePeakDbR: 0,
8369
+ momentaryLufs: Number.NaN,
8370
+ shortTermLufs: Number.NaN,
8371
+ integratedLufs: Number.NaN,
8372
+ gainReductionDb: Number.NaN
8373
+ };
6338
8374
  if (!options.sceneJson) {
6339
8375
  throw new Error("sceneJson is required.");
6340
8376
  }
6341
8377
  this.sampleRate = options.sampleRate ?? 48e3;
6342
8378
  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));
8379
+ this.meterIntervalFrames = requireIntegerOption(
8380
+ options.meterIntervalFrames,
8381
+ 2048,
8382
+ "meterIntervalFrames",
8383
+ 0
8384
+ );
8385
+ this.spectrumIntervalFrames = requireIntegerOption(
8386
+ options.spectrumIntervalFrames,
8387
+ 0,
8388
+ "spectrumIntervalFrames",
8389
+ 0
8390
+ );
6345
8391
  this.transport = transport;
6346
- this.meterIntervalFrames = Math.max(0, Math.floor(options.meterIntervalFrames ?? 2048));
6347
8392
  this.meterRing = options.meterSharedBuffer ? meterRingFromSharedBuffer(options.meterSharedBuffer, options.meterRingCapacity) : void 0;
6348
8393
  this.spectrumRing = options.spectrumSharedBuffer ? spectrumRingFromSharedBuffer(
6349
8394
  options.spectrumSharedBuffer,
@@ -6360,6 +8405,7 @@ var SonareWorkletProcessor = class {
6360
8405
  throw new Error("stripCount must match the scene strip count.");
6361
8406
  }
6362
8407
  this.realtime = this.mixer.createRealtimeBuffer();
8408
+ this.mixer.configureMeter(this.meterIntervalFrames > 0, 4);
6363
8409
  }
6364
8410
  process(inputs, outputs) {
6365
8411
  if (this.closed) {
@@ -6406,10 +8452,7 @@ var SonareWorkletProcessor = class {
6406
8452
  }
6407
8453
  }
6408
8454
  this.processedFrames += usable;
6409
- this.publishMeter(
6410
- this.realtime.outLeft.subarray(0, usable),
6411
- this.realtime.outRight.subarray(0, usable)
6412
- );
8455
+ this.publishMeter();
6413
8456
  this.publishSpectrum(
6414
8457
  this.realtime.outLeft.subarray(0, usable),
6415
8458
  this.realtime.outRight.subarray(0, usable)
@@ -6425,7 +8468,11 @@ var SonareWorkletProcessor = class {
6425
8468
  return;
6426
8469
  }
6427
8470
  if (message.type === "setMeterInterval") {
6428
- this.meterIntervalFrames = Math.max(0, Math.floor(message.frames));
8471
+ const frames = Math.max(0, Math.floor(message.frames));
8472
+ if (frames > 0 !== this.meterIntervalFrames > 0) {
8473
+ this.mixer.configureMeter(frames > 0, 4);
8474
+ }
8475
+ this.meterIntervalFrames = frames;
6429
8476
  return;
6430
8477
  }
6431
8478
  if (message.type === "scheduleInsertAutomation") {
@@ -6445,58 +8492,52 @@ var SonareWorkletProcessor = class {
6445
8492
  this.closed = true;
6446
8493
  }
6447
8494
  }
6448
- publishMeter(left, right) {
6449
- if (!this.transport || this.meterIntervalFrames <= 0) {
8495
+ publishMeter() {
8496
+ if (!this.transport && !this.meterRing || this.meterIntervalFrames <= 0) {
6450
8497
  return;
6451
8498
  }
6452
8499
  if (this.processedFrames - this.lastMeterFrame < this.meterIntervalFrames) {
6453
8500
  return;
6454
8501
  }
6455
8502
  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;
8503
+ if (!this.mixer.latchMeterSnapshot()) {
8504
+ return;
8505
+ }
8506
+ if (this.meterRing) {
8507
+ const meter2 = this.meterScratch;
8508
+ meter2.frame = this.processedFrames;
8509
+ meter2.peakDbL = this.mixer.meterScratchValue(0);
8510
+ meter2.peakDbR = this.mixer.meterScratchValue(1);
8511
+ meter2.rmsDbL = this.mixer.meterScratchValue(2);
8512
+ meter2.rmsDbR = this.mixer.meterScratchValue(3);
8513
+ meter2.correlation = this.mixer.meterScratchValue(4);
8514
+ meter2.truePeakDbL = this.mixer.meterScratchValue(5);
8515
+ meter2.truePeakDbR = this.mixer.meterScratchValue(6);
8516
+ this.writeMeterRing(meter2);
8517
+ return;
6475
8518
  }
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
8519
  const meter = {
6480
8520
  type: "meter",
6481
8521
  targetId: 0,
6482
8522
  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),
8523
+ peakDbL: this.mixer.meterScratchValue(0),
8524
+ peakDbR: this.mixer.meterScratchValue(1),
8525
+ rmsDbL: this.mixer.meterScratchValue(2),
8526
+ rmsDbR: this.mixer.meterScratchValue(3),
8527
+ correlation: this.mixer.meterScratchValue(4),
8528
+ truePeakDbL: this.mixer.meterScratchValue(5),
8529
+ truePeakDbR: this.mixer.meterScratchValue(6),
8530
+ // Declared unavailable rather than floored: the mixer worklet does not run
8531
+ // the K-weighting filters, and a floor value would read as silence.
6490
8532
  momentaryLufs: Number.NaN,
6491
8533
  shortTermLufs: Number.NaN,
6492
8534
  integratedLufs: Number.NaN,
6493
8535
  gainReductionDb: Number.NaN
6494
8536
  };
6495
- this.transport.onMeter?.(meter);
6496
- if (this.meterRing) {
6497
- this.writeMeterRing(meter);
8537
+ if (this.transport?.onMeter) {
8538
+ this.transport.onMeter(meter);
6498
8539
  } else {
6499
- this.transport.postMessage?.(meter);
8540
+ this.transport?.postMessage?.(meter);
6500
8541
  }
6501
8542
  }
6502
8543
  writeMeterRing(meter) {
@@ -6540,8 +8581,11 @@ var SonareWorkletProcessor = class {
6540
8581
  frame: this.processedFrames,
6541
8582
  bands: new Float32Array(this.spectrumBands)
6542
8583
  };
6543
- this.transport?.onSpectrum?.(spectrum);
6544
- this.transport?.postMessage?.(spectrum);
8584
+ if (this.transport?.onSpectrum) {
8585
+ this.transport.onSpectrum(spectrum);
8586
+ } else {
8587
+ this.transport?.postMessage?.(spectrum);
8588
+ }
6545
8589
  }
6546
8590
  computeSpectrum(left, right) {
6547
8591
  const n = Math.max(1, Math.min(left.length, right.length));
@@ -6609,13 +8653,232 @@ function registerSonareWorkletProcessor(name = "sonare-worklet-processor") {
6609
8653
  scope.registerProcessor(name, RegisteredSonareWorkletProcessor);
6610
8654
  }
6611
8655
 
8656
+ // src/worklet/playback-processor.ts
8657
+ var MAX_INPUT_CHANNELS = 8;
8658
+ var SPEAKER_LAYOUT_CHANNELS = { stereo: 2, "5.1": 6, "7.1": 8 };
8659
+ function configObject(config) {
8660
+ if (config === void 0) {
8661
+ return {};
8662
+ }
8663
+ return typeof config === "string" ? JSON.parse(config) : config;
8664
+ }
8665
+ function outputChannelCount(config) {
8666
+ if (config.target?.kind !== "speakers") {
8667
+ return 2;
8668
+ }
8669
+ const count = SPEAKER_LAYOUT_CHANNELS[config.target.layout ?? ""];
8670
+ if (count === void 0) {
8671
+ throw new RangeError('target.layout must be "stereo", "5.1" or "7.1" for a speakers target');
8672
+ }
8673
+ return count;
8674
+ }
8675
+ var _SonarePlaybackWorkletProcessor = class _SonarePlaybackWorkletProcessor {
8676
+ constructor(options = {}, port) {
8677
+ this.inputPlanes = [];
8678
+ this.outputPlanes = [];
8679
+ this.unsupportedInputBlocks = 0;
8680
+ this.destroyed = false;
8681
+ this.port = port;
8682
+ this.maxBlockSize = requireIntegerOption(options.maxBlockSize, 128, "maxBlockSize", 1);
8683
+ const scopeRate = globalThis.sampleRate;
8684
+ const sampleRate = options.sampleRate ?? (typeof scopeRate === "number" ? scopeRate : 48e3);
8685
+ const hrtf = options.hrtf ? HrtfSet.fromBytes(new Uint8Array(options.hrtf)) : void 0;
8686
+ try {
8687
+ this.renderer = new PlaybackRenderer({
8688
+ config: options.config ?? {},
8689
+ hrtf,
8690
+ sampleRate,
8691
+ maxBlockSize: this.maxBlockSize
8692
+ });
8693
+ } finally {
8694
+ hrtf?.delete();
8695
+ }
8696
+ this.native = this.renderer.native;
8697
+ this.outputChannels = this.native.outputChannels();
8698
+ this.acquirePlanes();
8699
+ }
8700
+ /**
8701
+ * Handles a control-plane message. AudioWorklet port handlers run on the
8702
+ * rendering thread between `process()` calls, which is what makes `reset`
8703
+ * safe here. A refused message is answered with an `error` message.
8704
+ */
8705
+ receiveMessage(message) {
8706
+ if (this.destroyed) {
8707
+ return;
8708
+ }
8709
+ try {
8710
+ switch (message.type) {
8711
+ case "config":
8712
+ this.renderer.setConfig(message.config);
8713
+ break;
8714
+ case "orientation":
8715
+ this.renderer.setHeadOrientation(message.yaw, message.pitch ?? 0, message.roll ?? 0);
8716
+ break;
8717
+ case "reset":
8718
+ this.renderer.reset();
8719
+ break;
8720
+ case "diagnostics":
8721
+ this.port?.postMessage?.({
8722
+ type: "diagnostics",
8723
+ diagnostics: this.diagnostics()
8724
+ });
8725
+ break;
8726
+ case "destroy":
8727
+ this.destroy();
8728
+ break;
8729
+ }
8730
+ } catch (error) {
8731
+ this.port?.postMessage?.({
8732
+ type: "error",
8733
+ request: message.type,
8734
+ message: error instanceof Error ? error.message : String(error)
8735
+ });
8736
+ }
8737
+ }
8738
+ /** The renderer's diagnostics plus `unsupported_input_blocks`, as a plain object. */
8739
+ diagnostics() {
8740
+ return {
8741
+ ...this.renderer.diagnostics(),
8742
+ unsupported_input_blocks: this.unsupportedInputBlocks
8743
+ };
8744
+ }
8745
+ process(inputs, outputs) {
8746
+ if (this.destroyed) {
8747
+ return false;
8748
+ }
8749
+ const output = outputs[0];
8750
+ const requested = output?.[0]?.length ?? 0;
8751
+ if (!output || requested === 0) {
8752
+ return true;
8753
+ }
8754
+ const frames = this.clampFrames(requested);
8755
+ if (this.inputPlanes[0]?.byteLength === 0 || this.outputPlanes[0]?.byteLength === 0) {
8756
+ this.acquirePlanes();
8757
+ }
8758
+ const input = inputs[0];
8759
+ const inChannels = input?.length ?? 0;
8760
+ let code;
8761
+ if (inChannels === 0) {
8762
+ code = this.native.processPreparedSilence(frames);
8763
+ } else if (inChannels > MAX_INPUT_CHANNELS) {
8764
+ this.unsupportedInputBlocks++;
8765
+ code = this.native.processPreparedSilence(frames);
8766
+ } else {
8767
+ for (let ch = 0; ch < inChannels; ch++) {
8768
+ const source = input[ch];
8769
+ const plane = this.inputPlanes[ch];
8770
+ const copied = Math.min(frames, source.length);
8771
+ plane.set(copied === source.length ? source : source.subarray(0, copied));
8772
+ if (copied < frames) {
8773
+ plane.fill(0, copied, frames);
8774
+ }
8775
+ }
8776
+ code = this.native.processPrepared(inChannels, frames);
8777
+ if (code !== 0) {
8778
+ this.unsupportedInputBlocks++;
8779
+ code = this.native.processPreparedSilence(frames);
8780
+ }
8781
+ }
8782
+ if (code === 0) {
8783
+ copyPlanesToOutput(output, this.outputPlanes, frames);
8784
+ } else {
8785
+ for (const channel of output) {
8786
+ channel.fill(0);
8787
+ }
8788
+ }
8789
+ return true;
8790
+ }
8791
+ destroy() {
8792
+ if (this.destroyed) {
8793
+ return;
8794
+ }
8795
+ this.destroyed = true;
8796
+ this.renderer.delete();
8797
+ }
8798
+ acquirePlanes() {
8799
+ this.inputPlanes = [];
8800
+ for (let ch = 0; ch < MAX_INPUT_CHANNELS; ch++) {
8801
+ this.inputPlanes.push(this.native.inputPlane(ch));
8802
+ }
8803
+ this.outputPlanes = [];
8804
+ for (let ch = 0; ch < this.outputChannels; ch++) {
8805
+ this.outputPlanes.push(this.native.outputPlane(ch));
8806
+ }
8807
+ }
8808
+ // Frames past the construction-time capacity are left silent rather than
8809
+ // reallocating on the audio thread.
8810
+ clampFrames(frames) {
8811
+ if (frames <= this.maxBlockSize) {
8812
+ return frames;
8813
+ }
8814
+ if (!_SonarePlaybackWorkletProcessor.warnedBlockOverflow) {
8815
+ _SonarePlaybackWorkletProcessor.warnedBlockOverflow = true;
8816
+ console.warn(
8817
+ `SonarePlaybackWorkletProcessor: requested ${frames} frames exceeds maxBlockSize ${this.maxBlockSize}; clamping.`
8818
+ );
8819
+ }
8820
+ return this.maxBlockSize;
8821
+ }
8822
+ };
8823
+ _SonarePlaybackWorkletProcessor.warnedBlockOverflow = false;
8824
+ var SonarePlaybackWorkletProcessor = _SonarePlaybackWorkletProcessor;
8825
+ function registerSonarePlaybackWorkletProcessor(name = "sonare-playback-processor") {
8826
+ const scope = globalThis;
8827
+ if (!scope.AudioWorkletProcessor || !scope.registerProcessor) {
8828
+ throw new Error("AudioWorkletProcessor is not available in this context.");
8829
+ }
8830
+ const Base = scope.AudioWorkletProcessor;
8831
+ class RegisteredSonarePlaybackWorkletProcessor extends Base {
8832
+ constructor(options) {
8833
+ super();
8834
+ const port = this.port;
8835
+ this.bridge = new SonarePlaybackWorkletProcessor(options?.processorOptions ?? {}, port);
8836
+ const onMessage = (event) => {
8837
+ if (isPlaybackMessage(event.data)) {
8838
+ this.bridge.receiveMessage(event.data);
8839
+ }
8840
+ };
8841
+ if (port?.addEventListener) {
8842
+ port.addEventListener("message", onMessage);
8843
+ port.start?.();
8844
+ } else if (port) {
8845
+ port.onmessage = onMessage;
8846
+ }
8847
+ }
8848
+ process(inputs, outputs) {
8849
+ return this.bridge.process(inputs, outputs);
8850
+ }
8851
+ }
8852
+ scope.registerProcessor(name, RegisteredSonarePlaybackWorkletProcessor);
8853
+ }
8854
+ function createSonarePlaybackNode(context, options = {}) {
8855
+ const config = configObject(options.config);
8856
+ const factory = options.nodeFactory ?? ((ctx, name, nodeOptions) => new AudioWorkletNode(ctx, name, nodeOptions));
8857
+ const processorOptions = {
8858
+ config: JSON.stringify(config),
8859
+ hrtf: options.hrtf,
8860
+ maxBlockSize: options.maxBlockSize,
8861
+ sampleRate: options.sampleRate ?? context.sampleRate
8862
+ };
8863
+ return factory(context, options.processorName ?? "sonare-playback-processor", {
8864
+ numberOfInputs: 1,
8865
+ numberOfOutputs: 1,
8866
+ outputChannelCount: [outputChannelCount(config)],
8867
+ channelCountMode: "max",
8868
+ channelInterpretation: "discrete",
8869
+ processorOptions
8870
+ });
8871
+ }
8872
+
6612
8873
  // src/worklet/voice-changer-processor.ts
6613
8874
  var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChangerWorkletProcessor {
6614
8875
  constructor(options = {}) {
8876
+ // Reused so the mono fan-out never builds an array on the audio thread.
8877
+ this.monoPlane = [new Float32Array(0)];
6615
8878
  this.destroyed = false;
6616
8879
  this.sampleRate = options.sampleRate ?? 48e3;
6617
8880
  this.blockSize = options.blockSize ?? 128;
6618
- this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 1));
8881
+ this.channelCount = requireChannelCount(options.channelCount, 1);
6619
8882
  this.changer = new RealtimeVoiceChanger(options.preset ?? "neutral-monitor");
6620
8883
  this.changer.prepare(this.sampleRate, this.blockSize, this.channelCount);
6621
8884
  this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
@@ -6668,7 +8931,8 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
6668
8931
  this.monoInput.fill(0, 0, frames2);
6669
8932
  }
6670
8933
  this.changer.processPreparedMono(frames2);
6671
- output[0].set(this.monoOutput.subarray(0, frames2));
8934
+ this.monoPlane[0] = this.monoOutput;
8935
+ copyPlanesToOutput(output, this.monoPlane, frames2);
6672
8936
  return true;
6673
8937
  }
6674
8938
  const frames = this.ensureInterleavedCapacity(requestedFrames, requestedChannels);
@@ -6686,12 +8950,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
6686
8950
  }
6687
8951
  }
6688
8952
  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
- }
8953
+ copyPlanesToOutput(output, this.planarChannels, frames);
6695
8954
  return true;
6696
8955
  }
6697
8956
  destroy() {
@@ -6801,6 +9060,7 @@ export {
6801
9060
  SonareEngineCommandType,
6802
9061
  SonareEngineTelemetryError,
6803
9062
  SonareEngineTelemetryType,
9063
+ SonarePlaybackWorkletProcessor,
6804
9064
  SonareRealtimeEngineNode,
6805
9065
  SonareRealtimeEngineWorkletProcessor,
6806
9066
  SonareRealtimeVoiceChangerWorkletProcessor,
@@ -6812,6 +9072,7 @@ export {
6812
9072
  createSonareEngineTelemetryRingBuffer,
6813
9073
  createSonareExternalMidiRingBuffer,
6814
9074
  createSonareMeterRingBuffer,
9075
+ createSonarePlaybackNode,
6815
9076
  createSonareScopeRingBuffer,
6816
9077
  createSonareSpectrumRingBuffer,
6817
9078
  decodeFrame,
@@ -6829,6 +9090,7 @@ export {
6829
9090
  readSonareMeterRingBuffer,
6830
9091
  readSonareScopeRingBuffer,
6831
9092
  readSonareSpectrumRingBuffer,
9093
+ registerSonarePlaybackWorkletProcessor,
6832
9094
  registerSonareRealtimeEngineWorkletProcessor,
6833
9095
  registerSonareRealtimeVoiceChangerWorkletProcessor,
6834
9096
  registerSonareWorkletProcessor,