hyperframes 0.8.145 → 0.8.147

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 (186) hide show
  1. package/dist/{add-B6OUH46W.js → add-5CNUY3EW.js} +15 -15
  2. package/dist/{animatedGifPrep-MIWRDJIO.js → animatedGifPrep-ZID2XJRQ.js} +10 -10
  3. package/dist/{asset-XJ2B2PU3.js → asset-TFEENZ7K.js} +2 -2
  4. package/dist/{assetCataloger-A5ZLSCMZ.js → assetCataloger-GX4B55VB.js} +4 -4
  5. package/dist/{auth-5HOVCZMV.js → auth-EJVZW6C3.js} +2 -2
  6. package/dist/{autoUpdate-WVNL5K3D.js → autoUpdate-MBNEZ2U4.js} +2 -2
  7. package/dist/{backgroundChecks-IGF54SFH.js → backgroundChecks-DNW5NPTY.js} +3 -3
  8. package/dist/backgroundChecksWorker.js +3 -3
  9. package/dist/{batchRender-T6JEGCQV.js → batchRender-T7S2CGJE.js} +5 -5
  10. package/dist/{beats-37T6Y6QP.js → beats-STXBGN6Z.js} +16 -16
  11. package/dist/{benchmark-ZVVLOWPP.js → benchmark-AC6IGEQP.js} +12 -12
  12. package/dist/{browser-4SSJOLH4.js → browser-FB5AK32T.js} +5 -5
  13. package/dist/{bundleWithLocalizedFonts-XWK423QP.js → bundleWithLocalizedFonts-HSCWGCFO.js} +2 -2
  14. package/dist/{capture-KZXOPBMO.js → capture-I24MXH3Y.js} +13 -13
  15. package/dist/{capture-2SF7YP57.js → capture-VEIVUHWB.js} +2 -2
  16. package/dist/{catalog-NLD4A2C6.js → catalog-JRC4VTTI.js} +15 -15
  17. package/dist/{catch-up-4BSRN6IR.js → catch-up-NTCK7Z3G.js} +6 -6
  18. package/dist/{check-IA44QQWM.js → check-7M7W4LPN.js} +38 -38
  19. package/dist/{checkBrowser-VM35KZMW.js → checkBrowser-2YRVCAYB.js} +118 -61
  20. package/dist/{chunk-OTHDVXOJ.js → chunk-2IDHEEAN.js} +3 -3
  21. package/dist/{chunk-VHFEMFYL.js → chunk-3BETQ4XI.js} +3 -3
  22. package/dist/{chunk-3KU3PFDW.js → chunk-3BYMCR4O.js} +85 -75
  23. package/dist/{chunk-FXJJCPPB.js → chunk-3HSWLZ6S.js} +1 -1
  24. package/dist/{chunk-ZF7SXQJ7.js → chunk-3PPO64IB.js} +2 -2
  25. package/dist/{chunk-PLR7Z6TC.js → chunk-3YUFN37G.js} +1 -1
  26. package/dist/{chunk-GRY3PN3K.js → chunk-4YVMMRHR.js} +2 -2
  27. package/dist/{chunk-ICKDMTLA.js → chunk-55RZG5SQ.js} +2 -2
  28. package/dist/{chunk-3IBMBNYY.js → chunk-5OKZLMQK.js} +10 -10
  29. package/dist/{chunk-DIRJ322I.js → chunk-66SAATRI.js} +3 -3
  30. package/dist/{chunk-GMAJB2P6.js → chunk-6BSIQJPW.js} +1 -1
  31. package/dist/{chunk-4QWD6JYN.js → chunk-6WD636E5.js} +3 -3
  32. package/dist/{chunk-5WVW7P23.js → chunk-A72OAGNB.js} +6 -6
  33. package/dist/{chunk-XBX4QJP7.js → chunk-AFIATIKS.js} +1 -1
  34. package/dist/{chunk-G75ENCEZ.js → chunk-AIJS74LG.js} +3 -3
  35. package/dist/{chunk-FBCULCAL.js → chunk-AL2YEVOT.js} +1 -1
  36. package/dist/{chunk-GEMJYBRU.js → chunk-B45OFFOQ.js} +2 -2
  37. package/dist/{chunk-VRW6EBHV.js → chunk-BAKPN2D2.js} +101 -94
  38. package/dist/{chunk-HB7FJOY4.js → chunk-BPUFVVSH.js} +1 -1
  39. package/dist/{chunk-BIAC7IJT.js → chunk-DDNS2674.js} +1 -1
  40. package/dist/{chunk-QFSCCQIR.js → chunk-DMKI64EZ.js} +1 -1
  41. package/dist/{chunk-EPE2HRJK.js → chunk-DYWSCPCR.js} +69 -64
  42. package/dist/{chunk-ZIYZ77MR.js → chunk-EGO6Z24R.js} +3 -3
  43. package/dist/{chunk-KC5XXX3X.js → chunk-EOOEC32M.js} +1 -1
  44. package/dist/{chunk-XIHCMVMG.js → chunk-FDL4HV44.js} +2 -2
  45. package/dist/{chunk-QZ3AZTXI.js → chunk-FJWEYVEM.js} +42 -16
  46. package/dist/{chunk-PUGIRFPX.js → chunk-FMNVTKX2.js} +4 -4
  47. package/dist/{chunk-IHTRYDFN.js → chunk-H7B6JSHI.js} +1 -1
  48. package/dist/{chunk-T42U2W2R.js → chunk-IXLUTE6N.js} +1 -1
  49. package/dist/{chunk-FDT6KQV4.js → chunk-JBQJKC23.js} +2 -2
  50. package/dist/{chunk-IEIJRMLZ.js → chunk-JDB65PPT.js} +62 -63
  51. package/dist/{chunk-UXO2L7HJ.js → chunk-JF5L5SQ6.js} +278 -65
  52. package/dist/{chunk-USDEMXXL.js → chunk-JYVGYKXG.js} +1 -1
  53. package/dist/{chunk-R42WWQVB.js → chunk-KPJMBC4K.js} +2 -2
  54. package/dist/{chunk-MAYCCGMQ.js → chunk-KVGDFUVO.js} +1 -1
  55. package/dist/{chunk-CFUXNBWH.js → chunk-LE7T4ML7.js} +1 -1
  56. package/dist/{chunk-MQZVBHEY.js → chunk-LFNQDTSH.js} +1 -1
  57. package/dist/{chunk-KIFHYXPB.js → chunk-MOL32UKR.js} +45 -11
  58. package/dist/{chunk-6LGNTUZN.js → chunk-N53FLAMZ.js} +7 -7
  59. package/dist/{chunk-ADF3X6GM.js → chunk-P6HELLPU.js} +2 -2
  60. package/dist/{chunk-5NRXZGKK.js → chunk-POKK6XXU.js} +9 -9
  61. package/dist/{chunk-GBGJ7A3D.js → chunk-QHRPB2JQ.js} +6 -6
  62. package/dist/{chunk-BFACGEII.js → chunk-QTTDKWSM.js} +2 -2
  63. package/dist/{chunk-U5QY7KTN.js → chunk-SEABQTBP.js} +11 -8
  64. package/dist/{chunk-ZEUONDQG.js → chunk-SYUGVPYH.js} +1 -1
  65. package/dist/{chunk-PJ3HDUCY.js → chunk-VAS6HQX3.js} +39 -9
  66. package/dist/{chunk-GQJZORQP.js → chunk-XBSPQ52S.js} +1 -1
  67. package/dist/{chunk-4ORLNTET.js → chunk-XOJDBRNF.js} +1 -1
  68. package/dist/{chunk-LAFJSYDL.js → chunk-XVIYZU3D.js} +1 -1
  69. package/dist/{chunk-OB5MJR4W.js → chunk-YCGU3UNU.js} +1 -1
  70. package/dist/{chunk-GXJEGZL2.js → chunk-Z5C4LEAD.js} +17 -18
  71. package/dist/chunk-Z6FLHIDV.js +3795 -0
  72. package/dist/{chunk-DDIVWXN7.js → chunk-ZFN6GQU4.js} +4 -4
  73. package/dist/{chunk-QLSM7K5S.js → chunk-ZLL72U3I.js} +2 -2
  74. package/dist/{chunk-ZSVUAVMT.js → chunk-ZQXNL5DL.js} +5 -5
  75. package/dist/{chunk-DWUKJQTO.js → chunk-ZVZSFDW3.js} +2 -2
  76. package/dist/{clean-RDKJB5FK.js → clean-3RY3Y2GX.js} +23 -23
  77. package/dist/cli.js +52 -52
  78. package/dist/{cloud-QVPVVNCW.js → cloud-MLPBFCVE.js} +4 -4
  79. package/dist/{cloudrun-3RE2UCQG.js → cloudrun-NQF5KFQI.js} +6 -6
  80. package/dist/{compare-LEDOY7MO.js → compare-6EZX2754.js} +32 -32
  81. package/dist/{compiler-F54OZZPM.js → compiler-ACCPOZMQ.js} +6 -6
  82. package/dist/{component-N42M67WY.js → component-F5PQHP6A.js} +3 -3
  83. package/dist/{compositions-VB2J2LZI.js → compositions-AUHWSI2H.js} +15 -15
  84. package/dist/{delete-Q6RWMJ4Z.js → delete-VUF4SHHJ.js} +2 -2
  85. package/dist/{deterministicFonts-U2BIMGPG.js → deterministicFonts-C5JQLCTS.js} +2 -2
  86. package/dist/{dist-Q4BI5A73.js → dist-GPDOV3IQ.js} +4 -4
  87. package/dist/{dist-KDJTFJHA.js → dist-HUMFCJ6T.js} +3 -3
  88. package/dist/{doctor-JMMUHDRV.js → doctor-3XHCA5JM.js} +15 -15
  89. package/dist/{events-RGUAHIBW.js → events-FKOIB3R3.js} +5 -5
  90. package/dist/{events-6ZBSIBE5.js → events-TIU72LZM.js} +4 -4
  91. package/dist/{feedback-FIID6MZW.js → feedback-V6VATYBE.js} +10 -10
  92. package/dist/{ffmpeg-T5SH2EFJ.js → ffmpeg-5V4FP3R6.js} +2 -2
  93. package/dist/{figma-BL3JJC36.js → figma-YBJM2VGE.js} +3 -3
  94. package/dist/fontLocalizeCli.js +14 -14
  95. package/dist/{get-P46GS7S6.js → get-3YVCGZBX.js} +2 -2
  96. package/dist/{gpuPolicy-AQ2BPLBK.js → gpuPolicy-ZOUG66M7.js} +10 -10
  97. package/dist/{grade-compare-GDCWU7ZZ.js → grade-compare-MYXIRKQI.js} +33 -33
  98. package/dist/{help-HJR3RWFG.js → help-WFZ2SUUR.js} +2 -2
  99. package/dist/{history-GGO5AWJB.js → history-HWPRJFR5.js} +49 -22
  100. package/dist/hyperframe-runtime.js +214 -68
  101. package/dist/hyperframe.manifest.json +1 -1
  102. package/dist/hyperframe.runtime.iife.js +214 -68
  103. package/dist/hyperframes-player.global.js +4 -5
  104. package/dist/hyperframes-slideshow.global.js +3 -4
  105. package/dist/{info-3GFWUBJC.js → info-WJDTQ4OZ.js} +11 -11
  106. package/dist/{init-36OPU2ZG.js → init-EXHDN7RO.js} +31 -31
  107. package/dist/{inspect-F2HEL5ON.js → inspect-EAOLZOMQ.js} +33 -33
  108. package/dist/{installer-NI6P6GC4.js → installer-LZ4X5TZZ.js} +6 -6
  109. package/dist/{keyframes-7OY3LMAZ.js → keyframes-FSZYNEDN.js} +8 -8
  110. package/dist/{lambda-6GJIWNJ3.js → lambda-3GAKHMC3.js} +7 -7
  111. package/dist/{layout-FTMLNL5A.js → layout-M2TQ2CIX.js} +33 -33
  112. package/dist/{lint-U6POHUON.js → lint-7C5UPTNQ.js} +11 -11
  113. package/dist/{lintProject-UBZ3TDHU.js → lintProject-SSEXWCH6.js} +3 -3
  114. package/dist/{list-IP5KI5Q2.js → list-GU5KBZ73.js} +2 -2
  115. package/dist/{login-4VWTVPHG.js → login-ZL2NYBIZ.js} +4 -4
  116. package/dist/{manager-YKMHR6LN.js → manager-5ORDVWJB.js} +3 -3
  117. package/dist/{media-treatment-XGV44AGV.js → media-treatment-QC3TAHVU.js} +82 -54
  118. package/dist/{models-UCXB6UR5.js → models-A54I4QR5.js} +14 -14
  119. package/dist/{motionShot-WPVNBXKA.js → motionShot-ZXPK562F.js} +35 -29
  120. package/dist/{normalize-audio-TYJ3UEHO.js → normalize-audio-CAP3DAE6.js} +10 -10
  121. package/dist/{open-HENOLVB2.js → open-GUSBNMDB.js} +6 -6
  122. package/dist/{parakeetRunner-VFQ6WFTD.js → parakeetRunner-GCCH72CO.js} +14 -14
  123. package/dist/{pipeline-6WPSC3H4.js → pipeline-SCJ2MWYH.js} +11 -11
  124. package/dist/{play-OI53WNUM.js → play-FP2QLMGM.js} +26 -26
  125. package/dist/{preflight-B4AFK3RG.js → preflight-ONF5AJNO.js} +5 -5
  126. package/dist/{present-NGMEQ4NO.js → present-H5SA4APW.js} +23 -23
  127. package/dist/{preview-VT2HWRWZ.js → preview-ATPVKA6K.js} +57 -26
  128. package/dist/{producer-7TYEAUF7.js → producer-VBYUC27R.js} +1 -1
  129. package/dist/{projectConfig-B2AF5DXY.js → projectConfig-SBRMQZGG.js} +8 -8
  130. package/dist/{publication-2PANQVC6.js → publication-5BQICRZO.js} +4 -4
  131. package/dist/{publish-O5OL42BG.js → publish-XDUZA3E3.js} +27 -26
  132. package/dist/{remove-background-T2JM6UVP.js → remove-background-GA6F7JZW.js} +12 -12
  133. package/dist/{render-Z253WHBK.js → render-KUVZFNQD.js} +27 -27
  134. package/dist/{render-W7OFXDOJ.js → render-PCFO45RK.js} +5 -5
  135. package/dist/{render-OLPKOJRU.js → render-XCYXFZTL.js} +12 -12
  136. package/dist/{render-batch-46IIBFKL.js → render-batch-7R2NRNVJ.js} +5 -5
  137. package/dist/{renderCancellation-QTZOCDJ2.js → renderCancellation-NIXEWRAO.js} +10 -10
  138. package/dist/renderSetupWorker.js +3 -3
  139. package/dist/{resolver-J2PL5FVJ.js → resolver-C4FPX72I.js} +6 -6
  140. package/dist/{sherpa-ZF42V3RN.js → sherpa-IQY52XU6.js} +14 -14
  141. package/dist/skills/media-use/references/media-treatments.md +5 -1
  142. package/dist/{skills-ACDOO3PX.js → skills-JSSAEKUW.js} +7 -7
  143. package/dist/{skillsUpdateCheck-L5OXVVPU.js → skillsUpdateCheck-XE5E2K2F.js} +3 -3
  144. package/dist/{slideshow-4OJEPWNZ.js → slideshow-YTWATJRT.js} +1 -1
  145. package/dist/{snapshot-UUVNZOW5.js → snapshot-QMQOE2DD.js} +34 -34
  146. package/dist/{src-BSLKXEGX.js → src-2GMPFYHL.js} +11 -9
  147. package/dist/{src-KS3AWFGT.js → src-R55QLGWI.js} +13 -13
  148. package/dist/{staticProjectServer-2TTVGPQH.js → staticProjectServer-YH2RHYKW.js} +20 -20
  149. package/dist/{status-LXPGSSKY.js → status-PTKNQD2E.js} +2 -2
  150. package/dist/studio/assets/{hyperframes-player-DZ5JrMDg.js → hyperframes-player-_eMxRXF_.js} +3 -3
  151. package/dist/studio/assets/{index-BqFRhNZa.js → index-6okhHdnH.js} +1 -1
  152. package/dist/studio/assets/{index-BOs6C0aZ.js → index-CEq9KmY2.js} +1 -1
  153. package/dist/studio/assets/{index-D8W0a0Z9.js → index-bXzd7tvF.js} +135 -135
  154. package/dist/studio/index.html +1 -1
  155. package/dist/{studio-api-WYWF7XRC.js → studio-api-T2TS2CHL.js} +16 -14
  156. package/dist/{studioServer-3FG4YTP7.js → studioServer-Q4MT44LT.js} +50 -50
  157. package/dist/{system-OQLOGGPJ.js → system-OXRG57F2.js} +2 -2
  158. package/dist/{telemetry-PAOOB4T4.js → telemetry-V5JYZDS4.js} +5 -5
  159. package/dist/{timeline-JIIJKKPC.js → timeline-KTCTOKF5.js} +34 -25
  160. package/dist/{tokens-3QRAM75Z.js → tokens-ZHREZPK6.js} +2 -2
  161. package/dist/{transcribe-QSBP5MUG.js → transcribe-4QDEWCE6.js} +12 -12
  162. package/dist/{transcribe-SZB6UMXE.js → transcribe-CRAV4FU3.js} +13 -13
  163. package/dist/{updateCheck-V3DSB26K.js → updateCheck-E7RMSK3E.js} +2 -2
  164. package/dist/{upgrade-L5GISOEV.js → upgrade-27PFTDCV.js} +2 -2
  165. package/dist/{urlDownloader-52FERT4X.js → urlDownloader-EAF6PLUP.js} +9 -9
  166. package/dist/{validate-HVVLX7KO.js → validate-OKNFDBMG.js} +27 -27
  167. package/dist/{video-MEOWRMFG.js → video-IEVS4FIH.js} +10 -10
  168. package/package.json +3 -2
  169. package/dist/chunk-AUWHD7RA.js +0 -3753
  170. package/dist/studio/chunk-6JY3MTIX.js +0 -1204
  171. package/dist/studio/chunk-EEXSYY5F.js +0 -190
  172. package/dist/studio/chunk-NZPJUZKT.js +0 -1538
  173. package/dist/studio/chunk-PZ5AY32C.js +0 -10
  174. package/dist/studio/chunk-TP6D3U37.js +0 -845
  175. package/dist/studio/chunk-Y2OHHNM2.js +0 -970
  176. package/dist/studio/domEditingLayers-PTP2ICGM.js +0 -47
  177. package/dist/studio/index.css +0 -165
  178. package/dist/studio/index.d.ts +0 -4318
  179. package/dist/studio/index.js +0 -100952
  180. package/dist/studio/player.d.ts +0 -30
  181. package/dist/studio/player.js +0 -10
  182. package/dist/studio/styles/tailwind-preset.d.ts +0 -39
  183. package/dist/studio/styles/tailwind-preset.js +0 -50
  184. package/dist/studio/styles.css +0 -1
  185. package/dist/studio/ui.d.ts +0 -297
  186. package/dist/studio/ui.js +0 -66
@@ -8,7 +8,7 @@ import {
8
8
  LOW_MEMORY_TOTAL_MB_THRESHOLD,
9
9
  getSystemTotalMb,
10
10
  isLowMemorySystem
11
- } from "./chunk-USDEMXXL.js";
11
+ } from "./chunk-JYVGYKXG.js";
12
12
  import {
13
13
  MEDIA_VISUAL_STYLE_PROPERTIES,
14
14
  RENDER_FRAME_ID_PREFIX,
@@ -26,7 +26,7 @@ import {
26
26
  renderFrameIdForRenderId,
27
27
  resolveAuthoredTimingWindow,
28
28
  toFps
29
- } from "./chunk-VRW6EBHV.js";
29
+ } from "./chunk-BAKPN2D2.js";
30
30
  import {
31
31
  AUDIO_GROUP_RENDER_ID_ATTR,
32
32
  COLOR_GRADING_SOURCE_HIDDEN_ATTR,
@@ -55,7 +55,7 @@ import {
55
55
  shiftRateLane,
56
56
  sourceTimeAt,
57
57
  timeAtSourceTime
58
- } from "./chunk-AUWHD7RA.js";
58
+ } from "./chunk-Z6FLHIDV.js";
59
59
  import {
60
60
  clampAudioGain,
61
61
  isAudibleVideoElement
@@ -75,11 +75,14 @@ import {
75
75
  FFMPEG_PATH_ENV,
76
76
  FFPROBE_PATH_ENV,
77
77
  findFfBinary
78
- } from "./chunk-PJ3HDUCY.js";
78
+ } from "./chunk-VAS6HQX3.js";
79
79
  import {
80
80
  parseNumeric,
81
81
  parseStartExpression
82
82
  } from "./chunk-3RM2ETQT.js";
83
+ import {
84
+ isPathInside
85
+ } from "./chunk-BSQ7XANM.js";
83
86
  import {
84
87
  parseHTML
85
88
  } from "./chunk-SDYDNHEE.js";
@@ -529,7 +532,7 @@ function resolveConfig(overrides) {
529
532
  merged.enableStreamingEncode = false;
530
533
  merged.streamingEncodeAutoDisabledOnWin32Compound = true;
531
534
  console.error(
532
- "[hyperframes] Windows compound-workaround auto-detected \u2014 disabling streaming-encode (platform=win32, software-GPU forced, workers=1). Field signal ts=1784131903. Override: PRODUCER_ENABLE_STREAMING_ENCODE=true."
535
+ "[hyperframes] Windows compound-workaround auto-detected, disabling streaming-encode (platform=win32, software-GPU forced, workers=1). Field signal ts=1784131903. Override: PRODUCER_ENABLE_STREAMING_ENCODE=true."
533
536
  );
534
537
  }
535
538
  if (merged.useDrawElement && merged.enablePageSideCompositing) {
@@ -1231,10 +1234,10 @@ function resolveBrowserGpuMode(mode, options = {}) {
1231
1234
  var _unverifiedHardwareGpuWarned = false;
1232
1235
  function buildUnverifiedHardwareGpuWarning(platform, cause) {
1233
1236
  if (cause === "probe-error") {
1234
- return "[hyperframes] browserGpuMode=hardware was requested, but the GPU probe could not run, so hardware acceleration is UNVERIFIED \u2014 if Chrome falls back to software WebGL the capture will run at CPU speed. Honouring the explicit request anyway.\n This is a probe failure, not evidence of a missing GPU: see the `browserGpuMode probe \u2192 software (probe failed ...)` line above for the underlying error, which usually means Chrome could not launch (bad HYPERFRAMES_BROWSER_PATH, missing shared libraries, or a denied sandbox) rather than a GPU problem.\n Run `hyperframes doctor` to check the Chrome install.";
1237
+ return "[hyperframes] browserGpuMode=hardware was requested, but the GPU probe could not run, so hardware acceleration is UNVERIFIED. If Chrome falls back to software WebGL the capture will run at CPU speed. Honouring the explicit request anyway.\n This is a probe failure, not evidence of a missing GPU: see the `browserGpuMode probe \u2192 software (probe failed ...)` line above for the underlying error, which usually means Chrome could not launch (bad HYPERFRAMES_BROWSER_PATH, missing shared libraries, or a denied sandbox) rather than a GPU problem.\n Run `hyperframes doctor` to check the Chrome install.";
1235
1238
  }
1236
1239
  const remediation = platform === "linux" ? "Inside Docker, the container needs GPU passthrough: `--gpus all` with the NVIDIA Container Toolkit installed, or `--device /dev/dri` for Mesa/AMD/Intel. The image also needs the matching userspace driver + libEGL. Verify with `hyperframes render --browser-gpu` and watch for this warning disappearing." : "Check that the host exposes a GPU to this process and that the graphics drivers are installed.";
1237
- return `[hyperframes] browserGpuMode=hardware was requested, but the WebGL probe found no hardware GPU \u2014 Chrome will silently fall back to software WebGL and the capture will run at CPU speed. Honouring the explicit request anyway.
1240
+ return `[hyperframes] browserGpuMode=hardware was requested, but the WebGL probe found no hardware GPU, so Chrome will silently fall back to software WebGL and the capture will run at CPU speed. Honouring the explicit request anyway.
1238
1241
  ${remediation}
1239
1242
  Pass --no-browser-gpu to select deterministic SwiftShader instead of waiting on a hardware path that is not there.`;
1240
1243
  }
@@ -2459,7 +2462,7 @@ async function sendBeginFrame(client, params) {
2459
2462
  }
2460
2463
  if (isPending) {
2461
2464
  throw new Error(
2462
- `[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries \u2014 CPU overloaded by parallel renders. Reduce concurrent renders or use --docker for isolation.`
2465
+ `[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries: CPU overloaded by parallel renders. Reduce concurrent renders or use --docker for isolation.`
2463
2466
  );
2464
2467
  }
2465
2468
  throw err;
@@ -2776,6 +2779,13 @@ async function injectVideoFramesBatch(page, updates) {
2776
2779
  const computedStyle = window.getComputedStyle(video);
2777
2780
  const opacityParsed = parseFloat(computedStyle.opacity);
2778
2781
  const computedOpacity = video.hasAttribute(colorGradingSourceHiddenAttr) ? 1 : Number.isNaN(opacityParsed) ? 1 : opacityParsed;
2782
+ const videoRect = video.getBoundingClientRect();
2783
+ const videoBox = {
2784
+ left: Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0,
2785
+ top: Number.isFinite(video.offsetTop) ? video.offsetTop : 0,
2786
+ width: video.offsetWidth > 0 ? video.offsetWidth : videoRect.width,
2787
+ height: video.offsetHeight > 0 ? video.offsetHeight : videoRect.height
2788
+ };
2779
2789
  if (isNewImage) {
2780
2790
  img = document.createElement("img");
2781
2791
  img.classList.add("__render_frame__");
@@ -2794,21 +2804,15 @@ async function injectVideoFramesBatch(page, updates) {
2794
2804
  img.style.setProperty(property, value);
2795
2805
  }
2796
2806
  }
2797
- {
2798
- const videoRect = video.getBoundingClientRect();
2799
- const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
2800
- const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
2801
- const offsetWidth = video.offsetWidth > 0 ? video.offsetWidth : videoRect.width;
2802
- const offsetHeight = video.offsetHeight > 0 ? video.offsetHeight : videoRect.height;
2803
- img.style.position = "absolute";
2804
- img.style.inset = "auto";
2805
- img.style.left = `${offsetLeft}px`;
2806
- img.style.top = `${offsetTop}px`;
2807
- img.style.right = "auto";
2808
- img.style.bottom = "auto";
2809
- img.style.width = `${offsetWidth}px`;
2810
- img.style.height = `${offsetHeight}px`;
2811
- }
2807
+ img.style.position = "absolute";
2808
+ img.style.inset = "auto";
2809
+ img.style.left = `${videoBox.left}px`;
2810
+ img.style.top = `${videoBox.top}px`;
2811
+ img.style.right = "auto";
2812
+ img.style.bottom = "auto";
2813
+ img.style.width = `${videoBox.width}px`;
2814
+ img.style.height = `${videoBox.height}px`;
2815
+ img.style.boxSizing = "border-box";
2812
2816
  img.style.objectFit = computedStyle.objectFit;
2813
2817
  img.style.objectPosition = computedStyle.objectPosition;
2814
2818
  img.style.zIndex = computedStyle.zIndex;
@@ -3167,7 +3171,7 @@ async function initDrawElementWorkerEncode(page) {
3167
3171
  }
3168
3172
  const workerSrc = `
3169
3173
  // Reuse one OffscreenCanvas across frames (dimensions are constant for a
3170
- // render) \u2014 a fresh canvas per frame churns ~w*h*4 bytes of backing store
3174
+ // render): a fresh canvas per frame churns ~w*h*4 bytes of backing store
3171
3175
  // every frame and pressures GC on the encode hot path.
3172
3176
  let oc = null, c = null;
3173
3177
  self.onmessage = async (e) => {
@@ -3943,7 +3947,7 @@ async function initThreeDProjectionInPage() {
3943
3947
  ok: false,
3944
3948
  groups: 0,
3945
3949
  quads: 0,
3946
- reason: "degenerate 3D geometry (zero-size or inline-box quad) \u2014 quad projection cannot reproduce Blink's lenient rendering"
3950
+ reason: "degenerate 3D geometry (zero-size or inline-box quad): quad projection cannot reproduce Blink's lenient rendering"
3947
3951
  };
3948
3952
  }
3949
3953
  if (!desc.shell && hasAnimatedStrictDescendant(desc.el)) {
@@ -3951,7 +3955,7 @@ async function initThreeDProjectionInPage() {
3951
3955
  ok: false,
3952
3956
  groups: 0,
3953
3957
  quads: 0,
3954
- reason: "3D quad contains GSAP-animated descendants \u2014 static texture would freeze them at init state"
3958
+ reason: "3D quad contains GSAP-animated descendants, a static texture would freeze them at init state"
3955
3959
  };
3956
3960
  }
3957
3961
  }
@@ -4963,7 +4967,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
4963
4967
  session.deGateReason = "render_mode_hint";
4964
4968
  session.deFallbackTrigger = "render_mode_hint";
4965
4969
  console.log(
4966
- "[engine] fast capture: falling back to screenshot \u2014 render-mode compatibility hint forced screenshot capture (e.g. raw requestAnimationFrame composition)."
4970
+ "[engine] fast capture falls back to screenshot: render-mode compatibility hint forced screenshot capture (e.g. raw requestAnimationFrame composition)."
4967
4971
  );
4968
4972
  }
4969
4973
  if (useDrawElement) {
@@ -4989,7 +4993,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
4989
4993
  session.deGateReason = "unsupported_chrome";
4990
4994
  session.deFallbackTrigger = "unsupported_chrome";
4991
4995
  console.log(
4992
- `[engine] fast capture: falling back to ${session.launchCaptureMode} capture \u2014 this Chrome build does not implement canvas.drawElementImage (Dev/Canary-only feature); run \`hyperframes browser ensure --force\` to fetch a supported build, or set HYPERFRAMES_BROWSER_PATH to one.`
4996
+ `[engine] fast capture falls back to ${session.launchCaptureMode} capture: this Chrome build does not implement canvas.drawElementImage (Dev/Canary-only feature); run \`hyperframes browser ensure --force\` to fetch a supported build, or set HYPERFRAMES_BROWSER_PATH to one.`
4993
4997
  );
4994
4998
  await routeToFallback();
4995
4999
  return;
@@ -4998,7 +5002,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
4998
5002
  session.deGateReason = "ffmpeg_no_psnr_filter";
4999
5003
  session.deFallbackTrigger = "ffmpeg_no_psnr_filter";
5000
5004
  console.warn(
5001
- `[engine] fast capture: falling back to ${session.launchCaptureMode} capture \u2014 host ffmpeg is missing or was built without the \`psnr\` filter (libpostproc), so drawElement self-verification cannot run. Install an ffmpeg build that includes libpostproc (or set HYPERFRAMES_FFMPEG_PATH to one) to re-enable fast capture.`
5005
+ `[engine] fast capture falls back to ${session.launchCaptureMode} capture: host ffmpeg is missing or was built without the \`psnr\` filter (libpostproc), so drawElement self-verification cannot run. Install an ffmpeg build that includes libpostproc (or set HYPERFRAMES_FFMPEG_PATH to one) to re-enable fast capture.`
5002
5006
  );
5003
5007
  await routeToFallback();
5004
5008
  return;
@@ -5008,7 +5012,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
5008
5012
  session.deGateReason = "swiftshader";
5009
5013
  session.deFallbackTrigger = "swiftshader";
5010
5014
  console.log(
5011
- `[engine] fast capture: falling back to ${session.launchCaptureMode} capture \u2014 SwiftShader (software rasterizer \u2014 no GPU egress to skip, drawElement is parity-or-slower; see fast-capture-limitations.md)`
5015
+ `[engine] fast capture falls back to ${session.launchCaptureMode} capture: SwiftShader (software rasterizer, no GPU egress to skip, drawElement is parity-or-slower; see fast-capture-limitations.md)`
5012
5016
  );
5013
5017
  await routeToFallback();
5014
5018
  } else {
@@ -5018,7 +5022,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
5018
5022
  session.deGateReason = `css_effect:${(cssFx.split(":")[0] ?? "").replace(/[^a-z-]/gi, "")}`;
5019
5023
  session.deFallbackTrigger = cssFx;
5020
5024
  console.log(
5021
- `[engine] fast capture: falling back to ${session.launchCaptureMode} capture \u2014 ${cssFx} detected (drawElementImage cannot reproduce it; see fast-capture-limitations.md)`
5025
+ `[engine] fast capture falls back to ${session.launchCaptureMode} capture: ${cssFx} detected (drawElementImage cannot reproduce it; see fast-capture-limitations.md)`
5022
5026
  );
5023
5027
  await routeToFallback();
5024
5028
  return;
@@ -5036,7 +5040,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
5036
5040
  session.deGateReason = "at_risk_timeline";
5037
5041
  session.deFallbackTrigger = "at_risk_timeline";
5038
5042
  console.log(
5039
- `[engine] fast capture: falling back to ${session.launchCaptureMode} capture \u2014 ${atRisk.size}/${totalFrames} frames animate a compositor-incompatible prop (blend/3D/clip/mask); drawElementImage drops these mid-animation (deterministic timeline gate; see fast-capture-limitations.md Lim 7)`
5043
+ `[engine] fast capture falls back to ${session.launchCaptureMode} capture: ${atRisk.size}/${totalFrames} frames animate a compositor-incompatible prop (blend/3D/clip/mask); drawElementImage drops these mid-animation (deterministic timeline gate; see fast-capture-limitations.md Lim 7)`
5040
5044
  );
5041
5045
  await routeToFallback();
5042
5046
  return;
@@ -5047,7 +5051,7 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
5047
5051
  session.deGateReason = "3d_init_failed";
5048
5052
  session.deFallbackTrigger = "3d_init_failed";
5049
5053
  console.log(
5050
- `[engine] fast capture: falling back to ${session.launchCaptureMode} capture \u2014 3D projection init failed (${threeD.reason ?? "unknown"})`
5054
+ `[engine] fast capture falls back to ${session.launchCaptureMode} capture: 3D projection init failed (${threeD.reason ?? "unknown"})`
5051
5055
  );
5052
5056
  await routeToFallback();
5053
5057
  return;
@@ -5121,7 +5125,7 @@ async function createCaptureSession(serverUrl, outputDir, options, onBeforeCaptu
5121
5125
  const drawElementTransparent = useDrawElement && options.format === "png";
5122
5126
  if (useDrawElement && (config?.enablePageSideCompositing ?? DEFAULT_CONFIG.enablePageSideCompositing)) {
5123
5127
  console.warn(
5124
- "[engine] useDrawElement is incompatible with page-side shader compositing \u2014 ignoring enablePageSideCompositing for this render. Prefer resolveConfig, which disables page-side compositing automatically for fast-capture renders."
5128
+ "[engine] useDrawElement is incompatible with page-side shader compositing, ignoring enablePageSideCompositing for this render. Prefer resolveConfig, which disables page-side compositing automatically for fast-capture renders."
5125
5129
  );
5126
5130
  }
5127
5131
  const supersampling = (options.deviceScaleFactor ?? 1) > 1;
@@ -5220,7 +5224,7 @@ async function constructCaptureSession(input) {
5220
5224
  }
5221
5225
  if (process.env.HF_FAST_CAPTURE_AUTOALPHA !== void 0) {
5222
5226
  console.warn(
5223
- "[engine] HF_FAST_CAPTURE_AUTOALPHA is retired and ignored \u2014 the requestPaint paint contract captures animated opacity natively (see drawElementService.ts)."
5227
+ "[engine] HF_FAST_CAPTURE_AUTOALPHA is retired and ignored: the requestPaint paint contract captures animated opacity natively (see drawElementService.ts)."
5224
5228
  );
5225
5229
  }
5226
5230
  if (useDrawElement && process.env.HF_FAST_CAPTURE_ROOT_PROPS !== "false") {
@@ -5338,7 +5342,7 @@ var HF_READY_DIAGNOSTIC_EXPR = `(async function() {
5338
5342
  await new Promise(function(r) { setTimeout(r, 0); });
5339
5343
  }
5340
5344
  // A thenable resolving via its own setTimeout(0) can still misreport as
5341
- // pending here (macrotask registration order, not resolution order) \u2014 a
5345
+ // pending here (macrotask registration order, not resolution order), a
5342
5346
  // narrow case that self-corrects on the next ~1s diagnostic tick.
5343
5347
  var pendingBuildReadyKeys = keys.filter(function(key) { return !settled[key]; });
5344
5348
  var rejectedBuildReadyKeys = keys.filter(function(key) { return settled[key] === "rejected"; });
@@ -5358,20 +5362,20 @@ function buildZeroDurationDiagnostic(diag) {
5358
5362
  const hints = [];
5359
5363
  if (diag.pendingBuildReadyKeys.length > 0) {
5360
5364
  hints.push(
5361
- `window.__hf.buildReady never resolved for: ${diag.pendingBuildReadyKeys.join(", ")}. The runtime holds render-ready until every registered buildReady promise settles \u2014 find where the composition registers that key and confirm it actually resolves.`
5365
+ `window.__hf.buildReady never resolved for: ${diag.pendingBuildReadyKeys.join(", ")}. The runtime holds render-ready until every registered buildReady promise settles. Find where the composition registers that key and confirm it actually resolves.`
5362
5366
  );
5363
5367
  }
5364
5368
  if (diag.rejectedBuildReadyKeys.length > 0) {
5365
5369
  hints.push(
5366
- `window.__hf.buildReady rejected for: ${diag.rejectedBuildReadyKeys.join(", ")}. That key's build promise failed rather than hanging \u2014 find why it rejects.`
5370
+ `window.__hf.buildReady rejected for: ${diag.rejectedBuildReadyKeys.join(", ")}. That key's build promise failed rather than hanging. Find why it rejects.`
5367
5371
  );
5368
5372
  }
5369
5373
  if (!diag.hasPlayer) {
5370
- hints.push("window.__player was never set \u2014 the HyperFrames runtime did not initialize.");
5374
+ hints.push("window.__player was never set: the HyperFrames runtime did not initialize.");
5371
5375
  }
5372
5376
  if (!diag.hasTimeline) {
5373
5377
  hints.push(
5374
- "No GSAP timeline registered (window.__timelines is empty). CSS/WAAPI/Lottie animations are usually auto-detected (the runtime infers duration from the longest running animation) \u2014 this composition's duration could not be inferred, which usually means an infinite/unbounded animation (e.g. animation-iteration-count: infinite, repeat: -1 WAAPI, or a looping Lottie clip) or a Three.js scene with no discoverable AnimationClip."
5378
+ "No GSAP timeline registered (window.__timelines is empty). CSS/WAAPI/Lottie animations are usually auto-detected (the runtime infers duration from the longest running animation), but this composition's duration could not be inferred, which usually means an infinite/unbounded animation (e.g. animation-iteration-count: infinite, repeat: -1 WAAPI, or a looping Lottie clip) or a Three.js scene with no discoverable AnimationClip."
5375
5379
  );
5376
5380
  }
5377
5381
  if (diag.declaredDuration <= 0 && !diag.hasTimeline) {
@@ -5380,7 +5384,7 @@ function buildZeroDurationDiagnostic(diag) {
5380
5384
  );
5381
5385
  }
5382
5386
  if (diag.hasSeek && diag.duration === 0 && diag.renderReady) {
5383
- hints.push("The runtime finished initializing but reported zero duration \u2014 this is permanent.");
5387
+ hints.push("The runtime finished initializing but reported zero duration. This is permanent.");
5384
5388
  }
5385
5389
  return `[FrameCapture] Composition has zero duration.
5386
5390
  Runtime ready: ${diag.renderReady}, __player: ${diag.hasPlayer}, __hf.seek: ${diag.hasSeek}, GSAP timeline: ${diag.hasTimeline}, data-duration: ${diag.declaredDuration > 0 ? diag.declaredDuration + "s" : "not set"}
@@ -5396,8 +5400,11 @@ async function pollHfReady(page, timeoutMs, intervalMs = 100) {
5396
5400
  const deadline = Date.now() + timeoutMs;
5397
5401
  let lastDiagnosticAt = 0;
5398
5402
  while (Date.now() < deadline) {
5399
- const ready = Boolean(await page.evaluate(readyExpr));
5400
- if (ready) return;
5403
+ const state = await page.evaluate(
5404
+ `typeof window.__hfStartupError === "string" ? window.__hfStartupError : ${readyExpr}`
5405
+ );
5406
+ if (typeof state === "string") throw new Error(state);
5407
+ if (state) return;
5401
5408
  const elapsed = timeoutMs - (deadline - Date.now());
5402
5409
  if (elapsed >= FAST_FAIL_AFTER_MS) {
5403
5410
  const now = Date.now();
@@ -5483,7 +5490,7 @@ async function pollSubCompositionTimelines(page, timeoutMs, {
5483
5490
  if (failures.length > 0 && now - start >= scriptFailureGraceMs) {
5484
5491
  scriptFailureBail = true;
5485
5492
  console.warn(
5486
- `[FrameCapture] Sub-composition timeline wait cut short after ${now - start}ms: script resource(s) failed to load (${failures.join(", ")}) \u2014 the timeline registration they carry can never arrive. Fix the script reference; the render proceeds without those animations.`
5493
+ `[FrameCapture] Sub-composition timeline wait cut short after ${now - start}ms: script resource(s) failed to load (${failures.join(", ")}), so the timeline registration they carry can never arrive. Fix the script reference; the render proceeds without those animations.`
5487
5494
  );
5488
5495
  break;
5489
5496
  }
@@ -5642,7 +5649,7 @@ var LIVE_MAP_MARKERS = [
5642
5649
  function buildLiveMapWarning(libraries) {
5643
5650
  return {
5644
5651
  code: "live_map_detected",
5645
- message: `Live map viewport(s) detected in the composition (${libraries.join(", ")}). Map tiles load over the network at render time, which the deterministic-render contract forbids \u2014 frames captured before tiles arrive ship a blank or partial map with no error. Bake the map to a video first (see the motion-graphics maps skill / bake-basemap.mjs) and use the baked file as the imagery layer.`,
5652
+ message: `Live map viewport(s) detected in the composition (${libraries.join(", ")}). Map tiles load over the network at render time, which the deterministic-render contract forbids: frames captured before tiles arrive ship a blank or partial map with no error. Bake the map to a video first (see the motion-graphics maps skill / bake-basemap.mjs) and use the baked file as the imagery layer.`,
5646
5653
  details: { sources: [...libraries] }
5647
5654
  };
5648
5655
  }
@@ -5890,7 +5897,7 @@ async function initializeSession(session) {
5890
5897
  }).map((img) => img.src || img.getAttribute("src") || "(no src)").join(", ");
5891
5898
  });
5892
5899
  console.warn(
5893
- `[FrameCapture] Some image elements did not load within ${pageReadyTimeout}ms: ${failedImages}. Continuing render \u2014 affected images may appear blank/missing in early frames.`
5900
+ `[FrameCapture] Some image elements did not load within ${pageReadyTimeout}ms: ${failedImages}. Continuing render. Affected images may appear blank/missing in early frames.`
5894
5901
  );
5895
5902
  }
5896
5903
  await decodeAllImages(page);
@@ -5906,7 +5913,7 @@ async function initializeSession(session) {
5906
5913
  return Array.from(document.querySelectorAll("video")).filter((v) => !skip.has(v.id)).filter((v) => v.readyState < 2 && !v.error).map((v) => v.src || v.getAttribute("src") || "(no src)").join(", ");
5907
5914
  }, skipVideoIds);
5908
5915
  console.warn(
5909
- `[FrameCapture] Some video elements did not decode within ${pageReadyTimeout}ms: ${failedVideos}. Continuing render \u2014 affected videos will appear as blank/black frames.`
5916
+ `[FrameCapture] Some video elements did not decode within ${pageReadyTimeout}ms: ${failedVideos}. Continuing render. Affected videos will appear as blank/black frames.`
5910
5917
  );
5911
5918
  }
5912
5919
  recordCaptureWarnings(
@@ -5996,7 +6003,7 @@ async function initializeSession(session) {
5996
6003
  }).map((img) => img.src || img.getAttribute("src") || "(no src)").join(", ");
5997
6004
  });
5998
6005
  console.warn(
5999
- `[FrameCapture] Some image elements did not load within ${pageReadyTimeout}ms: ${failedImages}. Continuing render \u2014 affected images may appear blank/missing in early frames.`
6006
+ `[FrameCapture] Some image elements did not load within ${pageReadyTimeout}ms: ${failedImages}. Continuing render. Affected images may appear blank/missing in early frames.`
6000
6007
  );
6001
6008
  }
6002
6009
  await decodeAllImages(page);
@@ -6012,7 +6019,7 @@ async function initializeSession(session) {
6012
6019
  return Array.from(document.querySelectorAll("video")).filter((v) => !skip.has(v.id)).filter((v) => v.readyState < 2 && !v.error).map((v) => v.src || v.getAttribute("src") || "(no src)").join(", ");
6013
6020
  }, bfSkipVideoIds);
6014
6021
  console.warn(
6015
- `[FrameCapture] Some video elements did not decode within ${pageReadyTimeout}ms: ${failedVideos}. Continuing render \u2014 affected videos will appear as blank/black frames.`
6022
+ `[FrameCapture] Some video elements did not decode within ${pageReadyTimeout}ms: ${failedVideos}. Continuing render. Affected videos will appear as blank/black frames.`
6016
6023
  );
6017
6024
  }
6018
6025
  recordCaptureWarnings(
@@ -6098,20 +6105,26 @@ async function prepareFrameForCapture(session, frameIndex, time, seekOptions, he
6098
6105
  seekOptions?.subFrameDivisions
6099
6106
  );
6100
6107
  const seekStart = Date.now();
6108
+ session.captureStep = "seeking the timeline";
6101
6109
  const hasPendingComposite = await seekPageTimeline(page, quantizedTime, seekOptions);
6110
+ session.captureStep = "decoding CSS background images";
6102
6111
  await decodeDynamicCssBackgroundImages(page);
6103
6112
  const seekMs = Date.now() - seekStart;
6104
6113
  const beforeCaptureStart = Date.now();
6105
6114
  if (session.onBeforeCapture) {
6115
+ session.captureStep = "injecting video frames";
6106
6116
  await session.onBeforeCapture(page, quantizedTime, heldVideoTime);
6107
6117
  }
6118
+ session.captureStep = "waiting for page seek handlers";
6108
6119
  await waitForPendingSeekCompletion(page);
6120
+ session.captureStep = "loading color-grading LUTs";
6109
6121
  await page.evaluate(async () => {
6110
6122
  const runtime = window.__hf?.colorGrading;
6111
6123
  await runtime?.waitForActiveLuts?.();
6112
6124
  });
6113
6125
  const beforeCaptureMs = Date.now() - beforeCaptureStart;
6114
6126
  if (hasPendingComposite && session.captureMode !== "beginframe" && session.captureMode !== "drawelement") {
6127
+ session.captureStep = "compositing the page";
6115
6128
  await page.evaluate(async () => {
6116
6129
  const w = window;
6117
6130
  if (typeof w.__hf_page_composite_prepare === "function") {
@@ -6131,6 +6144,7 @@ async function prepareFrameForCapture(session, frameIndex, time, seekOptions, he
6131
6144
  }
6132
6145
  });
6133
6146
  }
6147
+ session.captureStep = "capturing the frame";
6134
6148
  return { quantizedTime, seekMs, beforeCaptureMs };
6135
6149
  }
6136
6150
  function computeAuthoredClipBoundaryFrames(schedule, fps) {
@@ -6861,10 +6875,12 @@ async function captureAccumulatedFrame(session, frameIndex, absFrameIndex, plan)
6861
6875
  const fps = fpsToNumber(session.options.fps);
6862
6876
  const frameTime = quantizeSeekTime(absFrameIndex / fps, fps);
6863
6877
  const eventfulSeekStart = Date.now();
6878
+ session.captureStep = "seeking the timeline";
6864
6879
  await seekPageTimeline(session.page, frameTime, void 0);
6865
6880
  const totals = { seekMs: Date.now() - eventfulSeekStart, beforeCaptureMs: 0, screenshotMs: 0 };
6866
6881
  if (session.onBeforeCapture) {
6867
6882
  const injectStart = Date.now();
6883
+ session.captureStep = "injecting video frames";
6868
6884
  await session.onBeforeCapture(session.page, frameTime, frameTime);
6869
6885
  totals.beforeCaptureMs += Date.now() - injectStart;
6870
6886
  }
@@ -6903,6 +6919,7 @@ async function captureAccumulatedFrame(session, frameIndex, absFrameIndex, plan)
6903
6919
  accumulator.add(sample.buffer);
6904
6920
  }
6905
6921
  const restoreSeekStart = Date.now();
6922
+ session.captureStep = "seeking the timeline";
6906
6923
  await seekPageTimeline(session.page, frameTime, { suppressEvents: true });
6907
6924
  totals.seekMs += Date.now() - restoreSeekStart;
6908
6925
  const blended = accumulator.finish();
@@ -6978,7 +6995,7 @@ async function captureFrameToBuffer(session, frameIndex, time) {
6978
6995
  () => {
6979
6996
  session.deFrameTimeouts = (session.deFrameTimeouts ?? 0) + 1;
6980
6997
  console.log(
6981
- `[engine] fast capture: frame ${frameIndex} \u2014 capture exceeded ${DE_FRAME_TIMEOUT_MS}ms; renderer stalled after drawElementImage (PRINFRA-488). Failing the drawElement attempt so the whole render retries via screenshot.`
6998
+ `[engine] fast capture frame ${frameIndex}: capture exceeded ${DE_FRAME_TIMEOUT_MS}ms; renderer stalled after drawElementImage (PRINFRA-488). Failing the drawElement attempt so the whole render retries via screenshot.`
6982
6999
  );
6983
7000
  }
6984
7001
  ) : await captureFrameCore(session, frameIndex, time);
@@ -7076,6 +7093,7 @@ async function captureFramesBatchPipelined(session, frameIndices, times) {
7076
7093
  const startTime = Date.now();
7077
7094
  const fps = fpsToNumber(options.fps);
7078
7095
  const quantized = times.map((t) => quantizeTimeToFrame(t, fps));
7096
+ session.captureStep = "capturing the frame";
7079
7097
  const { encodeResults, failedAt, error } = await produceDrawElementFrameBatch(
7080
7098
  page,
7081
7099
  quantized,
@@ -7751,7 +7769,7 @@ function findHdrAutoPromotion(sources) {
7751
7769
  } : null;
7752
7770
  }
7753
7771
  function formatHdrAutoPromotionWarning(promotion) {
7754
- return `[Render] HDR auto-promotion triggered by "${promotion.triggeringAsset}" \u2014 output: ${promotion.output.colorSpace} / ${promotion.output.codec}`;
7772
+ return `[Render] HDR auto-promotion triggered by "${promotion.triggeringAsset}", output: ${promotion.output.colorSpace} / ${promotion.output.codec}`;
7755
7773
  }
7756
7774
  function analyzeCompositionHdr(colorSpaces) {
7757
7775
  let hasPq = false;
@@ -10951,6 +10969,7 @@ function unwrapTemplate(html) {
10951
10969
  // ../engine/src/services/extractionCache.ts
10952
10970
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
10953
10971
  import {
10972
+ createReadStream as createReadStream3,
10954
10973
  existsSync as existsSync7,
10955
10974
  lstatSync as lstatSync2,
10956
10975
  mkdirSync as mkdirSync5,
@@ -11016,6 +11035,15 @@ var COMPLETE_SENTINEL = ".hf-complete";
11016
11035
  var GC_MARKER = ".hf-last-gc";
11017
11036
  var SCHEMA_PREFIX = "hfcache-v6-";
11018
11037
  var KEY_HEX_CHARS = 16;
11038
+ async function readContentSha256(path, signal) {
11039
+ try {
11040
+ const hash = createHash2("sha256");
11041
+ for await (const chunk of createReadStream3(path, { signal })) hash.update(chunk);
11042
+ return hash.digest("hex");
11043
+ } catch {
11044
+ return null;
11045
+ }
11046
+ }
11019
11047
  function readKeyStat(videoPath) {
11020
11048
  try {
11021
11049
  const stat = statSync6(videoPath);
@@ -11026,9 +11054,9 @@ function readKeyStat(videoPath) {
11026
11054
  }
11027
11055
  function canonicalKeyBlob(input) {
11028
11056
  const durationForKey = Number.isFinite(input.duration) ? input.duration : -1;
11057
+ const identity = input.contentSha256 !== void 0 ? { c: input.contentSha256 } : { p: input.videoPath, m: input.mtimeMs };
11029
11058
  const blob = {
11030
- p: input.videoPath,
11031
- m: input.mtimeMs,
11059
+ ...identity,
11032
11060
  s: input.size,
11033
11061
  ms: input.mediaStart,
11034
11062
  d: durationForKey,
@@ -11274,6 +11302,7 @@ function extractionFrameCountForDuration(durationSeconds, fps, isVFR) {
11274
11302
  const frames = Number(frameCount);
11275
11303
  return Math.max(1, Number.isSafeInteger(frames) ? frames : Number.MAX_SAFE_INTEGER);
11276
11304
  }
11305
+ var DOWNLOAD_SUBDIR = "_downloads";
11277
11306
  var EXTRACT_CACHE_MIN_AGE_MS = 60 * 60 * 1e3;
11278
11307
  var GC_STALENESS_MS = 24 * 60 * 60 * 1e3;
11279
11308
  var SDR_TO_HDR_COLORSPACE_FILTER = "colorspace=all=bt2020:iall=bt709:range=tv";
@@ -11962,6 +11991,9 @@ function canHoldFinalFramePastEof(video) {
11962
11991
  return !video.loop && Number.isFinite(timelineDuration) && timelineDuration > 0;
11963
11992
  }
11964
11993
  var FINAL_FRAME_LOGICAL_DURATION_SECONDS = 1e-6;
11994
+ function isVideoPastTimelineEnd(video, timelineEnd) {
11995
+ return video.start >= timelineEnd;
11996
+ }
11965
11997
  function rebaseVideoToWindow(video, window2) {
11966
11998
  if (window2.preserveTimelinePhase) return;
11967
11999
  const start = Math.abs(window2.compositionStart) < 1e-9 ? 0 : window2.compositionStart;
@@ -12305,14 +12337,16 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
12305
12337
  const warnedSrcs = /* @__PURE__ */ new Set();
12306
12338
  for (const video of videos) {
12307
12339
  if (signal?.aborted) break;
12308
- if (options.timelineEnd !== void 0 && video.start >= options.timelineEnd) continue;
12340
+ if (options.timelineEnd !== void 0 && isVideoPastTimelineEnd(video, options.timelineEnd)) {
12341
+ continue;
12342
+ }
12309
12343
  try {
12310
12344
  let videoPath = video.src;
12311
12345
  if (!isHttpUrl(videoPath)) {
12312
12346
  videoPath = resolveProjectRelativeSrc(video.src, baseDir, compiledDir);
12313
12347
  }
12314
12348
  if (isHttpUrl(videoPath)) {
12315
- const downloadDir = join10(options.outputDir, "_downloads");
12349
+ const downloadDir = join10(options.outputDir, DOWNLOAD_SUBDIR);
12316
12350
  mkdirSync6(downloadDir, { recursive: true });
12317
12351
  videoPath = await downloadToTemp(
12318
12352
  videoPath,
@@ -12352,6 +12386,10 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
12352
12386
  }
12353
12387
  }
12354
12388
  breakdown.resolveMs = Date.now() - phase1Start;
12389
+ const contentKeyedDirs = [
12390
+ join10(options.outputDir, DOWNLOAD_SUBDIR),
12391
+ ...options.contentKeyedDirs ?? []
12392
+ ];
12355
12393
  const cacheKeyInputs = resolvedVideos.map(({ video, videoPath }) => {
12356
12394
  const stat = readKeyStat(videoPath);
12357
12395
  if (!stat) return null;
@@ -12359,7 +12397,8 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
12359
12397
  videoPath,
12360
12398
  mtimeMs: stat.mtimeMs,
12361
12399
  size: stat.size,
12362
- mediaStart: video.mediaStart
12400
+ mediaStart: video.mediaStart,
12401
+ contentKeyed: contentKeyedDirs.some((dir) => isPathInside(videoPath, dir))
12363
12402
  };
12364
12403
  });
12365
12404
  const phase2ProbeStart = Date.now();
@@ -12505,10 +12544,26 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
12505
12544
  });
12506
12545
  return { ...rehydrated, ownedByLookup: true };
12507
12546
  }
12547
+ const contentSha256ByPath = /* @__PURE__ */ new Map();
12548
+ async function hashContentKeyedSources(works) {
12549
+ if (!cacheRootDir) return;
12550
+ const paths = /* @__PURE__ */ new Set();
12551
+ for (const work of works) {
12552
+ const keyInput = cacheKeyInputs[work.index];
12553
+ if (keyInput?.contentKeyed) paths.add(keyInput.videoPath);
12554
+ }
12555
+ await Promise.all(
12556
+ [...paths].map(
12557
+ async (path) => contentSha256ByPath.set(path, await readContentSha256(path, signal))
12558
+ )
12559
+ );
12560
+ }
12508
12561
  function lookupCacheFor(work) {
12509
12562
  if (!cacheRootDir) return { work };
12510
12563
  const keyInput = cacheKeyInputs[work.index];
12511
12564
  if (!keyInput) return { work };
12565
+ const contentSha256 = keyInput.contentKeyed ? contentSha256ByPath.get(keyInput.videoPath) ?? null : void 0;
12566
+ if (contentSha256 === null) return { work };
12512
12567
  const transformParts = [
12513
12568
  work.sdrToHdrTransfer ? sdrToHdrTransformKey(work.sdrToHdrTransfer) : void 0,
12514
12569
  work.hdrToSdrTransformKey,
@@ -12520,6 +12575,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
12520
12575
  mtimeMs: keyInput.mtimeMs,
12521
12576
  size: keyInput.size,
12522
12577
  mediaStart: keyInput.mediaStart,
12578
+ contentSha256,
12523
12579
  duration: work.videoDuration,
12524
12580
  fps: fpsKey,
12525
12581
  format: work.format,
@@ -12776,8 +12832,11 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
12776
12832
  });
12777
12833
  }
12778
12834
  const cacheMisses = [];
12779
- for (const work of uniqueWorks.values()) {
12780
- if (uniqueOutcomes.has(work.dedupeKey)) continue;
12835
+ const worksToLookUp = [...uniqueWorks.values()].filter(
12836
+ (work) => !uniqueOutcomes.has(work.dedupeKey)
12837
+ );
12838
+ await hashContentKeyedSources(worksToLookUp);
12839
+ for (const work of worksToLookUp) {
12781
12840
  const lookup = lookupCacheFor(work);
12782
12841
  if ("work" in lookup) {
12783
12842
  cacheMisses.push(lookup);
@@ -13716,6 +13775,159 @@ function applyVolumeEnvelopeToWav(wavPath, keyframes, trackStart, baseVolume) {
13716
13775
  }
13717
13776
  }
13718
13777
 
13778
+ // ../core/dist/audio/audioFxTruePeak.js
13779
+ var TRUE_PEAK_OVERSAMPLE = 4;
13780
+ var TRUE_PEAK_DETECTOR_DELAY = 16;
13781
+ var KERNEL_HALF = TRUE_PEAK_OVERSAMPLE * TRUE_PEAK_DETECTOR_DELAY;
13782
+ var BRANCH_TAPS = 2 * TRUE_PEAK_DETECTOR_DELAY + 1;
13783
+ var TRUE_PEAK_WORKLET_SOURCE = `
13784
+ const TP_OS = ${TRUE_PEAK_OVERSAMPLE};
13785
+ const TP_HALF = ${KERNEL_HALF};
13786
+ const TP_BRANCH = ${BRANCH_TAPS};
13787
+ const TP_DELAY = ${TRUE_PEAK_DETECTOR_DELAY};
13788
+
13789
+ function besselI0(x) {
13790
+ let sum = 1, term = 1;
13791
+ for (let k = 1; k < 40; k++) {
13792
+ term *= (x / (2 * k)) * (x / (2 * k));
13793
+ sum += term;
13794
+ }
13795
+ return sum;
13796
+ }
13797
+
13798
+ /** Kaiser-windowed sinc, one array per polyphase branch, newest tap first. */
13799
+ function tpBranches() {
13800
+ const beta = 6;
13801
+ const norm = besselI0(beta);
13802
+ const branches = [];
13803
+ for (let p = 0; p < TP_OS; p++) {
13804
+ const taps = new Float64Array(TP_BRANCH);
13805
+ let sum = 0;
13806
+ for (let j = 0; j < TP_BRANCH; j++) {
13807
+ const k = p + TP_OS * j;
13808
+ if (k > 2 * TP_HALF) continue;
13809
+ const t = (k - TP_HALF) / TP_OS;
13810
+ const sinc = t === 0 ? 1 : Math.sin(Math.PI * t) / (Math.PI * t);
13811
+ const r = (k - TP_HALF) / TP_HALF;
13812
+ taps[j] = sinc * (besselI0(beta * Math.sqrt(Math.max(0, 1 - r * r))) / norm);
13813
+ sum += taps[j];
13814
+ }
13815
+ // Unity gain at DC on every branch, so a steady level reads as itself.
13816
+ for (let j = 0; j < TP_BRANCH; j++) taps[j] /= sum;
13817
+ branches.push(taps);
13818
+ }
13819
+ return branches;
13820
+ }
13821
+
13822
+ class HfTruePeak extends AudioWorkletProcessor {
13823
+ constructor(o) {
13824
+ super();
13825
+ this.p = o.processorOptions || {};
13826
+ this.look = Math.max(1, Math.round((this.p.lookahead ?? 3) * sampleRate / 1000));
13827
+ this.delayLen = this.look + TP_DELAY;
13828
+ this.branches = tpBranches();
13829
+ this.setLevels();
13830
+ this.clear();
13831
+ this.port.onmessage = (e) => {
13832
+ if (e.data && e.data.__hfDispose) { this.dead = true; return; }
13833
+ if (e.data && e.data.__hfReset) { this.clear(); return; }
13834
+ this.p = { ...this.p, ...e.data };
13835
+ this.setLevels();
13836
+ };
13837
+ }
13838
+ /** Back to the state of a fresh processor: no audio in the delay line, no gain reduction. */
13839
+ clear() {
13840
+ this.ch = [];
13841
+ this.rel = 1;
13842
+ this.dqIdx = new Int32Array(this.look + 2);
13843
+ this.dqVal = new Float64Array(this.look + 2);
13844
+ this.dqHead = 0;
13845
+ this.dqTail = 0;
13846
+ this.t = 0;
13847
+ this.avgRing = new Float64Array(this.look + 1).fill(1);
13848
+ this.avgPos = 0;
13849
+ this.avgSum = this.look + 1;
13850
+ }
13851
+ setLevels() {
13852
+ this.ceiling = Math.pow(10, (this.p.ceiling ?? -1) / 20);
13853
+ this.relCoef = 1 - Math.exp(-1 / (sampleRate * Math.max(1e-4, (this.p.release ?? 80) / 1000)));
13854
+ }
13855
+ channel(c) {
13856
+ if (!this.ch[c]) {
13857
+ this.ch[c] = {
13858
+ delay: new Float32Array(this.delayLen),
13859
+ w: 0,
13860
+ hist: new Float32Array(TP_BRANCH * 2),
13861
+ pos: 0,
13862
+ };
13863
+ }
13864
+ return this.ch[c];
13865
+ }
13866
+ /** Largest interpolated magnitude across channels for the newest sample. */
13867
+ peak(chans, inputs, n) {
13868
+ let peak = 0;
13869
+ for (let c = 0; c < chans; c++) {
13870
+ const s = this.channel(c);
13871
+ const x = inputs[c][n];
13872
+ s.pos = (s.pos + 1) % TP_BRANCH;
13873
+ s.hist[s.pos] = x;
13874
+ s.hist[s.pos + TP_BRANCH] = x;
13875
+ const base = s.pos + TP_BRANCH;
13876
+ for (let p = 0; p < TP_OS; p++) {
13877
+ const taps = this.branches[p];
13878
+ let acc = 0;
13879
+ for (let j = 0; j < TP_BRANCH; j++) acc += taps[j] * s.hist[base - j];
13880
+ const a = acc < 0 ? -acc : acc;
13881
+ if (a > peak) peak = a;
13882
+ }
13883
+ }
13884
+ return peak;
13885
+ }
13886
+ /** Windowed minimum of the required gain over the last look + 1 values. */
13887
+ windowMin(v) {
13888
+ const cap = this.dqIdx.length;
13889
+ while (this.dqTail !== this.dqHead && this.dqVal[(this.dqTail + cap - 1) % cap] >= v) {
13890
+ this.dqTail = (this.dqTail + cap - 1) % cap;
13891
+ }
13892
+ this.dqIdx[this.dqTail] = this.t;
13893
+ this.dqVal[this.dqTail] = v;
13894
+ this.dqTail = (this.dqTail + 1) % cap;
13895
+ while (this.dqIdx[this.dqHead] <= this.t - (this.look + 1)) {
13896
+ this.dqHead = (this.dqHead + 1) % cap;
13897
+ }
13898
+ this.t++;
13899
+ return this.dqVal[this.dqHead];
13900
+ }
13901
+ process(inputs, outputs) {
13902
+ if (this.dead) return false;
13903
+ const i = inputs[0], o = outputs[0];
13904
+ if (!i || !i.length) return true;
13905
+ const chans = i.length;
13906
+ const frames = i[0].length;
13907
+ for (let n = 0; n < frames; n++) {
13908
+ const peak = this.peak(chans, i, n);
13909
+ const need = peak > this.ceiling ? this.ceiling / peak : 1;
13910
+ const m = this.windowMin(need);
13911
+ this.avgSum += m - this.avgRing[this.avgPos];
13912
+ this.avgRing[this.avgPos] = m;
13913
+ this.avgPos = (this.avgPos + 1) % this.avgRing.length;
13914
+ const target = Math.min(1, this.avgSum / this.avgRing.length);
13915
+ const released = this.rel + (1 - this.rel) * this.relCoef;
13916
+ this.rel = released < target ? released : target;
13917
+ if (target >= 1 && this.rel > 0.9999999) this.rel = 1;
13918
+ for (let c = 0; c < chans; c++) {
13919
+ const s = this.channel(c);
13920
+ o[c][n] = s.delay[s.w] * this.rel;
13921
+ s.delay[s.w] = i[c][n];
13922
+ s.w = (s.w + 1) % this.delayLen;
13923
+ }
13924
+ }
13925
+ return true;
13926
+ }
13927
+ }
13928
+ registerProcessor("hf-truepeak", HfTruePeak);
13929
+ `;
13930
+
13719
13931
  // ../core/dist/audio/audioFxTail.js
13720
13932
  var MAX_FX_TAIL_SECONDS = 5;
13721
13933
  var TAIL_FLOOR = 1e-3;
@@ -13777,7 +13989,7 @@ import { join as join12 } from "path";
13777
13989
  import { pathToFileURL } from "url";
13778
13990
 
13779
13991
  // ../core/dist/generated/audio-fx-runtime-inline.js
13780
- var AUDIO_FX_RUNTIME_IIFE = '"use strict";(()=>{var Ae="data-fx-chain",yt=Ae.slice(5),j=1,M=(e="frequency",t="Frequency",n=1e3,o=20,a=2e4)=>({kind:"number",key:e,label:t,unit:"Hz",min:o,max:a,step:1,default:n,scale:"log",automatable:!0}),S=(e=.707,t="Bandwidth \\u2014 higher is narrower.")=>({kind:"number",key:"q",label:"Q",unit:"",min:.1,max:20,step:.01,default:e,scale:"log",automatable:!0,hint:t}),R=(e=-40,t=40,n=0)=>({kind:"number",key:"gain",label:"Gain",unit:"dB",min:e,max:t,step:.1,default:n,automatable:!0}),z={kind:"enum",key:"poles",label:"Slope",options:[{value:"1",label:"6 dB/oct"},{value:"2",label:"12 dB/oct"}],default:"2",hint:"Two poles is the usual biquad; one pole is gentler."},K=[{id:"gain",label:"Gain",group:"dynamics",description:"Raise or lower the whole signal. Automate it to duck under something else.",params:[R(-60,12,0)],web:"gain-node"},{id:"peaking",label:"Peaking EQ",group:"filter",description:"Boost or cut a band, leaving everything either side alone.",params:[M("frequency","Frequency",1e3),R(-40,40,0),S(1)],web:"biquad-peaking"},{id:"lowshelf",label:"Low Shelf",group:"filter",description:"Lift or drop everything below the corner frequency.",params:[M("frequency","Frequency",200,20,2e3),R(-40,40,0)],web:"biquad-lowshelf"},{id:"highshelf",label:"High Shelf",group:"filter",description:"Lift or drop everything above the corner frequency.",params:[M("frequency","Frequency",4e3,500,2e4),R(-40,40,0)],web:"biquad-highshelf"},{id:"highpass",label:"High-pass",group:"filter",description:"Remove low frequencies \\u2014 the usual fix for rumble on a voice.",params:[M("frequency","Cutoff",300,20,2e4),S(.707),z],web:"biquad-highpass"},{id:"lowpass",label:"Low-pass",group:"filter",description:"Remove high frequencies \\u2014 darkens or muffles a track.",params:[M("frequency","Cutoff",8e3,100,2e4),S(.707),z],web:"biquad-lowpass"},{id:"compressor",label:"Compressor",group:"dynamics",description:"Pull loud parts down so the quiet ones can come up.",params:[{kind:"number",key:"threshold",label:"Threshold",unit:"dB",min:-60,max:0,step:.5,default:-24,hint:"Level above which the compressor starts working."},{kind:"number",key:"ratio",label:"Ratio",unit:":1",min:1,max:20,step:.1,default:4},{kind:"number",key:"attack",label:"Attack",unit:"ms",min:.01,max:2e3,step:.1,default:20,scale:"log"},{kind:"number",key:"release",label:"Release",unit:"ms",min:.01,max:9e3,step:1,default:250,scale:"log"},{kind:"number",key:"knee",label:"Knee",unit:"",min:1,max:8,step:.01,default:2.83,hint:"1 is a hard corner; higher eases into it."},{kind:"number",key:"makeup",label:"Makeup",unit:"dB",min:0,max:36,step:.1,default:0},{kind:"number",key:"mix",label:"Mix",unit:"",min:0,max:1,step:.01,default:1,hint:"Below 1 blends the dry signal back in."}],web:"worklet-compressor"},{id:"limiter",label:"Limiter",group:"dynamics",description:"Hard ceiling \\u2014 nothing gets past the limit.",params:[{kind:"number",key:"limit",label:"Ceiling",unit:"dB",min:-24,max:0,step:.1,default:-1},{kind:"number",key:"attack",label:"Attack",unit:"ms",min:.1,max:80,step:.1,default:5},{kind:"number",key:"release",label:"Release",unit:"ms",min:1,max:8e3,step:1,default:50,scale:"log"},{kind:"number",key:"level_out",label:"Output",unit:"dB",min:-24,max:24,step:.1,default:0}],web:"worklet-limiter"},{id:"gate",label:"Noise Gate",group:"dynamics",description:"Silence the track when it drops below the threshold.",params:[{kind:"number",key:"threshold",label:"Threshold",unit:"dB",min:-80,max:0,step:.5,default:-35},{kind:"number",key:"range",label:"Range",unit:"dB",min:-80,max:0,step:.5,default:-24,hint:"How far down the gate pulls when closed."},{kind:"number",key:"ratio",label:"Ratio",unit:":1",min:1,max:20,step:.1,default:10},{kind:"number",key:"attack",label:"Attack",unit:"ms",min:.01,max:9e3,step:.1,default:1,scale:"log"},{kind:"number",key:"release",label:"Release",unit:"ms",min:.01,max:9e3,step:1,default:100,scale:"log"},{kind:"number",key:"knee",label:"Knee",unit:"",min:1,max:8,step:.01,default:2.83}],web:"worklet-gate"},{id:"saturate",label:"Saturation",group:"nonlinear",description:"Soft-clip the waveform for warmth or outright distortion.",params:[{kind:"enum",key:"type",label:"Curve",options:[{value:"tanh",label:"Tanh"},{value:"atan",label:"Arctan"},{value:"cubic",label:"Cubic"},{value:"exp",label:"Exponential"},{value:"alg",label:"Algebraic"},{value:"quintic",label:"Quintic"},{value:"sin",label:"Sine"},{value:"erf",label:"Error function"},{value:"hard",label:"Hard clip"}],default:"tanh"},{kind:"number",key:"threshold",label:"Threshold",unit:"dB",min:-40,max:0,step:.1,default:-6},{kind:"number",key:"output",automatable:!0,label:"Output",unit:"dB",min:-24,max:24,step:.1,default:0},{kind:"number",key:"oversample",label:"Oversample",unit:"x",min:1,max:8,step:1,default:4,hint:"Higher costs more but keeps aliasing down."}],web:"waveshaper"},{id:"bitcrush",label:"Bitcrush",group:"nonlinear",description:"Drop bit depth and sample rate for a lo-fi, digital sound.",params:[{kind:"number",key:"bits",label:"Bit depth",unit:"bit",min:1,max:32,step:.1,default:8},{kind:"number",key:"samples",label:"Sample hold",unit:"x",min:1,max:250,step:1,default:1,hint:"Repeats each sample N times \\u2014 a crude downsample."},{kind:"number",key:"mix",label:"Mix",unit:"",min:0,max:1,step:.01,default:1}],web:"worklet-bitcrush"},{id:"pitchshift",label:"Pitch shift",group:"time",description:"Shifts pitch up or down without changing playback speed. Adds ~50 ms of latency.",params:[{kind:"number",key:"semitones",label:"Semitones",unit:"st",min:-12,max:12,step:1,default:0},{kind:"number",key:"mix",label:"Mix",unit:"",min:0,max:1,step:.01,default:1}],web:"worklet-pitchshift"},{id:"delay",label:"Delay",group:"time",description:"Repeating echoes behind the dry signal.",params:[{kind:"number",key:"time",automatable:!0,label:"Time",unit:"ms",min:1,max:5e3,step:1,default:250,scale:"log"},{kind:"number",key:"feedback",automatable:!0,label:"Feedback",unit:"",min:.01,max:.95,step:.01,default:.35},{kind:"number",key:"mix",automatable:!0,label:"Mix",unit:"",min:0,max:1,step:.01,default:.4}],web:"delay-feedback"},{id:"chorus",label:"Chorus",group:"time",description:"Detuned copies of the signal for width and thickness.",params:[{kind:"number",key:"delay",automatable:!0,label:"Delay",unit:"ms",min:1,max:100,step:.1,default:7},{kind:"number",key:"depth",automatable:!0,label:"Depth",unit:"ms",min:0,max:10,step:.01,default:2},{kind:"number",key:"speed",automatable:!0,label:"Rate",unit:"Hz",min:.01,max:10,step:.01,default:1},{kind:"number",key:"mix",automatable:!0,label:"Mix",unit:"",min:0,max:1,step:.01,default:.5}],web:"chorus-lfo"},{id:"phaser",label:"Phaser",group:"time",description:"Sweeping notches moving through the spectrum.",params:[{kind:"number",key:"in_gain",automatable:!0,label:"Input",unit:"",min:0,max:1,step:.01,default:.4},{kind:"number",key:"out_gain",automatable:!0,label:"Output",unit:"",min:0,max:2,step:.01,default:.74},{kind:"number",key:"delay",label:"Delay",unit:"ms",min:.1,max:5,step:.1,default:3},{kind:"number",key:"decay",label:"Decay",unit:"",min:0,max:.99,step:.01,default:.4},{kind:"number",key:"speed",automatable:!0,label:"Rate",unit:"Hz",min:.1,max:2,step:.01,default:.5},{kind:"enum",key:"type",label:"Waveform",options:[{value:"0",label:"Triangular"},{value:"1",label:"Sinusoidal"}],default:"0"}],web:"allpass-phaser"},{id:"reverb",label:"Reverb",group:"time",description:"Room tail. Both ends convolve the same generated impulse, so preview matches render.",params:[{kind:"number",key:"size",label:"Room size",unit:"",min:.05,max:1,step:.01,default:.7},{kind:"number",key:"damping",label:"Damping",unit:"",min:0,max:1,step:.01,default:.5,hint:"Higher rolls the top off the tail faster."},{kind:"number",key:"wet",automatable:!0,label:"Wet",unit:"",min:0,max:1,step:.01,default:.35},{kind:"number",key:"dry",automatable:!0,label:"Dry",unit:"",min:0,max:1,step:.01,default:.7}],web:"convolver"}],E=new Map(K.map(e=>[e.id,e]));function F(e){return E.get(e)}var kt=K.map(e=>e.id);function k(e,t){let n=E.get(e);if(!n)return{};let o={};for(let a of n.params){let r=t?.[a.key];if(a.kind==="enum"){let s=a.options.some(l=>l.value===r);o[a.key]=s?r:a.default;continue}let i=typeof r=="number"?r:typeof r=="string"&&r.trim()!==""?Number(r):Number.NaN;o[a.key]=Number.isFinite(i)?Math.min(a.max,Math.max(a.min,i)):a.default}return o}var A=class extends Error{constructor(t){super(t),this.name="AudioFxChainError"}};function J(e){let t;try{t=JSON.parse(e)}catch(a){throw new A(`Chain file is not valid JSON: ${a.message}`)}if(typeof t!="object"||t===null)throw new A("Chain file must be a JSON object.");let n=t;if(n.version!==j)throw new A(`Unsupported chain version: ${String(n.version)}`);if(!Array.isArray(n.nodes))throw new A("Chain file is missing a `nodes` array.");let o=n.nodes.map(ye);return{version:j,nodes:o}}function H(e){return e.nodes.filter(t=>t.enabled!==!1)}function ye(e,t){if(typeof e!="object"||e===null)throw new A(`Node ${t} is not an object.`);let n=e;if(typeof n.type!="string"||!E.has(n.type))throw new A(`Node ${t} has unknown effect type: ${String(n.type)}`);return we({type:n.type,id:D(n.id),fromCarve:Y(n.fromCarve),fromPreset:D(n.fromPreset),label:D(n.label),fromEq:D(n.fromEq),fromLeveller:Y(n.fromLeveller),presetAmount:ke(n.presetAmount),enabled:n.enabled!==!1,params:k(n.type,n.params??void 0)})}function D(e){return typeof e=="string"&&e?e:void 0}function Y(e){return e===!0?!0:void 0}function ke(e){if(!(typeof e!="number"||!Number.isFinite(e)))return Math.min(1,Math.max(0,e))}function we(e){for(let t of Object.keys(e))e[t]===void 0&&delete e[t];return e}var ve=`\nconst dbToLin = (db) => Math.pow(10, db / 20);\n\n/**\n * One-pole envelope followers, one per channel.\n *\n * Per channel matters: the followers advance once per sample, so a single shared\n * follower stepped once per channel per sample. On stereo that ran a 20 ms attack\n * as 10 ms, and gave the right channel a gain computed from an envelope that had\n * already traversed the left \\u2014 so the two channels ducked by different amounts\n * from the same input and the image pumped.\n */\nclass EnvBank {\n constructor(attackMs, releaseMs) { this.set(attackMs, releaseMs); this.values = []; }\n set(attackMs, releaseMs) {\n this.a = Math.exp(-1 / (sampleRate * Math.max(1e-5, attackMs / 1000)));\n this.r = Math.exp(-1 / (sampleRate * Math.max(1e-5, releaseMs / 1000)));\n }\n push(ch, x) {\n const prev = this.values[ch] ?? 0;\n const m = Math.abs(x);\n const c = m > prev ? this.a : this.r;\n const next = m + c * (prev - m);\n this.values[ch] = next;\n return next;\n }\n}\n\n/** Soft-knee gain computer in dB, matching acompressor\'s shape. */\nfunction kneeGain(envDb, thresholdDb, ratio, kneeDb) {\n const over = envDb - thresholdDb;\n if (kneeDb > 0 && over > -kneeDb && over < kneeDb) {\n const t = (over + kneeDb) / (2 * kneeDb);\n return -((1 - 1 / ratio) * kneeDb * t * t);\n }\n return over > 0 ? -(over * (1 - 1 / ratio)) : 0;\n}\n\n// log10/exp per sample is the single most expensive thing a dynamics processor\n// can do on the audio thread, and every sample below the knee needs neither:\n// its gain is exactly unity. Comparing envelopes in the linear domain lets the\n// quiet majority of samples skip the transcendentals entirely.\nconst LN10_OVER_20 = Math.LN10 / 20;\nconst dbToLinFast = (db) => Math.exp(db * LN10_OVER_20);\n\nclass HfCompressor extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.env = new EnvBank(this.p.attack ?? 20, this.p.release ?? 250);\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n this.env.set(this.p.attack ?? 20, this.p.release ?? 250);\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const p = this.p;\n const makeup = dbToLin(p.makeup ?? 0);\n const mix = p.mix ?? 1;\n // Knee is expressed as a ratio in FFmpeg; convert to dB width.\n const kneeDb = 20 * Math.log10(Math.max(1.0001, p.knee ?? 2.83));\n const thresholdDb = p.threshold ?? -24;\n const ratio = p.ratio ?? 4;\n // Below this the gain computer returns unity, so the sample needs no\n // logarithm at all.\n const kneeStartLin = dbToLin(thresholdDb - kneeDb);\n const dry = 1 - mix;\n for (let ch = 0; ch < i.length; ch++) {\n const inp = i[ch], out = o[ch];\n for (let n = 0; n < inp.length; n++) {\n const x = inp[n];\n // Per-channel follower, and the sub-knee shortcut: below the knee the\n // gain is exactly unity, so the sample needs no logarithm at all.\n const env = this.env.push(ch, x);\n if (env <= kneeStartLin) {\n out[n] = x * makeup * mix + x * dry;\n continue;\n }\n const envDb = 20 * Math.log10(env);\n const g = dbToLinFast(kneeGain(envDb, thresholdDb, ratio, kneeDb));\n out[n] = x * g * makeup * mix + x * dry;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-compressor", HfCompressor);\n\nclass HfLimiter extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.env = new EnvBank(this.p.attack ?? 5, this.p.release ?? 50);\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n this.env.set(this.p.attack ?? 5, this.p.release ?? 50);\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const ceiling = dbToLin(this.p.limit ?? -1);\n const outGain = dbToLin(this.p.level_out ?? 0);\n for (let ch = 0; ch < i.length; ch++) {\n const inp = i[ch], out = o[ch];\n for (let n = 0; n < inp.length; n++) {\n const x = inp[n];\n const env = this.env.push(ch, x);\n // Only ever attenuate: a limiter that can raise level is a compressor.\n const g = env > ceiling ? ceiling / env : 1;\n out[n] = x * g * outGain;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-limiter", HfLimiter);\n\nclass HfGate extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.env = new EnvBank(this.p.attack ?? 1, this.p.release ?? 100);\n this.gains = [];\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n this.env.set(this.p.attack ?? 1, this.p.release ?? 100);\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const p = this.p;\n const threshold = dbToLin(p.threshold ?? -35);\n const floor = dbToLin(p.range ?? -24);\n const ratio = p.ratio ?? 10;\n // Knee is declared in the registry as a ratio, like the compressor\'s; a\n // hard threshold ignored it and chattered on material sitting right at the\n // gate point.\n const kneeDb = 20 * Math.log10(Math.max(1.0001, p.knee ?? 2.83));\n const kneeLin = dbToLin((p.threshold ?? -35) + kneeDb);\n for (let ch = 0; ch < i.length; ch++) {\n const inp = i[ch], out = o[ch];\n for (let n = 0; n < inp.length; n++) {\n const x = inp[n];\n const env = this.env.push(ch, x);\n let target = 1;\n // Above the threshold the gate is fully open; skip the pow entirely.\n if (env < threshold) {\n const under = env > 1e-9 ? threshold / env : 1e9;\n target = Math.max(floor, 1 / Math.pow(under, ratio - 1));\n if (!isFinite(target)) target = floor;\n }\n // Smooth toward the target so the gate does not click on every sample.\n const held = this.gains[ch] ?? 1;\n const c = target < held ? this.env.r : this.env.a;\n const smoothed = target + c * (held - target);\n this.gains[ch] = smoothed;\n out[n] = x * smoothed;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-gate", HfGate);\n\nclass HfBitcrush extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.holds = [];\n this.held = [];\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const p = this.p;\n const levels = Math.pow(2, (p.bits ?? 8) - 1);\n const step = Math.max(1, Math.round(p.samples ?? 1));\n const mix = p.mix ?? 1;\n for (let ch = 0; ch < i.length; ch++) {\n const inp = i[ch], out = o[ch];\n if (this.held[ch] === undefined) this.held[ch] = 0;\n if (this.holds[ch] === undefined) this.holds[ch] = 0;\n for (let n = 0; n < inp.length; n++) {\n // Per channel: advancing one shared counter only on the last channel\n // left every earlier channel either unheld or frozen for a whole\n // 128-sample quantum, clicking once a block.\n if (this.holds[ch] === 0) this.held[ch] = Math.round(inp[n] * levels) / levels;\n out[n] = this.held[ch] * mix + inp[n] * (1 - mix);\n this.holds[ch] = (this.holds[ch] + 1) % step;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-bitcrush", HfBitcrush);\n\n/** Linear-interpolated read, \\`delaySamples\\` behind the write head. */\nfunction readTap(ring, write, delaySamples) {\n const len = ring.length;\n const pos = (write - 1 - delaySamples + len) % len;\n const i0 = Math.floor(pos);\n const frac = pos - i0;\n const i1 = (i0 + 1) % len;\n return ring[i0] * (1 - frac) + ring[i1] * frac;\n}\n\n/** Equal-power-ish crossfade, zero at a tap\'s reset point \\u2014 hides the splice. */\nfunction xfade(phase) {\n return Math.sin(Math.PI * phase);\n}\n\n/**\n * Dual-tap granular delay line: two read taps 180\\xB0 apart in a 100 ms grain,\n * each sweeping at a speed relative to the write head that shifts pitch\n * without changing duration. One tap is always fading in as the other fades\n * out, which hides the splice each tap makes when it wraps.\n *\n * write/phase are block-level state, advanced once per SAMPLE across all\n * channels together (not once per channel) \\u2014 advancing them inside the\n * per-channel loop would move the tap 2x/4x too fast on a stereo/quad input.\n */\nclass HfPitchshift extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.grain = Math.round(sampleRate * 0.1);\n this.buf = [];\n this.write = 0;\n this.phase = 0;\n // Samples written so far, capped at one grain. The taps read up to a grain\n // behind the write head, so until this fills they would read the ring\'s\n // zeros \\u2014 the head of every clip came out attenuated or silent.\n this.filled = 0;\n // How much of the wet (pitch-shifted) path is currently in the output, and\n // where it is heading. Crossing between dry and wet is a ~50 ms jump in the\n // signal, so it is RAMPED rather than switched: a hard swap either way is a\n // click. Ramping in both directions is also what lets a node return to true\n // bypass at semitones 0 \\u2014 a one-way latch left preview stuck with the delay\n // that the render, building a fresh node from the attribute, does not have.\n this.wet = 0;\n this.wetTarget = 0;\n // ~15 ms one-pole, short enough to feel immediate on a slider drag and long\n // enough that the splice is inaudible.\n this.wetCoef = Math.exp(-1 / (sampleRate * 0.015));\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const p = this.p;\n const semitones = Math.max(-12, Math.min(12, p.semitones ?? 0));\n const mix = Math.max(0, Math.min(1, p.mix ?? 1));\n const grain = this.grain;\n const ringLen = grain * 2;\n const n = i[0] ? i[0].length : 0;\n for (let ch = 0; ch < i.length; ch++) {\n if (!this.buf[ch]) this.buf[ch] = new Float32Array(ringLen);\n }\n\n // Nothing to shift, or mixed fully out. The grain delay is ~grain/2\n // whatever the ratio, so at semitones=0 this degenerated into a pure 50 ms\n // delay of the signal \\u2014 while the copy for that exact setting reads\n // "Unchanged pitch".\n this.wetTarget = semitones === 0 ? 0 : mix;\n\n // Fully dry AND settled: take the cheap transparent path. The ring keeps\n // filling, so a later shift does not start cold.\n if (this.wetTarget === 0 && this.wet < 1e-4) {\n this.wet = 0;\n let w = this.write;\n for (let s = 0; s < n; s++) {\n for (let ch = 0; ch < i.length; ch++) {\n const x = i[ch][s];\n this.buf[ch][w] = x;\n o[ch][s] = x;\n }\n w = (w + 1) % ringLen;\n }\n this.write = w;\n this.filled = Math.min(grain, this.filled + n);\n return true;\n }\n\n const ratio = Math.pow(2, semitones / 12);\n const inc = (1 - ratio) / grain;\n let write = this.write, phase = this.phase, filled = this.filled, wetNow = this.wet;\n const target = this.wetTarget, coef = this.wetCoef;\n for (let s = 0; s < n; s++) {\n phase += inc;\n phase -= Math.floor(phase);\n const phaseB = (phase + 0.5) % 1;\n const gA = xfade(phase), gB = xfade(phaseB);\n // Ramp the wet path in as the ring fills rather than reading zeros:\n // 100 ms of unshifted audio at the head of a clip beats 50 ms of silence.\n const warm = filled >= grain ? 1 : filled / grain;\n wetNow = target + coef * (wetNow - target);\n const wetMix = wetNow * warm;\n for (let ch = 0; ch < i.length; ch++) {\n const ring = this.buf[ch];\n const inp = i[ch], out = o[ch];\n const x = inp[s];\n ring[write] = x;\n const wet =\n readTap(ring, write, phase * grain) * gA + readTap(ring, write, phaseB * grain) * gB;\n out[s] = x * (1 - wetMix) + wet * wetMix;\n }\n write = (write + 1) % ringLen;\n if (filled < grain) filled++;\n }\n this.write = write;\n this.phase = phase;\n this.filled = filled;\n this.wet = wetNow;\n return true;\n }\n}\nregisterProcessor("hf-pitchshift", HfPitchshift);\n`,I=new WeakMap;var Me=new WeakSet;function O(e){let t=I.get(e);return t||(t=(async()=>{if(!e.audioWorklet)throw new Error("AudioWorklet is unavailable \\u2014 the page needs a secure context (https, localhost or file://)");let n=`data:text/javascript;base64,${btoa(String.fromCharCode(...new TextEncoder().encode(ve)))}`;await e.audioWorklet.addModule(n),Me.add(e)})().catch(n=>{throw I.delete(e),n}),I.set(e,t)),t}function Fe(e,t,n){let o=.6+Math.max(0,Math.min(1,t))*2.6,a=Math.max(1,Math.floor(e*o)),r=new Float32Array(a),i=Math.round(t*1e3)*2654435761+Math.round(n*1e3)*40503>>>0,s=()=>(i=i*1664525+1013904223>>>0,i/4294967295*2-1),l=Math.max(.001,1-Math.max(0,Math.min(1,n))),u=0,d=0;for(let c=0;c<a;c++){u+=l*(s()-u);let b=u*Math.pow(1-c/a,2.5);r[c]=b,d+=b*b}let m=Math.sqrt(d);if(m>0)for(let c=0;c<a;c++)r[c]=(r[c]??0)/m;return r}var f=e=>typeof e=="number"?e:Number(e??0),B=e=>e/1e3;function te(e,t,n,o){let a=Math.max(1,Math.round(e.sampleRate)),r=e.createBuffer(1,a,e.sampleRate),i=r.getChannelData(0);for(let u=0;u<a;u++){let d=u/a;i[u]=t==="sine"?Math.sin(2*Math.PI*d):4*Math.abs((d+.75)%1-.5)-1}let s=e.createBufferSource();s.buffer=r,s.loop=!0,s.playbackRate.value=n;let l=(o*n%1+1)%1*(a/e.sampleRate);return s.start(typeof e.currentTime=="number"?e.currentTime:0,l),s}function ne(e){try{e.stop()}catch{}e.disconnect()}function L(e,t){return[{param:e},{param:t,map:n=>1-n}]}function ae(e,t,n){e.gain.value=n,t.gain.value=1-n}function G(e,t,n){return{input:e,output:e,update:t,automation:n,dispose:()=>e.disconnect()}}var He=new Set(["peaking","highpass","lowpass"]),Q=e=>Math.pow(10,e/20),Te=(e,t)=>{let n=e.createGain(),o=a=>{n.gain.value=Q(f(a.gain))};return o(t),G(n,o,{gain:[{param:n.gain,map:Q}]})};function T(e,t){return(n,o)=>{let a=n.createBiquadFilter();a.type=e;let r=i=>{a.frequency.value=f(i.frequency),i.q!==void 0&&(a.Q.value=f(i.q)),t&&(a.gain.value=f(i.gain))};return r(o),G(a,r,{frequency:[{param:a.frequency}],...He.has(e)?{q:[{param:a.Q}]}:{},...t?{gain:[{param:a.gain}]}:{}})}}function Ne(e){return(t,n)=>{let o=Math.tan(Math.PI*f(n.frequency)/t.sampleRate),a=e==="highpass"?t.createIIRFilter([1/(1+o),-1/(1+o)],[1,(o-1)/(o+1)]):t.createIIRFilter([o/(1+o),o/(1+o)],[1,(o-1)/(o+1)]);return G(a,()=>{})}}function N(e){return(t,n)=>{let o=new AudioWorkletNode(t,e,{processorOptions:{...n}});return{input:o,output:o,update:a=>o.port.postMessage({...a}),dispose:()=>{o.port.postMessage({__hfDispose:!0}),o.disconnect()}}}}var Pe={tanh:Math.tanh,atan:e=>2/Math.PI*Math.atan(Math.PI/2*e),cubic:e=>Math.abs(e)>=1?Math.sign(e):e-e**3/3,exp:e=>Math.sign(e)*(1-Math.exp(-Math.abs(e))),alg:e=>e/Math.sqrt(1+e*e),quintic:e=>Math.abs(e)>=1?Math.sign(e):e-e**5/5,sin:e=>Math.abs(e)>=1?Math.sign(e):Math.sin(Math.PI/2*e),erf:e=>Math.tanh(1.20211*e),hard:e=>Math.max(-1,Math.min(1,e))},_e=(e,t)=>{let n=e.createWaveShaper(),o=e.createGain(),a=e.createGain();o.connect(n).connect(a);let r=i=>{let s=Pe[String(i.type)]??Math.tanh,l=Math.pow(10,f(i.threshold)/20),u=8192,d=new Float32Array(u);for(let m=0;m<u;m++){let c=m/(u-1)*2-1;d[m]=s(c/Math.max(1e-6,l))*l}n.curve=d,n.oversample=f(i.oversample)>=4?"4x":f(i.oversample)>=2?"2x":"none",a.gain.value=Math.pow(10,f(i.output)/20)};return r(t),{input:o,output:a,update:r,automation:{output:[{param:a.gain,map:i=>Math.pow(10,i/20)}]},dispose:()=>{o.disconnect(),n.disconnect(),a.disconnect()}}},Re=(e,t)=>{let n=e.createGain(),o=e.createGain(),a=e.createDelay(5),r=e.createGain(),i=e.createGain(),s=e.createGain();n.connect(a),a.connect(r),r.connect(a),a.connect(i).connect(o),n.connect(s).connect(o);let l=u=>{a.delayTime.value=Math.min(5,f(u.time)/1e3),r.gain.value=f(u.feedback),ae(i,s,f(u.mix))};return l(t),{input:n,output:o,update:l,automation:{time:[{param:a.delayTime,map:u=>Math.min(5,B(u))}],feedback:[{param:r.gain}],mix:L(i.gain,s.gain)},dispose:()=>[n,o,a,r,i,s].forEach(u=>u.disconnect())}},De=(e,t,n)=>{let o=e.createGain(),a=e.createGain(),r=e.createDelay(.5),i=te(e,"sine",f(t.speed),n),s=e.createGain(),l=e.createGain(),u=e.createGain();i.connect(s).connect(r.delayTime),o.connect(r).connect(l).connect(a),o.connect(u).connect(a);let d=m=>{r.delayTime.value=f(m.delay)/1e3,s.gain.value=f(m.depth)/1e3,i.playbackRate.value=f(m.speed),ae(l,u,f(m.mix))};return d(t),{input:o,output:a,update:d,automation:{delay:[{param:r.delayTime,map:B}],depth:[{param:s.gain,map:B}],speed:[{param:i.playbackRate}],mix:L(l.gain,u.gain)},dispose:()=>{ne(i),[o,a,r,s,l,u].forEach(m=>m.disconnect())}}},Ce=6,Se=(e,t,n)=>{let o=e.createGain(),a=e.createGain(),r=e.createGain(),i=e.createGain(),s=te(e,String(t.type)==="1"?"sine":"triangle",f(t.speed),n),l=e.createGain(),u=e.createGain(),d=e.createGain(),m=[];o.connect(r);let c=r;for(let p=0;p<Ce;p++){let h=e.createBiquadFilter();h.type="allpass",h.Q.value=.7071,l.connect(h.frequency),c.connect(h),c=h,m.push(h)}s.connect(l),c.connect(u).connect(i),r.connect(d).connect(i),i.connect(a);let b=p=>{let h=1e3/Math.max(.1,f(p.delay));for(let g of m)g.frequency.value=h;l.gain.value=h*f(p.decay),s.playbackRate.value=f(p.speed),r.gain.value=f(p.in_gain),i.gain.value=f(p.out_gain),u.gain.value=1,d.gain.value=1};return b(t),{input:o,output:a,update:b,automation:{speed:[{param:s.playbackRate}],in_gain:[{param:r.gain}],out_gain:[{param:i.gain}]},dispose:()=>{ne(s),[o,a,r,i,l,u,d,...m].forEach(p=>p.disconnect())}}},Ee=(e,t)=>{let n=e.createGain(),o=e.createGain(),a=e.createConvolver(),r=e.createGain(),i=e.createGain();a.normalize=!1,n.connect(a).connect(r).connect(o),n.connect(i).connect(o);let s="",l=u=>{let d=`${f(u.size)}:${f(u.damping)}`;if(d!==s){let m=Fe(e.sampleRate,f(u.size),f(u.damping)),c=e.createBuffer(1,m.length,e.sampleRate);c.getChannelData(0).set(m),a.buffer=c,s=d}r.gain.value=f(u.wet),i.gain.value=f(u.dry)};return l(t),{input:n,output:o,update:l,automation:{wet:[{param:r.gain}],dry:[{param:i.gain}]},dispose:()=>[n,o,a,r,i].forEach(u=>u.disconnect())}},Ie={"gain-node":Te,"biquad-peaking":T("peaking",!0),"biquad-lowshelf":T("lowshelf",!0),"biquad-highshelf":T("highshelf",!0),"biquad-highpass":T("highpass",!1),"biquad-lowpass":T("lowpass",!1),"worklet-compressor":N("hf-compressor"),"worklet-limiter":N("hf-limiter"),"worklet-gate":N("hf-gate"),"worklet-bitcrush":N("hf-bitcrush"),"worklet-pitchshift":N("hf-pitchshift"),waveshaper:_e,"delay-feedback":Re,"chorus-lfo":De,"allpass-phaser":Se,convolver:Ee};function oe(e){return e.nodes.some(t=>F(t.type)?.web.startsWith("worklet-")??!1)}function Oe(e,t,n,o=0){let a=F(t);if(!a)throw new Error(`Unknown effect type: ${t}`);let r=k(t,n);if((t==="highpass"||t==="lowpass")&&String(r.poles)==="1")return Ne(t)(e,r,o);let i=Ie[a.web];if(!i)throw new Error(`No Web Audio builder for ${a.web}`);return i(e,r,o)}function Z(e){let t=[];for(let n of e){let o=n.fromPreset,a=t.at(-1);if(a&&a.preset===o)a.nodes.push(n);else{let r=typeof n.presetAmount=="number"?n.presetAmount:1;t.push({...o?{preset:o}:{},amount:Math.min(1,Math.max(0,r)),nodes:[n]})}}return t}function ee(e){return H(e).map(t=>{let n=k(t.type,t.params),o=n.poles!==void 0?`:${n.poles}`:"",a=String(n.poles)==="1"?`@${n.frequency}`:"",r=t.type==="phaser"?`~${n.type}`:"",i=t.fromPreset?`%${t.fromPreset}`:"";return`${t.type}${o}${a}${r}${i}`}).join("|")}function re(e,t,n=0){var m;let o=e.createGain(),a=e.createGain(),r=[],i=[],s=Z(H(t)),l=o;for(let c of s){let b=null;if(c.preset){let p=e.createGain(),h=e.createGain(),g=e.createGain(),v=e.createGain();g.gain.value=c.amount,h.gain.value=1-c.amount,l.connect(p),p.connect(h).connect(v),b={entry:p,wet:g,dry:h,join:v},l=p}for(let p of c.nodes){let h=Oe(e,p.type,p.params??{},n);l.connect(h.input),l=h.output,r.push({...p.id?{id:p.id}:{},type:p.type,handle:h})}b&&c.preset&&(l.connect(b.wet).connect(b.join),i.push({id:c.preset,...b}),l=b.join)}l.connect(a);let u=ee(t),d={};for(let c of i)(d[m=c.id]??(d[m]=[])).push(...L(c.wet.gain,c.dry.gain));return{input:o,output:a,presets:d,nodes:r,update(c){if(ee(c)!==u)return!1;H(c).forEach((p,h)=>{let g=r[h];g&&(g.handle.update(k(p.type,p.params)),p.id===void 0?delete g.id:g.id=p.id)});let b=0;for(let p of Z(H(c))){if(!p.preset)continue;let h=i[b++];!h||h.id!==p.preset||(h.wet.gain.value=p.amount,h.dry.gain.value=1-p.amount)}return!0},dispose(){for(let{handle:c}of r)c.dispose();for(let{entry:c,wet:b,dry:p,join:h}of i)c.disconnect(),b.disconnect(),p.disconnect(),h.disconnect();o.disconnect(),a.disconnect()}}}function Be(e){return 10**(e/20)}var ie=Be(12);var q=.1,V=10;var Le="data-automation",Et=Le.slice(5),C=1,Ge=512,x=class extends Error{constructor(t){super(t),this.name="AudioAutomationError"}},U="volume",se="rate";function _(e){if(e===U)return{kind:"volume"};if(e===se)return{kind:"rate"};let t=e.split(".");if(t.length===3&&t[0]==="fx"&&t[1]===qe){let a=t[2];return a?{kind:"preset",presetId:a}:null}if(t.length!==3||t[0]!=="fx")return null;let[,n,o]=t;return!n||!o?null:{kind:"fx",nodeId:n,param:o}}function ue(e,t){return`fx.${e}.${t}`}var qe="preset";var Ve={min:0,max:1,step:.01,unit:"",label:"Amount",scale:"linear",default:1},le={min:0,max:ie,step:.01,unit:"",label:"Volume",scale:"linear",default:1},de={min:q,max:V,step:.05,unit:"x",label:"Speed",scale:"log",default:1};function X(e,t){let n=_(e);if(!n)return null;if(n.kind==="volume")return le;if(n.kind==="rate")return de;if(n.kind==="preset")return t?.nodes.some(s=>s.fromPreset===n.presetId)?{...Ve,label:`${n.presetId} \\xB7 Amount`}:null;let o=t?.nodes.find(i=>i.id===n.nodeId);if(!o)return null;let a=F(o.type),r=a?.params.find(i=>i.key===n.param);return!r||r.kind!=="number"?null:{min:r.min,max:r.max,step:r.step,unit:r.unit,label:`${a?.label??o.type} \\xB7 ${r.label}`,scale:r.scale==="log"&&r.min>0?"log":"linear",default:r.default}}function P(e){if(typeof e=="number")return Number.isFinite(e)?e:null;if(typeof e=="string"&&e.trim()!==""){let t=Number(e);return Number.isFinite(t)?t:null}return null}function Ue(e){let t=P(e);return t===null||t===0?0:Math.min(1,Math.max(-1,t))}function ce(e,t){let n=P(e),o=P(t);if(n===null||o===null)return null;let a=s=>Math.min(.999,Math.max(.001,s)),r=a(n),i=a(o);return Math.abs(r-i)<1e-6?null:{x:r,y:i}}function Xe(e){let t=Ue(e?.curve),n=ce(e?.viaX,e?.viaY);return{...t?{curve:t}:{},...n?{viaX:n.x,viaY:n.y}:{}}}function $e(e,t){let n=P(e?.t),o=P(e?.v);if(n===null||o===null)return null;let a=t?Math.min(t.max,Math.max(t.min,o)):o;return{t:Math.max(0,n),v:a,...Xe(e)}}function me(e,t){let n=e.map(a=>$e(a,t)).filter(a=>a!==null).sort((a,r)=>a.t-r.t),o=[];for(let a of n)o.length>0&&o[o.length-1].t===a.t?o[o.length-1]=a:o.push(a);return o.slice(0,Ge)}function We(e){let t=[];for(let n of e.lanes){if(!_(n.target))continue;let o=n.target===U?le:n.target===se?de:null,a=me(n.points??[],o);a.length>0&&t.push({target:n.target,points:a})}return{version:C,lanes:t}}function pe(e,t){let n=[];for(let o of e.lanes){let a=X(o.target,t);if(!a)continue;let r=me(o.points,a);r.length>0&&n.push({target:o.target,points:r})}return{version:C,lanes:n}}function he(e){let t;try{t=JSON.parse(e)}catch(a){throw new x(`Automation is not valid JSON: ${a.message}`)}if(typeof t!="object"||t===null)throw new x("Automation must be a JSON object.");let n=t;if(n.version!==C)throw new x(`Unsupported automation version: ${String(n.version)}`);if(!Array.isArray(n.lanes))throw new x("Automation is missing a `lanes` array.");let o=n.lanes.map((a,r)=>{if(typeof a!="object"||a===null)throw new x(`Lane ${r} is not an object.`);let i=a;if(typeof i.target!="string"||!_(i.target))throw new x(`Lane ${r} has an unreadable target: ${String(i.target)}`);if(!Array.isArray(i.points))throw new x(`Lane ${r} is missing a \\`points\\` array.`);return{target:i.target,points:i.points}});return We({version:C,lanes:o})}function je(e,t){return t?Math.pow(e,Math.pow(2,2*t)):e}function ze(e,t){let n=e-.5,o=t-.5,a=.999,r=1e6,i=n>0?n/(a-e):n<0?-n/(e-(1-a)):0,s=o>0?o/(a-t):o<0?-o/(t-(1-a)):0,l=Math.min(r,Math.max(1,i,s));return{cx:e+n/l,cy:t+o/l,w:l}}function Ye(e,t,n){let{cx:o,cy:a,w:r}=ze(t,n),i=2-2*r,s=e*i-1+2*r*o,l=-e*i-2*r*o,u=Ke(s,l,e),d=1-u,m=d*d+2*r*u*d+u*u;return m>0?(2*r*a*u*d+u*u)/m:e}function Ke(e,t,n){if(Math.abs(e)<1e-12)return Math.abs(t)<1e-12?n:-n/t;let o=Math.sqrt(Math.max(0,t*t-4*e*n)),a=(-t+o)/(2*e),r=(-t-o)/(2*e),i=s=>s>=-1e-9&&s<=1+1e-9;return i(a)?Math.min(1,Math.max(0,a)):i(r)?Math.min(1,Math.max(0,r)):n}function Je(e,t){let n=ce(t.viaX,t.viaY);return n?Ye(Math.min(1,Math.max(0,e)),n.x,n.y):je(e,t.curve)}function Qe(e,t,n,o){return o==="log"&&e>0&&t>0?Math.exp(Math.log(e)+(Math.log(t)-Math.log(e))*n):e+(t-e)*n}function $(e,t,n="linear"){let o=e.points;if(o.length===0)return 0;let a=o[0];if(t<=a.t)return a.v;let r=o[o.length-1];if(t>=r.t)return r.v;let i=0,s=o.length-1;for(;s-i>1;){let m=i+s>>1;o[m].t<=t?i=m:s=m}let l=o[i],u=o[s],d=u.t-l.t;return d<=0?u.v:Qe(l.v,u.v,Je((t-l.t)/d,l),n)}function fe(e){return e.points.length<=1||e.points.every(t=>t.v===e.points[0].v)}function be(e,t,n,o,a="linear"){let r=Math.max(2,Math.floor(o)),i=new Float32Array(r),s=n-t;for(let l=0;l<r;l+=1)i[l]=$(e,t+s*l/(r-1),a);return i}var Ze=100,et=8,tt=4096,W=e=>e;function nt(e,t,n){return!(!!e.curve||e.viaX!==void 0&&e.viaY!==void 0)&&t==="linear"&&!n}function at(e){let t=Math.ceil(e*Ze);return Math.min(tt,Math.max(et,t))}function ot(e,t,n,o,a){let{scheduledAt:r,elapsed:i,rate:s}=o,l=a??W,u=w=>r+(w-i)/s,d=e.points[t],m=e.points[t+1],c=u(m.t);if(c<=r)return null;let b=Math.max(r,u(d.t)),p=c-b;if(p<=0)return null;if(nt(d,n,a))return{kind:"ramp",time:c,value:l(m.v)};let h=at(p),g=be(e,Math.max(d.t,i),m.t,h,n),v=new Float32Array(h);for(let w=0;w<h;w+=1)v[w]=l(g[w]??0);return{kind:"curve",time:b,duration:p,values:v}}function rt(e,t,n,o){let a=[];for(let i=0;i+1<e.points.length;i+=1){let s=ot(e,i,t,n,o);s&&a.push(s)}let r=a[0];return r?.kind==="curve"&&r.time<=n.scheduledAt||a.unshift({kind:"set",time:n.scheduledAt,value:(o??W)($(e,n.elapsed,t))}),a}function it(e,t){for(let n of t)if(n.kind==="set")e.setValueAtTime(n.value,n.time);else if(n.kind==="ramp")e.linearRampToValueAtTime(n.value,n.time);else try{e.setValueCurveAtTime(n.values,n.time,n.duration)}catch{let o=n.values[n.values.length-1]??0;e.linearRampToValueAtTime(o,n.time+n.duration)}}function st(e){for(let t of e)t.param.cancelScheduledValues(0)}function ut(e,t,n,o){if(t.points.length!==0)for(let a of e){if(st([a]),fe(t)){let r=(a.map??W)(t.points[0].v);a.param.setValueAtTime(r,o.scheduledAt);continue}it(a.param,rt(t,n,o,a.map))}}function ge(e,t,n,o,a){let r=new Map(n.filter(s=>s.id).map(s=>[s.id,s.handle])),i=[];for(let s of e.lanes){let l=_(s.target);if(!l)continue;let u=l.kind==="preset"?a?.[l.presetId]:l.kind==="fx"?r.get(l.nodeId)?.automation?.[l.param]:void 0;if(!u||u.length===0)continue;let d=X(s.target,t);d&&(ut(u,s,d.scale,o),i.push(...u))}return i}var lt=5,dt=.001;function y(e,t,n){let o=Number(k(e.type,e.params)[t]??0);if(!e.id||!n)return Number.isFinite(o)?o:0;let a=n.lanes.find(r=>r.target===ue(e.id??"",t));return!a||a.points.length===0?Number.isFinite(o)?o:0:a.points.reduce((r,i)=>Math.max(r,i.v),-1/0)}function ct(e,t){let n=Math.min(5,e/1e3);if(n<=0)return 0;let o=Math.max(0,Math.min(.999,t));return o<=0?n:Math.ceil(Math.log(dt)/Math.log(o))*n}function mt(e,t){return y(e,"wet",t)>0?.6+Math.max(0,Math.min(1,y(e,"size",t)))*2.6:0}function pt(e,t){return y(e,"mix",t)>0?ct(y(e,"time",t),y(e,"feedback",t)):0}function ht(e,t){return y(e,"mix",t)>0?y(e,"delay",t)/1e3:0}function ft(e,t){return y(e,"mix",t)>0?.2:0}function bt(e,t){if(e.enabled===!1)return 0;switch(e.type){case"reverb":return mt(e,t);case"delay":return pt(e,t);case"chorus":return ht(e,t);case"pitchshift":return ft(e,t);default:return 0}}function xe(e,t){let n=e.nodes.reduce((o,a)=>o+bt(a,t),0);return Math.min(lt,n)}function gt(e,t,n,o,a){let r=e.createBuffer(n,o,a);for(let i=0;i<n;i++){let s=t[i];s&&r.getChannelData(i).set(s)}return r}function xt(e){let t=[];for(let n=0;n<e.numberOfChannels;n++)t.push(new Float32Array(e.getChannelData(n)));return t}async function At(e,t,n,o){let a=J(n),r=Math.max(1,e.length),i=e[0]?.length??0;if(i===0)return e;let s=o?pe(he(o),a):null,l=Math.ceil(xe(a,s??void 0)*t),u=new OfflineAudioContext(r,i+l,t);oe(a)&&await O(u);let d=u.createBufferSource();d.buffer=gt(u,e,r,i,t);let m=re(u,a);return s&&ge(s,a,m.nodes,{scheduledAt:0,elapsed:0,rate:1},m.presets),d.connect(m.input),m.output.connect(u.destination),d.start(),xt(await u.startRendering())}window.__HF_AUDIO_FX={render:At};})();\n';
13992
+ var AUDIO_FX_RUNTIME_IIFE = '"use strict";(()=>{var we="data-fx-chain",Mt=we.slice(5),W=1,T=(e="frequency",t="Frequency",n=1e3,o=20,a=2e4)=>({kind:"number",key:e,label:t,unit:"Hz",min:o,max:a,step:1,default:n,scale:"log",automatable:!0}),C=(e=.707,t="Bandwidth: higher is narrower.")=>({kind:"number",key:"q",label:"Q",unit:"",min:.1,max:20,step:.01,default:e,scale:"log",automatable:!0,hint:t}),R=(e=-40,t=40,n=0)=>({kind:"number",key:"gain",label:"Gain",unit:"dB",min:e,max:t,step:.1,default:n,automatable:!0}),K={kind:"enum",key:"poles",label:"Slope",options:[{value:"1",label:"6 dB/oct"},{value:"2",label:"12 dB/oct"}],default:"2",hint:"Two poles is the usual biquad; one pole is gentler."},z=[{id:"gain",label:"Gain",group:"dynamics",description:"Raise or lower the whole signal. Automate it to duck under something else.",params:[R(-60,12,0)],web:"gain-node"},{id:"peaking",label:"Peaking EQ",group:"filter",description:"Boost or cut a band, leaving everything either side alone.",params:[T("frequency","Frequency",1e3),R(-40,40,0),C(1)],web:"biquad-peaking"},{id:"lowshelf",label:"Low Shelf",group:"filter",description:"Lift or drop everything below the corner frequency.",params:[T("frequency","Frequency",200,20,2e3),R(-40,40,0)],web:"biquad-lowshelf"},{id:"highshelf",label:"High Shelf",group:"filter",description:"Lift or drop everything above the corner frequency.",params:[T("frequency","Frequency",4e3,500,2e4),R(-40,40,0)],web:"biquad-highshelf"},{id:"highpass",label:"High-pass",group:"filter",description:"Remove low frequencies: the usual fix for rumble on a voice.",params:[T("frequency","Cutoff",300,20,2e4),C(.707),K],web:"biquad-highpass"},{id:"lowpass",label:"Low-pass",group:"filter",description:"Remove high frequencies, which darkens or muffles a track.",params:[T("frequency","Cutoff",8e3,100,2e4),C(.707),K],web:"biquad-lowpass"},{id:"compressor",label:"Compressor",group:"dynamics",description:"Pull loud parts down so the quiet ones can come up.",params:[{kind:"number",key:"threshold",label:"Threshold",unit:"dB",min:-60,max:0,step:.5,default:-24,hint:"Level above which the compressor starts working."},{kind:"number",key:"ratio",label:"Ratio",unit:":1",min:1,max:20,step:.1,default:4},{kind:"number",key:"attack",label:"Attack",unit:"ms",min:.01,max:2e3,step:.1,default:20,scale:"log"},{kind:"number",key:"release",label:"Release",unit:"ms",min:.01,max:9e3,step:1,default:250,scale:"log"},{kind:"number",key:"knee",label:"Knee",unit:"",min:1,max:8,step:.01,default:2.83,hint:"1 is a hard corner; higher eases into it."},{kind:"number",key:"makeup",label:"Makeup",unit:"dB",min:0,max:36,step:.1,default:0},{kind:"number",key:"mix",label:"Mix",unit:"",min:0,max:1,step:.01,default:1,hint:"Below 1 blends the dry signal back in."}],web:"worklet-compressor"},{id:"limiter",label:"Limiter",group:"dynamics",description:"Hard ceiling: nothing gets past the limit.",params:[{kind:"number",key:"limit",label:"Ceiling",unit:"dB",min:-24,max:0,step:.1,default:-1},{kind:"number",key:"attack",label:"Attack",unit:"ms",min:.1,max:80,step:.1,default:5},{kind:"number",key:"release",label:"Release",unit:"ms",min:1,max:8e3,step:1,default:50,scale:"log"},{kind:"number",key:"level_out",label:"Output",unit:"dB",min:-24,max:24,step:.1,default:0}],web:"worklet-limiter"},{id:"truepeak",label:"True-peak limiter",group:"dynamics",description:"Lookahead limiter that holds a 4x estimate of the inter-sample peak at a dBTP ceiling; full-band noise can land up to about 1.7 dB above it. Adds lookahead plus about 0.3 ms of latency.",params:[{kind:"number",key:"ceiling",label:"Ceiling",unit:"dBTP",min:-24,max:0,step:.1,default:-1,hint:"Target true peak, measured on a 4x oversampled signal. Dense full-band sound can end up to about 1.7 dB above it."},{kind:"number",key:"lookahead",label:"Lookahead",unit:"ms",min:.5,max:10,step:.1,default:3,hint:"How far ahead the gain starts falling. Longer is smoother and adds the same latency."},{kind:"number",key:"release",label:"Release",unit:"ms",min:10,max:2e3,step:1,default:80,scale:"log"}],web:"worklet-truepeak"},{id:"gate",label:"Noise Gate",group:"dynamics",description:"Silence the track when it drops below the threshold.",params:[{kind:"number",key:"threshold",label:"Threshold",unit:"dB",min:-80,max:0,step:.5,default:-35},{kind:"number",key:"range",label:"Range",unit:"dB",min:-80,max:0,step:.5,default:-24,hint:"How far down the gate pulls when closed."},{kind:"number",key:"ratio",label:"Ratio",unit:":1",min:1,max:20,step:.1,default:10},{kind:"number",key:"attack",label:"Attack",unit:"ms",min:.01,max:9e3,step:.1,default:1,scale:"log"},{kind:"number",key:"release",label:"Release",unit:"ms",min:.01,max:9e3,step:1,default:100,scale:"log"},{kind:"number",key:"knee",label:"Knee",unit:"",min:1,max:8,step:.01,default:2.83}],web:"worklet-gate"},{id:"saturate",label:"Saturation",group:"nonlinear",description:"Soft-clip the waveform for warmth or outright distortion.",params:[{kind:"enum",key:"type",label:"Curve",options:[{value:"tanh",label:"Tanh"},{value:"atan",label:"Arctan"},{value:"cubic",label:"Cubic"},{value:"exp",label:"Exponential"},{value:"alg",label:"Algebraic"},{value:"quintic",label:"Quintic"},{value:"sin",label:"Sine"},{value:"erf",label:"Error function"},{value:"hard",label:"Hard clip"}],default:"tanh"},{kind:"number",key:"threshold",label:"Threshold",unit:"dB",min:-40,max:0,step:.1,default:-6},{kind:"number",key:"output",automatable:!0,label:"Output",unit:"dB",min:-24,max:24,step:.1,default:0},{kind:"number",key:"oversample",label:"Oversample",unit:"x",min:1,max:8,step:1,default:4,hint:"Higher costs more but keeps aliasing down."}],web:"waveshaper"},{id:"bitcrush",label:"Bitcrush",group:"nonlinear",description:"Drop bit depth and sample rate for a lo-fi, digital sound.",params:[{kind:"number",key:"bits",label:"Bit depth",unit:"bit",min:1,max:32,step:.1,default:8},{kind:"number",key:"samples",label:"Sample hold",unit:"x",min:1,max:250,step:1,default:1,hint:"Repeats each sample N times, a crude downsample."},{kind:"number",key:"mix",label:"Mix",unit:"",min:0,max:1,step:.01,default:1}],web:"worklet-bitcrush"},{id:"pitchshift",label:"Pitch shift",group:"time",description:"Shifts pitch up or down without changing playback speed. Adds ~50 ms of latency.",params:[{kind:"number",key:"semitones",label:"Semitones",unit:"st",min:-12,max:12,step:1,default:0},{kind:"number",key:"mix",label:"Mix",unit:"",min:0,max:1,step:.01,default:1}],web:"worklet-pitchshift"},{id:"delay",label:"Delay",group:"time",description:"Repeating echoes behind the dry signal.",params:[{kind:"number",key:"time",automatable:!0,label:"Time",unit:"ms",min:1,max:5e3,step:1,default:250,scale:"log"},{kind:"number",key:"feedback",automatable:!0,label:"Feedback",unit:"",min:.01,max:.95,step:.01,default:.35},{kind:"number",key:"mix",automatable:!0,label:"Mix",unit:"",min:0,max:1,step:.01,default:.4}],web:"delay-feedback"},{id:"chorus",label:"Chorus",group:"time",description:"Detuned copies of the signal for width and thickness.",params:[{kind:"number",key:"delay",automatable:!0,label:"Delay",unit:"ms",min:1,max:100,step:.1,default:7},{kind:"number",key:"depth",automatable:!0,label:"Depth",unit:"ms",min:0,max:10,step:.01,default:2},{kind:"number",key:"speed",automatable:!0,label:"Rate",unit:"Hz",min:.01,max:10,step:.01,default:1},{kind:"number",key:"mix",automatable:!0,label:"Mix",unit:"",min:0,max:1,step:.01,default:.5}],web:"chorus-lfo"},{id:"phaser",label:"Phaser",group:"time",description:"Sweeping notches moving through the spectrum.",params:[{kind:"number",key:"in_gain",automatable:!0,label:"Input",unit:"",min:0,max:1,step:.01,default:.4},{kind:"number",key:"out_gain",automatable:!0,label:"Output",unit:"",min:0,max:2,step:.01,default:.74},{kind:"number",key:"delay",label:"Delay",unit:"ms",min:.1,max:5,step:.1,default:3},{kind:"number",key:"decay",label:"Decay",unit:"",min:0,max:.99,step:.01,default:.4},{kind:"number",key:"speed",automatable:!0,label:"Rate",unit:"Hz",min:.1,max:2,step:.01,default:.5},{kind:"enum",key:"type",label:"Waveform",options:[{value:"0",label:"Triangular"},{value:"1",label:"Sinusoidal"}],default:"0"}],web:"allpass-phaser"},{id:"reverb",label:"Reverb",group:"time",description:"Room tail. Both ends convolve the same generated impulse, so preview matches render.",params:[{kind:"number",key:"size",label:"Room size",unit:"",min:.05,max:1,step:.01,default:.7},{kind:"number",key:"damping",label:"Damping",unit:"",min:0,max:1,step:.01,default:.5,hint:"Higher rolls the top off the tail faster."},{kind:"number",key:"wet",automatable:!0,label:"Wet",unit:"",min:0,max:1,step:.01,default:.35},{kind:"number",key:"dry",automatable:!0,label:"Dry",unit:"",min:0,max:1,step:.01,default:.7}],web:"convolver"}],L=new Map(z.map(e=>[e.id,e]));function P(e){return L.get(e)}var Ft=z.map(e=>e.id);function y(e,t){let n=L.get(e);if(!n)return{};let o={};for(let a of n.params){let r=t?.[a.key];if(a.kind==="enum"){let s=a.options.some(l=>l.value===r);o[a.key]=s?r:a.default;continue}let i=typeof r=="number"?r:typeof r=="string"&&r.trim()!==""?Number(r):Number.NaN;o[a.key]=Number.isFinite(i)?Math.min(a.max,Math.max(a.min,i)):a.default}return o}var x=class extends Error{constructor(t){super(t),this.name="AudioFxChainError"}};function J(e){let t;try{t=JSON.parse(e)}catch(a){throw new x(`Chain file is not valid JSON: ${a.message}`)}if(typeof t!="object"||t===null)throw new x("Chain file must be a JSON object.");let n=t;if(n.version!==W)throw new x(`Unsupported chain version: ${String(n.version)}`);if(!Array.isArray(n.nodes))throw new x("Chain file is missing a `nodes` array.");let o=n.nodes.map(ve);return{version:W,nodes:o}}function w(e){return e.nodes.filter(t=>t.enabled!==!1)}function ve(e,t){if(typeof e!="object"||e===null)throw new x(`Node ${t} is not an object.`);let n=e;if(typeof n.type!="string"||!L.has(n.type))throw new x(`Node ${t} has unknown effect type: ${String(n.type)}`);return Fe({type:n.type,id:E(n.id),fromCarve:Y(n.fromCarve),fromPreset:E(n.fromPreset),label:E(n.label),fromEq:E(n.fromEq),fromLeveller:Y(n.fromLeveller),presetAmount:Me(n.presetAmount),enabled:n.enabled!==!1,params:y(n.type,n.params??void 0)})}function E(e){return typeof e=="string"&&e?e:void 0}function Y(e){return e===!0?!0:void 0}function Me(e){if(!(typeof e!="number"||!Number.isFinite(e)))return Math.min(1,Math.max(0,e))}function Fe(e){for(let t of Object.keys(e))e[t]===void 0&&delete e[t];return e}function Te(e,t){return Math.max(1,Math.round(e*t/1e3))}function Q(e,t){return Te(e,t)+16}var Z=`\nconst TP_OS = 4;\nconst TP_HALF = 64;\nconst TP_BRANCH = 33;\nconst TP_DELAY = 16;\n\nfunction besselI0(x) {\n let sum = 1, term = 1;\n for (let k = 1; k < 40; k++) {\n term *= (x / (2 * k)) * (x / (2 * k));\n sum += term;\n }\n return sum;\n}\n\n/** Kaiser-windowed sinc, one array per polyphase branch, newest tap first. */\nfunction tpBranches() {\n const beta = 6;\n const norm = besselI0(beta);\n const branches = [];\n for (let p = 0; p < TP_OS; p++) {\n const taps = new Float64Array(TP_BRANCH);\n let sum = 0;\n for (let j = 0; j < TP_BRANCH; j++) {\n const k = p + TP_OS * j;\n if (k > 2 * TP_HALF) continue;\n const t = (k - TP_HALF) / TP_OS;\n const sinc = t === 0 ? 1 : Math.sin(Math.PI * t) / (Math.PI * t);\n const r = (k - TP_HALF) / TP_HALF;\n taps[j] = sinc * (besselI0(beta * Math.sqrt(Math.max(0, 1 - r * r))) / norm);\n sum += taps[j];\n }\n // Unity gain at DC on every branch, so a steady level reads as itself.\n for (let j = 0; j < TP_BRANCH; j++) taps[j] /= sum;\n branches.push(taps);\n }\n return branches;\n}\n\nclass HfTruePeak extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.look = Math.max(1, Math.round((this.p.lookahead ?? 3) * sampleRate / 1000));\n this.delayLen = this.look + TP_DELAY;\n this.branches = tpBranches();\n this.setLevels();\n this.clear();\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n if (e.data && e.data.__hfReset) { this.clear(); return; }\n this.p = { ...this.p, ...e.data };\n this.setLevels();\n };\n }\n /** Back to the state of a fresh processor: no audio in the delay line, no gain reduction. */\n clear() {\n this.ch = [];\n this.rel = 1;\n this.dqIdx = new Int32Array(this.look + 2);\n this.dqVal = new Float64Array(this.look + 2);\n this.dqHead = 0;\n this.dqTail = 0;\n this.t = 0;\n this.avgRing = new Float64Array(this.look + 1).fill(1);\n this.avgPos = 0;\n this.avgSum = this.look + 1;\n }\n setLevels() {\n this.ceiling = Math.pow(10, (this.p.ceiling ?? -1) / 20);\n this.relCoef = 1 - Math.exp(-1 / (sampleRate * Math.max(1e-4, (this.p.release ?? 80) / 1000)));\n }\n channel(c) {\n if (!this.ch[c]) {\n this.ch[c] = {\n delay: new Float32Array(this.delayLen),\n w: 0,\n hist: new Float32Array(TP_BRANCH * 2),\n pos: 0,\n };\n }\n return this.ch[c];\n }\n /** Largest interpolated magnitude across channels for the newest sample. */\n peak(chans, inputs, n) {\n let peak = 0;\n for (let c = 0; c < chans; c++) {\n const s = this.channel(c);\n const x = inputs[c][n];\n s.pos = (s.pos + 1) % TP_BRANCH;\n s.hist[s.pos] = x;\n s.hist[s.pos + TP_BRANCH] = x;\n const base = s.pos + TP_BRANCH;\n for (let p = 0; p < TP_OS; p++) {\n const taps = this.branches[p];\n let acc = 0;\n for (let j = 0; j < TP_BRANCH; j++) acc += taps[j] * s.hist[base - j];\n const a = acc < 0 ? -acc : acc;\n if (a > peak) peak = a;\n }\n }\n return peak;\n }\n /** Windowed minimum of the required gain over the last look + 1 values. */\n windowMin(v) {\n const cap = this.dqIdx.length;\n while (this.dqTail !== this.dqHead && this.dqVal[(this.dqTail + cap - 1) % cap] >= v) {\n this.dqTail = (this.dqTail + cap - 1) % cap;\n }\n this.dqIdx[this.dqTail] = this.t;\n this.dqVal[this.dqTail] = v;\n this.dqTail = (this.dqTail + 1) % cap;\n while (this.dqIdx[this.dqHead] <= this.t - (this.look + 1)) {\n this.dqHead = (this.dqHead + 1) % cap;\n }\n this.t++;\n return this.dqVal[this.dqHead];\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const chans = i.length;\n const frames = i[0].length;\n for (let n = 0; n < frames; n++) {\n const peak = this.peak(chans, i, n);\n const need = peak > this.ceiling ? this.ceiling / peak : 1;\n const m = this.windowMin(need);\n this.avgSum += m - this.avgRing[this.avgPos];\n this.avgRing[this.avgPos] = m;\n this.avgPos = (this.avgPos + 1) % this.avgRing.length;\n const target = Math.min(1, this.avgSum / this.avgRing.length);\n const released = this.rel + (1 - this.rel) * this.relCoef;\n this.rel = released < target ? released : target;\n if (target >= 1 && this.rel > 0.9999999) this.rel = 1;\n for (let c = 0; c < chans; c++) {\n const s = this.channel(c);\n o[c][n] = s.delay[s.w] * this.rel;\n s.delay[s.w] = i[c][n];\n s.w = (s.w + 1) % this.delayLen;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-truepeak", HfTruePeak);\n`;var Pe=`\nconst dbToLin = (db) => Math.pow(10, db / 20);\n\n/**\n * One-pole envelope followers, one per channel.\n *\n * Per channel matters: the followers advance once per sample, so a single shared\n * follower stepped once per channel per sample. On stereo that ran a 20 ms attack\n * as 10 ms, and gave the right channel a gain computed from an envelope that had\n * already traversed the left, so the two channels ducked by different amounts\n * from the same input and the image pumped.\n */\nclass EnvBank {\n constructor(attackMs, releaseMs) { this.set(attackMs, releaseMs); this.values = []; }\n set(attackMs, releaseMs) {\n this.a = Math.exp(-1 / (sampleRate * Math.max(1e-5, attackMs / 1000)));\n this.r = Math.exp(-1 / (sampleRate * Math.max(1e-5, releaseMs / 1000)));\n }\n push(ch, x) {\n const prev = this.values[ch] ?? 0;\n const m = Math.abs(x);\n const c = m > prev ? this.a : this.r;\n const next = m + c * (prev - m);\n this.values[ch] = next;\n return next;\n }\n}\n\n/** Soft-knee gain computer in dB, matching acompressor\'s shape. */\nfunction kneeGain(envDb, thresholdDb, ratio, kneeDb) {\n const over = envDb - thresholdDb;\n if (kneeDb > 0 && over > -kneeDb && over < kneeDb) {\n const t = (over + kneeDb) / (2 * kneeDb);\n return -((1 - 1 / ratio) * kneeDb * t * t);\n }\n return over > 0 ? -(over * (1 - 1 / ratio)) : 0;\n}\n\n// log10/exp per sample is the single most expensive thing a dynamics processor\n// can do on the audio thread, and every sample below the knee needs neither:\n// its gain is exactly unity. Comparing envelopes in the linear domain lets the\n// quiet majority of samples skip the transcendentals entirely.\nconst LN10_OVER_20 = Math.LN10 / 20;\nconst dbToLinFast = (db) => Math.exp(db * LN10_OVER_20);\n\nclass HfCompressor extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.env = new EnvBank(this.p.attack ?? 20, this.p.release ?? 250);\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n this.env.set(this.p.attack ?? 20, this.p.release ?? 250);\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const p = this.p;\n const makeup = dbToLin(p.makeup ?? 0);\n const mix = p.mix ?? 1;\n // Knee is expressed as a ratio in FFmpeg; convert to dB width.\n const kneeDb = 20 * Math.log10(Math.max(1.0001, p.knee ?? 2.83));\n const thresholdDb = p.threshold ?? -24;\n const ratio = p.ratio ?? 4;\n // Below this the gain computer returns unity, so the sample needs no\n // logarithm at all.\n const kneeStartLin = dbToLin(thresholdDb - kneeDb);\n const dry = 1 - mix;\n for (let ch = 0; ch < i.length; ch++) {\n const inp = i[ch], out = o[ch];\n for (let n = 0; n < inp.length; n++) {\n const x = inp[n];\n // Per-channel follower, and the sub-knee shortcut: below the knee the\n // gain is exactly unity, so the sample needs no logarithm at all.\n const env = this.env.push(ch, x);\n if (env <= kneeStartLin) {\n out[n] = x * makeup * mix + x * dry;\n continue;\n }\n const envDb = 20 * Math.log10(env);\n const g = dbToLinFast(kneeGain(envDb, thresholdDb, ratio, kneeDb));\n out[n] = x * g * makeup * mix + x * dry;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-compressor", HfCompressor);\n\nclass HfLimiter extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.env = new EnvBank(this.p.attack ?? 5, this.p.release ?? 50);\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n this.env.set(this.p.attack ?? 5, this.p.release ?? 50);\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const ceiling = dbToLin(this.p.limit ?? -1);\n const outGain = dbToLin(this.p.level_out ?? 0);\n for (let ch = 0; ch < i.length; ch++) {\n const inp = i[ch], out = o[ch];\n for (let n = 0; n < inp.length; n++) {\n const x = inp[n];\n const env = this.env.push(ch, x);\n // Only ever attenuate: a limiter that can raise level is a compressor.\n const g = env > ceiling ? ceiling / env : 1;\n out[n] = x * g * outGain;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-limiter", HfLimiter);\n\nclass HfGate extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.env = new EnvBank(this.p.attack ?? 1, this.p.release ?? 100);\n this.gains = [];\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n this.env.set(this.p.attack ?? 1, this.p.release ?? 100);\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const p = this.p;\n const threshold = dbToLin(p.threshold ?? -35);\n const floor = dbToLin(p.range ?? -24);\n const ratio = p.ratio ?? 10;\n // Knee is declared in the registry as a ratio, like the compressor\'s; a\n // hard threshold ignored it and chattered on material sitting right at the\n // gate point.\n const kneeDb = 20 * Math.log10(Math.max(1.0001, p.knee ?? 2.83));\n const kneeLin = dbToLin((p.threshold ?? -35) + kneeDb);\n for (let ch = 0; ch < i.length; ch++) {\n const inp = i[ch], out = o[ch];\n for (let n = 0; n < inp.length; n++) {\n const x = inp[n];\n const env = this.env.push(ch, x);\n let target = 1;\n // Above the threshold the gate is fully open; skip the pow entirely.\n if (env < threshold) {\n const under = env > 1e-9 ? threshold / env : 1e9;\n target = Math.max(floor, 1 / Math.pow(under, ratio - 1));\n if (!isFinite(target)) target = floor;\n }\n // Smooth toward the target so the gate does not click on every sample.\n const held = this.gains[ch] ?? 1;\n const c = target < held ? this.env.r : this.env.a;\n const smoothed = target + c * (held - target);\n this.gains[ch] = smoothed;\n out[n] = x * smoothed;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-gate", HfGate);\n\nclass HfBitcrush extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.holds = [];\n this.held = [];\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const p = this.p;\n const levels = Math.pow(2, (p.bits ?? 8) - 1);\n const step = Math.max(1, Math.round(p.samples ?? 1));\n const mix = p.mix ?? 1;\n for (let ch = 0; ch < i.length; ch++) {\n const inp = i[ch], out = o[ch];\n if (this.held[ch] === undefined) this.held[ch] = 0;\n if (this.holds[ch] === undefined) this.holds[ch] = 0;\n for (let n = 0; n < inp.length; n++) {\n // Per channel: advancing one shared counter only on the last channel\n // left every earlier channel either unheld or frozen for a whole\n // 128-sample quantum, clicking once a block.\n if (this.holds[ch] === 0) this.held[ch] = Math.round(inp[n] * levels) / levels;\n out[n] = this.held[ch] * mix + inp[n] * (1 - mix);\n this.holds[ch] = (this.holds[ch] + 1) % step;\n }\n }\n return true;\n }\n}\nregisterProcessor("hf-bitcrush", HfBitcrush);\n\n/** Linear-interpolated read, \\`delaySamples\\` behind the write head. */\nfunction readTap(ring, write, delaySamples) {\n const len = ring.length;\n const pos = (write - 1 - delaySamples + len) % len;\n const i0 = Math.floor(pos);\n const frac = pos - i0;\n const i1 = (i0 + 1) % len;\n return ring[i0] * (1 - frac) + ring[i1] * frac;\n}\n\n/** Equal-power-ish crossfade, zero at a tap\'s reset point, which hides the splice. */\nfunction xfade(phase) {\n return Math.sin(Math.PI * phase);\n}\n\n/**\n * Dual-tap granular delay line: two read taps 180\\xB0 apart in a 100 ms grain,\n * each sweeping at a speed relative to the write head that shifts pitch\n * without changing duration. One tap is always fading in as the other fades\n * out, which hides the splice each tap makes when it wraps.\n *\n * write/phase are block-level state, advanced once per SAMPLE across all\n * channels together (not once per channel). Advancing them inside the\n * per-channel loop would move the tap 2x/4x too fast on a stereo/quad input.\n */\nclass HfPitchshift extends AudioWorkletProcessor {\n constructor(o) {\n super();\n this.p = o.processorOptions || {};\n this.grain = Math.round(sampleRate * 0.1);\n this.buf = [];\n this.write = 0;\n this.phase = 0;\n // Samples written so far, capped at one grain. The taps read up to a grain\n // behind the write head, so until this fills they would read the ring\'s\n // zeros, so the head of every clip came out attenuated or silent.\n this.filled = 0;\n // How much of the wet (pitch-shifted) path is currently in the output, and\n // where it is heading. Crossing between dry and wet is a ~50 ms jump in the\n // signal, so it is RAMPED rather than switched: a hard swap either way is a\n // click. Ramping in both directions is also what lets a node return to true\n // bypass at semitones 0. A one-way latch left preview stuck with the delay\n // that the render, building a fresh node from the attribute, does not have.\n this.wet = 0;\n this.wetTarget = 0;\n // ~15 ms one-pole, short enough to feel immediate on a slider drag and long\n // enough that the splice is inaudible.\n this.wetCoef = Math.exp(-1 / (sampleRate * 0.015));\n this.port.onmessage = (e) => {\n if (e.data && e.data.__hfDispose) { this.dead = true; return; }\n this.p = { ...this.p, ...e.data };\n };\n }\n process(inputs, outputs) {\n if (this.dead) return false;\n const i = inputs[0], o = outputs[0];\n if (!i || !i.length) return true;\n const p = this.p;\n const semitones = Math.max(-12, Math.min(12, p.semitones ?? 0));\n const mix = Math.max(0, Math.min(1, p.mix ?? 1));\n const grain = this.grain;\n const ringLen = grain * 2;\n const n = i[0] ? i[0].length : 0;\n for (let ch = 0; ch < i.length; ch++) {\n if (!this.buf[ch]) this.buf[ch] = new Float32Array(ringLen);\n }\n\n // Nothing to shift, or mixed fully out. The grain delay is ~grain/2\n // whatever the ratio, so at semitones=0 this degenerated into a pure 50 ms\n // delay of the signal, while the copy for that exact setting reads\n // "Unchanged pitch".\n this.wetTarget = semitones === 0 ? 0 : mix;\n\n // Fully dry AND settled: take the cheap transparent path. The ring keeps\n // filling, so a later shift does not start cold.\n if (this.wetTarget === 0 && this.wet < 1e-4) {\n this.wet = 0;\n let w = this.write;\n for (let s = 0; s < n; s++) {\n for (let ch = 0; ch < i.length; ch++) {\n const x = i[ch][s];\n this.buf[ch][w] = x;\n o[ch][s] = x;\n }\n w = (w + 1) % ringLen;\n }\n this.write = w;\n this.filled = Math.min(grain, this.filled + n);\n return true;\n }\n\n const ratio = Math.pow(2, semitones / 12);\n const inc = (1 - ratio) / grain;\n let write = this.write, phase = this.phase, filled = this.filled, wetNow = this.wet;\n const target = this.wetTarget, coef = this.wetCoef;\n for (let s = 0; s < n; s++) {\n phase += inc;\n phase -= Math.floor(phase);\n const phaseB = (phase + 0.5) % 1;\n const gA = xfade(phase), gB = xfade(phaseB);\n // Ramp the wet path in as the ring fills rather than reading zeros:\n // 100 ms of unshifted audio at the head of a clip beats 50 ms of silence.\n const warm = filled >= grain ? 1 : filled / grain;\n wetNow = target + coef * (wetNow - target);\n const wetMix = wetNow * warm;\n for (let ch = 0; ch < i.length; ch++) {\n const ring = this.buf[ch];\n const inp = i[ch], out = o[ch];\n const x = inp[s];\n ring[write] = x;\n const wet =\n readTap(ring, write, phase * grain) * gA + readTap(ring, write, phaseB * grain) * gB;\n out[s] = x * (1 - wetMix) + wet * wetMix;\n }\n write = (write + 1) % ringLen;\n if (filled < grain) filled++;\n }\n this.write = write;\n this.phase = phase;\n this.filled = filled;\n this.wet = wetNow;\n return true;\n }\n}\nregisterProcessor("hf-pitchshift", HfPitchshift);\n`+Z,S=new WeakMap;var He=new WeakSet;function O(e){let t=S.get(e);return t||(t=(async()=>{if(!e.audioWorklet)throw new Error("AudioWorklet is unavailable: the page needs a secure context (https, localhost or file://)");let n=`data:text/javascript;base64,${btoa(String.fromCharCode(...new TextEncoder().encode(Pe)))}`;await e.audioWorklet.addModule(n),He.add(e)})().catch(n=>{throw S.delete(e),n}),S.set(e,t)),t}function _e(e,t,n){let o=.6+Math.max(0,Math.min(1,t))*2.6,a=Math.max(1,Math.floor(e*o)),r=new Float32Array(a),i=Math.round(t*1e3)*2654435761+Math.round(n*1e3)*40503>>>0,s=()=>(i=i*1664525+1013904223>>>0,i/4294967295*2-1),l=Math.max(.001,1-Math.max(0,Math.min(1,n))),u=0,d=0;for(let c=0;c<a;c++){u+=l*(s()-u);let b=u*Math.pow(1-c/a,2.5);r[c]=b,d+=b*b}let m=Math.sqrt(d);if(m>0)for(let c=0;c<a;c++)r[c]=(r[c]??0)/m;return r}var f=e=>typeof e=="number"?e:Number(e??0),B=e=>e/1e3;function ae(e,t,n,o){let a=Math.max(1,Math.round(e.sampleRate)),r=e.createBuffer(1,a,e.sampleRate),i=r.getChannelData(0);for(let u=0;u<a;u++){let d=u/a;i[u]=t==="sine"?Math.sin(2*Math.PI*d):4*Math.abs((d+.75)%1-.5)-1}let s=e.createBufferSource();s.buffer=r,s.loop=!0,s.playbackRate.value=n;let l=(o*n%1+1)%1*(a/e.sampleRate);return s.start(typeof e.currentTime=="number"?e.currentTime:0,l),s}function oe(e){try{e.stop()}catch{}e.disconnect()}function I(e,t){return[{param:e},{param:t,map:n=>1-n}]}function re(e,t,n){e.gain.value=n,t.gain.value=1-n}function q(e,t,n){return{input:e,output:e,update:t,automation:n,dispose:()=>e.disconnect()}}var Ne=new Set(["peaking","highpass","lowpass"]),ee=e=>Math.pow(10,e/20),Re=(e,t)=>{let n=e.createGain(),o=a=>{n.gain.value=ee(f(a.gain))};return o(t),q(n,o,{gain:[{param:n.gain,map:ee}]})};function H(e,t){return(n,o)=>{let a=n.createBiquadFilter();a.type=e;let r=i=>{a.frequency.value=f(i.frequency),i.q!==void 0&&(a.Q.value=f(i.q)),t&&(a.gain.value=f(i.gain))};return r(o),q(a,r,{frequency:[{param:a.frequency}],...Ne.has(e)?{q:[{param:a.Q}]}:{},...t?{gain:[{param:a.gain}]}:{}})}}function Ee(e){return(t,n)=>{let o=Math.tan(Math.PI*f(n.frequency)/t.sampleRate),a=e==="highpass"?t.createIIRFilter([1/(1+o),-1/(1+o)],[1,(o-1)/(o+1)]):t.createIIRFilter([o/(1+o),o/(1+o)],[1,(o-1)/(o+1)]);return q(a,()=>{})}}function M(e,t=!1){return(n,o)=>{let a=new AudioWorkletNode(n,e,{processorOptions:{...o}});return{input:a,output:a,update:r=>a.port.postMessage({...r}),...t?{reset:()=>a.port.postMessage({__hfReset:!0})}:{},dispose:()=>{a.port.postMessage({__hfDispose:!0}),a.disconnect()}}}}var De={tanh:Math.tanh,atan:e=>2/Math.PI*Math.atan(Math.PI/2*e),cubic:e=>Math.abs(e)>=1?Math.sign(e):e-e**3/3,exp:e=>Math.sign(e)*(1-Math.exp(-Math.abs(e))),alg:e=>e/Math.sqrt(1+e*e),quintic:e=>Math.abs(e)>=1?Math.sign(e):e-e**5/5,sin:e=>Math.abs(e)>=1?Math.sign(e):Math.sin(Math.PI/2*e),erf:e=>Math.tanh(1.20211*e),hard:e=>Math.max(-1,Math.min(1,e))},Ce=(e,t)=>{let n=e.createWaveShaper(),o=e.createGain(),a=e.createGain();o.connect(n).connect(a);let r=i=>{let s=De[String(i.type)]??Math.tanh,l=Math.pow(10,f(i.threshold)/20),u=8192,d=new Float32Array(u);for(let m=0;m<u;m++){let c=m/(u-1)*2-1;d[m]=s(c/Math.max(1e-6,l))*l}n.curve=d,n.oversample=f(i.oversample)>=4?"4x":f(i.oversample)>=2?"2x":"none",a.gain.value=Math.pow(10,f(i.output)/20)};return r(t),{input:o,output:a,update:r,automation:{output:[{param:a.gain,map:i=>Math.pow(10,i/20)}]},dispose:()=>{o.disconnect(),n.disconnect(),a.disconnect()}}},Le=(e,t)=>{let n=e.createGain(),o=e.createGain(),a=e.createDelay(5),r=e.createGain(),i=e.createGain(),s=e.createGain();n.connect(a),a.connect(r),r.connect(a),a.connect(i).connect(o),n.connect(s).connect(o);let l=u=>{a.delayTime.value=Math.min(5,f(u.time)/1e3),r.gain.value=f(u.feedback),re(i,s,f(u.mix))};return l(t),{input:n,output:o,update:l,automation:{time:[{param:a.delayTime,map:u=>Math.min(5,B(u))}],feedback:[{param:r.gain}],mix:I(i.gain,s.gain)},dispose:()=>[n,o,a,r,i,s].forEach(u=>u.disconnect())}},Se=(e,t,n)=>{let o=e.createGain(),a=e.createGain(),r=e.createDelay(.5),i=ae(e,"sine",f(t.speed),n),s=e.createGain(),l=e.createGain(),u=e.createGain();i.connect(s).connect(r.delayTime),o.connect(r).connect(l).connect(a),o.connect(u).connect(a);let d=m=>{r.delayTime.value=f(m.delay)/1e3,s.gain.value=f(m.depth)/1e3,i.playbackRate.value=f(m.speed),re(l,u,f(m.mix))};return d(t),{input:o,output:a,update:d,automation:{delay:[{param:r.delayTime,map:B}],depth:[{param:s.gain,map:B}],speed:[{param:i.playbackRate}],mix:I(l.gain,u.gain)},dispose:()=>{oe(i),[o,a,r,s,l,u].forEach(m=>m.disconnect())}}},Oe=6,Be=(e,t,n)=>{let o=e.createGain(),a=e.createGain(),r=e.createGain(),i=e.createGain(),s=ae(e,String(t.type)==="1"?"sine":"triangle",f(t.speed),n),l=e.createGain(),u=e.createGain(),d=e.createGain(),m=[];o.connect(r);let c=r;for(let p=0;p<Oe;p++){let h=e.createBiquadFilter();h.type="allpass",h.Q.value=.7071,l.connect(h.frequency),c.connect(h),c=h,m.push(h)}s.connect(l),c.connect(u).connect(i),r.connect(d).connect(i),i.connect(a);let b=p=>{let h=1e3/Math.max(.1,f(p.delay));for(let g of m)g.frequency.value=h;l.gain.value=h*f(p.decay),s.playbackRate.value=f(p.speed),r.gain.value=f(p.in_gain),i.gain.value=f(p.out_gain),u.gain.value=1,d.gain.value=1};return b(t),{input:o,output:a,update:b,automation:{speed:[{param:s.playbackRate}],in_gain:[{param:r.gain}],out_gain:[{param:i.gain}]},dispose:()=>{oe(s),[o,a,r,i,l,u,d,...m].forEach(p=>p.disconnect())}}},Ie=(e,t)=>{let n=e.createGain(),o=e.createGain(),a=e.createConvolver(),r=e.createGain(),i=e.createGain();a.normalize=!1,n.connect(a).connect(r).connect(o),n.connect(i).connect(o);let s="",l=u=>{let d=`${f(u.size)}:${f(u.damping)}`;if(d!==s){let m=_e(e.sampleRate,f(u.size),f(u.damping)),c=e.createBuffer(1,m.length,e.sampleRate);c.getChannelData(0).set(m),a.buffer=c,s=d}r.gain.value=f(u.wet),i.gain.value=f(u.dry)};return l(t),{input:n,output:o,update:l,automation:{wet:[{param:r.gain}],dry:[{param:i.gain}]},dispose:()=>[n,o,a,r,i].forEach(u=>u.disconnect())}},qe={"gain-node":Re,"biquad-peaking":H("peaking",!0),"biquad-lowshelf":H("lowshelf",!0),"biquad-highshelf":H("highshelf",!0),"biquad-highpass":H("highpass",!1),"biquad-lowpass":H("lowpass",!1),"worklet-compressor":M("hf-compressor"),"worklet-limiter":M("hf-limiter"),"worklet-truepeak":M("hf-truepeak",!0),"worklet-gate":M("hf-gate"),"worklet-bitcrush":M("hf-bitcrush"),"worklet-pitchshift":M("hf-pitchshift"),waveshaper:Ce,"delay-feedback":Le,"chorus-lfo":Se,"allpass-phaser":Be,convolver:Ie};function ie(e){return e.nodes.some(t=>P(t.type)?.web.startsWith("worklet-")??!1)}function Ge(e,t,n,o=0){let a=P(t);if(!a)throw new Error(`Unknown effect type: ${t}`);let r=y(t,n);if((t==="highpass"||t==="lowpass")&&String(r.poles)==="1")return Ee(t)(e,r,o);let i=qe[a.web];if(!i)throw new Error(`No Web Audio builder for ${a.web}`);return i(e,r,o)}function te(e){let t=[];for(let n of e){let o=n.fromPreset,a=t.at(-1);if(a&&a.preset===o)a.nodes.push(n);else{let r=typeof n.presetAmount=="number"?n.presetAmount:1;t.push({...o?{preset:o}:{},amount:Math.min(1,Math.max(0,r)),nodes:[n]})}}return t}function ne(e){return w(e).map(t=>{let n=y(t.type,t.params),o=n.poles!==void 0?`:${n.poles}`:"",a=String(n.poles)==="1"?`@${n.frequency}`:"",r=t.type==="phaser"?`~${n.type}`:"",i=t.fromPreset?`%${t.fromPreset}`:"",s=t.type==="truepeak"?`^${n.lookahead}`:"";return`${t.type}${o}${a}${r}${s}${i}`}).join("|")}function se(e,t,n=0){var m;let o=e.createGain(),a=e.createGain(),r=[],i=[],s=te(w(t)),l=o;for(let c of s){let b=null;if(c.preset){let p=e.createGain(),h=e.createGain(),g=e.createGain(),F=e.createGain();g.gain.value=c.amount,h.gain.value=1-c.amount,l.connect(p),p.connect(h).connect(F),b={entry:p,wet:g,dry:h,join:F},l=p}for(let p of c.nodes){let h=Ge(e,p.type,p.params??{},n);l.connect(h.input),l=h.output,r.push({...p.id?{id:p.id}:{},type:p.type,handle:h})}b&&c.preset&&(l.connect(b.wet).connect(b.join),i.push({id:c.preset,...b}),l=b.join)}l.connect(a);let u=ne(t),d={};for(let c of i)(d[m=c.id]??(d[m]=[])).push(...I(c.wet.gain,c.dry.gain));return{input:o,output:a,presets:d,nodes:r,update(c){if(ne(c)!==u)return!1;w(c).forEach((p,h)=>{let g=r[h];g&&(g.handle.update(y(p.type,p.params)),p.id===void 0?delete g.id:g.id=p.id)});let b=0;for(let p of te(w(c))){if(!p.preset)continue;let h=i[b++];!h||h.id!==p.preset||(h.wet.gain.value=p.amount,h.dry.gain.value=1-p.amount)}return!0},reset(){for(let{handle:c}of r)c.reset?.()},dispose(){for(let{handle:c}of r)c.dispose();for(let{entry:c,wet:b,dry:p,join:h}of i)c.disconnect(),b.disconnect(),p.disconnect(),h.disconnect();o.disconnect(),a.disconnect()}}}function Ue(e){return 10**(e/20)}var ue=Ue(12);var G=.1,U=10;var Ve="data-automation",Gt=Ve.slice(5),D=1,$e=512,A=class extends Error{constructor(t){super(t),this.name="AudioAutomationError"}},V="volume",le="rate";function N(e){if(e===V)return{kind:"volume"};if(e===le)return{kind:"rate"};let t=e.split(".");if(t.length===3&&t[0]==="fx"&&t[1]===je){let a=t[2];return a?{kind:"preset",presetId:a}:null}if(t.length!==3||t[0]!=="fx")return null;let[,n,o]=t;return!n||!o?null:{kind:"fx",nodeId:n,param:o}}function ce(e,t){return`fx.${e}.${t}`}var je="preset";var Xe={min:0,max:1,step:.01,unit:"",label:"Amount",scale:"linear",default:1},de={min:0,max:ue,step:.01,unit:"",label:"Volume",scale:"linear",default:1},me={min:G,max:U,step:.05,unit:"x",label:"Speed",scale:"log",default:1};function $(e,t){let n=N(e);if(!n)return null;if(n.kind==="volume")return de;if(n.kind==="rate")return me;if(n.kind==="preset")return t?.nodes.some(s=>s.fromPreset===n.presetId)?{...Xe,label:`${n.presetId} \\xB7 Amount`}:null;let o=t?.nodes.find(i=>i.id===n.nodeId);if(!o)return null;let a=P(o.type),r=a?.params.find(i=>i.key===n.param);return!r||r.kind!=="number"?null:{min:r.min,max:r.max,step:r.step,unit:r.unit,label:`${a?.label??o.type} \\xB7 ${r.label}`,scale:r.scale==="log"&&r.min>0?"log":"linear",default:r.default}}function _(e){if(typeof e=="number")return Number.isFinite(e)?e:null;if(typeof e=="string"&&e.trim()!==""){let t=Number(e);return Number.isFinite(t)?t:null}return null}function We(e){let t=_(e);return t===null||t===0?0:Math.min(1,Math.max(-1,t))}function pe(e,t){let n=_(e),o=_(t);if(n===null||o===null)return null;let a=s=>Math.min(.999,Math.max(.001,s)),r=a(n),i=a(o);return Math.abs(r-i)<1e-6?null:{x:r,y:i}}function Ke(e){let t=We(e?.curve),n=pe(e?.viaX,e?.viaY);return{...t?{curve:t}:{},...n?{viaX:n.x,viaY:n.y}:{}}}function Ye(e,t){let n=_(e?.t),o=_(e?.v);if(n===null||o===null)return null;let a=t?Math.min(t.max,Math.max(t.min,o)):o;return{t:Math.max(0,n),v:a,...Ke(e)}}function he(e,t){let n=e.map(a=>Ye(a,t)).filter(a=>a!==null).sort((a,r)=>a.t-r.t),o=[];for(let a of n)o.length>0&&o[o.length-1].t===a.t?o[o.length-1]=a:o.push(a);return o.slice(0,$e)}function ze(e){let t=[];for(let n of e.lanes){if(!N(n.target))continue;let o=n.target===V?de:n.target===le?me:null,a=he(n.points??[],o);a.length>0&&t.push({target:n.target,points:a})}return{version:D,lanes:t}}function fe(e,t){let n=[];for(let o of e.lanes){let a=$(o.target,t);if(!a)continue;let r=he(o.points,a);r.length>0&&n.push({target:o.target,points:r})}return{version:D,lanes:n}}function be(e){let t;try{t=JSON.parse(e)}catch(a){throw new A(`Automation is not valid JSON: ${a.message}`)}if(typeof t!="object"||t===null)throw new A("Automation must be a JSON object.");let n=t;if(n.version!==D)throw new A(`Unsupported automation version: ${String(n.version)}`);if(!Array.isArray(n.lanes))throw new A("Automation is missing a `lanes` array.");let o=n.lanes.map((a,r)=>{if(typeof a!="object"||a===null)throw new A(`Lane ${r} is not an object.`);let i=a;if(typeof i.target!="string"||!N(i.target))throw new A(`Lane ${r} has an unreadable target: ${String(i.target)}`);if(!Array.isArray(i.points))throw new A(`Lane ${r} is missing a \\`points\\` array.`);return{target:i.target,points:i.points}});return ze({version:D,lanes:o})}function Je(e,t){return t?Math.pow(e,Math.pow(2,2*t)):e}function Qe(e,t){let n=e-.5,o=t-.5,a=.999,r=1e6,i=n>0?n/(a-e):n<0?-n/(e-(1-a)):0,s=o>0?o/(a-t):o<0?-o/(t-(1-a)):0,l=Math.min(r,Math.max(1,i,s));return{cx:e+n/l,cy:t+o/l,w:l}}function Ze(e,t,n){let{cx:o,cy:a,w:r}=Qe(t,n),i=2-2*r,s=e*i-1+2*r*o,l=-e*i-2*r*o,u=et(s,l,e),d=1-u,m=d*d+2*r*u*d+u*u;return m>0?(2*r*a*u*d+u*u)/m:e}function et(e,t,n){if(Math.abs(e)<1e-12)return Math.abs(t)<1e-12?n:-n/t;let o=Math.sqrt(Math.max(0,t*t-4*e*n)),a=(-t+o)/(2*e),r=(-t-o)/(2*e),i=s=>s>=-1e-9&&s<=1+1e-9;return i(a)?Math.min(1,Math.max(0,a)):i(r)?Math.min(1,Math.max(0,r)):n}function tt(e,t){let n=pe(t.viaX,t.viaY);return n?Ze(Math.min(1,Math.max(0,e)),n.x,n.y):Je(e,t.curve)}function nt(e,t,n,o){return o==="log"&&e>0&&t>0?Math.exp(Math.log(e)+(Math.log(t)-Math.log(e))*n):e+(t-e)*n}function j(e,t,n="linear"){let o=e.points;if(o.length===0)return 0;let a=o[0];if(t<=a.t)return a.v;let r=o[o.length-1];if(t>=r.t)return r.v;let i=0,s=o.length-1;for(;s-i>1;){let m=i+s>>1;o[m].t<=t?i=m:s=m}let l=o[i],u=o[s],d=u.t-l.t;return d<=0?u.v:nt(l.v,u.v,tt((t-l.t)/d,l),n)}function ge(e){return e.points.length<=1||e.points.every(t=>t.v===e.points[0].v)}function Ae(e,t,n,o,a="linear"){let r=Math.max(2,Math.floor(o)),i=new Float32Array(r),s=n-t;for(let l=0;l<r;l+=1)i[l]=j(e,t+s*l/(r-1),a);return i}var at=100,ot=8,rt=4096,X=e=>e;function it(e,t,n){return!(!!e.curve||e.viaX!==void 0&&e.viaY!==void 0)&&t==="linear"&&!n}function st(e){let t=Math.ceil(e*at);return Math.min(rt,Math.max(ot,t))}function ut(e,t,n,o,a){let{scheduledAt:r,elapsed:i,rate:s}=o,l=a??X,u=v=>r+(v-i)/s,d=e.points[t],m=e.points[t+1],c=u(m.t);if(c<=r)return null;let b=Math.max(r,u(d.t)),p=c-b;if(p<=0)return null;if(it(d,n,a))return{kind:"ramp",time:c,value:l(m.v)};let h=st(p),g=Ae(e,Math.max(d.t,i),m.t,h,n),F=new Float32Array(h);for(let v=0;v<h;v+=1)F[v]=l(g[v]??0);return{kind:"curve",time:b,duration:p,values:F}}function lt(e,t,n,o){let a=[];for(let i=0;i+1<e.points.length;i+=1){let s=ut(e,i,t,n,o);s&&a.push(s)}let r=a[0];return r?.kind==="curve"&&r.time<=n.scheduledAt||a.unshift({kind:"set",time:n.scheduledAt,value:(o??X)(j(e,n.elapsed,t))}),a}function ct(e,t){for(let n of t)if(n.kind==="set")e.setValueAtTime(n.value,n.time);else if(n.kind==="ramp")e.linearRampToValueAtTime(n.value,n.time);else try{e.setValueCurveAtTime(n.values,n.time,n.duration)}catch{let o=n.values[n.values.length-1]??0;e.linearRampToValueAtTime(o,n.time+n.duration)}}function dt(e){for(let t of e)t.param.cancelScheduledValues(0)}function mt(e,t,n,o){if(t.points.length!==0)for(let a of e){if(dt([a]),ge(t)){let r=(a.map??X)(t.points[0].v);a.param.setValueAtTime(r,o.scheduledAt);continue}ct(a.param,lt(t,n,o,a.map))}}function xe(e,t,n,o,a){let r=new Map(n.filter(s=>s.id).map(s=>[s.id,s.handle])),i=[];for(let s of e.lanes){let l=N(s.target);if(!l)continue;let u=l.kind==="preset"?a?.[l.presetId]:l.kind==="fx"?r.get(l.nodeId)?.automation?.[l.param]:void 0;if(!u||u.length===0)continue;let d=$(s.target,t);d&&(mt(u,s,d.scale,o),i.push(...u))}return i}var pt=5,ht=.001;function k(e,t,n){let o=Number(y(e.type,e.params)[t]??0);if(!e.id||!n)return Number.isFinite(o)?o:0;let a=n.lanes.find(r=>r.target===ce(e.id??"",t));return!a||a.points.length===0?Number.isFinite(o)?o:0:a.points.reduce((r,i)=>Math.max(r,i.v),-1/0)}function ft(e,t){let n=Math.min(5,e/1e3);if(n<=0)return 0;let o=Math.max(0,Math.min(.999,t));return o<=0?n:Math.ceil(Math.log(ht)/Math.log(o))*n}function bt(e,t){return k(e,"wet",t)>0?.6+Math.max(0,Math.min(1,k(e,"size",t)))*2.6:0}function gt(e,t){return k(e,"mix",t)>0?ft(k(e,"time",t),k(e,"feedback",t)):0}function At(e,t){return k(e,"mix",t)>0?k(e,"delay",t)/1e3:0}function xt(e,t){return k(e,"mix",t)>0?.2:0}function yt(e,t){if(e.enabled===!1)return 0;switch(e.type){case"reverb":return bt(e,t);case"delay":return gt(e,t);case"chorus":return At(e,t);case"pitchshift":return xt(e,t);default:return 0}}function ye(e,t){let n=e.nodes.reduce((o,a)=>o+yt(a,t),0);return Math.min(pt,n)}function ke(e,t){return w(e).filter(n=>n.type==="truepeak").reduce((n,o)=>{let{lookahead:a}=y(o.type,o.params);return n+Q(Number(a),t)},0)}function kt(e,t,n,o,a){let r=e.createBuffer(n,o,a);for(let i=0;i<n;i++){let s=t[i];s&&r.getChannelData(i).set(s)}return r}function wt(e,t){let n=[];for(let o=0;o<e.numberOfChannels;o++)n.push(new Float32Array(e.getChannelData(o).subarray(t)));return n}async function vt(e,t,n,o){let a=J(n),r=Math.max(1,e.length),i=e[0]?.length??0;if(i===0)return e;let s=o?fe(be(o),a):null,l=Math.ceil(ye(a,s??void 0)*t),u=ke(a,t),d=new OfflineAudioContext(r,i+l+u,t);ie(a)&&await O(d);let m=d.createBufferSource();m.buffer=kt(d,e,r,i,t);let c=se(d,a);return s&&xe(s,a,c.nodes,{scheduledAt:0,elapsed:0,rate:1},c.presets),m.connect(c.input),c.output.connect(d.destination),m.start(),wt(await d.startRendering(),u)}window.__HF_AUDIO_FX={render:vt};})();\n';
13781
13993
  function getAudioFxRuntimeScript() {
13782
13994
  return AUDIO_FX_RUNTIME_IIFE;
13783
13995
  }
@@ -15213,7 +15425,7 @@ function expectedFramesForTask(task) {
15213
15425
  return Math.max(0, Math.ceil((task.endFrame - task.startFrame) / stride));
15214
15426
  }
15215
15427
  function synthesizeSilentWorkerExitError(result, expectedFrames) {
15216
- return `worker ${result.workerId} exited without terminal error string (framesCaptured=${result.framesCaptured}, expected=${expectedFrames}, range=[${result.startFrame}, ${result.endFrame})). Field signal ts=1784042064 \u2014 this class of failure has been reported; consider re-run with --workers=1 to isolate.`;
15428
+ return `worker ${result.workerId} exited without terminal error string (framesCaptured=${result.framesCaptured}, expected=${expectedFrames}, range=[${result.startFrame}, ${result.endFrame})). Field signal ts=1784042064: this class of failure has been reported; consider re-run with --workers=1 to isolate.`;
15217
15429
  }
15218
15430
  function flagSilentWorkerExits(results) {
15219
15431
  for (const r of results) {
@@ -15510,7 +15722,7 @@ async function psnrForDiskSample(framePath, truth, workerId, idx) {
15510
15722
  if (isFfmpegInfrastructureFailure(err)) {
15511
15723
  const detail = err instanceof Error ? err.message : String(err);
15512
15724
  throw new Error(
15513
- `[Parallel] drawElement disk self-verify aborted (worker ${workerId}, frame ${idx}): ffmpeg or the \`psnr\` filter is unavailable \u2014 ${detail}. The preflight in initDrawElementOrTransparentBackground normally catches this at bootstrap; if you hit this after a successful preflight, ffmpeg was replaced mid-render or HYPERFRAMES_FFMPEG_PATH now points at a different binary.`,
15725
+ `[Parallel] drawElement disk self-verify aborted (worker ${workerId}, frame ${idx}): ffmpeg or the \`psnr\` filter is unavailable (${detail}). The preflight in initDrawElementOrTransparentBackground normally catches this at bootstrap; if you hit this after a successful preflight, ffmpeg was replaced mid-render or HYPERFRAMES_FFMPEG_PATH now points at a different binary.`,
15514
15726
  { cause: err }
15515
15727
  );
15516
15728
  }
@@ -15918,7 +16130,7 @@ var DEFAULT_BLACK_ONLY_FRACTION_THRESHOLD = 1e-3;
15918
16130
  function diffGpuParityFrames(a, b, options = {}) {
15919
16131
  if (a.width !== b.width || a.height !== b.height) {
15920
16132
  throw new Error(
15921
- `diffGpuParityFrames: frame size mismatch \u2014 A is ${a.width}x${a.height}, B is ${b.width}x${b.height}`
16133
+ `diffGpuParityFrames: frame size mismatch: A is ${a.width}x${a.height}, B is ${b.width}x${b.height}`
15922
16134
  );
15923
16135
  }
15924
16136
  const expectedLen = a.width * a.height * 4;
@@ -15937,7 +16149,7 @@ function diffGpuParityFrames(a, b, options = {}) {
15937
16149
  const contentSum = options.contentSumThreshold ?? DEFAULT_CONTENT_SUM_THRESHOLD;
15938
16150
  if (contentSum <= blackSum) {
15939
16151
  throw new Error(
15940
- `diffGpuParityFrames: contentSumThreshold (${contentSum}) must be strictly greater than blackSumThreshold (${blackSum}) \u2014 overlapping thresholds would double-classify borderline pixels`
16152
+ `diffGpuParityFrames: contentSumThreshold (${contentSum}) must be strictly greater than blackSumThreshold (${blackSum}): overlapping thresholds would double-classify borderline pixels`
15941
16153
  );
15942
16154
  }
15943
16155
  const total = a.width * a.height;
@@ -16024,7 +16236,7 @@ function verifyGpuParity(a, b, options = {}) {
16024
16236
  const region = bb ? ` (bbox ${bb.x},${bb.y} ${bb.width}x${bb.height})` : "";
16025
16237
  return {
16026
16238
  ok: false,
16027
- reason: `hardware-GPU frame has ${diff.blackOnlyInA.pixels} pixel(s) (${(diff.blackOnlyInA.fraction * 100).toFixed(3)}%) that are solid-black while software-GPU frame has content there${region} \u2014 likely shape-dependent hardware-GPU capture bug`,
16239
+ reason: `hardware-GPU frame has ${diff.blackOnlyInA.pixels} pixel(s) (${(diff.blackOnlyInA.fraction * 100).toFixed(3)}%) that are solid-black while software-GPU frame has content there${region}, likely a shape-dependent hardware-GPU capture bug`,
16028
16240
  diff
16029
16241
  };
16030
16242
  }
@@ -16033,7 +16245,7 @@ function verifyGpuParity(a, b, options = {}) {
16033
16245
  const region = bb ? ` (bbox ${bb.x},${bb.y} ${bb.width}x${bb.height})` : "";
16034
16246
  return {
16035
16247
  ok: false,
16036
- reason: `software-GPU frame has ${diff.blackOnlyInB.pixels} pixel(s) (${(diff.blackOnlyInB.fraction * 100).toFixed(3)}%) that are solid-black while hardware-GPU frame has content there${region} \u2014 unexpected inverse pattern, worth investigating`,
16248
+ reason: `software-GPU frame has ${diff.blackOnlyInB.pixels} pixel(s) (${(diff.blackOnlyInB.fraction * 100).toFixed(3)}%) that are solid-black while hardware-GPU frame has content there${region}, an unexpected inverse pattern, worth investigating`,
16037
16249
  diff
16038
16250
  };
16039
16251
  }
@@ -16409,6 +16621,7 @@ export {
16409
16621
  parseImageElements,
16410
16622
  extractVideoFramesRange,
16411
16623
  resolvePlayableVideoDuration,
16624
+ isVideoPastTimelineEnd,
16412
16625
  resolveTimelineExtractionWindow,
16413
16626
  resolveFinalFrameExtractionWindow,
16414
16627
  resolveVideoExtractionWindow,