@litfamily/litopencode 1.0.30 → 1.0.32

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 (270) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/dist/activation-probe.js +1 -0
  5. package/dist/activation-prompt-utils.d.ts +1 -1
  6. package/dist/activation-reentry.d.ts +20 -0
  7. package/dist/activation-reentry.js +110 -1
  8. package/dist/activation-routing.d.ts +2 -1
  9. package/dist/activation-routing.js +100 -11
  10. package/dist/activation-workflow-prompts.d.ts +4 -0
  11. package/dist/activation-workflow-prompts.js +30 -0
  12. package/dist/activation.d.ts +1 -1
  13. package/dist/activation.js +44 -5
  14. package/dist/adhd-next.d.ts +51 -0
  15. package/dist/adhd-next.js +150 -0
  16. package/dist/cli/managed-skill-assets.d.ts +1 -1
  17. package/dist/cli/managed-skill-assets.js +7 -1
  18. package/dist/cli/native-skill-integrity.js +1 -1
  19. package/dist/cli.js +7 -1
  20. package/dist/commands.d.ts +28 -1
  21. package/dist/commands.js +48 -1
  22. package/dist/features.d.ts +80 -1
  23. package/dist/features.js +105 -1
  24. package/dist/ledger.js +12 -1
  25. package/dist/secret-shapes.js +5 -5
  26. package/dist/skills.d.ts +22 -1
  27. package/dist/skills.js +36 -0
  28. package/dist/start-work-typed.d.ts +34 -0
  29. package/dist/start-work-typed.js +156 -0
  30. package/dist/state.d.ts +2 -0
  31. package/dist/state.js +2 -0
  32. package/dist/tools.js +22 -2
  33. package/dist/tui/mission-band.d.ts +20 -1
  34. package/dist/tui/mission-band.js +73 -16
  35. package/dist/tui/mission-looks.d.ts +38 -0
  36. package/dist/tui/mission-looks.js +73 -0
  37. package/dist/tui/mission-model.d.ts +2 -0
  38. package/dist/tui/mission-model.js +3 -1
  39. package/dist/tui/mission-rainbow.d.ts +33 -0
  40. package/dist/tui/mission-rainbow.js +90 -0
  41. package/dist/tui/mission-source.d.ts +48 -0
  42. package/dist/tui/mission-source.js +184 -2
  43. package/dist/tui/mission-text.d.ts +2 -0
  44. package/dist/tui/mission-text.js +9 -1
  45. package/dist/tui/mission-threads.d.ts +50 -0
  46. package/dist/tui/mission-threads.js +119 -0
  47. package/dist/tui.js +84 -6
  48. package/dist/uiux-visual-catalog.js +8 -1
  49. package/dist/v2/session-hooks.js +38 -5
  50. package/dist/workflow-stack.d.ts +53 -0
  51. package/dist/workflow-stack.js +196 -0
  52. package/docs/assets/readme/badge-version.svg +1 -1
  53. package/docs/assets/skills/lit-schematic.webp +0 -0
  54. package/docs/reference-Ko-KR.md +30 -2
  55. package/docs/reference.md +34 -2
  56. package/package.json +1 -1
  57. package/skills/frontend-ui-ux/SKILL.md +3 -2
  58. package/skills/frontend-ui-ux/kit/components.css +414 -0
  59. package/skills/frontend-ui-ux/kit/fonts.json +22 -0
  60. package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  61. package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  62. package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  63. package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  64. package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  65. package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  66. package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  67. package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  68. package/skills/frontend-ui-ux/kit/ui.js +226 -0
  69. package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
  70. package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
  71. package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
  72. package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
  73. package/skills/frontend-ui-ux/references/house-styles.md +236 -0
  74. package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
  75. package/skills/frontend-ui-ux/references/production.md +3 -1
  76. package/skills/frontend-ui-ux/references/slop-register.md +4 -4
  77. package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  78. package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  79. package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
  80. package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
  81. package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
  82. package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
  83. package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
  84. package/skills/lit-adhd/SKILL.md +208 -0
  85. package/skills/lit-adhd/references/brief-format.md +43 -0
  86. package/skills/lit-adhd/references/card-examples.md +82 -0
  87. package/skills/lit-adhd/references/status-taxonomy.md +29 -0
  88. package/skills/lit-adhd/scripts/rewind.mjs +835 -0
  89. package/skills/lit-diagram-drawer/SKILL.md +4 -3
  90. package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  91. package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  92. package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  93. package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  94. package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  95. package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
  96. package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
  97. package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
  98. package/skills/lit-docx/SKILL.md +2 -1
  99. package/skills/lit-handoff/SKILL.md +2 -0
  100. package/skills/lit-hijack/SKILL.md +422 -0
  101. package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
  102. package/skills/lit-hijack/references/clean-room-build.md +111 -0
  103. package/skills/lit-hijack/references/distil-and-licence.md +100 -0
  104. package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
  105. package/skills/lit-hijack/references/parity-and-report.md +110 -0
  106. package/skills/lit-hijack/scripts/hijack.mjs +993 -0
  107. package/skills/lit-plan/SKILL.md +2 -0
  108. package/skills/lit-pptx/SKILL.md +27 -11
  109. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  110. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  111. package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  112. package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
  113. package/skills/lit-pptx/references/density-and-fill.md +9 -3
  114. package/skills/lit-pptx/references/direction-step.md +21 -6
  115. package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
  116. package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
  117. package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
  118. package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
  119. package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
  120. package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  121. package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  122. package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
  123. package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
  124. package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  125. package/skills/lit-pptx/references/layout-families.md +65 -47
  126. package/skills/lit-pptx/references/title-treatments.md +83 -134
  127. package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
  128. package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
  129. package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
  130. package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
  131. package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
  132. package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
  133. package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
  134. package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
  135. package/skills/lit-pptx/references/tonalities/night.md +1 -1
  136. package/skills/lit-pptx/references/tonalities/paper.md +2 -2
  137. package/skills/lit-pptx/references/tonalities/riso.md +137 -0
  138. package/skills/lit-pptx/references/tonalities/signal.md +3 -3
  139. package/skills/lit-pptx/references/tonalities/studio.md +7 -6
  140. package/skills/lit-pptx/run.mjs +2 -1
  141. package/skills/lit-pptx/scripts/compile-deck.js +1 -1
  142. package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
  143. package/skills/lit-pptx/scripts/craft_extras.py +56 -3
  144. package/skills/lit-pptx/scripts/deck_output.py +75 -90
  145. package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
  146. package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
  147. package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
  148. package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  149. package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
  150. package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  151. package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
  152. package/skills/lit-pptx/scripts/qa_deck.py +10 -11
  153. package/skills/lit-pptx/scripts/render_pages.py +118 -0
  154. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
  155. package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  156. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
  157. package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  158. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
  159. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
  160. package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  161. package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  162. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
  163. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
  164. package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  165. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
  166. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
  167. package/skills/lit-recap/SKILL.md +4 -0
  168. package/skills/lit-schematic/SKILL.md +118 -0
  169. package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  170. package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  171. package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  172. package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  173. package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  174. package/skills/lit-schematic/references/isometric-craft.md +101 -0
  175. package/skills/lit-schematic/references/journal-figures.md +72 -0
  176. package/skills/lit-schematic/references/kit-api.md +134 -0
  177. package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  178. package/skills/lit-schematic/references/pid-drafting.md +103 -0
  179. package/skills/lit-schematic/references/pid-symbols.md +163 -0
  180. package/skills/lit-schematic/references/verification.md +100 -0
  181. package/skills/lit-schematic/scripts/doctor.mjs +57 -0
  182. package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  183. package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
  184. package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  185. package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  186. package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  187. package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  188. package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  189. package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  190. package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  191. package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  192. package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  193. package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  194. package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  195. package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  196. package/skills/lit-schematic/scripts/motion.mjs +129 -0
  197. package/skills/lit-schematic/scripts/qa.mjs +92 -0
  198. package/skills/lit-schematic/scripts/render.mjs +233 -0
  199. package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  200. package/skills/lit-scientific-visualization/SKILL.md +27 -4
  201. package/skills/lit-typographic-motion/SKILL.md +19 -17
  202. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
  203. package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
  204. package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
  205. package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
  206. package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
  207. package/skills/lit-typographic-motion/engine/board.mjs +138 -0
  208. package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
  209. package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
  210. package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
  211. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
  212. package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
  213. package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
  214. package/skills/lit-typographic-motion/engine/house.mjs +42 -0
  215. package/skills/lit-typographic-motion/engine/look.mjs +65 -8
  216. package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
  217. package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
  218. package/skills/lit-typographic-motion/engine/score.mjs +594 -0
  219. package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
  220. package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
  221. package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
  222. package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
  223. package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
  224. package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
  225. package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
  226. package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
  227. package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
  228. package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
  229. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
  230. package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
  231. package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
  232. package/skills/lit-typographic-motion/engine/text.mjs +1 -1
  233. package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
  234. package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  235. package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  236. package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  237. package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  238. package/skills/lit-typographic-motion/gate.mjs +18 -2
  239. package/skills/lit-typographic-motion/probe.mjs +13 -1
  240. package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
  241. package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
  242. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
  243. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
  244. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
  245. package/skills/lit-typographic-motion/references/cover.md +56 -13
  246. package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
  247. package/skills/lit-typographic-motion/references/depth.md +112 -0
  248. package/skills/lit-typographic-motion/references/runtime.md +21 -6
  249. package/skills/lit-typographic-motion/references/sound.md +76 -0
  250. package/skills/lit-typographic-motion/references/stage.md +50 -7
  251. package/skills/lit-typographic-motion/references/studio.md +162 -0
  252. package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
  253. package/skills/lit-typographic-motion/references/treatment.md +108 -9
  254. package/skills/lit-typographic-motion/render.mjs +60 -3
  255. package/skills/lit-typographic-motion/requirements-audio.txt +3 -3
  256. package/skills/lit-typographic-motion/styles/paper.json +185 -0
  257. package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
  258. package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
  259. package/skills/lit-typographic-motion/styles/tidal.json +187 -0
  260. package/skills/litresearch/SKILL.md +2 -0
  261. package/skills/managed-skill-manifest.json +520 -91
  262. package/skills/readme-studio/SKILL.md +1 -1
  263. package/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
  264. package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
  265. package/skills/review-work/SKILL.md +2 -0
  266. package/skills/start-work/SKILL.md +8 -0
  267. package/skills/workflow-loop/SKILL.md +4 -0
  268. package/tools/check-pack-payload.mjs +4 -2
  269. package/tools/payload-substance-parity.json +4 -3
  270. package/tools/qa-cover-recipes.mjs +4 -3
@@ -0,0 +1,594 @@
1
+ // The per-film score (sound mode `score`, the default for rich films): a layered house-family track
2
+ // built in product code from the film itself, with no samples and no borrowed melodies. The film's own
3
+ // words seed its key, mode, tempo, progression, motif and percussion; its shots give the arrangement
4
+ // (intro, builds, the drop on the turn, a break, an outro) and a bar line lands exactly on the drop. Each
5
+ // stem is its own buffer with its own pattern: kick, clap, hats, perc, bass, chords, lead, arrangement fx
6
+ // and the accents the shots ask for. The mix high-passes every non-bass stem, pans, ducks bass, chords
7
+ // and lead under the kick, sends to a reverb and a tempo delay, compresses the bus and ends in the shared
8
+ // master stage (BS.1770-4 at -16 LUFS, a lookahead limiter under -2 dBFS). The stems are kept as files.
9
+ import { createHash } from "node:crypto";
10
+ import { mkdirSync, writeFileSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { accentsFor } from "./accents.mjs";
13
+ import { RATE as SR, masterTo, samplePeakDbfs, wavBytes } from "./loudness.mjs";
14
+ import { readStyle } from "./styles.mjs";
15
+ import { fnv1a32, mulberry32 } from "./text.mjs";
16
+
17
+ // Each genre's kit: tempo band, kick pattern, bass and chord idiom, swing (share of a 16th).
18
+ const KITS = Object.freeze({
19
+ house: { bpm: [120, 126], kick: [0, 4, 8, 12], bass: [2, 6, 10, 14], stab: [3, 6, 11, 14], openHat: true, pad: false, swing: 0 },
20
+ "tech-house": { bpm: [124, 128], kick: [0, 4, 8, 12], bass: [1, 2, 3, 5, 6, 7, 9, 10, 11, 13, 14, 15], stab: [6, 14], openHat: true, pad: false, swing: 0.04 },
21
+ "deep-house": { bpm: [118, 122], kick: [0, 4, 8, 12], bass: [3, 6, 10, 14], stab: [2, 10], openHat: true, pad: true, swing: 0.08 },
22
+ garage: { bpm: [130, 134], kick: [0, 10], bass: [0, 3, 7, 10], stab: [3, 7, 11, 15], openHat: false, pad: true, swing: 0.16 }
23
+ });
24
+ export const SCORE_GENRES = Object.freeze(Object.keys(KITS));
25
+
26
+ const KEY_NAMES = ["C", "Db", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
27
+ const QUALITY = { maj: [0, 4, 7], min: [0, 3, 7], maj7: [0, 4, 7, 11], min7: [0, 3, 7, 10], dom7: [0, 4, 7, 10], maj9: [0, 4, 7, 11, 14], min9: [0, 3, 7, 10, 14] };
28
+ const ROMAN = ["I", "bII", "II", "bIII", "III", "IV", "#IV", "V", "bVI", "VI", "bVII", "VII"];
29
+ const PROGRESSIONS = {
30
+ major: [
31
+ [[0, "maj"], [7, "maj"], [9, "min"], [5, "maj"]],
32
+ [[9, "min7"], [5, "maj7"], [0, "maj"], [7, "maj"]],
33
+ [[0, "maj7"], [4, "min7"], [5, "maj7"], [7, "dom7"]],
34
+ [[5, "maj7"], [7, "maj"], [4, "min7"], [9, "min7"]],
35
+ [[0, "maj7"], [9, "min7"], [2, "min7"], [7, "dom7"]],
36
+ [[2, "min9"], [7, "dom7"], [0, "maj9"], [0, "maj9"]]
37
+ ],
38
+ minor: [
39
+ [[0, "min"], [8, "maj"], [3, "maj"], [10, "maj"]],
40
+ [[0, "min7"], [5, "min7"], [10, "maj"], [3, "maj7"]],
41
+ [[0, "min"], [10, "maj"], [8, "maj"], [10, "maj"]],
42
+ [[0, "min7"], [7, "min7"], [8, "maj7"], [5, "min7"]],
43
+ [[8, "maj7"], [10, "maj"], [0, "min9"], [0, "min9"]],
44
+ [[0, "min9"], [5, "min7"], [0, "min9"], [7, "min7"]]
45
+ ]
46
+ };
47
+ const PENTA = { major: [0, 2, 4, 7, 9], minor: [0, 3, 5, 7, 10] };
48
+ const chordName = ([offset, quality]) => {
49
+ const roman = ROMAN[offset];
50
+ const minorish = quality.startsWith("min");
51
+ return `${minorish ? roman.toLowerCase() : roman}${quality.replace(/^(?:maj|min)$/u, "").replace(/^min/u, "")}`;
52
+ };
53
+
54
+ // The stems in mix order with their gain, pan, high-pass and sidechain depth (the share the kick ducks it) and sends.
55
+ const MIX = Object.freeze({
56
+ kick: { gain: 1.0, pan: 0, hp: 0, duck: 0, reverb: 0, delay: 0 },
57
+ clap: { gain: 0.5, pan: 0, hp: 180, duck: 0, reverb: 0.22, delay: 0 },
58
+ hats: { gain: 0.3, pan: 0.28, hp: 5000, duck: 0, reverb: 0.06, delay: 0 },
59
+ perc: { gain: 0.34, pan: -0.32, hp: 220, duck: 0.15, reverb: 0.16, delay: 0.1 },
60
+ bass: { gain: 0.62, pan: 0, hp: 0, duck: 0.85, reverb: 0, delay: 0 },
61
+ chords: { gain: 0.36, pan: 0, hp: 160, duck: 0.6, reverb: 0.3, delay: 0.12 },
62
+ lead: { gain: 0.3, pan: 0.16, hp: 260, duck: 0.3, reverb: 0.34, delay: 0.3 },
63
+ fx: { gain: 0.42, pan: 0, hp: 90, duck: 0, reverb: 0.28, delay: 0 },
64
+ accents: { gain: 0.62, pan: 0, hp: 60, duck: 0, reverb: 0.12, delay: 0 }
65
+ });
66
+ export const SCORE_STEMS = Object.freeze(Object.keys(MIX));
67
+
68
+ const sectionAt = (sections, t) => sections.find((section) => t >= section.t0 && t < section.t1) ?? null;
69
+
70
+ // The shots as an arrangement: an intro, builds up to the turn, the drop on the turn, a break before a
71
+ // second drop when the film has room, and an outro.
72
+ function arrange(beats, turn) {
73
+ const last = beats.length - 1;
74
+ const after = last - turn;
75
+ return beats.map((beat, i) => {
76
+ let kind;
77
+ if (i === turn) kind = "drop";
78
+ else if (i < turn) kind = i === 0 ? "intro" : "build";
79
+ else if (i === last) kind = "outro";
80
+ else kind = after >= 3 && i === turn + 1 ? "break" : "drop";
81
+ return { kind, t0: Number(beat.t0), t1: Number(beat.t1), beat: i };
82
+ });
83
+ }
84
+
85
+ // The film's score plan: genre, key, tempo, progression, motif, sections, kick times and arrangement fx.
86
+ // Pure and seeded from the treatment, so two films differ and one film always plans the same.
87
+ export function planScore({ treatment, durationSec }) {
88
+ const genre = treatment.sound?.genre ?? readStyle(treatment.style).score.genre;
89
+ const kit = KITS[genre];
90
+ const beats = Array.isArray(treatment.beats) && treatment.beats.length ? treatment.beats : [{ t0: 0, t1: durationSec }];
91
+ const fingerprint = [treatment.request, treatment.idea, treatment.story?.turn, treatment.protagonist?.name, ...beats.map((b) => `${b.purpose}|${b.t0}|${b.t1}`)].join("\n");
92
+ const seed = fnv1a32(fingerprint);
93
+ const rand = mulberry32(seed);
94
+ const tonic = seed % 12;
95
+ const mode = rand() < (genre === "deep-house" ? 0.45 : 0.62) ? "minor" : "major";
96
+ const bpm = kit.bpm[0] + Math.floor(rand() * (kit.bpm[1] - kit.bpm[0] + 1));
97
+ const progression = PROGRESSIONS[mode][Math.floor(rand() * PROGRESSIONS[mode].length)];
98
+ const turn = Number.isInteger(treatment.story?.beat) && treatment.story.beat < beats.length ? treatment.story.beat : Math.min(beats.length - 1, Math.floor(beats.length / 2));
99
+ const sections = arrange(beats, turn).map((section, i, all) => ({ ...section, t1: i === all.length - 1 ? durationSec : section.t1 }));
100
+ const drop = sections.find((section) => section.kind === "drop");
101
+ const stepSec = 60 / bpm / 4;
102
+ const barSec = 16 * stepSec;
103
+ // The grid is placed so that a bar line falls exactly on the drop.
104
+ const origin = drop.t0 - Math.ceil(drop.t0 / barSec - 1e-9) * barSec;
105
+ const steps = [];
106
+ for (let n = Math.ceil(-origin / stepSec - 1e-9); origin + n * stepSec < durationSec - 1e-9; n++) {
107
+ const pos = ((n % 16) + 16) % 16;
108
+ const t = origin + n * stepSec + (pos % 2 ? kit.swing * stepSec : 0);
109
+ if (t < 0 || t >= durationSec) continue;
110
+ steps.push({ n, pos, bar: Math.floor(n / 16), t, section: sectionAt(sections, t) });
111
+ }
112
+ // The last half bar before the drop is the riser's: no kick there, so the drop hits from silence.
113
+ const preDrop = drop.t0 - barSec / 2;
114
+ const kicks = steps
115
+ .filter((step) => step.section && ["build", "drop", "outro"].includes(step.section.kind) && kit.kick.includes(step.pos) && !(step.t >= preDrop && step.t < drop.t0) && !(step.section.kind === "outro" && step.t > step.section.t1 - barSec / 2))
116
+ .map((step) => Number(step.t.toFixed(6)));
117
+ // A motif of one bar on the pentatonic scale, a random walk of six to eight notes.
118
+ const motif = [];
119
+ let degree = Math.floor(rand() * 3);
120
+ for (let pos = 0; pos < 16; pos++) {
121
+ if (rand() < (pos % 4 === 0 ? 0.62 : 0.4)) {
122
+ degree = Math.max(0, Math.min(7, degree + Math.floor(rand() * 5) - 2));
123
+ motif.push({ pos, degree });
124
+ }
125
+ }
126
+ const percPattern = Array.from({ length: 16 }, (_, pos) => (pos % 4 !== 0 && rand() < 0.38 ? 1 + Math.floor(rand() * 3) : 0));
127
+ const fx = [];
128
+ sections.forEach((section, i) => {
129
+ if (i === 0) return;
130
+ const into = section.kind === "drop";
131
+ const span = Math.min(into ? 2 * barSec : barSec / 2, section.t0 - sections[i - 1].t0);
132
+ fx.push({ kind: "riser", start: Number((section.t0 - span).toFixed(6)), t: section.t0, strength: into ? 1 : 0.45 });
133
+ if (into) fx.push({ kind: "impact", t: section.t0 });
134
+ if (sections[i - 1].kind === "drop" && !into) fx.push({ kind: "downlifter", t: section.t0, end: Number(Math.min(section.t1, section.t0 + barSec).toFixed(6)) });
135
+ if (!into) fx.push({ kind: "reverse", start: Number((section.t0 - Math.min(barSec / 2, section.t0 - sections[i - 1].t0)).toFixed(6)), t: section.t0 });
136
+ });
137
+ return { genre, key: KEY_NAMES[tonic], tonic, mode, bpm, seed, progression: progression.map(chordName), chords: progression, motif, percPattern, sections, kicks, fx, stepSec, barSec, origin, steps };
138
+ }
139
+
140
+ // The cue sheet the look's sound question reads: what each shot's `sound` line asks for, on the cut it names.
141
+ function cuesFor(beats, cuts, durationSec) {
142
+ const cues = [];
143
+ const delta = (time, cut) => Number(((time - cut) * 1000).toFixed(3));
144
+ beats.forEach((beat, index) => {
145
+ const accent = accentsFor(beat);
146
+ const cut = cuts[index] ?? beat.t0;
147
+ const next = cuts[index + 1] ?? durationSec;
148
+ if (accent.hit && cut < durationSec) cues.push({ kind: "hit", beat: index, time: cut, cutTime: cut, deltaMs: 0 });
149
+ if (accent.rise && next <= durationSec + 1e-9) cues.push({ kind: "rise", beat: index, time: next, cutTime: next, deltaMs: 0 });
150
+ if (accent.thin) cues.push({ kind: "thin", beat: index, time: cut, cutTime: cut, deltaMs: 0 });
151
+ if (accent.cadence && index === beats.length - 1) cues.push({ kind: "cadence", beat: index, time: Math.max(0, durationSec - 1.6), cutTime: durationSec, deltaMs: delta(Math.max(0, durationSec - 1.6), durationSec - 1.6) });
152
+ });
153
+ cuts.forEach((cut, index) => { if (index > 0 && !cues.some((cue) => cue.beat === index && cue.kind === "hit")) cues.push({ kind: "cut", beat: index, time: cut, cutTime: cut, deltaMs: 0 }); });
154
+ return cues.sort((a, b) => a.time - b.time || a.beat - b.beat);
155
+ }
156
+
157
+ // ---- synthesis helpers ----
158
+ const midiHz = (m) => 440 * 2 ** ((m - 69) / 12);
159
+ // A one-pole coefficient, stable for any cutoff: below 1 Hz it would turn negative and diverge, so the
160
+ // cutoff is clamped to 1 Hz (the pad filter's closed end) and the result always sits in (0, 1).
161
+ export const onePole = (hz) => 1 - Math.exp((-2 * Math.PI * Math.max(1, hz)) / SR);
162
+
163
+ function rbj(type, hz, q = 0.707) {
164
+ const w = (2 * Math.PI * Math.min(Math.max(1, hz), SR * 0.45)) / SR;
165
+ const cos = Math.cos(w);
166
+ const alpha = Math.sin(w) / (2 * q);
167
+ const a0 = 1 + alpha;
168
+ const b = type === "hp" ? [(1 + cos) / 2, -(1 + cos), (1 + cos) / 2] : [(1 - cos) / 2, 1 - cos, (1 - cos) / 2];
169
+ return { b: b.map((v) => v / a0), a: [1, (-2 * cos) / a0, (1 - alpha) / a0] };
170
+ }
171
+
172
+ function filter(x, { b, a }) {
173
+ let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
174
+ for (let i = 0; i < x.length; i++) {
175
+ const v = b[0] * x[i] + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
176
+ x2 = x1; x1 = x[i]; y2 = y1; y1 = v;
177
+ x[i] = v;
178
+ }
179
+ return x;
180
+ }
181
+
182
+ class Stem {
183
+ constructor(n) { this.L = new Float64Array(n); this.R = new Float64Array(n); }
184
+
185
+ // Adds a mono buffer at sample s0, panned -1..1 with constant power, scaled by gain.
186
+ add(s0, mono, pan = 0, gain = 1) {
187
+ const gl = Math.cos(((pan + 1) * Math.PI) / 4) * Math.SQRT2 * gain;
188
+ const gr = Math.sin(((pan + 1) * Math.PI) / 4) * Math.SQRT2 * gain;
189
+ const end = Math.min(this.L.length, s0 + mono.length);
190
+ for (let s = Math.max(0, s0); s < end; s++) { this.L[s] += gl * mono[s - s0]; this.R[s] += gr * mono[s - s0]; }
191
+ }
192
+ }
193
+
194
+ const buffer = (sec) => new Float64Array(Math.max(1, Math.round(sec * SR)));
195
+
196
+ function kickVoice() {
197
+ const b = buffer(0.36);
198
+ let phase = 0;
199
+ for (let i = 0; i < b.length; i++) {
200
+ const t = i / SR;
201
+ phase += (2 * Math.PI * (46 + 110 * Math.exp(-t / 0.035))) / SR;
202
+ b[i] = Math.exp(-t / 0.2) * Math.sin(phase) + (t < 0.004 ? 0.3 * (1 - t / 0.004) : 0);
203
+ }
204
+ return b;
205
+ }
206
+
207
+ function noiseVoice(rand, sec, decay, { hp = 0, lp = 0, bursts = 1, gap = 0.011 } = {}) {
208
+ const b = buffer(sec);
209
+ for (let i = 0; i < b.length; i++) {
210
+ const t = i / SR;
211
+ let env = 0;
212
+ for (let k = 0; k < bursts; k++) if (t >= k * gap) env = Math.max(env, Math.exp(-(t - k * gap) / (k === bursts - 1 ? decay : 0.006)));
213
+ b[i] = (rand() * 2 - 1) * env;
214
+ }
215
+ if (hp) filter(b, rbj("hp", hp));
216
+ if (lp) filter(b, rbj("lp", lp));
217
+ return b;
218
+ }
219
+
220
+ function toneVoice(hz, sec, decay, { partials = [1], attack = 0.002 } = {}) {
221
+ const b = buffer(sec);
222
+ for (let i = 0; i < b.length; i++) {
223
+ const t = i / SR;
224
+ let v = 0;
225
+ partials.forEach((amp, k) => { v += amp * Math.sin(2 * Math.PI * hz * (k + 1) * t); });
226
+ b[i] = v * Math.min(1, t / attack) * Math.exp(-t / decay);
227
+ }
228
+ return b;
229
+ }
230
+
231
+ // A band-limited wavetable: the first harmonics of a saw or a square, one cycle in 2048 samples.
232
+ const TABLE = 2048;
233
+ function wavetable(amps) {
234
+ const t = new Float64Array(TABLE);
235
+ let peak = 0;
236
+ for (let i = 0; i < TABLE; i++) {
237
+ let v = 0;
238
+ amps.forEach((a, k) => { v += a * Math.sin((2 * Math.PI * (k + 1) * i) / TABLE); });
239
+ t[i] = v;
240
+ peak = Math.max(peak, Math.abs(v));
241
+ }
242
+ return t.map((v) => v / peak);
243
+ }
244
+ const SAW = wavetable([1, 0.5, 0.33, 0.25, 0.2, 0.16, 0.14, 0.12, 0.1, 0.08]);
245
+ const SQUARE = wavetable([1, 0, 0.33, 0, 0.2, 0, 0.14, 0, 0.11]);
246
+
247
+ // A filtered table voice: each note is a detuned pair, under an amp envelope and a low-pass that moves
248
+ // from `cutoff` to `cutoffEnd` over the note. The cutoff passes through onePole's clamp.
249
+ function tableVoice(table, notes, sec, { attack = 0.004, decay = 0.25, sustain = 0, release = 0.05, cutoff = 2400, cutoffEnd = 600, detune = 8, phase = 0 } = {}) {
250
+ const b = buffer(sec + release);
251
+ const hold = Math.round(sec * SR);
252
+ for (const note of notes) {
253
+ for (const cents of [-detune, detune]) {
254
+ const inc = (midiHz(note) * 2 ** (cents / 1200) * TABLE) / SR;
255
+ let ph = (phase * TABLE + (cents > 0 ? TABLE / 3 : 0)) % TABLE;
256
+ for (let i = 0; i < b.length; i++) {
257
+ const t = i / SR;
258
+ const env = Math.min(1, t / attack) * (sustain + (1 - sustain) * Math.exp(-t / decay)) * (i < hold ? 1 : Math.max(0, 1 - (i - hold) / (release * SR)));
259
+ ph = (ph + inc) % TABLE;
260
+ b[i] += (env * table[ph | 0]) / notes.length;
261
+ }
262
+ }
263
+ }
264
+ let f = 0;
265
+ for (let i = 0; i < b.length; i++) {
266
+ const u = Math.min(1, i / Math.max(1, b.length));
267
+ f += onePole(cutoff + (cutoffEnd - cutoff) * u) * (b[i] - f);
268
+ b[i] = f;
269
+ }
270
+ return b;
271
+ }
272
+
273
+ function voicing(plan, chord, octave = 48) {
274
+ const [offset, quality] = chord;
275
+ const root = octave + plan.tonic + offset - (offset > 6 ? 12 : 0);
276
+ return QUALITY[quality].map((interval) => root + interval);
277
+ }
278
+
279
+ // The chord under time t: one per bar from the grid's origin; the last bar of the film resolves home.
280
+ function chordAt(plan, t, durationSec) {
281
+ const bar = Math.floor((t - plan.origin) / plan.barSec);
282
+ if (t >= durationSec - plan.barSec && plan.sections.length > 1) return [0, plan.mode === "major" ? "maj7" : "min9"];
283
+ return plan.chords[((bar % plan.chords.length) + plan.chords.length) % plan.chords.length];
284
+ }
285
+
286
+ // A small Schroeder reverb per channel: four damped combs into two all-passes, the right channel offset.
287
+ function reverbReturn([inL, inR]) {
288
+ const pre = rbj("hp", 300);
289
+ const run = (input, spread) => {
290
+ const x = filter(Float64Array.from(input), pre);
291
+ const y = new Float64Array(x.length);
292
+ for (const d of [1687, 1601, 1491, 1422].map((n) => n + spread)) {
293
+ const line = new Float64Array(d);
294
+ let k = 0, damp = 0;
295
+ for (let i = 0; i < x.length; i++) {
296
+ const out = line[k];
297
+ damp = out * 0.7 + damp * 0.3;
298
+ line[k] = x[i] + damp * 0.8;
299
+ y[i] += out * 0.25;
300
+ k = (k + 1) % d;
301
+ }
302
+ }
303
+ for (const d of [556, 441].map((n) => n + spread)) {
304
+ const line = new Float64Array(d);
305
+ let k = 0;
306
+ for (let i = 0; i < y.length; i++) {
307
+ const buf = line[k];
308
+ const v = -0.5 * y[i] + buf;
309
+ line[k] = y[i] + 0.5 * buf;
310
+ y[i] = v;
311
+ k = (k + 1) % d;
312
+ }
313
+ }
314
+ return y;
315
+ };
316
+ return { L: run(inL, 0), R: run(inR, 23) };
317
+ }
318
+
319
+ // A tempo delay: a dotted eighth left and an eighth right, low-passed in the feedback path.
320
+ function delayReturn([inL, inR], leftSec, rightSec) {
321
+ const run = (input, sec) => {
322
+ const d = Math.max(1, Math.round(sec * SR));
323
+ const y = new Float64Array(input.length);
324
+ const c = onePole(3200);
325
+ let lp = 0;
326
+ for (let i = 0; i < input.length; i++) {
327
+ lp += c * ((i >= d ? y[i - d] : 0) - lp);
328
+ y[i] = (i >= d ? input[i - d] : 0) + 0.36 * lp;
329
+ }
330
+ return y;
331
+ };
332
+ return { L: run(inL, leftSec), R: run(inR, rightSec) };
333
+ }
334
+
335
+ // Bus compression in place on the peak-normalized bus: 10 ms attack, 150 ms release, 2.5:1 over -10 dBFS.
336
+ function compressor(L, R, norm) {
337
+ const thresholdDbfs = -10, ratio = 2.5;
338
+ const att = Math.exp(-1 / (0.01 * SR)), rel = Math.exp(-1 / (0.15 * SR));
339
+ let env = 0, sum = 0, maxReductionDb = 0;
340
+ for (let s = 0; s < L.length; s++) {
341
+ const x = Math.max(Math.abs(L[s]), Math.abs(R[s])) * norm;
342
+ env = x > env ? att * env + (1 - att) * x : rel * env + (1 - rel) * x;
343
+ const over = Math.max(0, 20 * Math.log10(Math.max(env, 1e-9)) - thresholdDbfs);
344
+ const reductionDb = over - over / ratio;
345
+ maxReductionDb = Math.max(maxReductionDb, reductionDb);
346
+ const g = 10 ** (-reductionDb / 20);
347
+ L[s] *= g; R[s] *= g;
348
+ sum += g;
349
+ }
350
+ return { thresholdDbfs, ratio, maxReductionDb, meanGain: L.length ? sum / L.length : 1 };
351
+ }
352
+
353
+ // ---- the score ----
354
+
355
+ // Builds the score: the mixed track, every stem, the plan and the cue sheet. `cuts[i]` is the film time
356
+ // where shot i starts on screen.
357
+ export function buildScore({ treatment, frameCount, fps, cuts = [] }) {
358
+ const sampleCount = Math.round((frameCount * SR) / fps);
359
+ const durationSec = sampleCount / SR;
360
+ const plan = planScore({ treatment, durationSec });
361
+ const beats = Array.isArray(treatment.beats) ? treatment.beats : [];
362
+ const cues = cuesFor(beats, cuts, durationSec);
363
+ const rand = mulberry32(plan.seed ^ 0x9e3779b9);
364
+ const stems = Object.fromEntries(SCORE_STEMS.map((name) => [name, new Stem(sampleCount)]));
365
+ const at = (t) => Math.round(t * SR);
366
+ const kit = KITS[plan.genre];
367
+ const kick = kickVoice();
368
+ const drop = plan.sections.find((section) => section.kind === "drop");
369
+
370
+ for (const t of plan.kicks) stems.kick.add(at(t), kick);
371
+ for (const step of plan.steps) {
372
+ const section = step.section;
373
+ if (!section) continue;
374
+ const { kind } = section;
375
+ const full = kind === "build" || kind === "drop";
376
+ const progress = (step.t - section.t0) / Math.max(1e-6, section.t1 - section.t0);
377
+ const roll = step.t >= drop.t0 - plan.barSec && step.t < drop.t0;
378
+ // Clap on two and four; a snare roll that tightens over the last bar into the drop.
379
+ if (full && (step.pos === 4 || step.pos === 12) && !roll) stems.clap.add(at(step.t), noiseVoice(rand, 0.3, 0.13, { hp: 900, lp: 5200, bursts: 3 }), 0, 1);
380
+ if (roll) {
381
+ const u = 1 - (drop.t0 - step.t) / plan.barSec;
382
+ if (u < 0.5 ? step.pos % 2 === 0 : true) stems.clap.add(at(step.t), noiseVoice(rand, 0.12, 0.05, { hp: 700, lp: 6000 }), 0, 0.35 + 0.65 * u);
383
+ if (u >= 0.75) stems.clap.add(at(step.t + plan.stepSec / 2), noiseVoice(rand, 0.08, 0.03, { hp: 700, lp: 6000 }), 0, 0.8 * u);
384
+ }
385
+ // Hats: closed sixteenths with offbeat accents, open hats on the offbeat in the drop.
386
+ const hatsOn = kind === "intro" ? step.pos % 2 === 0 : kind === "break" ? false : kind === "outro" ? step.pos % 2 === 0 : true;
387
+ if (hatsOn) stems.hats.add(at(step.t), noiseVoice(rand, 0.06, 0.018, { hp: 7000 }), 0, (step.pos % 4 === 2 ? 0.9 : 0.5) * (kind === "intro" ? 0.35 + 0.5 * progress : 1));
388
+ if (kind === "drop" && kit.openHat && step.pos % 4 === 2) stems.hats.add(at(step.t), noiseVoice(rand, 0.3, 0.12, { hp: 6000 }), -0.4, 0.55);
389
+ // Perc: the film's own sixteen-step pattern of rims and toms, everywhere but the break's first half.
390
+ const hit = plan.percPattern[step.pos];
391
+ if (hit && !(kind === "break" && progress < 0.5)) {
392
+ const tone = toneVoice([0, 520, 380, 760][hit], 0.16, hit === 3 ? 0.03 : 0.06, { partials: [1, 0.3] });
393
+ const click = noiseVoice(rand, 0.02, 0.004, { hp: 2000 });
394
+ for (let i = 0; i < click.length; i++) tone[i] += 0.5 * click[i];
395
+ stems.perc.add(at(step.t), tone, hit === 2 ? -0.5 : 0.35, kind === "intro" ? 0.5 : 0.85);
396
+ }
397
+ if (kind !== "intro" && kind !== "break" && step.pos % 2 === 1) stems.perc.add(at(step.t), noiseVoice(rand, 0.05, 0.012, { hp: 4500 }), 0.6, 0.25);
398
+ // Bass line: the kit's pattern on the chord root, from the build on, out in the break.
399
+ const chord = chordAt(plan, step.t, durationSec);
400
+ const root = 36 + plan.tonic + chord[0] - (chord[0] > 6 ? 12 : 0);
401
+ if (((kind === "build" && progress > 0.25) || kind === "drop" || (kind === "outro" && progress < 0.5)) && kit.bass.includes(step.pos)) {
402
+ const octave = step.pos % 8 === 6 && plan.genre !== "deep-house" ? 12 : 0;
403
+ stems.bass.add(at(step.t), tableVoice(SAW, [root + octave], plan.stepSec * 0.9, { decay: 0.12, sustain: 0.5, cutoff: 1400, cutoffEnd: 300, detune: 4 }), 0, 0.75);
404
+ }
405
+ // Chord stabs on the kit's steps in builds and drops.
406
+ if (full && kit.stab.includes(step.pos)) stems.chords.add(at(step.t), tableVoice(SAW, voicing(plan, chord, 60), plan.stepSec * 1.4, { decay: 0.09, cutoff: kind === "drop" ? 4200 : 1800 + 2200 * progress, cutoffEnd: 900, detune: 11 }), 0, 0.9);
407
+ // Lead: the motif in the drop and the break, an octave up and drier in the break.
408
+ if (kind === "drop" || kind === "break") {
409
+ const note = plan.motif.find((m) => m.pos === step.pos);
410
+ if (note) {
411
+ const degree = note.degree + (step.bar % 4 === 3 && note.pos >= 8 ? 2 : 0);
412
+ const midi = 72 + plan.tonic + PENTA[plan.mode][degree % 5] + 12 * Math.floor(degree / 5) + (kind === "break" ? 12 : 0);
413
+ stems.lead.add(at(step.t), tableVoice(SQUARE, [midi], plan.stepSec * 1.8, { decay: 0.16, sustain: 0.2, cutoff: 5200, cutoffEnd: 1400, detune: 6 }), 0, kind === "break" ? 0.6 : 1);
414
+ }
415
+ }
416
+ }
417
+
418
+ // Sustained layers, one chord per bar: the sub under the drop and outro, the pad in the intro, build,
419
+ // break and outro (and under the drop for the pad genres), opening its filter across the section.
420
+ for (let bar = Math.floor(-plan.origin / plan.barSec) - 1; plan.origin + bar * plan.barSec < durationSec; bar++) {
421
+ const t0 = Math.max(0, plan.origin + bar * plan.barSec);
422
+ const t1 = Math.min(durationSec, plan.origin + (bar + 1) * plan.barSec);
423
+ if (t1 <= t0) continue;
424
+ const mid = (t0 + t1) / 2;
425
+ const chord = chordAt(plan, mid, durationSec);
426
+ const section = sectionAt(plan.sections, mid);
427
+ if (!section) continue;
428
+ const root = 36 + plan.tonic + chord[0] - (chord[0] > 6 ? 12 : 0);
429
+ if (section.kind === "drop" || section.kind === "outro") {
430
+ const sub = toneVoice(midiHz(root), t1 - t0 + 0.05, 1e6, { attack: 0.01 });
431
+ const tail = Math.round(0.05 * SR);
432
+ for (let i = 0; i < tail; i++) sub[sub.length - 1 - i] *= i / tail;
433
+ stems.bass.add(at(t0), sub, 0, 0.55);
434
+ }
435
+ if (section.kind !== "drop" || kit.pad) {
436
+ // A bar may start before its section (the grid can begin before 0 s), so clamp the progress.
437
+ const u = Math.min(1, Math.max(0, (t0 - section.t0) / Math.max(1e-6, section.t1 - section.t0)));
438
+ const open = section.kind === "intro" || section.kind === "build" ? 700 + 3000 * u : section.kind === "break" ? 2600 : 1800;
439
+ stems.chords.add(at(t0), tableVoice(SAW, voicing(plan, chord, 48), t1 - t0, { attack: 0.25, decay: 1e6, sustain: 1, release: 0.4, cutoff: open, cutoffEnd: open * 1.3, detune: 14, phase: (bar * 0.37) % 1 }), 0, section.kind === "drop" ? 0.35 : 0.6);
440
+ }
441
+ }
442
+
443
+ // Arrangement fx: risers into each change (the strongest into the drop), the impact on the drop, a
444
+ // downlifter after it, and a reverse swell into each change that is not a drop.
445
+ for (const f of plan.fx) {
446
+ if (f.kind === "riser") {
447
+ const len = Math.max(1, at(f.t) - at(f.start));
448
+ const b = new Float64Array(len);
449
+ let lp = 0, ph = 0;
450
+ for (let i = 0; i < len; i++) {
451
+ const u = i / len;
452
+ lp += onePole(300 + 7000 * u * u) * (rand() * 2 - 1 - lp);
453
+ ph += (2 * Math.PI * midiHz(60 + plan.tonic + 24 * u)) / SR;
454
+ b[i] = f.strength * u * u * (0.8 * lp + 0.25 * Math.sin(ph));
455
+ }
456
+ stems.fx.add(at(f.start), b, 0, 1);
457
+ } else if (f.kind === "impact") {
458
+ const boom = toneVoice(42, 1.6, 0.5, { attack: 0.001 });
459
+ const crash = noiseVoice(rand, 1.4, 0.45, { hp: 3000 });
460
+ for (let i = 0; i < boom.length; i++) boom[i] = 0.9 * boom[i] + 0.35 * (crash[i] ?? 0);
461
+ stems.fx.add(at(f.t), boom, 0, 1);
462
+ } else if (f.kind === "reverse") {
463
+ const sec = Math.max(1 / SR, f.t - f.start);
464
+ const tail = tableVoice(SAW, voicing(plan, chordAt(plan, f.t, durationSec), 60), sec, { attack: 0.002, decay: sec * 0.45, sustain: 0, release: 0.01, cutoff: 4200, cutoffEnd: 900, detune: 10 });
465
+ tail.reverse();
466
+ stems.fx.add(at(f.start), tail, 0, 0.5);
467
+ } else {
468
+ const len = Math.max(1, at(f.end) - at(f.t));
469
+ const b = new Float64Array(len);
470
+ let lp = 0;
471
+ for (let i = 0; i < len; i++) {
472
+ const u = 1 - i / len;
473
+ lp += onePole(200 + 5000 * u * u) * (rand() * 2 - 1 - lp);
474
+ b[i] = 0.6 * u * u * lp;
475
+ }
476
+ stems.fx.add(at(f.t), b, 0, 1);
477
+ }
478
+ }
479
+
480
+ // The accents stem: what each shot's own `sound` line asks for, and a soft tick on every other cut.
481
+ const tonicMidi = 72 + plan.tonic;
482
+ for (const cue of cues) {
483
+ if (cue.kind === "hit") stems.accents.add(at(cue.time), toneVoice(55, 0.5, 0.18, { partials: [1, 0.4] }), 0, 0.7);
484
+ else if (cue.kind === "cadence") stems.accents.add(at(cue.time), tableVoice(SAW, voicing(plan, [0, plan.mode === "major" ? "maj9" : "min9"], 60), 1.2, { attack: 0.01, decay: 0.8, cutoff: 3200, cutoffEnd: 800 }), 0, 0.7);
485
+ else if (cue.kind === "cut") stems.accents.add(at(cue.time), toneVoice(midiHz(tonicMidi + 12 + (plan.mode === "major" ? 7 : 3)), 0.3, 0.07), 0.1, 0.5);
486
+ }
487
+
488
+ // The mix: high-pass, gain and pan per stem, the sidechain from the kick times, then the sends.
489
+ const duck = new Float64Array(sampleCount).fill(1);
490
+ for (const t of plan.kicks) {
491
+ const s0 = at(t);
492
+ for (let i = 0; i < at(0.4) && s0 + i < sampleCount; i++) {
493
+ const dt = i / SR;
494
+ duck[s0 + i] = Math.min(duck[s0 + i], 1 - (dt < 0.004 ? dt / 0.004 : Math.exp(-(dt - 0.004) / 0.07)));
495
+ }
496
+ }
497
+ const reverbIn = [new Float64Array(sampleCount), new Float64Array(sampleCount)];
498
+ const delayIn = [new Float64Array(sampleCount), new Float64Array(sampleCount)];
499
+ const processed = {};
500
+ for (const name of SCORE_STEMS) {
501
+ const m = MIX[name];
502
+ const { L, R } = stems[name];
503
+ if (m.hp) { filter(L, rbj("hp", m.hp)); filter(R, rbj("hp", m.hp)); }
504
+ const pl = Math.cos(((m.pan + 1) * Math.PI) / 4) * Math.SQRT2;
505
+ const pr = Math.sin(((m.pan + 1) * Math.PI) / 4) * Math.SQRT2;
506
+ for (let s = 0; s < sampleCount; s++) {
507
+ const g = m.gain * (1 - m.duck * (1 - duck[s]));
508
+ L[s] *= g * pl;
509
+ R[s] *= g * pr;
510
+ reverbIn[0][s] += m.reverb * L[s]; reverbIn[1][s] += m.reverb * R[s];
511
+ delayIn[0][s] += m.delay * L[s]; delayIn[1][s] += m.delay * R[s];
512
+ }
513
+ processed[name] = { L, R };
514
+ }
515
+ const reverb = reverbReturn(reverbIn);
516
+ const delay = delayReturn(delayIn, (60 / plan.bpm) * 0.75, (60 / plan.bpm) * 0.5);
517
+
518
+ // The bus: every stem and both returns, compressed, faded at the end, then mastered.
519
+ const busL = new Float64Array(sampleCount), busR = new Float64Array(sampleCount);
520
+ for (const { L, R } of [...Object.values(processed), reverb, delay]) for (let s = 0; s < sampleCount; s++) { busL[s] += L[s]; busR[s] += R[s]; }
521
+ let peak = 0;
522
+ for (let s = 0; s < sampleCount; s++) peak = Math.max(peak, Math.abs(busL[s]), Math.abs(busR[s]));
523
+ const norm = peak > 0 ? 1 / peak : 1;
524
+ const comp = compressor(busL, busR, norm);
525
+ const fade = Math.min(at(0.4), Math.floor(sampleCount / 10));
526
+ for (let i = 0; i < fade; i++) { busL[sampleCount - 1 - i] *= i / fade; busR[sampleCount - 1 - i] *= i / fade; }
527
+ const mastered = masterTo(busL, busR);
528
+ // The stems are scaled so they sum to the master before its limiter: the same factor for each.
529
+ const scale = norm * mastered.gain * comp.meanGain;
530
+ const out = (st) => ({ left: Float32Array.from(st.L, (v) => v * scale), right: Float32Array.from(st.R, (v) => v * scale) });
531
+ return {
532
+ left: Float32Array.from(mastered.left), right: Float32Array.from(mastered.right), sampleCount, plan, cues,
533
+ stems: Object.fromEntries(SCORE_STEMS.map((name) => [name, out(processed[name])])),
534
+ returns: { reverb: out(reverb), delay: out(delay) },
535
+ lufs: Number(mastered.lufs.toFixed(2)), peakDbfs: Number(samplePeakDbfs([mastered.left, mastered.right]).toFixed(2)), loudnessMethod: "BS.1770-4",
536
+ bus: { ratio: comp.ratio, thresholdDbfs: comp.thresholdDbfs, maxReductionDb: Number(comp.maxReductionDb.toFixed(2)) }
537
+ };
538
+ }
539
+
540
+ const rmsDbfs = (st) => {
541
+ let s = 0;
542
+ for (let i = 0; i < st.left.length; i++) s += st.left[i] * st.left[i] + st.right[i] * st.right[i];
543
+ const v = Math.sqrt(s / Math.max(1, 2 * st.left.length));
544
+ return v > 0 ? Number((20 * Math.log10(v)).toFixed(2)) : -Infinity;
545
+ };
546
+
547
+ // How far the bass dips right after a kick in the drop, against 150-210 ms later (dB).
548
+ export function sidechainDepth(score) {
549
+ const drop = score.plan.sections.find((section) => section.kind === "drop");
550
+ const bass = score.stems.bass.left;
551
+ const rms = (a, b) => { let s = 0; for (let i = a; i < b; i++) s += bass[i] * bass[i]; return Math.sqrt(s / Math.max(1, b - a)); };
552
+ let near = 0, away = 0;
553
+ for (const k of score.plan.kicks.filter((t) => t >= drop.t0 && t + 0.25 < drop.t1)) {
554
+ const i = Math.round(k * SR);
555
+ near += rms(i + Math.round(0.005 * SR), i + Math.round(0.035 * SR));
556
+ away += rms(i + Math.round(0.15 * SR), i + Math.round(0.21 * SR));
557
+ }
558
+ return near > 0 && away > 0 ? Number((20 * Math.log10(away / near)).toFixed(2)) : 0;
559
+ }
560
+
561
+ const pcmOf = ({ left, right }) => {
562
+ const pcm = new Int16Array(left.length * 2);
563
+ for (let i = 0; i < left.length; i++) {
564
+ pcm[2 * i] = Math.round(Math.max(-1, Math.min(1, left[i])) * 32767);
565
+ pcm[2 * i + 1] = Math.round(Math.max(-1, Math.min(1, right[i])) * 32767);
566
+ }
567
+ return wavBytes(pcm);
568
+ };
569
+
570
+ // Writes `sound/score.wav`, every stem and return under `sound/stems/`, and `sound-cues.json`.
571
+ export function writeScore({ treatment, frameCount, fps, cuts = [], dir }) {
572
+ const score = buildScore({ treatment, frameCount, fps, cuts });
573
+ mkdirSync(path.join(dir, "sound", "stems"), { recursive: true });
574
+ const file = path.join(dir, "sound", "score.wav");
575
+ const wav = pcmOf(score);
576
+ writeFileSync(file, wav);
577
+ const stems = [];
578
+ for (const [name, st] of [...Object.entries(score.stems), ...Object.entries(score.returns).map(([key, value]) => [`return-${key}`, value])]) {
579
+ const rel = `sound/stems/${name}.wav`;
580
+ writeFileSync(path.join(dir, rel), pcmOf(st));
581
+ stems.push({ name, file: rel, rmsDbfs: rmsDbfs(st), ...(name.startsWith("return-") ? { kind: "return" } : { gain: MIX[name].gain, pan: MIX[name].pan, highpassHz: MIX[name].hp, sidechain: MIX[name].duck }) });
582
+ }
583
+ const { plan } = score;
584
+ const summary = {
585
+ schema: "litopencode.motion-sound-cues/v1", mode: "score", file, sha256: createHash("sha256").update(wav).digest("hex"),
586
+ sampleRate: SR, sampleCount: score.sampleCount, durationSec: Number((score.sampleCount / SR).toFixed(6)), frameCount, fps,
587
+ genre: plan.genre, key: `${plan.key} ${plan.mode}`, tempo: plan.bpm, progression: plan.progression.join("-"),
588
+ sections: plan.sections.map(({ kind, t0, t1, beat }) => ({ kind, t0, t1, beat })), fx: plan.fx, stems,
589
+ sidechainDb: sidechainDepth(score), bus: score.bus, lufs: score.lufs, peakDbfs: score.peakDbfs, loudnessMethod: score.loudnessMethod,
590
+ cuts: cuts.map((t) => Math.round(t * 10000) / 10000), cues: score.cues
591
+ };
592
+ writeFileSync(path.join(dir, "sound-cues.json"), `${JSON.stringify(summary, null, 2)}\n`);
593
+ return summary;
594
+ }