@hraness/dawg 0.2.0 → 0.4.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 (140) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/DAWG.md +762 -11
  3. package/README.md +79 -30
  4. package/core/chords.ts +1724 -0
  5. package/core/diff.ts +261 -0
  6. package/core/euclid.ts +670 -0
  7. package/core/fx.ts +1065 -0
  8. package/core/key.ts +43 -0
  9. package/core/kits.ts +320 -0
  10. package/core/params.ts +111 -0
  11. package/core/pitch.ts +60 -0
  12. package/core/rhythm.ts +287 -0
  13. package/core/score.ts +1054 -14
  14. package/core/sdk/eval-child.ts +125 -0
  15. package/core/sdk/eval.ts +260 -0
  16. package/core/sdk/print.ts +675 -0
  17. package/core/sdk/sync-chords.ts +54 -0
  18. package/core/sdk/v1.ts +4159 -0
  19. package/core/slug.ts +19 -0
  20. package/core/synth.ts +1001 -0
  21. package/package.json +8 -5
  22. package/src/agent/agent.ts +317 -16
  23. package/src/agent/brief.ts +50 -5
  24. package/src/agent/chord-tools.ts +357 -0
  25. package/src/agent/drum-tools.ts +135 -0
  26. package/src/agent/gateway.ts +215 -41
  27. package/src/agent/models.ts +633 -0
  28. package/src/agent/ops.ts +3 -18
  29. package/src/agent/pack-tools.ts +369 -0
  30. package/src/agent/planner.ts +55 -2
  31. package/src/agent/preview-tool.ts +270 -0
  32. package/src/agent/provider.ts +234 -106
  33. package/src/agent/rhythm-tools.ts +145 -0
  34. package/src/agent/sse.ts +31 -7
  35. package/src/agent/tools.ts +705 -14
  36. package/src/agent/usage.ts +296 -0
  37. package/src/agent/workspace.ts +683 -0
  38. package/src/agent/xcb-agent.ts +35 -14
  39. package/src/agent/xcb.ts +233 -21
  40. package/src/audio/audition.ts +93 -0
  41. package/src/audio/cache.ts +160 -0
  42. package/src/audio/click.ts +125 -0
  43. package/src/audio/effects/bus.ts +147 -0
  44. package/src/audio/effects/chain.ts +99 -0
  45. package/src/audio/effects/common.ts +251 -0
  46. package/src/audio/effects/convolution.ts +301 -0
  47. package/src/audio/effects/drive.ts +142 -0
  48. package/src/audio/effects/duck.ts +122 -0
  49. package/src/audio/effects/dynamics.ts +121 -0
  50. package/src/audio/effects/filter.ts +319 -0
  51. package/src/audio/effects/modulation.ts +174 -0
  52. package/src/audio/effects/space.ts +337 -0
  53. package/src/audio/engine.ts +430 -33
  54. package/src/audio/kits.ts +200 -0
  55. package/src/audio/live.ts +152 -0
  56. package/src/audio/packs.ts +1787 -0
  57. package/src/audio/player.ts +19 -4
  58. package/src/audio/preview.ts +470 -0
  59. package/src/audio/random.ts +15 -0
  60. package/src/audio/render-worker.ts +68 -0
  61. package/src/audio/renderer.ts +174 -0
  62. package/src/audio/sampler.ts +420 -0
  63. package/src/audio/samples.ts +1025 -0
  64. package/src/audio/synth/oscillators.ts +268 -0
  65. package/src/audio/synth/voice.ts +555 -0
  66. package/src/audio/synth/zzfx.ts +137 -0
  67. package/src/audio/wav.ts +550 -334
  68. package/src/audio/wavetable-maker.ts +717 -0
  69. package/src/audio/wavetable.ts +624 -0
  70. package/src/auth/cli.ts +146 -27
  71. package/src/auth/credentials.ts +167 -41
  72. package/src/auth/discover.ts +481 -0
  73. package/src/auth/login.ts +885 -128
  74. package/src/auth/openrouter.ts +206 -0
  75. package/src/auth/picker.ts +282 -0
  76. package/src/auth/runner.ts +25 -2
  77. package/src/auth/tui.ts +60 -43
  78. package/src/commands/drums.ts +299 -0
  79. package/src/commands/edit.ts +192 -0
  80. package/src/commands/fx.ts +360 -0
  81. package/src/commands/help.ts +535 -0
  82. package/src/commands/history.ts +32 -19
  83. package/src/commands/music.ts +34 -15
  84. package/src/commands/pack.ts +423 -0
  85. package/src/commands/rhythm.ts +230 -0
  86. package/src/commands/sample.ts +462 -0
  87. package/src/commands/synth.ts +229 -0
  88. package/src/commands/wavetable.ts +370 -0
  89. package/src/main.ts +2136 -170
  90. package/src/media/analyze.ts +364 -0
  91. package/src/media/backend.ts +253 -0
  92. package/src/media/cli.ts +186 -0
  93. package/src/media/download.ts +281 -0
  94. package/src/media/dsp.ts +281 -0
  95. package/src/media/import.ts +130 -0
  96. package/src/media/lyrics.ts +201 -0
  97. package/src/media/notes.ts +363 -0
  98. package/src/media/paths.ts +168 -0
  99. package/src/media/process.ts +226 -0
  100. package/src/media/registry.ts +9 -0
  101. package/src/media/sidecar.ts +72 -0
  102. package/src/media/stemdeck.ts +254 -0
  103. package/src/media/stems.ts +173 -0
  104. package/src/media/tools.ts +397 -0
  105. package/src/media/types.ts +92 -0
  106. package/src/media/vendor/basic-pitch.ts +261 -0
  107. package/src/media/vendor/drums.ts +817 -0
  108. package/src/media/vendor/grid.ts +203 -0
  109. package/src/media/vendor/util.ts +139 -0
  110. package/src/media/vendor/wav.ts +233 -0
  111. package/src/media/wavetable.ts +202 -0
  112. package/src/project/check.ts +80 -0
  113. package/src/project/init.ts +253 -0
  114. package/src/project/sync.ts +432 -0
  115. package/src/project/typecheck.ts +149 -0
  116. package/src/render.ts +49 -6
  117. package/src/session/attach.ts +3 -4
  118. package/src/session/daemon.ts +25 -8
  119. package/src/session/delta.ts +249 -0
  120. package/src/session/naming.ts +6 -38
  121. package/src/session/port.ts +26 -6
  122. package/src/session/rebase.ts +38 -8
  123. package/src/session/store.ts +116 -21
  124. package/src/tui/audition.ts +501 -0
  125. package/src/tui/euclid.ts +472 -0
  126. package/src/tui/menu.ts +2125 -0
  127. package/src/tui/play-chords.ts +538 -0
  128. package/src/tui/play-mode.ts +442 -0
  129. package/src/tui/play-session.ts +984 -0
  130. package/src/tui/sketch.ts +108 -0
  131. package/src/web/fetch.ts +340 -0
  132. package/src/web/http.ts +137 -0
  133. package/src/web/search.ts +684 -0
  134. package/tui/activity.ts +42 -3
  135. package/tui/app.ts +398 -33
  136. package/tui/drums.ts +44 -0
  137. package/tui/grammar.ts +281 -0
  138. package/tui/highway.ts +24 -2
  139. package/tui/layers.ts +14 -2
  140. package/tui/play-strip.ts +188 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Drum kits: the synthesized kits from `core/kits.ts` rendered sample by
3
+ * sample, and the one catalog `/kit`, `/menu` and the agent list.
4
+ *
5
+ * The renderer calls `kitDrumSample` only for a track with `kit` set, so the
6
+ * default voices in `wav.ts` stay byte-identical. Everything here is plain
7
+ * arithmetic on the seeded noise `wav.ts` passes in, so a kit renders the
8
+ * same buffer on every run.
9
+ */
10
+ import type { DrumVoice } from "../../core/drums.ts";
11
+ import {
12
+ SYNTH_KITS,
13
+ type DrumBodyParams,
14
+ type HatParams,
15
+ type SynthKit,
16
+ synthKit,
17
+ } from "../../core/kits.ts";
18
+ import type { TrackScore } from "../../core/score.ts";
19
+ import { DEFAULT_KITS } from "./packs.ts";
20
+
21
+ /** Per-hit oscillator and filter state. */
22
+ export type KitVoiceState = {
23
+ phase: number;
24
+ band: number;
25
+ dark: number;
26
+ metalPrevious: number;
27
+ holdLeft: number;
28
+ held: number;
29
+ };
30
+
31
+ export function newKitVoiceState(): KitVoiceState {
32
+ return {
33
+ phase: 0,
34
+ band: 0,
35
+ dark: 0,
36
+ metalPrevious: 0,
37
+ holdLeft: 0,
38
+ held: 0,
39
+ };
40
+ }
41
+
42
+ /** 808 hi-hat oscillator frequencies (six detuned squares). */
43
+ const METAL_HZ = [205.3, 304.4, 369.6, 522.7, 540, 800] as const;
44
+
45
+ function body(
46
+ params: DrumBodyParams,
47
+ t: number,
48
+ noise: number,
49
+ state: KitVoiceState,
50
+ sampleRate: number,
51
+ ): number {
52
+ const frequency =
53
+ params.base + params.sweep * Math.exp(-t * params.sweepRate);
54
+ state.phase += frequency / sampleRate;
55
+ return (
56
+ Math.sin(2 * Math.PI * state.phase) *
57
+ Math.exp(-t * params.decay) *
58
+ params.level +
59
+ noise * params.click * Math.exp(-t * params.clickDecay)
60
+ );
61
+ }
62
+
63
+ function hat(
64
+ params: HatParams,
65
+ t: number,
66
+ noise: number,
67
+ bright: number,
68
+ state: KitVoiceState,
69
+ ): number {
70
+ state.dark += 0.3 * (noise - state.dark);
71
+ const noisy = params.tone * bright + (1 - params.tone) * state.dark * 0.8;
72
+ let source = noisy;
73
+ if (params.metal > 0) {
74
+ let square = 0;
75
+ for (const hz of METAL_HZ) square += (t * hz) % 1 < 0.5 ? 1 : -1;
76
+ // First difference: keep the metallic top end, drop the low beating.
77
+ const metal = (square / METAL_HZ.length - state.metalPrevious) * 1.6;
78
+ state.metalPrevious = square / METAL_HZ.length;
79
+ source = params.metal * metal + (1 - params.metal) * noisy;
80
+ }
81
+ return source * params.level * Math.exp(-t * params.decay);
82
+ }
83
+
84
+ /** One sample of `voice` in `kit` at `t` seconds after the hit. */
85
+ export function kitDrumSample(
86
+ kit: SynthKit,
87
+ voice: DrumVoice,
88
+ t: number,
89
+ noise: number,
90
+ bright: number,
91
+ state: KitVoiceState,
92
+ sampleRate: number,
93
+ ): number {
94
+ let sample: number;
95
+ if (voice === "kick") sample = body(kit.kick, t, noise, state, sampleRate);
96
+ else if (voice === "tom") sample = body(kit.tom, t, noise, state, sampleRate);
97
+ else if (voice === "snare") {
98
+ const p = kit.snare;
99
+ state.phase += p.tone / sampleRate;
100
+ const wires = p.snap * bright + (1 - p.snap) * noise;
101
+ sample =
102
+ Math.sin(2 * Math.PI * state.phase) *
103
+ p.toneLevel *
104
+ Math.exp(-t * p.toneDecay) +
105
+ wires * p.noise * Math.exp(-t * p.noiseDecay);
106
+ } else if (voice === "clap") {
107
+ const p = kit.clap;
108
+ state.band += p.band * (bright - state.band);
109
+ const span = p.gap * 3;
110
+ const burst = t < span ? (Math.floor(t / p.gap) % 2 === 0 ? 1 : 0.35) : 0;
111
+ sample =
112
+ state.band *
113
+ p.level *
114
+ (burst + 0.8 * Math.exp(-(t - span) * p.tail) * +(t >= span));
115
+ } else if (voice === "hat") sample = hat(kit.hat, t, noise, bright, state);
116
+ else if (voice === "openhat")
117
+ sample = hat(kit.openhat, t, noise, bright, state);
118
+ else {
119
+ const p = kit.rim;
120
+ sample =
121
+ (Math.sin(2 * Math.PI * p.high * t) * 0.6 +
122
+ Math.sin(2 * Math.PI * p.low * t) * 0.4) *
123
+ Math.exp(-t * p.decay) *
124
+ p.level;
125
+ }
126
+ if (kit.drive > 0) {
127
+ const amount = 1 + kit.drive * 4;
128
+ sample = Math.tanh(sample * amount) / Math.tanh(amount);
129
+ }
130
+ if (kit.bits > 0) {
131
+ const steps = 2 ** (kit.bits - 1);
132
+ sample = Math.round(sample * steps) / steps;
133
+ }
134
+ if (kit.hold > 1) {
135
+ if (state.holdLeft <= 0) {
136
+ state.held = sample;
137
+ state.holdLeft = kit.hold;
138
+ }
139
+ state.holdLeft -= 1;
140
+ sample = state.held;
141
+ }
142
+ return sample * kit.gain;
143
+ }
144
+
145
+ /** Longest synth-kit one-shot in the score; 0 without kits (so renders without kits are unchanged). */
146
+ export function kitTailSeconds(score: TrackScore): number {
147
+ let seconds = 0;
148
+ for (const track of score.tracks)
149
+ seconds = Math.max(seconds, synthKit(track.kit)?.seconds ?? 0);
150
+ return seconds;
151
+ }
152
+
153
+ // ── catalog ────────────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * One entry of the kit catalog. `synth` kits render offline from the
157
+ * parameters in `core/kits.ts`; `sample` kits are pack banks fetched on
158
+ * first use (`src/audio/packs.ts`). New kinds (a user's own folder of
159
+ * samples, more packs) slot in here without touching the pickers.
160
+ */
161
+ export type KitEntry = Readonly<{
162
+ name: string;
163
+ kind: "synth" | "sample";
164
+ label: string;
165
+ detail: string;
166
+ /** The prompt command that puts this kit on the focused track. */
167
+ command: string;
168
+ }>;
169
+
170
+ /** The default synth voices, listed first. */
171
+ export const DEFAULT_SYNTH_KIT = "default";
172
+
173
+ export function kitCatalog(): readonly KitEntry[] {
174
+ const synth: KitEntry[] = [
175
+ {
176
+ name: DEFAULT_SYNTH_KIT,
177
+ kind: "synth",
178
+ label: "Default (synth)",
179
+ detail: "the built-in voices",
180
+ command: `/kit ${DEFAULT_SYNTH_KIT}`,
181
+ },
182
+ ...SYNTH_KITS.map((entry): KitEntry => ({
183
+ name: entry.name,
184
+ kind: "synth",
185
+ label: entry.label,
186
+ detail: entry.description,
187
+ command: `/kit ${entry.name}`,
188
+ })),
189
+ ];
190
+ const sample = Object.entries(DEFAULT_KITS).map(
191
+ ([name, entry]): KitEntry => ({
192
+ name,
193
+ kind: "sample",
194
+ label: `${name} ${entry.bank || entry.pack}`,
195
+ detail: `${entry.pack} samples (fetched once)`,
196
+ command: `/kit ${name}`,
197
+ }),
198
+ );
199
+ return Object.freeze([...synth, ...sample]);
200
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Live voices for play mode: one note of one track, rendered through the
3
+ * same offline renderer the loop uses (`renderScorePcm`), so a key sounds
4
+ * exactly like the note it records as: the track's instrument voice (synth,
5
+ * drum kit or sampler), filter, pan, delay and reverb. Nothing is forked;
6
+ * this module only builds a one-note score and caches the result.
7
+ *
8
+ * The offline renderer is untouched, so loop renders and exports stay
9
+ * byte-identical. Live audio never reaches a render or an export: the engine
10
+ * mixes it into the stream only.
11
+ */
12
+ import {
13
+ SCORE_LIMITS,
14
+ TrackScore,
15
+ isSamplerInstrument,
16
+ type Track,
17
+ } from "../../core/score.ts";
18
+ import { sampleKey, type SampleBank } from "./samples.ts";
19
+ import { RENDER_CHANNELS, renderScorePcm } from "./wav.ts";
20
+
21
+ /** A rendered live note: interleaved stereo 16-bit PCM. */
22
+ export type LiveNotePcm = Readonly<{ pcm: Int16Array; frames: number }>;
23
+
24
+ export type LiveNoteRequest = Readonly<{
25
+ score: TrackScore;
26
+ trackId: string;
27
+ pitch: number;
28
+ /** 0..1, as score notes store it. */
29
+ velocity: number;
30
+ /** How long the key is held. */
31
+ seconds: number;
32
+ /** Decoded sampler voices; without it sampler tracks are silent. */
33
+ samples?: SampleBank;
34
+ }>;
35
+
36
+ /** Longest a single live note is rendered for (a held sustain pedal). */
37
+ export const MAX_LIVE_NOTE_SECONDS = 8;
38
+ const CACHE_ENTRIES = 96;
39
+
40
+ /**
41
+ * Renders and caches live notes. Repeated keys hit the cache, so the key
42
+ * handler only pays for a render the first time a pitch/length is played.
43
+ */
44
+ export class LiveSynth {
45
+ private readonly cache = new Map<string, LiveNotePcm>();
46
+
47
+ public constructor(private readonly sampleRate: number) {}
48
+
49
+ public get rate(): number {
50
+ return this.sampleRate;
51
+ }
52
+
53
+ public get cached(): number {
54
+ return this.cache.size;
55
+ }
56
+
57
+ public render(request: LiveNoteRequest): LiveNotePcm | undefined {
58
+ const track = request.score.tracks.find(
59
+ (candidate) => candidate.id === request.trackId,
60
+ );
61
+ if (!track) return undefined;
62
+ const { score } = request;
63
+ const ticksPerSecond = (score.ticksPerBeat * score.tempoBpm) / 60;
64
+ const seconds = Math.max(
65
+ 0.01,
66
+ Math.min(MAX_LIVE_NOTE_SECONDS, request.seconds),
67
+ );
68
+ const durationTicks = Math.max(
69
+ 1,
70
+ Math.min(SCORE_LIMITS.maxTick, Math.round(seconds * ticksPerSecond)),
71
+ );
72
+ const velocity = Math.max(0, Math.min(1, request.velocity));
73
+ const pitch = Math.max(0, Math.min(127, Math.round(request.pitch)));
74
+ const key = JSON.stringify([
75
+ liveTrack(track),
76
+ pitch,
77
+ velocity,
78
+ durationTicks,
79
+ score.tempoBpm,
80
+ score.ticksPerBeat,
81
+ this.sampleRate,
82
+ samplerDigest(track, request.samples),
83
+ ]);
84
+ const hit = this.cache.get(key);
85
+ if (hit) {
86
+ // Refresh LRU order.
87
+ this.cache.delete(key);
88
+ this.cache.set(key, hit);
89
+ return hit;
90
+ }
91
+ const beats = durationTicks / score.ticksPerBeat;
92
+ const bars = Math.max(
93
+ 1,
94
+ Math.min(SCORE_LIMITS.maxBars, Math.ceil(beats / score.beatsPerBar)),
95
+ );
96
+ const single = new TrackScore({
97
+ tempoBpm: score.tempoBpm,
98
+ beatsPerBar: score.beatsPerBar,
99
+ bars,
100
+ ticksPerBeat: score.ticksPerBeat,
101
+ tracks: [liveTrack(track)],
102
+ notes: [
103
+ {
104
+ id: "live",
105
+ trackId: track.id,
106
+ startTick: 0,
107
+ durationTicks,
108
+ pitch,
109
+ velocity,
110
+ },
111
+ ],
112
+ });
113
+ const audio = renderScorePcm(single, {
114
+ sampleRate: this.sampleRate,
115
+ maxSeconds: MAX_LIVE_NOTE_SECONDS + 4,
116
+ ...(request.samples ? { samples: request.samples } : {}),
117
+ });
118
+ const frames = audibleFrames(audio.pcm);
119
+ const rendered: LiveNotePcm = {
120
+ pcm: audio.pcm.subarray(0, frames * RENDER_CHANNELS),
121
+ frames,
122
+ };
123
+ this.cache.set(key, rendered);
124
+ while (this.cache.size > CACHE_ENTRIES) {
125
+ const oldest = this.cache.keys().next().value;
126
+ if (oldest === undefined) break;
127
+ this.cache.delete(oldest);
128
+ }
129
+ return rendered;
130
+ }
131
+ }
132
+
133
+ /** The track as it sounds live: audible regardless of mute and solo. */
134
+ function liveTrack(track: Track): Track {
135
+ const { solo: _solo, ...rest } = track;
136
+ return { ...rest, muted: false };
137
+ }
138
+
139
+ function samplerDigest(track: Track, bank: SampleBank | undefined): string[] {
140
+ if (!isSamplerInstrument(track.instrument) || !track.sampler || !bank)
141
+ return [];
142
+ return Object.keys(track.sampler.voices)
143
+ .sort()
144
+ .map((voice) => bank.voices.get(sampleKey(track.id, voice))?.sha256 ?? "-");
145
+ }
146
+
147
+ /** Frames up to the last non-silent sample, so mixing skips the dead tail. */
148
+ function audibleFrames(pcm: Int16Array): number {
149
+ let last = pcm.length - 1;
150
+ while (last >= 0 && pcm[last] === 0) last -= 1;
151
+ return Math.ceil((last + 1) / RENDER_CHANNELS);
152
+ }