rvlib-shipkit 2.2.0 → 2.3.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 (107) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.json +1 -1
  3. package/src/agent-time/mask.ts +43 -0
  4. package/src/agent-time/render.ts +187 -0
  5. package/src/agent-time/report.ts +335 -0
  6. package/src/agent-time/shape.ts +271 -0
  7. package/src/agent-time/signals.ts +84 -0
  8. package/src/agent-time/store.ts +379 -0
  9. package/src/agent-time/timeline.ts +218 -0
  10. package/src/agent-time/turns.ts +135 -0
  11. package/src/cli/cmd_Commands.ts +5 -0
  12. package/src/cli/cmd_Review.ts +948 -0
  13. package/src/cli/cmd_Time.ts +112 -0
  14. package/src/cli/cmd_Trailer.ts +259 -6
  15. package/src/cli/kit/verb.ts +14 -4
  16. package/src/cli.ts +6 -4
  17. package/src/collections/collection-types.ts +6 -1
  18. package/src/collections/providers.ts +3 -0
  19. package/src/collections/review.ts +70 -0
  20. package/src/core/layout.ts +2 -0
  21. package/src/reflect/prompt-corpus.ts +15 -119
  22. package/src/reflect/transcript.ts +254 -0
  23. package/src/review/anim.ts +228 -0
  24. package/src/review/auto.ts +141 -0
  25. package/src/review/format.ts +150 -0
  26. package/src/review/http.ts +23 -0
  27. package/src/review/marks.ts +43 -0
  28. package/src/review/media.ts +45 -0
  29. package/src/review/model.ts +854 -0
  30. package/src/review/project.ts +110 -0
  31. package/src/review/role.ts +48 -0
  32. package/src/review/routes.ts +228 -0
  33. package/src/review/store.ts +1002 -0
  34. package/src/review/threads.ts +237 -0
  35. package/src/review/trailer-desk.ts +385 -0
  36. package/src/review/trailer-model.ts +183 -0
  37. package/src/review/trailer-routes.ts +136 -0
  38. package/src/review/unit.ts +165 -0
  39. package/src/review/watchers.ts +54 -0
  40. package/src/serve/embed.ts +35 -0
  41. package/src/serve/hub-app/activity-view.tsx +1 -1
  42. package/src/serve/hub-app/activity.css +0 -4
  43. package/src/serve/hub-app/dock-view.tsx +5 -4
  44. package/src/serve/hub-app/hash-route.ts +28 -1
  45. package/src/serve/hub-app/header-bar.tsx +3 -1
  46. package/src/serve/hub-app/home-st.ts +10 -2
  47. package/src/serve/hub-app/home-strip.tsx +3 -2
  48. package/src/serve/hub-app/home.html +1 -0
  49. package/src/serve/hub-app/hub-app.tsx +80 -11
  50. package/src/serve/hub-app/hub.css +45 -13
  51. package/src/serve/hub-app/icons.tsx +9 -0
  52. package/src/serve/hub-app/index.html +1 -0
  53. package/src/serve/hub-app/insights-view.tsx +4 -3
  54. package/src/serve/hub-app/keys.ts +14 -10
  55. package/src/serve/hub-app/layout-st.ts +63 -2
  56. package/src/serve/hub-app/planned-view.tsx +3 -2
  57. package/src/serve/hub-app/planned.css +0 -1
  58. package/src/serve/hub-app/project-list.css +22 -4
  59. package/src/serve/hub-app/project-list.tsx +41 -8
  60. package/src/serve/hub-app/projects-svc.ts +4 -0
  61. package/src/serve/hub-app/repo-sections.ts +15 -3
  62. package/src/serve/hub-app/repo-st.ts +45 -3
  63. package/src/serve/hub-app/review/clip-st.ts +208 -0
  64. package/src/serve/hub-app/review/kind-view.tsx +171 -0
  65. package/src/serve/hub-app/review/kit.tsx +113 -0
  66. package/src/serve/hub-app/review/review-answer.tsx +239 -0
  67. package/src/serve/hub-app/review/review-board.tsx +46 -0
  68. package/src/serve/hub-app/review/review-list.tsx +156 -0
  69. package/src/serve/hub-app/review/review-page.tsx +101 -0
  70. package/src/serve/hub-app/review/review-st.ts +779 -0
  71. package/src/serve/hub-app/review/review-views.tsx +503 -0
  72. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  73. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  74. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  75. package/src/serve/hub-app/review.css +329 -0
  76. package/src/serve/hub-app/theme-st.ts +56 -0
  77. package/src/serve/hub-app/tree-build.ts +4 -0
  78. package/src/serve/hub-app/tree-row.tsx +1 -1
  79. package/src/serve/hub-app/tree-svc.ts +4 -0
  80. package/src/serve/hub-runtime.ts +5 -3
  81. package/src/serve/same-origin.ts +5 -1
  82. package/src/serve/serve-hub.ts +8 -0
  83. package/src/shipkit-files.ts +9 -5
  84. package/src/trailer/adapter.ts +58 -3
  85. package/src/trailer/build.ts +300 -41
  86. package/src/trailer/ffmpeg.ts +22 -0
  87. package/src/trailer/footage.ts +109 -40
  88. package/src/trailer/grammar.ts +444 -69
  89. package/src/trailer/graph.ts +199 -14
  90. package/src/trailer/holes.ts +72 -0
  91. package/src/trailer/load-unit.ts +20 -3
  92. package/src/trailer/look.ts +17 -3
  93. package/src/trailer/passage.ts +129 -0
  94. package/src/trailer/plan.ts +80 -21
  95. package/src/trailer/prune.ts +193 -0
  96. package/src/trailer/score/compose.ts +240 -0
  97. package/src/trailer/score/dsp.ts +272 -0
  98. package/src/trailer/score/palette.ts +188 -0
  99. package/src/trailer/score/pattern.ts +91 -0
  100. package/src/trailer/score/score.ts +214 -0
  101. package/src/trailer/score/sound.ts +112 -0
  102. package/src/trailer/score/synth.ts +361 -0
  103. package/src/trailer/states.ts +76 -0
  104. package/src/trailer/stitch.ts +92 -9
  105. package/src/trailer/titles.ts +123 -18
  106. package/src/trailer/vocab.ts +23 -4
  107. package/src/trailer/words.ts +254 -0
@@ -0,0 +1,272 @@
1
+ // the signal blocks the score's synth is made of, each a pure function over Float32Arrays: the ADSR points, the biquad
2
+ // (the Web Audio formulas, so an instrument sounds as it does in a browser synth), the compressor, the soft ceiling and
3
+ // an FFT convolution for the room. Hot loops sit in top level functions over their arrays, no closure per sample
4
+
5
+ /** [seconds from note start, gain] points; the last one is 0 at release end. A note released during its attack or
6
+ * decay releases from the level it reached, never from the peak, so a short note never clicks */
7
+ export function envPoints(env: { a: number; d: number; s: number; r: number }, dur: number, peak: number): [number, number][] {
8
+ const a = Math.max(0.001, env.a)
9
+ const d = Math.max(0, env.d)
10
+ const s = Math.max(0, Math.min(1, env.s)) * peak
11
+ const r = Math.max(0.005, env.r)
12
+ const off = Math.max(0, dur)
13
+ const pts: [number, number][] = [[0, 0]]
14
+ if (off <= a) pts.push([off, peak * (off / a)])
15
+ else {
16
+ pts.push([a, peak])
17
+ if (off <= a + d) pts.push([off, d === 0 ? s : peak + (s - peak) * ((off - a) / d)])
18
+ else {
19
+ pts.push([a + d, s])
20
+ pts.push([off, s])
21
+ }
22
+ }
23
+ const last = pts[pts.length - 1]
24
+ pts.push([(last?.[0] ?? off) + r, 0])
25
+ return pts.filter((p, i) => i === 0 || p[0] !== pts[i - 1]?.[0] || p[1] !== pts[i - 1]?.[1])
26
+ }
27
+
28
+ /** multiplies `buf` (its first sample at `t0` seconds after the note start) by the envelope's points */
29
+ export function applyEnvelope(buf: Float32Array, n: number, t0: number, rate: number, pts: readonly [number, number][]): void {
30
+ let k = 1
31
+ for (let i = 0; i < n; i++) {
32
+ const t = t0 + i / rate
33
+ while (k < pts.length - 1 && t > pts[k]![0]) k++
34
+ const p = pts[k - 1]!
35
+ const q = pts[k]!
36
+ const g = t >= q[0] ? q[1] : q[0] === p[0] ? q[1] : p[1] + (q[1] - p[1]) * ((t - p[0]) / (q[0] - p[0]))
37
+ buf[i] = buf[i]! * (t < 0 ? 0 : g)
38
+ }
39
+ }
40
+
41
+ export type FilterType = 'lowpass' | 'highpass' | 'bandpass'
42
+
43
+ /** a biquad in place, Web Audio's coefficients: lowpass and highpass read q in dB, bandpass as a plain q */
44
+ export function biquad(buf: Float32Array, n: number, rate: number, type: FilterType, freq: number, q: number): void {
45
+ const f = Math.max(1, Math.min(rate / 2 - 1, freq))
46
+ const w0 = (2 * Math.PI * f) / rate
47
+ const cw = Math.cos(w0)
48
+ const sw = Math.sin(w0)
49
+ let b0: number, b1: number, b2: number
50
+ let alpha: number
51
+ if (type === 'bandpass') {
52
+ alpha = sw / (2 * Math.max(1e-4, q))
53
+ b0 = alpha
54
+ b1 = 0
55
+ b2 = -alpha
56
+ } else {
57
+ alpha = sw / (2 * 10 ** (q / 20))
58
+ const sign = type === 'lowpass' ? 1 : -1
59
+ b0 = (1 - sign * cw) / 2
60
+ b1 = sign * (1 - sign * cw)
61
+ b2 = b0
62
+ }
63
+ const a0 = 1 + alpha
64
+ biquadRun(buf, n, b0 / a0, b1 / a0, b2 / a0, (-2 * cw) / a0, (1 - alpha) / a0)
65
+ }
66
+
67
+ function biquadRun(buf: Float32Array, n: number, b0: number, b1: number, b2: number, a1: number, a2: number): void {
68
+ let z1 = 0
69
+ let z2 = 0
70
+ for (let i = 0; i < n; i++) {
71
+ const x = buf[i]!
72
+ const y = b0 * x + z1
73
+ z1 = b1 * x - a1 * y + z2
74
+ z2 = b2 * x - a2 * y
75
+ buf[i] = y
76
+ }
77
+ }
78
+
79
+ const CEILING = 0.98
80
+ const KNEE = 0.7
81
+
82
+ /** linear below the knee, tanh into the ceiling above it (slope 1 at the knee, so the join is smooth) */
83
+ export function softClip(x: number): number {
84
+ const a = Math.abs(x)
85
+ if (a <= KNEE) return x
86
+ return Math.sign(x) * (KNEE + (CEILING - KNEE) * Math.tanh((a - KNEE) / (CEILING - KNEE)))
87
+ }
88
+
89
+ export interface CompressorDef {
90
+ /** dB */
91
+ threshold: number
92
+ /** dB */
93
+ knee: number
94
+ ratio: number
95
+ /** seconds */
96
+ attack: number
97
+ release: number
98
+ }
99
+
100
+ const lin = (db: number): number => 10 ** (db / 20)
101
+ const dbOf = (x: number): number => 20 * Math.log10(Math.max(1e-12, x))
102
+
103
+ /** the static curve of a browser compressor: a knee shaped by `k`, then the ratio's line; the knee's `k` is searched so
104
+ * the curve meets the ratio's slope at the knee's top, and the output is lifted by its makeup gain */
105
+ function staticCurve(c: CompressorDef): { saturate: (x: number) => number; makeup: number } {
106
+ const linThr = lin(c.threshold)
107
+ const kneeThrDb = c.threshold + c.knee
108
+ const kneeThr = lin(kneeThrDb)
109
+ const slope = 1 / c.ratio
110
+ const kneeCurve = (x: number, k: number): number => (x < linThr ? x : linThr + (1 - Math.exp(-k * (x - linThr))) / k)
111
+ const slopeAt = (x: number, k: number): number => {
112
+ if (x < linThr) return 1
113
+ const x2 = x * 1.001
114
+ return (dbOf(kneeCurve(x2, k)) - dbOf(kneeCurve(x, k))) / (dbOf(x2) - dbOf(x))
115
+ }
116
+ let lo = 0.1
117
+ let hi = 10000
118
+ let k = 5
119
+ for (let i = 0; i < 15; i++) {
120
+ if (slopeAt(kneeThr, k) < slope) hi = k
121
+ else lo = k
122
+ k = Math.sqrt(lo * hi)
123
+ }
124
+ const yKneeDb = dbOf(kneeCurve(kneeThr, k))
125
+ const saturate = (x: number): number => (x < kneeThr ? kneeCurve(x, k) : lin(yKneeDb + slope * (dbOf(x) - kneeThrDb)))
126
+ return { saturate, makeup: (1 / saturate(1)) ** 0.6 }
127
+ }
128
+
129
+ /** a stereo compressor in place, 6 ms of lookahead: the gain follows the louder channel, falling at `attack` and
130
+ * rising at `release`, in dB */
131
+ export function compress(l: Float32Array, r: Float32Array, rate: number, c: CompressorDef): void {
132
+ const { saturate, makeup } = staticCurve(c)
133
+ const n = l.length
134
+ // the curve sampled in dB of input, 0.05 dB a step, from -60 dB up to +24 dB
135
+ const lo = -60
136
+ const step = 0.05
137
+ const table = new Float32Array(Math.ceil((24 - lo) / step) + 2)
138
+ for (let i = 0; i < table.length; i++) {
139
+ const x = lin(lo + i * step)
140
+ table[i] = dbOf(saturate(x) / x)
141
+ }
142
+ compressRun(l, r, n, Math.round(0.006 * rate), Math.exp(-1 / (Math.max(1e-4, c.attack) * rate)), Math.exp(-1 / (Math.max(1e-4, c.release) * rate)), table, lo, step, makeup)
143
+ }
144
+
145
+ function compressRun(l: Float32Array, r: Float32Array, n: number, delay: number, att: number, rel: number, table: Float32Array, lo: number, step: number, makeup: number): void {
146
+ const dl = new Float32Array(delay)
147
+ const dr = new Float32Array(delay)
148
+ let env = 0
149
+ const last = table.length - 1
150
+ for (let i = 0; i < n; i++) {
151
+ const x = Math.max(Math.abs(l[i]!), Math.abs(r[i]!))
152
+ let target = 0
153
+ if (x > 1e-3) {
154
+ const p = (20 * Math.log10(x) - lo) / step
155
+ const j = Math.min(last - 1, Math.max(0, p | 0))
156
+ const f = Math.min(1, Math.max(0, p - j))
157
+ target = table[j]! + (table[j + 1]! - table[j]!) * f
158
+ }
159
+ env = target < env ? target + (env - target) * att : target + (env - target) * rel
160
+ const g = makeup * 10 ** (env / 20)
161
+ const k = i % delay
162
+ const yl = dl[k]!
163
+ const yr = dr[k]!
164
+ dl[k] = l[i]!
165
+ dr[k] = r[i]!
166
+ l[i] = yl * g
167
+ r[i] = yr * g
168
+ }
169
+ }
170
+
171
+ /** an in place radix 2 FFT; `inverse` scales by 1/n */
172
+ export function fft(re: Float64Array, im: Float64Array, inverse: boolean): void {
173
+ const n = re.length
174
+ for (let i = 1, j = 0; i < n; i++) {
175
+ let bit = n >> 1
176
+ for (; j & bit; bit >>= 1) j ^= bit
177
+ j ^= bit
178
+ if (i < j) {
179
+ const tr = re[i]!
180
+ re[i] = re[j]!
181
+ re[j] = tr
182
+ const ti = im[i]!
183
+ im[i] = im[j]!
184
+ im[j] = ti
185
+ }
186
+ }
187
+ const sign = inverse ? 1 : -1
188
+ for (let len = 2; len <= n; len <<= 1) {
189
+ const ang = (sign * 2 * Math.PI) / len
190
+ const wr = Math.cos(ang)
191
+ const wi = Math.sin(ang)
192
+ const half = len >> 1
193
+ for (let i = 0; i < n; i += len) {
194
+ let cr = 1
195
+ let ci = 0
196
+ for (let k = 0; k < half; k++) {
197
+ const a = i + k
198
+ const b = a + half
199
+ const xr = re[b]! * cr - im[b]! * ci
200
+ const xi = re[b]! * ci + im[b]! * cr
201
+ re[b] = re[a]! - xr
202
+ im[b] = im[a]! - xi
203
+ re[a] = re[a]! + xr
204
+ im[a] = im[a]! + xi
205
+ const nr = cr * wr - ci * wi
206
+ ci = cr * wi + ci * wr
207
+ cr = nr
208
+ }
209
+ }
210
+ }
211
+ if (inverse) for (let i = 0; i < n; i++) {
212
+ re[i] = re[i]! / n
213
+ im[i] = im[i]! / n
214
+ }
215
+ }
216
+
217
+ /** adds `gain` times (inL, inR) convolved with the stereo impulse (irL, irR) to (outL, outR): overlap add, both
218
+ * channels in one complex transform a block */
219
+ export function convolveInto(inL: Float32Array, inR: Float32Array, irL: Float32Array, irR: Float32Array, outL: Float32Array, outR: Float32Array, gain: number): void {
220
+ const m = irL.length
221
+ let size = 1
222
+ while (size < 4 * m) size <<= 1
223
+ const block = size - m + 1
224
+ // the two impulses' spectra from one transform of irL + i irR, split by conjugate symmetry
225
+ const hr = new Float64Array(size)
226
+ const hi = new Float64Array(size)
227
+ hr.set(irL)
228
+ hi.set(irR)
229
+ fft(hr, hi, false)
230
+ const lr = new Float64Array(size)
231
+ const li = new Float64Array(size)
232
+ const rr = new Float64Array(size)
233
+ const ri = new Float64Array(size)
234
+ for (let k = 0; k < size; k++) {
235
+ const j = (size - k) % size
236
+ lr[k] = (hr[k]! + hr[j]!) / 2
237
+ li[k] = (hi[k]! - hi[j]!) / 2
238
+ rr[k] = (hi[k]! + hi[j]!) / 2
239
+ ri[k] = (hr[j]! - hr[k]!) / 2
240
+ }
241
+ const xr = new Float64Array(size)
242
+ const xi = new Float64Array(size)
243
+ const yr = new Float64Array(size)
244
+ const yi = new Float64Array(size)
245
+ const n = outL.length
246
+ for (let at = 0; at < inL.length; at += block) {
247
+ const len = Math.min(block, inL.length - at)
248
+ xr.fill(0)
249
+ xi.fill(0)
250
+ let loud = false
251
+ for (let i = 0; i < len; i++) {
252
+ xr[i] = inL[at + i]!
253
+ xi[i] = inR[at + i]!
254
+ if (xr[i] !== 0 || xi[i] !== 0) loud = true
255
+ }
256
+ if (!loud) continue
257
+ fft(xr, xi, false)
258
+ spectrumTimes(xr, xi, lr, li, yr, yi)
259
+ fft(yr, yi, true)
260
+ for (let i = 0; i < size && at + i < n; i++) outL[at + i] = outL[at + i]! + gain * yr[i]!
261
+ spectrumTimes(xr, xi, rr, ri, yr, yi)
262
+ fft(yr, yi, true)
263
+ for (let i = 0; i < size && at + i < n; i++) outR[at + i] = outR[at + i]! + gain * yi[i]!
264
+ }
265
+ }
266
+
267
+ function spectrumTimes(ar: Float64Array, ai: Float64Array, br: Float64Array, bi: Float64Array, or: Float64Array, oi: Float64Array): void {
268
+ for (let k = 0; k < ar.length; k++) {
269
+ or[k] = ar[k]! * br[k]! - ai[k]! * bi[k]!
270
+ oi[k] = ar[k]! * bi[k]! + ai[k]! * br[k]!
271
+ }
272
+ }
@@ -0,0 +1,188 @@
1
+ import type { InstrumentDef } from './pattern.ts'
2
+
3
+ /** one cycle of a wave from its harmonic levels (the fundamental first), for an instrument's `wave` */
4
+ export function additive(levels: number[], samples = 64): number[] {
5
+ return Array.from({ length: samples }, (_, i) =>
6
+ levels.reduce((sum, level, k) => sum + level * Math.sin((2 * Math.PI * (k + 1) * i) / samples), 0),
7
+ )
8
+ }
9
+
10
+ // the default instrument palette; a score spreads one and overrides what it needs: `lead: { ...INST.lead, duty: 0.25 }`.
11
+ // With no `export const instruments` in its code, a score plays these
12
+ export const INST = {
13
+ lead: {
14
+ osc: 'pulse',
15
+ duty: 0.5,
16
+ vol: 0.4,
17
+ env: { a: 0.015, d: 0.15, s: 0.65, r: 0.1 },
18
+ vibrato: { delay: 0.18, depth: 0.12, rate: 5.5 },
19
+ reverb: 0.2,
20
+ },
21
+ lead25: {
22
+ osc: 'pulse',
23
+ duty: 0.25,
24
+ vol: 0.38,
25
+ env: { a: 0.015, d: 0.12, s: 0.6, r: 0.1 },
26
+ vibrato: { delay: 0.2, depth: 0.12, rate: 5.5 },
27
+ reverb: 0.2,
28
+ },
29
+ flute: {
30
+ osc: 'triangle',
31
+ vol: 0.55,
32
+ env: { a: 0.03, d: 0.2, s: 0.75, r: 0.15 },
33
+ vibrato: { delay: 0.25, depth: 0.15, rate: 5 },
34
+ reverb: 0.35,
35
+ },
36
+ ocarina: {
37
+ osc: 'triangle',
38
+ vol: 0.5,
39
+ env: { a: 0.045, d: 0.2, s: 0.8, r: 0.2 },
40
+ vibrato: { delay: 0.3, depth: 0.12, rate: 4.8 },
41
+ reverb: 0.35,
42
+ },
43
+ // odd harmonics, as a clarinet's
44
+ reed: {
45
+ osc: 'wave',
46
+ wave: additive([1, 0.05, 0.45, 0.04, 0.2, 0.02, 0.08]),
47
+ tone: 2600,
48
+ vol: 0.4,
49
+ env: { a: 0.03, d: 0.2, s: 0.75, r: 0.14 },
50
+ vibrato: { delay: 0.3, depth: 0.1, rate: 5 },
51
+ reverb: 0.3,
52
+ },
53
+ // a soft electric piano: a round body, a touch of the fourth harmonic for the tine
54
+ keys: {
55
+ osc: 'wave',
56
+ wave: additive([1, 0.4, 0.1, 0.14, 0.03]),
57
+ vol: 0.34,
58
+ env: { a: 0.004, d: 0.55, s: 0.1, r: 0.3 },
59
+ reverb: 0.3,
60
+ },
61
+ glock: {
62
+ osc: 'wave',
63
+ wave: additive([1, 0, 0, 0.25]),
64
+ vol: 0.3,
65
+ env: { a: 0.002, d: 0.9, s: 0, r: 0.6 },
66
+ reverb: 0.45,
67
+ },
68
+ cello: {
69
+ osc: 'saw',
70
+ tone: 1500,
71
+ vol: 0.26,
72
+ env: { a: 0.09, d: 0.3, s: 0.85, r: 0.3 },
73
+ detune: 7,
74
+ unison: 2,
75
+ vibrato: { delay: 0.35, depth: 0.1, rate: 5 },
76
+ reverb: 0.35,
77
+ },
78
+ bell: { osc: 'sine', vol: 0.4, env: { a: 0.002, d: 0.6, s: 0.1, r: 0.5 }, reverb: 0.4 },
79
+ pluck: { osc: 'pulse', duty: 0.125, vol: 0.3, env: { a: 0.002, d: 0.12, s: 0.15, r: 0.05 }, reverb: 0.2 },
80
+ arp: { osc: 'pulse', duty: 0.25, vol: 0.22, env: { a: 0.002, d: 0.08, s: 0.3, r: 0.03 }, pan: 0.25, reverb: 0.15 },
81
+ harmony: {
82
+ osc: 'pulse',
83
+ duty: 0.25,
84
+ vol: 0.28,
85
+ env: { a: 0.01, d: 0.15, s: 0.55, r: 0.1 },
86
+ pan: -0.3,
87
+ reverb: 0.25,
88
+ },
89
+ pad: {
90
+ osc: 'saw',
91
+ vol: 0.16,
92
+ env: { a: 0.35, d: 0.4, s: 0.7, r: 0.6 },
93
+ detune: 8,
94
+ unison: 2,
95
+ filter: { type: 'lowpass', freq: 1400, q: 0.7 },
96
+ reverb: 0.5,
97
+ },
98
+ strings: {
99
+ osc: 'saw',
100
+ vol: 0.2,
101
+ env: { a: 0.12, d: 0.3, s: 0.8, r: 0.3 },
102
+ detune: 6,
103
+ unison: 2,
104
+ filter: { type: 'lowpass', freq: 2600, q: 0.8 },
105
+ vibrato: { delay: 0.3, depth: 0.08, rate: 5 },
106
+ reverb: 0.4,
107
+ },
108
+ brass: {
109
+ osc: 'saw',
110
+ vol: 0.3,
111
+ env: { a: 0.03, d: 0.2, s: 0.7, r: 0.12 },
112
+ filter: { type: 'lowpass', freq: 2200, q: 1.2 },
113
+ vibrato: { delay: 0.25, depth: 0.1, rate: 5.5 },
114
+ reverb: 0.25,
115
+ },
116
+ bass: { osc: 'triangle', vol: 0.65, env: { a: 0.002, d: 0.06, s: 0.9, r: 0.05 } },
117
+ bassPulse: {
118
+ osc: 'pulse',
119
+ duty: 0.5,
120
+ vol: 0.3,
121
+ env: { a: 0.002, d: 0.1, s: 0.7, r: 0.04 },
122
+ filter: { type: 'lowpass', freq: 900 },
123
+ },
124
+ bassSaw: {
125
+ osc: 'saw',
126
+ vol: 0.32,
127
+ env: { a: 0.002, d: 0.12, s: 0.6, r: 0.05 },
128
+ filter: { type: 'lowpass', freq: 700, q: 2 },
129
+ },
130
+ kick: {
131
+ osc: 'sine',
132
+ vol: 0.75,
133
+ env: { a: 0, d: 0.16, s: 0, r: 0.03 },
134
+ sweep: { semis: -24, time: 0.07 },
135
+ pitch: 52,
136
+ },
137
+ snare: {
138
+ osc: 'noise',
139
+ noise: 'white',
140
+ vol: 0.35,
141
+ env: { a: 0, d: 0.12, s: 0, r: 0.04 },
142
+ filter: { type: 'bandpass', freq: 1800, q: 0.8 },
143
+ reverb: 0.15,
144
+ },
145
+ hat: {
146
+ osc: 'noise',
147
+ noise: 'white',
148
+ vol: 0.1,
149
+ env: { a: 0, d: 0.03, s: 0, r: 0.012 },
150
+ filter: { type: 'highpass', freq: 6500 },
151
+ },
152
+ openhat: {
153
+ osc: 'noise',
154
+ noise: 'white',
155
+ vol: 0.09,
156
+ env: { a: 0, d: 0.18, s: 0, r: 0.06 },
157
+ filter: { type: 'highpass', freq: 5500 },
158
+ },
159
+ crash: {
160
+ osc: 'noise',
161
+ noise: 'white',
162
+ vol: 0.18,
163
+ env: { a: 0, d: 0.9, s: 0, r: 0.3 },
164
+ filter: { type: 'highpass', freq: 4000 },
165
+ reverb: 0.3,
166
+ },
167
+ tom: { osc: 'sine', vol: 0.55, env: { a: 0, d: 0.18, s: 0, r: 0.05 }, sweep: { semis: -7, time: 0.15 } },
168
+ shaker: {
169
+ osc: 'noise',
170
+ noise: 'white',
171
+ vol: 0.08,
172
+ env: { a: 0.005, d: 0.04, s: 0, r: 0.01 },
173
+ filter: { type: 'highpass', freq: 8000 },
174
+ },
175
+ // a pure sine under the bass register, felt more than heard; play it in octave 1 or 2
176
+ sub: { osc: 'sine', vol: 0.7, env: { a: 0.01, d: 0.2, s: 0.85, r: 0.12 } },
177
+ clap: {
178
+ osc: 'noise',
179
+ noise: 'white',
180
+ vol: 0.3,
181
+ env: { a: 0.002, d: 0.14, s: 0, r: 0.06 },
182
+ filter: { type: 'bandpass', freq: 1200, q: 1.4 },
183
+ reverb: 0.3,
184
+ },
185
+ } satisfies Record<string, InstrumentDef>
186
+
187
+ /** the drum kit letters most songs use */
188
+ export const KIT = { k: 'kick', s: 'snare', h: 'hat', o: 'openhat', c: 'crash', x: 'shaker' }
@@ -0,0 +1,91 @@
1
+ // the score's note grammar and its instruments: one token a grid row, an instrument as plain data the synth plays.
2
+ // Pure: no audio here (synth.ts renders)
3
+
4
+ /** oscillator families: chiptune roots (pulse, wave, noise) plus the smoother modern ones */
5
+ export type Osc = 'pulse' | 'saw' | 'triangle' | 'sine' | 'wave' | 'noise'
6
+
7
+ export interface InstrumentDef {
8
+ osc: Osc
9
+ /** 0..1 */
10
+ vol: number
11
+ /** ADSR in seconds, sustain as a 0..1 level */
12
+ env: { a: number; d: number; s: number; r: number }
13
+ /** pulse only: duty cycle 0.05..0.95 */
14
+ duty?: number
15
+ /** wave only: one cycle of samples in -1..1, any length */
16
+ wave?: number[]
17
+ /** pulse, saw and wave: the frequency in Hz above which the harmonics fall away, default 3000; higher is brighter */
18
+ tone?: number
19
+ /** noise only: 'white' hiss or 'metal' (short period, pitched) */
20
+ noise?: 'white' | 'metal'
21
+ /** detune in cents, and extra unison voices spread by it */
22
+ detune?: number
23
+ unison?: number
24
+ filter?: { type: 'lowpass' | 'highpass' | 'bandpass'; freq: number; q?: number }
25
+ /** depth in semitones, rate in Hz, delay in seconds from the note start */
26
+ vibrato?: { delay: number; depth: number; rate: number }
27
+ /** portamento between consecutive notes, seconds */
28
+ glide?: number
29
+ /** -1 left .. 1 right */
30
+ pan?: number
31
+ /** 0..1 send to the score's reverb */
32
+ reverb?: number
33
+ /** pitch slide from each note start: `semis` over `time` seconds (kicks, toms, zaps) */
34
+ sweep?: { semis: number; time: number }
35
+ /** the midi note a drum letter plays this instrument at, default 60 */
36
+ pitch?: number
37
+ }
38
+
39
+ // one token a row, whitespace separated, '|' ignored:
40
+ // C4 C#4 Db4 note (octave 0..8) . hold the previous note
41
+ // - silence k drum letter (from `drums`)
42
+ // zero length modifiers glued before a row token: `@lead:C4` switches instrument, `v8:C4` sets the volume 0..15
43
+ // (15 = the instrument's vol) from that note on
44
+ type Row = { kind: 'note'; midi: number } | { kind: 'hold' } | { kind: 'off' } | { kind: 'drum'; letter: string }
45
+
46
+ export interface RowEvent {
47
+ row: Row
48
+ instrument?: string
49
+ vol?: number
50
+ }
51
+
52
+ const NOTE_RE = /^([A-G])([#b]?)(\d)$/
53
+ const SEMI: Record<string, number> = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }
54
+
55
+ export function noteToMidi(token: string): number | null {
56
+ const m = NOTE_RE.exec(token)
57
+ if (!m) return null
58
+ const [, letter, acc, oct] = m
59
+ const base = SEMI[letter ?? '']
60
+ if (base === undefined || oct === undefined) return null
61
+ return 12 * (Number(oct) + 1) + base + (acc === '#' ? 1 : acc === 'b' ? -1 : 0)
62
+ }
63
+
64
+ export function midiToHz(midi: number): number {
65
+ return 440 * 2 ** ((midi - 69) / 12)
66
+ }
67
+
68
+ export function parsePattern(text: string, drums: Record<string, string> = {}): RowEvent[] {
69
+ const out: RowEvent[] = []
70
+ for (const raw of text.split(/\s+/)) {
71
+ if (raw === '' || raw === '|') continue
72
+ const parts = raw.split(':')
73
+ const body = parts.pop() ?? ''
74
+ const ev: RowEvent = { row: { kind: 'off' } }
75
+ for (const mod of parts) {
76
+ if (mod.startsWith('@')) ev.instrument = mod.slice(1)
77
+ else if (/^v\d{1,2}$/.test(mod)) ev.vol = Number(mod.slice(1))
78
+ else throw new Error(`pattern token "${raw}": unknown modifier "${mod}"`)
79
+ }
80
+ if (body === '.') ev.row = { kind: 'hold' }
81
+ else if (body === '-') ev.row = { kind: 'off' }
82
+ else if (drums[body] !== undefined) ev.row = { kind: 'drum', letter: body }
83
+ else {
84
+ const midi = noteToMidi(body)
85
+ if (midi === null) throw new Error(`pattern token "${raw}": not a note, a drum letter, '.' or '-'`)
86
+ ev.row = { kind: 'note', midi }
87
+ }
88
+ out.push(ev)
89
+ }
90
+ return out
91
+ }