rvlib-shipkit 2.1.1 → 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.
- package/CHANGELOG.md +50 -0
- package/package.json +2 -1
- package/src/agent-time/mask.ts +43 -0
- package/src/agent-time/render.ts +187 -0
- package/src/agent-time/report.ts +335 -0
- package/src/agent-time/shape.ts +271 -0
- package/src/agent-time/signals.ts +84 -0
- package/src/agent-time/store.ts +379 -0
- package/src/agent-time/timeline.ts +218 -0
- package/src/agent-time/turns.ts +135 -0
- package/src/assets/kits/identity-kit.ts +7 -3
- package/src/assets/run-assets.ts +18 -4
- package/src/checks/SK004-gitignore-policy.check.ts +11 -6
- package/src/checks/SK013-one-way-in-policy.check.ts +13 -8
- package/src/cli/cmd_Calls.ts +53 -0
- package/src/cli/cmd_Commands.ts +163 -43
- package/src/cli/cmd_Review.ts +948 -0
- package/src/cli/cmd_Shot.ts +63 -0
- package/src/cli/cmd_Start.ts +8 -4
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +361 -0
- package/src/cli/kit/builtins.ts +40 -0
- package/src/cli/kit/calls.ts +177 -0
- package/src/cli/kit/families.ts +67 -0
- package/src/cli/kit/index.ts +8 -0
- package/src/cli/kit/pages-check.ts +36 -0
- package/src/cli/kit/project-cli.ts +73 -0
- package/src/cli/kit/verb.ts +167 -0
- package/src/cli.ts +15 -2
- package/src/collections/collection-types.ts +6 -1
- package/src/collections/providers.ts +3 -0
- package/src/collections/review.ts +70 -0
- package/src/core/layout.ts +12 -2
- package/src/core/migrate-layout.ts +31 -8
- package/src/core/one-way-in-policy.ts +12 -4
- package/src/reflect/prompt-corpus.ts +15 -114
- package/src/reflect/transcript.ts +254 -0
- package/src/review/anim.ts +228 -0
- package/src/review/auto.ts +141 -0
- package/src/review/format.ts +150 -0
- package/src/review/http.ts +23 -0
- package/src/review/marks.ts +43 -0
- package/src/review/media.ts +45 -0
- package/src/review/model.ts +854 -0
- package/src/review/project.ts +110 -0
- package/src/review/role.ts +48 -0
- package/src/review/routes.ts +228 -0
- package/src/review/store.ts +1002 -0
- package/src/review/threads.ts +237 -0
- package/src/review/trailer-desk.ts +385 -0
- package/src/review/trailer-model.ts +183 -0
- package/src/review/trailer-routes.ts +136 -0
- package/src/review/unit.ts +165 -0
- package/src/review/watchers.ts +54 -0
- package/src/scaffold/cli-skeleton.ts +149 -0
- package/src/scaffold/files/agent-index.md +2 -2
- package/src/scaffold/files/tsconfig.json +3 -0
- package/src/scaffold/files/verify-skill.md +1 -1
- package/src/scaffold/project-scaffold.ts +90 -12
- package/src/serve/embed.ts +35 -0
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/activity.css +0 -4
- package/src/serve/hub-app/dock-view.tsx +5 -4
- package/src/serve/hub-app/hash-route.ts +28 -1
- package/src/serve/hub-app/header-bar.tsx +3 -1
- package/src/serve/hub-app/home-st.ts +10 -2
- package/src/serve/hub-app/home-strip.tsx +3 -2
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +80 -11
- package/src/serve/hub-app/hub.css +45 -13
- package/src/serve/hub-app/icons.tsx +9 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/insights-view.tsx +4 -3
- package/src/serve/hub-app/keys.ts +14 -10
- package/src/serve/hub-app/layout-st.ts +63 -2
- package/src/serve/hub-app/planned-view.tsx +3 -2
- package/src/serve/hub-app/planned.css +0 -1
- package/src/serve/hub-app/project-list.css +22 -4
- package/src/serve/hub-app/project-list.tsx +41 -8
- package/src/serve/hub-app/projects-svc.ts +4 -0
- package/src/serve/hub-app/repo-sections.ts +15 -3
- package/src/serve/hub-app/repo-st.ts +45 -3
- package/src/serve/hub-app/review/clip-st.ts +208 -0
- package/src/serve/hub-app/review/kind-view.tsx +171 -0
- package/src/serve/hub-app/review/kit.tsx +113 -0
- package/src/serve/hub-app/review/review-answer.tsx +239 -0
- package/src/serve/hub-app/review/review-board.tsx +46 -0
- package/src/serve/hub-app/review/review-list.tsx +156 -0
- package/src/serve/hub-app/review/review-page.tsx +101 -0
- package/src/serve/hub-app/review/review-st.ts +779 -0
- package/src/serve/hub-app/review/review-views.tsx +503 -0
- package/src/serve/hub-app/review/trailer-page.tsx +416 -0
- package/src/serve/hub-app/review/trailer-st.ts +602 -0
- package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
- package/src/serve/hub-app/review.css +329 -0
- package/src/serve/hub-app/theme-st.ts +56 -0
- package/src/serve/hub-app/tree-build.ts +4 -0
- package/src/serve/hub-app/tree-row.tsx +1 -1
- package/src/serve/hub-app/tree-svc.ts +4 -0
- package/src/serve/hub-runtime.ts +5 -3
- package/src/serve/same-origin.ts +5 -1
- package/src/serve/serve-hub.ts +8 -0
- package/src/shipkit-files.ts +12 -4
- package/src/shot/client.ts +67 -0
- package/src/shot/clip-encoder.ts +146 -0
- package/src/shot/dom-clip.ts +124 -0
- package/src/shot/serve-shot.ts +54 -0
- package/src/shot/shooter.ts +343 -0
- package/src/shot/shot-ask.ts +189 -0
- package/src/shot/shot.shipkit.ts +14 -0
- package/src/smoke/run-smoke.ts +2 -2
- package/src/start-repo.ts +29 -14
- package/src/suite/judge-suites.ts +56 -10
- package/src/suite/select.ts +2 -5
- package/src/suite/suite-cli.ts +3 -2
- package/src/trailer/adapter.ts +78 -0
- package/src/trailer/build.ts +603 -0
- package/src/trailer/ffmpeg.ts +72 -0
- package/src/trailer/footage.ts +173 -0
- package/src/trailer/grammar.ts +1290 -0
- package/src/trailer/graph.ts +508 -0
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +31 -0
- package/src/trailer/look.ts +124 -0
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +192 -0
- package/src/trailer/prune.ts +193 -0
- package/src/trailer/score/compose.ts +240 -0
- package/src/trailer/score/dsp.ts +272 -0
- package/src/trailer/score/palette.ts +188 -0
- package/src/trailer/score/pattern.ts +91 -0
- package/src/trailer/score/score.ts +214 -0
- package/src/trailer/score/sound.ts +112 -0
- package/src/trailer/score/synth.ts +361 -0
- package/src/trailer/states.ts +76 -0
- package/src/trailer/stitch.ts +233 -0
- package/src/trailer/titles.ts +379 -0
- package/src/trailer/url.ts +108 -0
- package/src/trailer/vertical-draw.ts +286 -0
- package/src/trailer/vertical.ts +148 -0
- package/src/trailer/vocab.ts +59 -0
- package/src/trailer/words.ts +254 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// composition helpers for a score's `code` and `play` lines: they only build pattern strings in the grammar of
|
|
2
|
+
// pattern.ts, so a score stays plain note data once evaluated. Chords are written per bar, bars split by '|', a bar
|
|
3
|
+
// holding several chords splits evenly: 'C | Am | F G | C'.
|
|
4
|
+
|
|
5
|
+
import { noteToMidi } from './pattern.ts'
|
|
6
|
+
|
|
7
|
+
const NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
|
8
|
+
|
|
9
|
+
export function midiToNote(midi: number): string {
|
|
10
|
+
const oct = Math.floor(midi / 12) - 1
|
|
11
|
+
if (oct < 0 || oct > 8) throw new Error(`note ${midi} outside octaves 0..8`)
|
|
12
|
+
return `${NAMES[midi % 12]}${oct}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** joins pattern fragments into one row string */
|
|
16
|
+
export function seq(...parts: string[]): string {
|
|
17
|
+
return parts.join(' ')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rep(part: string, n: number): string {
|
|
21
|
+
return Array.from({ length: n }, () => part).join(' ')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function splitMods(tok: string): { mods: string; body: string } {
|
|
25
|
+
const i = tok.lastIndexOf(':')
|
|
26
|
+
return i === -1 ? { mods: '', body: tok } : { mods: tok.slice(0, i + 1), body: tok.slice(i + 1) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** shifts every note token by `semis` (plain rows or `mel` shorthand), modifiers, holds, rests and drums kept */
|
|
30
|
+
export function transpose(text: string, semis: number): string {
|
|
31
|
+
return text
|
|
32
|
+
.split(/\s+/)
|
|
33
|
+
.filter((t) => t !== '')
|
|
34
|
+
.map((tok) => {
|
|
35
|
+
const { mods, body } = splitMods(tok)
|
|
36
|
+
const [note = '', len] = body.split('/')
|
|
37
|
+
const midi = noteToMidi(note)
|
|
38
|
+
return midi === null ? tok : mods + midiToNote(midi + semis) + (len === undefined ? '' : `/${len}`)
|
|
39
|
+
})
|
|
40
|
+
.join(' ')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* durations shorthand: `E5/4` is E5 held 4 rows, `-/2` two rows of silence,
|
|
45
|
+
* a bare token lasts `unit` rows. Modifiers glue before the note:
|
|
46
|
+
* `v10:E5/4`, `@bell:C6/2`. Drum letters work the same way.
|
|
47
|
+
*/
|
|
48
|
+
export function mel(text: string, unit = 1): string {
|
|
49
|
+
const out: string[] = []
|
|
50
|
+
for (const tok of text.split(/\s+/)) {
|
|
51
|
+
if (tok === '' || tok === '|') continue
|
|
52
|
+
const slash = tok.lastIndexOf('/')
|
|
53
|
+
const head = slash === -1 ? tok : tok.slice(0, slash)
|
|
54
|
+
const len = slash === -1 ? unit : Number(tok.slice(slash + 1))
|
|
55
|
+
if (!Number.isInteger(len) || len < 1) throw new Error(`mel token "${tok}": bad length`)
|
|
56
|
+
const { body } = splitMods(head)
|
|
57
|
+
out.push(head)
|
|
58
|
+
for (let i = 1; i < len; i++) out.push(body === '-' ? '-' : '.')
|
|
59
|
+
}
|
|
60
|
+
return out.join(' ')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// chord qualities as semitone stacks above the root
|
|
64
|
+
const QUALITIES: Record<string, number[]> = {
|
|
65
|
+
'': [0, 4, 7],
|
|
66
|
+
m: [0, 3, 7],
|
|
67
|
+
'7': [0, 4, 7, 10],
|
|
68
|
+
m7: [0, 3, 7, 10],
|
|
69
|
+
maj7: [0, 4, 7, 11],
|
|
70
|
+
dim: [0, 3, 6],
|
|
71
|
+
dim7: [0, 3, 6, 9],
|
|
72
|
+
aug: [0, 4, 8],
|
|
73
|
+
sus4: [0, 5, 7],
|
|
74
|
+
sus2: [0, 2, 7],
|
|
75
|
+
add9: [0, 4, 7, 14],
|
|
76
|
+
m9: [0, 3, 7, 10, 14],
|
|
77
|
+
'6': [0, 4, 7, 9],
|
|
78
|
+
m6: [0, 3, 7, 9],
|
|
79
|
+
'5': [0, 7],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Chord {
|
|
83
|
+
/** pitch class 0..11 of the bass note (the slash note when given) */
|
|
84
|
+
bass: number
|
|
85
|
+
root: number
|
|
86
|
+
tones: number[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 'C', 'F#m7', 'Bbmaj7', 'G/B' */
|
|
90
|
+
export function parseChord(sym: string): Chord {
|
|
91
|
+
const [main = '', slash] = sym.split('/')
|
|
92
|
+
const m = /^([A-G])([#b]?)(.*)$/.exec(main)
|
|
93
|
+
if (!m) throw new Error(`chord "${sym}": no root`)
|
|
94
|
+
const pc = (l: string, a: string) => ((noteToMidi(`${l}${a}4`) ?? 0) + 120) % 12
|
|
95
|
+
const root = pc(m[1] ?? 'C', m[2] ?? '')
|
|
96
|
+
const q = QUALITIES[m[3] ?? '']
|
|
97
|
+
if (!q) throw new Error(`chord "${sym}": unknown quality "${m[3]}"`)
|
|
98
|
+
let bass = root
|
|
99
|
+
if (slash !== undefined) {
|
|
100
|
+
const s = /^([A-G])([#b]?)$/.exec(slash)
|
|
101
|
+
if (!s) throw new Error(`chord "${sym}": bad slash bass`)
|
|
102
|
+
bass = pc(s[1] ?? 'C', s[2] ?? '')
|
|
103
|
+
}
|
|
104
|
+
return { root, bass, tones: q }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface Seg {
|
|
108
|
+
chord: Chord
|
|
109
|
+
rows: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** the chord chart as segments; `bar` is rows per bar */
|
|
113
|
+
export function chart(text: string, bar = 16): Seg[] {
|
|
114
|
+
const segs: Seg[] = []
|
|
115
|
+
for (const b of text.split('|')) {
|
|
116
|
+
const syms = b.split(/\s+/).filter((s) => s !== '')
|
|
117
|
+
if (syms.length === 0) continue
|
|
118
|
+
if (bar % syms.length !== 0) throw new Error(`chart bar "${b}" does not split ${bar} rows evenly`)
|
|
119
|
+
for (const s of syms) segs.push({ chord: parseChord(s), rows: bar / syms.length })
|
|
120
|
+
}
|
|
121
|
+
return segs
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** lowest midi of pitch class `pc` at or above the C of `oct` */
|
|
125
|
+
function at(pc: number, oct: number): number {
|
|
126
|
+
return 12 * (oct + 1) + pc
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type BassStyle =
|
|
130
|
+
| 'hold' // root held the whole segment
|
|
131
|
+
| 'half' // root, then fifth at half
|
|
132
|
+
| 'quarter' // root on every beat
|
|
133
|
+
| 'octave' // eighths bouncing root, octave
|
|
134
|
+
| 'drive' // straight eighths on the root, octave jump on the last
|
|
135
|
+
| 'gallop' // eighth + two sixteenths per beat
|
|
136
|
+
| 'walk' // quarters root, third, fifth, a step to the next root
|
|
137
|
+
| 'waltz' // root on the beat, chord fifth on beats 2 and 3 (3 beat bars)
|
|
138
|
+
| 'pulse' // root on beats 1 and 3, rests between
|
|
139
|
+
|
|
140
|
+
/** a bass line over a chart, in octave `oct` (2 = the usual register) */
|
|
141
|
+
export function bass(text: string, style: BassStyle, oct = 2, bar = 16, beat = 4): string {
|
|
142
|
+
const segs = chart(text, bar)
|
|
143
|
+
const out: string[] = []
|
|
144
|
+
segs.forEach((seg, i) => {
|
|
145
|
+
const r = at(seg.chord.bass, oct)
|
|
146
|
+
const fifth = at(seg.chord.root, oct) + 7
|
|
147
|
+
const third = at(seg.chord.root, oct) + (seg.chord.tones[1] ?? 4)
|
|
148
|
+
const next = segs[(i + 1) % segs.length]?.chord.bass ?? seg.chord.bass
|
|
149
|
+
const rows: string[] = []
|
|
150
|
+
const put = (row: number, midi: number | '-') => {
|
|
151
|
+
rows[row] = midi === '-' ? '-' : midiToNote(midi)
|
|
152
|
+
}
|
|
153
|
+
for (let k = 0; k < seg.rows; k++) rows.push('.')
|
|
154
|
+
switch (style) {
|
|
155
|
+
case 'hold':
|
|
156
|
+
put(0, r)
|
|
157
|
+
break
|
|
158
|
+
case 'half':
|
|
159
|
+
put(0, r)
|
|
160
|
+
if (seg.rows >= 2) put(seg.rows / 2, fifth > r + 9 ? fifth - 12 : fifth)
|
|
161
|
+
break
|
|
162
|
+
case 'quarter':
|
|
163
|
+
for (let k = 0; k < seg.rows; k += beat) put(k, r)
|
|
164
|
+
break
|
|
165
|
+
case 'octave':
|
|
166
|
+
for (let k = 0; k < seg.rows; k += beat / 2) put(k, (k / (beat / 2)) % 2 === 0 ? r : r + 12)
|
|
167
|
+
break
|
|
168
|
+
case 'drive':
|
|
169
|
+
for (let k = 0; k < seg.rows; k += beat / 2) put(k, k === seg.rows - beat / 2 && seg.rows >= bar ? r + 12 : r)
|
|
170
|
+
break
|
|
171
|
+
case 'gallop':
|
|
172
|
+
for (let k = 0; k < seg.rows; k += beat) {
|
|
173
|
+
put(k, r)
|
|
174
|
+
if (k + 2 < seg.rows) put(k + 2, r)
|
|
175
|
+
if (k + 3 < seg.rows) put(k + 3, k + beat >= seg.rows ? r + 12 : r)
|
|
176
|
+
}
|
|
177
|
+
break
|
|
178
|
+
case 'walk': {
|
|
179
|
+
const target = at(next, oct)
|
|
180
|
+
const approach = target > r ? target - 1 : target + 1
|
|
181
|
+
const steps = [r, third, fifth > r + 9 ? fifth - 12 : fifth, approach]
|
|
182
|
+
for (let k = 0, j = 0; k < seg.rows; k += beat, j++) put(k, steps[Math.min(j, 3)] ?? r)
|
|
183
|
+
if (seg.rows <= beat * 2) {
|
|
184
|
+
rows.fill('.')
|
|
185
|
+
put(0, r)
|
|
186
|
+
if (seg.rows > beat) put(beat, approach)
|
|
187
|
+
}
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
case 'waltz':
|
|
191
|
+
for (let k = 0; k < seg.rows; k += beat) put(k, k === 0 ? r : fifth > r + 9 ? fifth - 12 : fifth)
|
|
192
|
+
break
|
|
193
|
+
case 'pulse':
|
|
194
|
+
for (let k = 0; k < seg.rows; k += beat * 2) {
|
|
195
|
+
put(k, r)
|
|
196
|
+
if (k + beat < seg.rows) put(k + beat, '-')
|
|
197
|
+
}
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
out.push(...rows)
|
|
201
|
+
})
|
|
202
|
+
return out.join(' ')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* an arpeggio over a chart: `shape` indexes the chord tones upward (0 root,
|
|
207
|
+
* 1 third, 2 fifth, 3 the root an octave up for a triad...), one index per
|
|
208
|
+
* step of `step` rows, cycling through the segment.
|
|
209
|
+
*/
|
|
210
|
+
export function arp(text: string, shape: number[], oct = 4, step = 2, bar = 16): string {
|
|
211
|
+
const out: string[] = []
|
|
212
|
+
for (const seg of chart(text, bar)) {
|
|
213
|
+
const n = seg.chord.tones.length
|
|
214
|
+
for (let k = 0, j = 0; k < seg.rows; k += step, j++) {
|
|
215
|
+
const idx = shape[j % shape.length] ?? 0
|
|
216
|
+
const iv = (seg.chord.tones[idx % n] ?? 0) + 12 * Math.floor(idx / n)
|
|
217
|
+
out.push(midiToNote(at(seg.chord.root, oct) + iv))
|
|
218
|
+
for (let h = 1; h < step && k + h < seg.rows; h++) out.push('.')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out.join(' ')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* one voice of a held chord: tone `which` of each chord, placed at or above
|
|
226
|
+
* `floor` (a midi note), re-struck per segment. Stack two or three pad tracks
|
|
227
|
+
* on different tones for a full chord.
|
|
228
|
+
*/
|
|
229
|
+
export function voice(text: string, which: number, floor: number, bar = 16, restrike = 0): string {
|
|
230
|
+
const out: string[] = []
|
|
231
|
+
for (const seg of chart(text, bar)) {
|
|
232
|
+
const n = seg.chord.tones.length
|
|
233
|
+
const iv = (seg.chord.tones[which % n] ?? 0) + 12 * Math.floor(which / n)
|
|
234
|
+
let m = seg.chord.root + iv
|
|
235
|
+
while (m < floor) m += 12
|
|
236
|
+
const note = midiToNote(m)
|
|
237
|
+
for (let k = 0; k < seg.rows; k++) out.push(k === 0 || (restrike > 0 && k % restrike === 0) ? note : '.')
|
|
238
|
+
}
|
|
239
|
+
return out.join(' ')
|
|
240
|
+
}
|
|
@@ -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' }
|