@olegkoval/agent-skills 1.60.0 → 1.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.github/copilot-instructions.md +2 -0
  2. package/.github/prompts/carousel-maker.prompt.md +102 -0
  3. package/.github/prompts/shorts-animator.prompt.md +103 -0
  4. package/.kiro/steering/carousel-maker.md +101 -0
  5. package/.kiro/steering/shorts-animator.md +102 -0
  6. package/.windsurf/rules/carousel-maker.md +100 -0
  7. package/.windsurf/rules/shorts-animator.md +101 -0
  8. package/README.md +4 -2
  9. package/adapters/claude/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  10. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  11. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  12. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  13. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  14. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  15. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  16. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  17. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  18. package/adapters/claude/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  19. package/adapters/claude/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  20. package/adapters/claude/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  21. package/adapters/claude/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  22. package/adapters/claude/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  23. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  24. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  25. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  26. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  27. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  28. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  29. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  30. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  31. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  32. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  33. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  34. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  35. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  36. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  37. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  38. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  39. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  40. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  41. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  42. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  43. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  44. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  45. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  46. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  47. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  48. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  49. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  50. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  51. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  52. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  53. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  54. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  55. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  56. package/adapters/claude/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  57. package/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +30 -0
  58. package/adapters/claude/olko-github-pr/skills/lekker-review/references/context-gathering.md +3 -0
  59. package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/finding-state.mjs +207 -0
  60. package/adapters/codex/olko-creative/README.md +2 -0
  61. package/adapters/cursor/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  62. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  63. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  64. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  65. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  66. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  67. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  68. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  69. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  70. package/adapters/cursor/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  71. package/adapters/cursor/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  72. package/adapters/cursor/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  73. package/adapters/cursor/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  74. package/adapters/cursor/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  75. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  76. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  77. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  78. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  79. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  80. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  81. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  82. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  83. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  84. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  85. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  86. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  87. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  88. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  89. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  90. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  91. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  92. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  93. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  94. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  95. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  96. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  97. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  98. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  99. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  100. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  101. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  102. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  103. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  104. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  105. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  106. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  107. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  108. package/adapters/cursor/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  109. package/adapters/grok/olko-creative/skills/carousel-maker/SKILL.md +112 -0
  110. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  111. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  112. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  113. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  114. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  115. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  116. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  117. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  118. package/adapters/grok/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  119. package/adapters/grok/olko-creative/skills/shorts-animator/SKILL.md +113 -0
  120. package/adapters/grok/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  121. package/adapters/grok/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  122. package/adapters/grok/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  123. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  124. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  125. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  126. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  127. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  128. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  129. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  130. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  131. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  132. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  133. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  134. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  135. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  136. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  137. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  138. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  139. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  140. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  141. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  142. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  143. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  144. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  145. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  146. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  147. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  148. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  149. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  150. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  151. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  152. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  153. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  154. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  155. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  156. package/adapters/grok/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  157. package/adapters/hermes/olko-creative/README.md +2 -0
  158. package/catalog/skills.json +46 -0
  159. package/package.json +1 -1
  160. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  161. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  162. package/plugins/olko-creative/skills/carousel-maker/SKILL.md +111 -0
  163. package/plugins/olko-creative/skills/carousel-maker/scripts/cm.ts +220 -0
  164. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/check.ts +68 -0
  165. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/layout.ts +75 -0
  166. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/schema.ts +54 -0
  167. package/plugins/olko-creative/skills/carousel-maker/scripts/domain/types.ts +33 -0
  168. package/plugins/olko-creative/skills/carousel-maker/scripts/edge/io.ts +87 -0
  169. package/plugins/olko-creative/skills/carousel-maker/scripts/package.json +4 -0
  170. package/plugins/olko-creative/skills/carousel-maker/scripts/test/domain.test.ts +99 -0
  171. package/plugins/olko-creative/skills/carousel-maker/scripts/tsconfig.json +16 -0
  172. package/plugins/olko-creative/skills/shorts-animator/SKILL.md +112 -0
  173. package/plugins/olko-creative/skills/shorts-animator/references/kit.md +59 -0
  174. package/plugins/olko-creative/skills/shorts-animator/references/looks.md +32 -0
  175. package/plugins/olko-creative/skills/shorts-animator/references/writing.md +9 -0
  176. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/compose.ts +290 -0
  177. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/dsp.ts +193 -0
  178. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/instruments.ts +185 -0
  179. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/master.ts +55 -0
  180. package/plugins/olko-creative/skills/shorts-animator/scripts/audio/soundtrack.ts +119 -0
  181. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/captions.ts +35 -0
  182. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/check.ts +96 -0
  183. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/look.ts +26 -0
  184. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/patch.ts +38 -0
  185. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/schema.ts +81 -0
  186. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/timeline.ts +75 -0
  187. package/plugins/olko-creative/skills/shorts-animator/scripts/domain/types.ts +91 -0
  188. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/chrome.ts +169 -0
  189. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/doctor.ts +52 -0
  190. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/media.ts +105 -0
  191. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/page.ts +53 -0
  192. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/probe.ts +75 -0
  193. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/sound.ts +46 -0
  194. package/plugins/olko-creative/skills/shorts-animator/scripts/edge/voice.ts +183 -0
  195. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/gouache.json +7 -0
  196. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/ink.json +6 -0
  197. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/oil.json +7 -0
  198. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/pastel.json +7 -0
  199. package/plugins/olko-creative/skills/shorts-animator/scripts/looks/watercolor.json +6 -0
  200. package/plugins/olko-creative/skills/shorts-animator/scripts/package.json +4 -0
  201. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/globals.d.ts +199 -0
  202. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/kit.ts +856 -0
  203. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/paint.ts +268 -0
  204. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/stage.ts +181 -0
  205. package/plugins/olko-creative/skills/shorts-animator/scripts/runtime/tsconfig.json +12 -0
  206. package/plugins/olko-creative/skills/shorts-animator/scripts/sa.ts +262 -0
  207. package/plugins/olko-creative/skills/shorts-animator/scripts/test/domain.test.ts +107 -0
  208. package/plugins/olko-creative/skills/shorts-animator/scripts/tsconfig.json +16 -0
  209. package/plugins/olko-creative/skills/shorts-animator/scripts/tts.py +20 -0
  210. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  211. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  212. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  213. package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +30 -0
  214. package/plugins/olko-github-pr/skills/lekker-review/references/context-gathering.md +3 -0
  215. package/plugins/olko-github-pr/skills/lekker-review/scripts/finding-state.mjs +207 -0
  216. package/plugins/olko-github-pr/skills/lekker-review/workflow.js +46 -3
  217. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  218. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  219. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  220. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  221. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  222. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
  223. package/scripts/lib/catalog.mjs +2 -1
@@ -0,0 +1,290 @@
1
+ // The music: presets, chord voicing and the arrangement. Pure given the two rngs; the only effect is
2
+ // writing into the buses it allocates itself.
3
+ import { SR, biquad, busAdd, busMixFrom, delay, human, makeBus, reverb } from "./dsp.ts";
4
+ import type { Bus, Rng, Voice } from "./dsp.ts";
5
+ import {
6
+ bass, brush, ep, epChord, hat, kick, outroImpact, outroRiser, pad, pluck, shaker, snare, softSynth, strum, vinyl,
7
+ } from "./instruments.ts";
8
+
9
+ // key = MIDI root; prog = chords as scale degrees in the key, one per bar; ext = chord size (4 = sevenths,
10
+ // 5 = ninths). Chords are voiced in the middle register with the root left to the bass.
11
+ export type Preset = {
12
+ readonly bpm: number; readonly root: number; readonly scale: readonly number[]; readonly prog: readonly number[];
13
+ readonly ext: number; readonly parts: readonly string[]; readonly swing: number; readonly verb: number;
14
+ readonly duck: number; readonly low?: number; readonly drums?: number;
15
+ };
16
+ const MAJ: readonly number[] = [0, 2, 4, 5, 7, 9, 11], MIN: readonly number[] = [0, 2, 3, 5, 7, 8, 10];
17
+
18
+ export const PRESETS: Readonly<Record<string, Preset>> = {
19
+ kinetic: { bpm: 118, root: 57, scale: MIN, prog: [0, 5, 2, 6], ext: 5, parts: ["kick4", "snare", "hatOff", "bassHouse", "epStab", "pad"], swing: 0, verb: 0.2, duck: 0.6 },
20
+ keynote: { bpm: 100, root: 60, scale: MAJ, prog: [0, 5, 3, 4], ext: 4, parts: ["kickHalf", "shaker16", "epPulse", "epHold", "pad", "bassLong"], swing: 0, verb: 0.3, duck: 0.85 },
21
+ aura: { bpm: 78, root: 63, scale: MAJ, prog: [0, 3, 5, 4], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.45, duck: 1 },
22
+ paper: { bpm: 104, root: 65, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "hat8", "pluck8", "bassBounce"], swing: 0.14, verb: 0.22, duck: 0.85 },
23
+ terminal: { bpm: 112, root: 62, scale: MIN, prog: [0, 5, 6, 0], ext: 4, parts: ["kick4", "hat16", "bass8", "synthArp", "pad"], swing: 0, verb: 0.2, duck: 0.65, low: 0.7, drums: 0.85 },
24
+ data: { bpm: 86, root: 67, scale: MAJ, prog: [1, 4, 0, 5], ext: 5, parts: ["kickBoom", "snareBoom", "hat8", "epHold", "bassBoom", "vinyl"], swing: 0.18, verb: 0.25, duck: 0.85 },
25
+ story: { bpm: 96, root: 62, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "shaker8", "guitar8", "epHold", "bassLong"], swing: 0.08, verb: 0.3, duck: 0.9 },
26
+ // Shorts Animator's moods: beds for a voice to sit on, no drums in the quiet ones
27
+ tender: { bpm: 70, root: 60, scale: MAJ, prog: [0, 5, 3, 4], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.5, duck: 1 },
28
+ hopeful: { bpm: 84, root: 62, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["pad", "guitar8", "bassLong"], swing: 0.06, verb: 0.38, duck: 1 },
29
+ night: { bpm: 64, root: 57, scale: MIN, prog: [0, 5, 3, 6], ext: 5, parts: ["pad", "epHold", "bassSwell"], swing: 0, verb: 0.55, duck: 1 },
30
+ wonder: { bpm: 76, root: 65, scale: MAJ, prog: [3, 4, 0, 5], ext: 5, parts: ["pad", "epArp", "bassSwell"], swing: 0, verb: 0.5, duck: 1 },
31
+ playful: { bpm: 100, root: 65, scale: MAJ, prog: [0, 4, 5, 3], ext: 4, parts: ["kickSoft", "brush", "pluck8", "bassBounce"], swing: 0.14, verb: 0.22, duck: 0.9 },
32
+ };
33
+
34
+ // ---------- harmony ----------
35
+ const chord = (root: number, scale: readonly number[], deg: number, size = 4): number[] =>
36
+ Array.from({ length: size }, (_, k) => { const s = deg + k * 2; return root + scale[s % 7] + 12 * Math.floor(s / 7); });
37
+ const bassNote = (root: number, scale: readonly number[], deg: number): number => {
38
+ let m = root + scale[deg % 7];
39
+ while (m > 45) m -= 12;
40
+ while (m < 33) m += 12;
41
+ return m;
42
+ };
43
+ function nextWithPc(from: number, pc: number): number {
44
+ let m = from + 1;
45
+ while (m % 12 !== pc) m++;
46
+ return m;
47
+ }
48
+ // rootless close voicing that tops out by B4, as near as it can to the last chord's lowest note
49
+ function voicing(root: number, scale: readonly number[], deg: number, size: number, prev: readonly number[] | null): number[] {
50
+ const tones = chord(root, scale, deg, size).map((m) => ((m % 12) + 12) % 12);
51
+ const pcs = size >= 4 ? tones.slice(1) : tones;
52
+ const want = prev ? (prev[0] + 56) / 2 : 56; // led by the last chord, pulled back toward the middle
53
+ const cands = Array.from({ length: 14 }, (_, i) => 51 + i).filter((lo) => lo % 12 === pcs[0]).map((lo) => {
54
+ const v = pcs.slice(1).reduce((acc, pc) => [...acc, nextWithPc(acc[acc.length - 1], pc)], [lo]);
55
+ return { v, score: Math.abs(lo - want) + (v[v.length - 1] > 71 ? 100 : 0) };
56
+ });
57
+ return cands.reduce((best, c) => (c.score < best.score ? c : best)).v;
58
+ }
59
+
60
+ // ---------- the arrangement ----------
61
+ export type Timing = { readonly duration: number; readonly outroAt?: number };
62
+ type Ctx = {
63
+ readonly P: Preset; readonly r: Rng; readonly ham: Rng;
64
+ readonly spb: number; readonly bar: number; readonly s16: number; readonly end: number;
65
+ readonly drums: Bus; readonly inst: Bus; readonly low: Bus; readonly send: Bus;
66
+ readonly voices: readonly (readonly number[])[]; readonly roots: readonly number[];
67
+ };
68
+ type Bar = { readonly b: number; readonly t0: number; readonly ch: readonly number[]; readonly rt: number; readonly len: number };
69
+
70
+ const hasPart = (c: Ctx, p: string): boolean => c.P.parts.includes(p);
71
+ const intro = (c: Ctx, bar: Bar): boolean => bar.b === 0 && c.P.bpm < 90; // slow beds open on the harmony alone
72
+ const before = (c: Ctx, t: number): boolean => t < c.end - 0.02;
73
+ // every call draws one rng value for the humanising jitter, so the call order is part of the sound
74
+ function at(c: Ctx, bar: Bar, q: number): number {
75
+ const swing = q % 2 ? c.P.swing * c.s16 * 2 : 0;
76
+ return bar.t0 + q * c.s16 + swing + (c.r() - 0.5) * 0.01;
77
+ }
78
+ const steps = (step: number, count = 16 / step): number[] => Array.from({ length: count }, (_, i) => i * step);
79
+
80
+ // stops at the first step that falls past the end
81
+ function walk(c: Ctx, bar: Bar, qs: readonly number[], fn: (q: number, t: number) => void): void {
82
+ for (const q of qs) {
83
+ const t = Math.max(0, at(c, bar, q));
84
+ if (!before(c, t)) break;
85
+ fn(q, t);
86
+ }
87
+ }
88
+ // skips the steps that fall past the end, keeps going
89
+ function visit(c: Ctx, bar: Bar, qs: readonly number[], clampT: boolean, fn: (q: number, t: number) => void): void {
90
+ for (const q of qs) {
91
+ const raw = at(c, bar, q), t = clampT ? Math.max(0, raw) : raw;
92
+ if (before(c, t)) fn(q, t);
93
+ }
94
+ }
95
+ const instSend = (c: Ctx, v: Voice, t: number, ig: number, pan: number, sg: number): void => {
96
+ busAdd(c.inst, v, t, ig, pan);
97
+ busAdd(c.send, v, t, sg);
98
+ };
99
+
100
+ // ---------- drums: each step returns the kick times it played (the sidechain listens to them) ----------
101
+ type DrumStep = (c: Ctx, bar: Bar, q: number, t: number) => number[];
102
+ const kickAt = (c: Ctx, t: number, v: Float32Array, gain: number): number[] => { busAdd(c.drums, v, t, gain); return [t]; };
103
+ const snareAt = (c: Ctx, t: number, s: Float32Array, dg: number, sg: number): number[] => { busAdd(c.drums, s, t, dg); busAdd(c.send, s, t, sg); return []; };
104
+ const tick = (c: Ctx, v: Float32Array, t: number, gain: number, pan: number): number[] => { busAdd(c.drums, v, t, gain, pan); return []; };
105
+
106
+ // the order is the order the rng is drawn in
107
+ const DRUM_STEPS: readonly (readonly [string, DrumStep])[] = [
108
+ ["kick4", (c, _b, q, t) => (q % 4 === 0 ? kickAt(c, t, kick(c.r), 0.75 * human(c.r, 0.08)) : [])],
109
+ ["kickHalf", (c, _b, q, t) => (q === 0 || q === 8 ? kickAt(c, t, kick(c.r, { punch: 0.6, dec: 0.32 }), 0.45) : [])],
110
+ ["kickSoft", (c, bar, q, t) => (q === 0 || q === 8 || (q === 6 && bar.b % 2 === 1) ? kickAt(c, t, kick(c.r, { punch: 0.7, dec: 0.3 }), 0.55) : [])],
111
+ ["kickBoom", (c, _b, q, t) => (q === 0 || q === 7 || q === 10 ? kickAt(c, t, kick(c.r, { pitch: 48, dec: 0.4 }), q === 7 ? 0.4 : 0.65) : [])],
112
+ ["snare", (c, _b, q, t) => (q === 4 || q === 12 ? snareAt(c, t, snare(c.r, { tone: 200, top: 8000 }), 0.42, 0.18) : [])],
113
+ ["snareBoom", (c, _b, q, t) => (q === 4 || q === 12 ? snareAt(c, t, snare(c.r, { dec: 0.16, top: 6000 }), 0.5, 0.12) : [])],
114
+ ["brush", (c, _b, q, t) => {
115
+ if (q !== 4 && q !== 12) return [];
116
+ const s = brush(c.r);
117
+ busAdd(c.drums, s, t, 0.5, 0.1); busAdd(c.send, s, t, 0.2);
118
+ return [];
119
+ }],
120
+ ["hatOff", (c, _b, q, t) => (q % 4 === 2 ? tick(c, hat(c.r), t, 0.3 * human(c.r, 0.3), 0.25) : [])],
121
+ ["hat8", (c, _b, q, t) => (q % 2 === 0 ? tick(c, hat(c.r), t, (q % 4 === 2 ? 0.2 : 0.11) * human(c.r, 0.4), 0.3) : [])],
122
+ ["shaker16", (c, _b, q, t) => tick(c, shaker(c.r), t, (q % 4 === 2 ? 0.2 : q % 2 ? 0.07 : 0.12) * human(c.r, 0.4), 0.35)],
123
+ ["shaker8", (c, _b, q, t) => (q % 2 === 0 ? tick(c, shaker(c.r), t, (q % 4 === 2 ? 0.2 : 0.1) * human(c.r, 0.4), 0.35) : [])],
124
+ ["hat16", (c, _b, q, t) => tick(c, hat(c.r), t, (q % 4 === 2 ? 0.16 : 0.08) * human(c.r, 0.4), -0.2)],
125
+ ];
126
+
127
+ // recursion (not a loop) so the kick times come back as a value; stops at the first step past the end
128
+ function drumWalk(c: Ctx, bar: Bar, q: number): number[] {
129
+ if (q >= 16) return [];
130
+ const t = Math.max(0, at(c, bar, q));
131
+ if (!before(c, t)) return [];
132
+ const here = DRUM_STEPS.flatMap(([name, fn]) => (hasPart(c, name) ? fn(c, bar, q, t) : []));
133
+ return [...here, ...drumWalk(c, bar, q + 1)];
134
+ }
135
+
136
+ // ---------- harmony and bass parts ----------
137
+ type Part = (c: Ctx, bar: Bar) => void;
138
+
139
+ const padPart: Part = (c, bar) => {
140
+ const p = pad(bar.ch, bar.len + 1.2, c.r, { cutoff: c.P.bpm < 90 ? 2800 : 2400, attack: intro(c, bar) ? 1.4 : 0.5 });
141
+ instSend(c, p, bar.t0, hasPart(c, "epArp") ? 0.5 : 0.28, 0, 0.4);
142
+ };
143
+ const epHoldPart: Part = (c, bar) => visit(c, bar, bar.b % 2 ? [0, 10] : [0], false, (q, t) => {
144
+ const v = epChord(bar.ch, q ? c.spb * 1.5 : bar.len + 0.3, 0.42, c.r, c.ham);
145
+ instSend(c, v, Math.max(0, t), q ? 0.3 : 0.45, 0, 0.3);
146
+ });
147
+ const epStabPart: Part = (c, bar) => {
148
+ if (intro(c, bar)) return;
149
+ visit(c, bar, [3, 6, 10, 14], false, (q, t) => instSend(c, epChord(bar.ch, c.spb * 0.55, 0.5, c.r, c.ham, { roll: 0.004 }), t, 0.5, q % 2 ? 0.15 : -0.15, 0.25));
150
+ };
151
+ const epPulsePart: Part = (c, bar) => walk(c, bar, steps(2), (q, t) => {
152
+ const m = q % 8 === 0 ? bar.ch[0] : q % 4 === 0 ? bar.ch[2] ?? bar.ch[1] : bar.ch[1];
153
+ const v = ep(m - 12 >= 48 ? m - 12 : m, c.spb * 0.9, 0.38 * human(c.r, 0.15), c.ham);
154
+ instSend(c, v, t, 0.38, q % 4 ? 0.2 : -0.2, 0.25);
155
+ });
156
+ const epArpPart: Part = (c, bar) => walk(c, bar, steps(4), (q, t) => {
157
+ const v = ep(bar.ch[(q / 4) % bar.ch.length], c.spb * 3, 0.3 * human(c.r, 0.2), c.ham);
158
+ instSend(c, v, t, 0.42, (q / 4 - 1.5) * 0.3, 0.5);
159
+ });
160
+ const PLUCK8_PAT = [0, 2, 1, 3, 2, 1, 0, 2];
161
+ const pluck8Part: Part = (c, bar) => {
162
+ if (intro(c, bar)) return;
163
+ // steps 3 and 7 are rests: they draw no jitter and cannot end the walk
164
+ walk(c, bar, [0, 1, 2, 4, 5, 6].map((k) => k * 2), (q, t) => {
165
+ const k = q / 2, s = pluck(bar.ch[PLUCK8_PAT[k] % bar.ch.length], 0.8, c.r, { bright: 0.28, decay: 0.993 });
166
+ instSend(c, s, t, human(c.r, 0.2), (k % 3 - 1) * 0.3, 0.3);
167
+ });
168
+ };
169
+ const GUITAR8_PAT = [0, 1, 2, 3, 2, 1, 2, 3];
170
+ const guitar8Part: Part = (c, bar) => walk(c, bar, steps(2), (q, t) => {
171
+ const k = q / 2, ch = bar.ch;
172
+ const m = k === 0 ? (ch[0] - 12 >= 45 ? ch[0] - 12 : ch[0]) : ch[GUITAR8_PAT[k] % ch.length];
173
+ const s = pluck(m, 1.2, c.r, { bright: 0.22, decay: 0.996 });
174
+ instSend(c, s, t, 0.9 * human(c.r, 0.2), k % 2 ? 0.25 : -0.25, 0.3);
175
+ });
176
+ const SYNTH_ARP_PAT = [0, 1, 2, 1, 3, 2];
177
+ const synthArpPart: Part = (c, bar) => {
178
+ if (intro(c, bar)) return;
179
+ walk(c, bar, steps(1), (q, t) => {
180
+ const m = bar.ch[SYNTH_ARP_PAT[q % 6] % bar.ch.length] - 12;
181
+ const s = softSynth(m < 45 ? m + 12 : m, c.s16 * 1.6, 1900 + 700 * Math.sin((bar.b * 16 + q) / 10));
182
+ instSend(c, s, t, 0.22, q % 2 ? 0.25 : -0.25, 0.22);
183
+ });
184
+ };
185
+
186
+ const lowAt = (c: Ctx, v: Float32Array, t: number, gain: number): void => busAdd(c.low, v, t, gain);
187
+ const bassHousePart: Part = (c, bar) => {
188
+ if (intro(c, bar)) return;
189
+ visit(c, bar, [2, 6, 10, 14], false, (q, t) => lowAt(c, bass(q === 14 ? bar.rt + 7 : bar.rt, c.spb * 0.45), t, 0.55));
190
+ };
191
+ const bassLongPart: Part = (c, bar) => visit(c, bar, [0, 8], true, (_q, t) => lowAt(c, bass(bar.rt, c.spb * 1.9, { a: 0.02, r: 0.2 }), t, 0.45));
192
+ const bassSwellPart: Part = (c, bar) => lowAt(c, bass(bar.rt, bar.len + 0.4, { a: 0.6, r: 0.6, drive: 1 }), bar.t0, 0.4);
193
+ const BOUNCE: readonly (readonly [number, number])[] = [[0, 0], [6, 7], [8, 0], [14, 7]];
194
+ const bassBouncePart: Part = (c, bar) => {
195
+ if (intro(c, bar)) return;
196
+ BOUNCE.forEach(([q, d]) => visit(c, bar, [q], true, (_q, t) => lowAt(c, bass(bar.rt + d, c.spb * 0.45, { tone: 0.3 }), t, 0.5)));
197
+ };
198
+ const bass8Part: Part = (c, bar) => {
199
+ if (intro(c, bar)) return;
200
+ visit(c, bar, steps(2), true, (_q, t) => lowAt(c, bass(bar.rt, c.spb * 0.4), t, 0.34));
201
+ };
202
+ const bassBoomPart: Part = (c, bar) => visit(c, bar, [0, 10], true, (q, t) => lowAt(c, bass(bar.rt, c.spb * (q ? 1.2 : 1.5), { a: 0.01, r: 0.15 }), t, 0.55));
203
+
204
+ // the order is the order the rng is drawn in: harmony first, then bass
205
+ const PARTS: readonly (readonly [string, Part])[] = [
206
+ ["pad", padPart], ["epHold", epHoldPart], ["epStab", epStabPart], ["epPulse", epPulsePart], ["epArp", epArpPart],
207
+ ["pluck8", pluck8Part], ["guitar8", guitar8Part], ["synthArp", synthArpPart],
208
+ ["bassHouse", bassHousePart], ["bassLong", bassLongPart], ["bassSwell", bassSwellPart],
209
+ ["bassBounce", bassBouncePart], ["bass8", bass8Part], ["bassBoom", bassBoomPart],
210
+ ];
211
+
212
+ function renderBar(c: Ctx, bar: Bar): number[] {
213
+ const kicks = intro(c, bar) ? [] : drumWalk(c, bar, 0);
214
+ PARTS.forEach(([name, part]) => { if (hasPart(c, name)) part(c, bar); });
215
+ return kicks;
216
+ }
217
+
218
+ // the end: a low swell, one deep hit, the home chord left ringing
219
+ function ending(c: Ctx, duration: number): void {
220
+ const { P, r, ham, spb, end } = c;
221
+ const home = voicing(P.root, P.scale, 0, P.ext || 4, c.voices[c.voices.length - 1]);
222
+ const riserSec = Math.min(1.6, spb * 3);
223
+ busAdd(c.inst, outroRiser(r, riserSec), end - riserSec, 0.14);
224
+ const hit = outroImpact(r, P.root);
225
+ busAdd(c.inst, hit, end, 0.3); busAdd(c.send, hit, end, 0.2);
226
+ const tail = Math.max(2.4, duration - end + 1.8);
227
+ if (hasPart(c, "guitar8") || hasPart(c, "pluck8")) {
228
+ const s = strum([bassNote(P.root, P.scale, 0) + 12, ...home], tail, r, { bright: 0.24, decay: 0.998 });
229
+ busAdd(c.inst, s, end, 1.1); busAdd(c.send, s, end, 0.4);
230
+ } else {
231
+ const ch = epChord(home, tail, 0.45, r, ham, { roll: 0.03 });
232
+ busAdd(c.inst, ch, end, 0.55); busAdd(c.send, ch, end, 0.5);
233
+ }
234
+ const p = pad(home, tail, r, { cutoff: 2200, attack: 0.05, release: 1.8 });
235
+ busAdd(c.inst, p, end, 0.22); busAdd(c.send, p, end, 0.4);
236
+ busAdd(c.low, bass(bassNote(P.root, P.scale, 0), Math.max(1.5, duration - end + 0.6), { a: 0.005, r: 1.2 }), end, 0.45);
237
+ if (P.parts.some((x) => x.startsWith("kick"))) busAdd(c.drums, kick(r, { dec: 0.5 }), end, 0.7);
238
+ }
239
+
240
+ // ---------- mix ----------
241
+ function duckAround(duck: Float32Array, t: number, depth: number): void {
242
+ const i0 = Math.round(t * SR);
243
+ for (let i = 0; i < 0.22 * SR && i0 + i < duck.length; i++) duck[i0 + i] = Math.min(duck[i0 + i], 1 - depth + depth * Math.pow(i / (0.22 * SR), 0.6));
244
+ }
245
+ // sidechain: the harmony breathes under each kick (deep for the dance beds, barely for the gentle ones)
246
+ function sidechain(c: Ctx, kicks: readonly number[]): void {
247
+ const depth = 1 - c.P.duck;
248
+ if (depth <= 0.01) return;
249
+ const { inst, low } = c, duck = new Float32Array(inst.n).fill(1);
250
+ kicks.forEach((t) => duckAround(duck, t, depth));
251
+ for (let i = 0; i < inst.n; i++) { inst.L[i] *= duck[i]; inst.R[i] *= duck[i]; low.L[i] *= duck[i] * 0.7 + 0.3; low.R[i] *= duck[i] * 0.7 + 0.3; }
252
+ }
253
+ // a dark room: the reverb only hears the middle of the spectrum
254
+ function darken(send: Bus): void {
255
+ const sl = [biquad("lp", 6500), biquad("lp", 6500)], sh = [biquad("hp", 220), biquad("hp", 220)];
256
+ for (let i = 0; i < send.n; i++) { send.L[i] = sl[0](sh[0](send.L[i])); send.R[i] = sl[1](sh[1](send.R[i])); }
257
+ }
258
+ function mixDown(c: Ctx, total: number, duration: number): Bus {
259
+ const { P, send, spb } = c;
260
+ darken(send);
261
+ const wet = reverb(send, { room: P.verb > 0.4 ? 0.88 : 0.82, damp: 0.45 });
262
+ const echo = P.bpm >= 100 ? delay(send, spb * 0.75, 0.28, 2400) : null;
263
+ const music = makeBus(total);
264
+ busMixFrom(music, c.drums, P.drums ?? 1); busMixFrom(music, c.inst, 1); busMixFrom(music, c.low, P.low ?? 1); busMixFrom(music, wet, P.verb * 2.2);
265
+ if (echo) busMixFrom(music, echo, 0.1);
266
+ // fade in over half a beat, out after the ring
267
+ const fin = Math.round(spb * SR * 0.5), fout0 = Math.round((duration + 0.2) * SR), fout1 = Math.round((duration + 2.6) * SR);
268
+ for (let i = 0; i < music.n; i++) {
269
+ const g = (i < fin ? i / fin : 1) * (i > fout0 ? Math.max(0, 1 - (i - fout0) / (fout1 - fout0)) : 1);
270
+ music.L[i] *= g; music.R[i] *= g;
271
+ }
272
+ return music;
273
+ }
274
+
275
+ export function composeMusic(P: Preset, timing: Timing, r: Rng, ham: Rng): Bus {
276
+ const { duration } = timing;
277
+ const spb = 60 / P.bpm, bar = spb * 4, total = duration + 3; // ring-out
278
+ const end = timing.outroAt ?? Math.max(0, duration - 3);
279
+ const nBars = Math.ceil(duration / bar) + 1;
280
+ const deg = (b: number): number => P.prog[b % P.prog.length];
281
+ const voices = Array.from({ length: nBars }).reduce<number[][]>((acc, _, b) => [...acc, voicing(P.root, P.scale, deg(b), P.ext || 4, acc[b - 1] ?? null)], []);
282
+ const roots = Array.from({ length: nBars }, (_, b) => bassNote(P.root, P.scale, deg(b)));
283
+ const c: Ctx = { P, r, ham, spb, bar, s16: spb / 4, end, drums: makeBus(total), inst: makeBus(total), low: makeBus(total), send: makeBus(total), voices, roots };
284
+ const bars = Array.from({ length: nBars }, (_, b) => b).filter((b) => b * bar < end - 0.01);
285
+ const kicks = bars.flatMap((b) => renderBar(c, { b, t0: b * bar, ch: voices[b], rt: roots[b], len: Math.min(bar, end - b * bar) }));
286
+ if (hasPart(c, "vinyl")) busAdd(c.inst, vinyl(duration + 1, r), 0, 0.5);
287
+ ending(c, duration);
288
+ sidechain(c, kicks);
289
+ return mixDown(c, total, duration);
290
+ }
@@ -0,0 +1,193 @@
1
+ // DSP kit: rng, filters, oscillators, envelopes, noise, reverb, delay, mixing.
2
+ // The only state is a typed array a function allocates (or is handed as its output buffer), plus a filter's
3
+ // own memory and a seeded rng closure.
4
+ export const SR = 48000;
5
+ export const TAU = Math.PI * 2;
6
+
7
+ export type Rng = () => number;
8
+ export type Stereo = { readonly L: Float32Array; readonly R: Float32Array };
9
+ export type Bus = Stereo & { readonly n: number };
10
+ export type Voice = Float32Array | Stereo;
11
+
12
+ export const rng = (seed: number): Rng => {
13
+ let s = seed >>> 0 || 1;
14
+ return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296);
15
+ };
16
+ export const human = (r: Rng, amt = 0.1): number => 1 - amt / 2 + r() * amt;
17
+ export const mtof = (m: number): number => 440 * Math.pow(2, (m - 69) / 12);
18
+ export const dbToGain = (db: number): number => Math.pow(10, db / 20);
19
+ export const clamp = (x: number, a: number, b: number): number => Math.max(a, Math.min(b, x));
20
+
21
+ // ---------- buses and mixing ----------
22
+ export function makeBus(sec: number): Bus {
23
+ const n = Math.ceil(sec * SR);
24
+ return { L: new Float32Array(n), R: new Float32Array(n), n };
25
+ }
26
+ const isStereo = (v: Voice): v is Stereo => !(v instanceof Float32Array);
27
+
28
+ // mix a mono or stereo voice in at time `at` (seconds), equal-power pan -1..1
29
+ export function busAdd(bus: Bus, v: Voice, at: number, gain = 1, pan = 0): void {
30
+ const i0 = Math.round(at * SR);
31
+ const pl = Math.cos((pan + 1) * Math.PI / 4) * Math.SQRT2, pr = Math.sin((pan + 1) * Math.PI / 4) * Math.SQRT2;
32
+ const vl = isStereo(v) ? v.L : v, vr = isStereo(v) ? v.R : v;
33
+ for (let i = 0; i < vl.length; i++) {
34
+ const j = i0 + i;
35
+ if (j < 0) continue;
36
+ if (j >= bus.n) break;
37
+ bus.L[j] += vl[i] * gain * pl;
38
+ bus.R[j] += vr[i] * gain * pr;
39
+ }
40
+ }
41
+ export function busMixFrom(dst: Bus, b: Bus, gain = 1): void {
42
+ for (let i = 0; i < dst.n && i < b.n; i++) { dst.L[i] += b.L[i] * gain; dst.R[i] += b.R[i] * gain; }
43
+ }
44
+ export function busGain(bus: Bus, k: number): void {
45
+ for (let i = 0; i < bus.n; i++) { bus.L[i] *= k; bus.R[i] *= k; }
46
+ }
47
+
48
+ // ---------- typed-array helpers ----------
49
+ export const tabulate = (n: number, f: (i: number) => number): Float32Array => Float32Array.from({ length: n }, (_, i) => f(i));
50
+ export function addInto(dst: Float32Array, src: Float32Array, off: number, f: (x: number) => number): void {
51
+ for (let i = 0; i < src.length && i + off < dst.length; i++) dst[i + off] += f(src[i]);
52
+ }
53
+ export function scaleBy(a: Float32Array, g: (i: number) => number): Float32Array {
54
+ for (let i = 0; i < a.length; i++) a[i] *= g(i);
55
+ return a;
56
+ }
57
+ export function mulInto(a: Float32Array, b: Float32Array): Float32Array {
58
+ for (let i = 0; i < a.length; i++) a[i] *= b[i] ?? 0;
59
+ return a;
60
+ }
61
+
62
+ // ---------- RBJ biquad ----------
63
+ export type FilterType = "lp" | "hp" | "bp" | "lowshelf" | "highshelf";
64
+ type Coefs = readonly [number, number, number, number, number, number];
65
+ type Terms = { readonly cs: number; readonly a: number; readonly A: number };
66
+
67
+ const shelf = (sg: number) => ({ cs, a, A }: Terms): Coefs => {
68
+ const s = Math.sqrt(A) * 2 * a;
69
+ return [
70
+ A * ((A + 1) - sg * (A - 1) * cs + s), 2 * sg * A * ((A - 1) - sg * (A + 1) * cs), A * ((A + 1) - sg * (A - 1) * cs - s),
71
+ (A + 1) + sg * (A - 1) * cs + s, -2 * sg * ((A - 1) + sg * (A + 1) * cs), (A + 1) + sg * (A - 1) * cs - s,
72
+ ];
73
+ };
74
+ const COEF: Record<FilterType, (t: Terms) => Coefs> = {
75
+ lp: ({ cs, a }) => { const b0 = (1 - cs) / 2; return [b0, 1 - cs, b0, 1 + a, -2 * cs, 1 - a]; },
76
+ hp: ({ cs, a }) => { const b0 = (1 + cs) / 2; return [b0, -(1 + cs), b0, 1 + a, -2 * cs, 1 - a]; },
77
+ bp: ({ cs, a }) => [a, 0, -a, 1 + a, -2 * cs, 1 - a],
78
+ lowshelf: shelf(1),
79
+ highshelf: shelf(-1),
80
+ };
81
+ function coef(type: FilterType, f: number, q: number, db: number): readonly [number, number, number, number, number] {
82
+ const w = TAU * clamp(f, 10, SR / 2.2) / SR, cs = Math.cos(w), sn = Math.sin(w);
83
+ const [b0, b1, b2, a0, a1, a2] = COEF[type]({ cs, a: sn / (2 * q), A: Math.pow(10, db / 40) });
84
+ return [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0];
85
+ }
86
+ export type Filter = (x: number) => number;
87
+ export type Biquad = Filter & { set(f: number, q?: number): void };
88
+
89
+ // the filter's own memory is a closure; `set` retunes it mid-stream (the riser sweeps its band)
90
+ export function biquad(type: FilterType, f: number, q = 0.707, db = 0): Biquad {
91
+ let c = coef(type, f, q, db);
92
+ let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
93
+ const run: Filter = (x) => { const y = c[0] * x + c[1] * x1 + c[2] * x2 - c[3] * y1 - c[4] * y2; x2 = x1; x1 = x; y2 = y1; y1 = y; return y; };
94
+ const set = (nf: number, nq = q): void => { c = coef(type, nf, nq, db); };
95
+ return Object.assign(run, { set });
96
+ }
97
+ export function filt(buf: Float32Array, ...fs: readonly Filter[]): Float32Array {
98
+ for (const f of fs) for (let i = 0; i < buf.length; i++) buf[i] = f(buf[i]);
99
+ return buf;
100
+ }
101
+
102
+ // ---------- oscillators, envelopes, noise ----------
103
+ // band-limited saw (polyBLEP)
104
+ function blep(t: number, dt: number): number {
105
+ if (t < dt) { const u = t / dt; return u + u - u * u - 1; }
106
+ if (t > 1 - dt) { const u = (t - 1) / dt; return u * u + u + u + 1; }
107
+ return 0;
108
+ }
109
+ const SHAPES = {
110
+ sine: (ph: number) => Math.sin(TAU * ph),
111
+ saw: (ph: number, dt: number) => 2 * ph - 1 - blep(ph, dt),
112
+ };
113
+ export function osc(kind: keyof typeof SHAPES, freq: number, sec: number, phase = 0): Float32Array {
114
+ const n = Math.ceil(sec * SR), out = new Float32Array(n), shape = SHAPES[kind], dt = freq / SR;
115
+ let ph = phase;
116
+ for (let i = 0; i < n; i++) {
117
+ out[i] = shape(ph, dt);
118
+ ph += dt; if (ph >= 1) ph -= 1;
119
+ }
120
+ return out;
121
+ }
122
+ // attack / decay / sustain / release envelope over `sec`
123
+ export function adsr(sec: number, a: number, d: number, s: number, r: number): Float32Array {
124
+ const n = Math.ceil(sec * SR), A = a * SR, D = d * SR, R = r * SR, hold = n - R;
125
+ return tabulate(n, (i) => {
126
+ const v = i < A ? i / A : i < A + D ? 1 - (1 - s) * ((i - A) / D) : s;
127
+ return i > hold ? v * Math.max(0, 1 - (i - hold) / R) : v;
128
+ });
129
+ }
130
+ export const expDecay = (sec: number, tau: number): Float32Array => tabulate(Math.ceil(sec * SR), (i) => Math.exp(-i / (tau * SR)));
131
+
132
+ export function noise(sec: number, r: Rng, pink = false): Float32Array {
133
+ const n = Math.ceil(sec * SR), o = new Float32Array(n);
134
+ if (!pink) { for (let i = 0; i < n; i++) o[i] = r() * 2 - 1; return o; }
135
+ let b0 = 0, b1 = 0, b2 = 0;
136
+ for (let i = 0; i < n; i++) {
137
+ const w = r() * 2 - 1;
138
+ b0 = 0.99765 * b0 + w * 0.099046; b1 = 0.963 * b1 + w * 0.2965164; b2 = 0.57 * b2 + w * 1.0526913;
139
+ o[i] = (b0 + b1 + b2 + w * 0.1848) * 0.2;
140
+ }
141
+ return o;
142
+ }
143
+
144
+ // ---------- Freeverb (Jezar), stereo, tuned for 48 kHz ----------
145
+ type Lines = { readonly buf: readonly Float32Array[]; readonly idx: Int32Array };
146
+ const makeLines = (taps: readonly number[], spread: number): Lines => ({ buf: taps.map((t) => new Float32Array(t + spread)), idx: new Int32Array(taps.length) });
147
+
148
+ function combSum(c: Lines, flt: Float64Array, x: number, room: number, damp: number): number {
149
+ let y = 0;
150
+ for (let j = 0; j < c.buf.length; j++) {
151
+ const b = c.buf[j], o = b[c.idx[j]];
152
+ flt[j] = o * (1 - damp) + flt[j] * damp;
153
+ b[c.idx[j]] = x + flt[j] * room;
154
+ c.idx[j] = (c.idx[j] + 1) % b.length;
155
+ y += o;
156
+ }
157
+ return y;
158
+ }
159
+ function allpass(a: Lines, input: number): number {
160
+ let y = input;
161
+ for (let j = 0; j < a.buf.length; j++) {
162
+ const b = a.buf[j], o = b[a.idx[j]], v = -y + o;
163
+ b[a.idx[j]] = y + o * 0.5;
164
+ a.idx[j] = (a.idx[j] + 1) % b.length;
165
+ y = v;
166
+ }
167
+ return y;
168
+ }
169
+ export function reverb(bus: Bus, { room = 0.82, damp = 0.35, wet = 1 } = {}): Bus {
170
+ const k = SR / 44100;
171
+ const combT = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617].map((x) => Math.round(x * k));
172
+ const apT = [556, 441, 341, 225].map((x) => Math.round(x * k));
173
+ const out = makeBus(bus.n / SR);
174
+ const channels: readonly (readonly [Float32Array, Float32Array, number])[] = [[bus.L, out.L, 0], [bus.R, out.R, Math.round(23 * k)]];
175
+ for (const [src, dst, spread] of channels) {
176
+ const combs = makeLines(combT, spread), aps = makeLines(apT, spread), flt = new Float64Array(combT.length);
177
+ for (let n = 0; n < bus.n; n++) dst[n] = allpass(aps, combSum(combs, flt, src[n] * 0.015, room, damp)) * wet;
178
+ }
179
+ return out;
180
+ }
181
+
182
+ // ping-pong delay
183
+ export function delay(bus: Bus, time: number, fb = 0.35, lp = 4500): Bus {
184
+ const d = Math.round(time * SR), out = makeBus(bus.n / SR);
185
+ const bl = new Float32Array(d), br = new Float32Array(d);
186
+ const fl = biquad("lp", lp), fr = biquad("lp", lp);
187
+ for (let i = 0, j = 0; i < bus.n; i++, j = (j + 1) % d) {
188
+ const ol = bl[j], or = br[j];
189
+ bl[j] = fl(bus.L[i] + or * fb); br[j] = fr(bus.R[i] * 0.4 + ol * fb);
190
+ out.L[i] = ol; out.R[i] = or;
191
+ }
192
+ return out;
193
+ }