@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.
- package/CHANGELOG.md +201 -0
- package/DAWG.md +762 -11
- package/README.md +79 -30
- package/core/chords.ts +1724 -0
- package/core/diff.ts +261 -0
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/key.ts +43 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/pitch.ts +60 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +1054 -14
- package/core/sdk/eval-child.ts +125 -0
- package/core/sdk/eval.ts +260 -0
- package/core/sdk/print.ts +675 -0
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +4159 -0
- package/core/slug.ts +19 -0
- package/core/synth.ts +1001 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +317 -16
- package/src/agent/brief.ts +50 -5
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +55 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +705 -14
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +35 -14
- package/src/agent/xcb.ts +233 -21
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +99 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +301 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +337 -0
- package/src/audio/engine.ts +430 -33
- package/src/audio/kits.ts +200 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +420 -0
- package/src/audio/samples.ts +1025 -0
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +550 -334
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/auth/cli.ts +146 -27
- package/src/auth/credentials.ts +167 -41
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +885 -128
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +25 -2
- package/src/auth/tui.ts +60 -43
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +192 -0
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +535 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +34 -15
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +462 -0
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +2136 -170
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +186 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +397 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/media/wavetable.ts +202 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +49 -6
- package/src/session/attach.ts +3 -4
- package/src/session/daemon.ts +25 -8
- package/src/session/delta.ts +249 -0
- package/src/session/naming.ts +6 -38
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +2125 -0
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +984 -0
- package/src/tui/sketch.ts +108 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +684 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +398 -33
- package/tui/drums.ts +44 -0
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +24 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +188 -0
package/core/rhythm.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rhythm rows on a score: the glue between `core/euclid.ts` generators and
|
|
3
|
+
* the stored notes.
|
|
4
|
+
*
|
|
5
|
+
* A track's `rhythm` rows each own one voice lane (a drum voice, a one-shot
|
|
6
|
+
* sampler voice, or a pitch). `withTrackRhythm` replaces the rows and
|
|
7
|
+
* regenerates exactly those lanes' notes, so an edit is one next score and
|
|
8
|
+
* therefore one revision and one undo step. Other notes on the track, and
|
|
9
|
+
* every other track, are untouched. Generated notes carry deterministic ids
|
|
10
|
+
* (`rh.<track hash>.<voice>.<n>`), so the same rows always produce the same
|
|
11
|
+
* score and `diffScores` sees only real changes.
|
|
12
|
+
*/
|
|
13
|
+
import { isDrumInstrument, parseDrumVoice, drumVoicePitch } from "./drums.ts";
|
|
14
|
+
import { expandRow, rowSummary, type RhythmRow } from "./euclid.ts";
|
|
15
|
+
import { pitchToMidi } from "./pitch.ts";
|
|
16
|
+
import {
|
|
17
|
+
SCORE_LIMITS,
|
|
18
|
+
ScoreValidationError,
|
|
19
|
+
TrackScore,
|
|
20
|
+
isSamplerInstrument,
|
|
21
|
+
samplerVoiceSlots,
|
|
22
|
+
type Note,
|
|
23
|
+
type Track,
|
|
24
|
+
} from "./score.ts";
|
|
25
|
+
|
|
26
|
+
/** MIDI pitch a row's voice plays on `track`, or undefined when unknown. */
|
|
27
|
+
export function rhythmVoicePitch(
|
|
28
|
+
track: Track,
|
|
29
|
+
voice: string,
|
|
30
|
+
): number | undefined {
|
|
31
|
+
const name = voice.trim();
|
|
32
|
+
if (isSamplerInstrument(track.instrument) && track.sampler) {
|
|
33
|
+
const slot = samplerVoiceSlots(track.sampler).get(name);
|
|
34
|
+
if (slot !== undefined) return slot;
|
|
35
|
+
}
|
|
36
|
+
if (isDrumInstrument(track.instrument)) {
|
|
37
|
+
const drum = parseDrumVoice(name);
|
|
38
|
+
if (drum) return drumVoicePitch(drum);
|
|
39
|
+
}
|
|
40
|
+
if (/^\d{1,3}$/.test(name)) {
|
|
41
|
+
const midi = Number(name);
|
|
42
|
+
return midi <= 127 ? midi : undefined;
|
|
43
|
+
}
|
|
44
|
+
const midi = pitchToMidi(name);
|
|
45
|
+
return Number.isNaN(midi) ? undefined : midi;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function trackHash(trackId: string): string {
|
|
49
|
+
let hash = 0x811c9dc5;
|
|
50
|
+
for (let index = 0; index < trackId.length; index += 1) {
|
|
51
|
+
hash ^= trackId.charCodeAt(index);
|
|
52
|
+
hash = Math.imul(hash, 0x01000193);
|
|
53
|
+
}
|
|
54
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function idVoice(voice: string): string {
|
|
58
|
+
return voice.replace(/[^A-Za-z0-9_#-]/g, "_").slice(0, 32);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The notes `row` generates on `track` over the score's loop. */
|
|
62
|
+
export function expandTrackRow(
|
|
63
|
+
score: TrackScore,
|
|
64
|
+
track: Track,
|
|
65
|
+
row: RhythmRow,
|
|
66
|
+
): Note[] {
|
|
67
|
+
const pitch = rhythmVoicePitch(track, row.voice);
|
|
68
|
+
if (pitch === undefined)
|
|
69
|
+
throw new ScoreValidationError(
|
|
70
|
+
`track ${track.id} has no voice "${row.voice}"`,
|
|
71
|
+
"invalid-track",
|
|
72
|
+
);
|
|
73
|
+
const loopTicks = score.bars * score.beatsPerBar * score.ticksPerBeat;
|
|
74
|
+
const prefix = `rh.${trackHash(track.id)}.${idVoice(row.voice)}.`;
|
|
75
|
+
return expandRow(row, { ticksPerBeat: score.ticksPerBeat, loopTicks }).map(
|
|
76
|
+
(hit, index) => ({
|
|
77
|
+
id: `${prefix}${index}`,
|
|
78
|
+
trackId: track.id,
|
|
79
|
+
startTick: hit.startTick,
|
|
80
|
+
durationTicks: Math.min(hit.durationTicks, SCORE_LIMITS.maxTick),
|
|
81
|
+
pitch,
|
|
82
|
+
velocity: hit.velocity,
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Pitches the rows own on `track` (unresolvable voices are skipped). */
|
|
88
|
+
function ownedPitches(track: Track, rows: readonly RhythmRow[]): Set<number> {
|
|
89
|
+
const pitches = new Set<number>();
|
|
90
|
+
for (const row of rows) {
|
|
91
|
+
const pitch = rhythmVoicePitch(track, row.voice);
|
|
92
|
+
if (pitch !== undefined) pitches.add(pitch);
|
|
93
|
+
}
|
|
94
|
+
return pitches;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The score with `trackId`'s rows replaced by `rows` (empty removes them)
|
|
99
|
+
* and the lanes of the old and new rows regenerated. Throws
|
|
100
|
+
* `ScoreValidationError` for an unknown track or voice, or past the note
|
|
101
|
+
* limit.
|
|
102
|
+
*/
|
|
103
|
+
export function withTrackRhythm(
|
|
104
|
+
score: TrackScore,
|
|
105
|
+
trackId: string,
|
|
106
|
+
rows: readonly RhythmRow[],
|
|
107
|
+
): TrackScore {
|
|
108
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
109
|
+
if (!track)
|
|
110
|
+
throw new ScoreValidationError(`no track ${trackId}`, "invalid-track");
|
|
111
|
+
const nextTrack: Track = { ...track };
|
|
112
|
+
const tracks = score.tracks.map((candidate) =>
|
|
113
|
+
candidate.id === trackId
|
|
114
|
+
? { ...candidate, rhythm: rows.length > 0 ? rows : null }
|
|
115
|
+
: candidate,
|
|
116
|
+
);
|
|
117
|
+
const cleared = ownedPitches(track, [...(track.rhythm ?? []), ...rows]);
|
|
118
|
+
const generated = rows.flatMap((row) =>
|
|
119
|
+
expandTrackRow(score, nextTrack, row),
|
|
120
|
+
);
|
|
121
|
+
const notes = [
|
|
122
|
+
...score.notes.filter(
|
|
123
|
+
(note) => note.trackId !== trackId || !cleared.has(note.pitch),
|
|
124
|
+
),
|
|
125
|
+
...generated,
|
|
126
|
+
];
|
|
127
|
+
if (notes.length > SCORE_LIMITS.maxNotes)
|
|
128
|
+
throw new ScoreValidationError(
|
|
129
|
+
`score cannot contain more than ${SCORE_LIMITS.maxNotes} notes`,
|
|
130
|
+
"invalid-score",
|
|
131
|
+
);
|
|
132
|
+
return new TrackScore({ ...score.toJSON(), tracks, notes });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Adds or replaces the row for `row.voice` on the track. */
|
|
136
|
+
export function setRhythmRow(
|
|
137
|
+
score: TrackScore,
|
|
138
|
+
trackId: string,
|
|
139
|
+
row: RhythmRow,
|
|
140
|
+
): TrackScore {
|
|
141
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
142
|
+
const current = track?.rhythm ?? [];
|
|
143
|
+
const index = current.findIndex((existing) => existing.voice === row.voice);
|
|
144
|
+
const rows =
|
|
145
|
+
index < 0
|
|
146
|
+
? [...current, row]
|
|
147
|
+
: current.map((existing, at) => (at === index ? row : existing));
|
|
148
|
+
return withTrackRhythm(score, trackId, rows);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Drops the row for `voice`. With `keepNotes` its generated notes stay as
|
|
153
|
+
* plain notes (the row is "frozen"); otherwise the lane is cleared.
|
|
154
|
+
*/
|
|
155
|
+
export function removeRhythmRow(
|
|
156
|
+
score: TrackScore,
|
|
157
|
+
trackId: string,
|
|
158
|
+
voice: string,
|
|
159
|
+
keepNotes = false,
|
|
160
|
+
): TrackScore {
|
|
161
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
162
|
+
const current = track?.rhythm ?? [];
|
|
163
|
+
const rows = current.filter((row) => row.voice !== voice);
|
|
164
|
+
if (rows.length === current.length) return score;
|
|
165
|
+
if (!keepNotes) return withTrackRhythm(score, trackId, rows);
|
|
166
|
+
return new TrackScore({
|
|
167
|
+
...score.toJSON(),
|
|
168
|
+
tracks: score.tracks.map((candidate) =>
|
|
169
|
+
candidate.id === trackId
|
|
170
|
+
? { ...candidate, rhythm: rows.length > 0 ? rows : null }
|
|
171
|
+
: candidate,
|
|
172
|
+
),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Every track's rows regenerated for the current loop length (after a
|
|
178
|
+
* `bars` change, or for a score fresh from `song.ts`). Returns `score`
|
|
179
|
+
* itself when nothing changes.
|
|
180
|
+
*/
|
|
181
|
+
export function refreshRhythm(score: TrackScore): TrackScore {
|
|
182
|
+
let next = score;
|
|
183
|
+
for (const track of score.tracks)
|
|
184
|
+
if (track.rhythm && !rhythmInSync(next, track.id))
|
|
185
|
+
next = withTrackRhythm(next, track.id, track.rhythm);
|
|
186
|
+
return next;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** True when the track's owned lanes hold exactly what its rows generate. */
|
|
190
|
+
export function rhythmInSync(score: TrackScore, trackId: string): boolean {
|
|
191
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
192
|
+
if (!track?.rhythm) return true;
|
|
193
|
+
let expected: Note[];
|
|
194
|
+
try {
|
|
195
|
+
expected = track.rhythm.flatMap((row) => expandTrackRow(score, track, row));
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const owned = ownedPitches(track, track.rhythm);
|
|
200
|
+
const actual = score.notes.filter(
|
|
201
|
+
(note) => note.trackId === trackId && owned.has(note.pitch),
|
|
202
|
+
);
|
|
203
|
+
return sameNotes(expected, actual);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** True when `row`'s lane on the track holds exactly what the row generates. */
|
|
207
|
+
export function rowInSync(
|
|
208
|
+
score: TrackScore,
|
|
209
|
+
track: Track,
|
|
210
|
+
row: RhythmRow,
|
|
211
|
+
): boolean {
|
|
212
|
+
const pitch = rhythmVoicePitch(track, row.voice);
|
|
213
|
+
if (pitch === undefined) return false;
|
|
214
|
+
let expected: Note[];
|
|
215
|
+
try {
|
|
216
|
+
expected = expandTrackRow(score, track, row);
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const actual = score.notes.filter(
|
|
221
|
+
(note) => note.trackId === track.id && note.pitch === pitch,
|
|
222
|
+
);
|
|
223
|
+
return sameNotes(expected, actual);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function sameNotes(
|
|
227
|
+
expected: readonly Note[],
|
|
228
|
+
actual: readonly Note[],
|
|
229
|
+
): boolean {
|
|
230
|
+
if (actual.length !== expected.length) return false;
|
|
231
|
+
const key = (note: Note) =>
|
|
232
|
+
`${note.pitch}|${note.startTick}|${note.durationTicks}|${note.velocity}`;
|
|
233
|
+
const want = new Map<string, number>();
|
|
234
|
+
for (const note of expected)
|
|
235
|
+
want.set(key(note), (want.get(key(note)) ?? 0) + 1);
|
|
236
|
+
for (const note of actual) {
|
|
237
|
+
const left = want.get(key(note));
|
|
238
|
+
if (!left) return false;
|
|
239
|
+
want.set(key(note), left - 1);
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Keeps rows and their lanes consistent after an arbitrary edit from
|
|
246
|
+
* `previous` to `next`: when the loop grid changed (bars, meter, ticks per
|
|
247
|
+
* beat) every row regenerates; otherwise a row whose lane was hand-edited
|
|
248
|
+
* is frozen (dropped, its notes kept as plain notes). Returns `next` itself
|
|
249
|
+
* when nothing needs to change.
|
|
250
|
+
*/
|
|
251
|
+
export function reconcileRhythm(
|
|
252
|
+
previous: TrackScore,
|
|
253
|
+
next: TrackScore,
|
|
254
|
+
): TrackScore {
|
|
255
|
+
const regridded =
|
|
256
|
+
previous.bars !== next.bars ||
|
|
257
|
+
previous.beatsPerBar !== next.beatsPerBar ||
|
|
258
|
+
previous.ticksPerBeat !== next.ticksPerBeat;
|
|
259
|
+
let out = next;
|
|
260
|
+
for (const track of next.tracks) {
|
|
261
|
+
if (!track.rhythm) continue;
|
|
262
|
+
if (regridded) {
|
|
263
|
+
if (!rhythmInSync(out, track.id))
|
|
264
|
+
out = withTrackRhythm(out, track.id, track.rhythm);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const current = out.tracks.find((candidate) => candidate.id === track.id)!;
|
|
268
|
+
const kept = track.rhythm.filter((row) => rowInSync(out, current, row));
|
|
269
|
+
if (kept.length !== track.rhythm.length)
|
|
270
|
+
out = new TrackScore({
|
|
271
|
+
...out.toJSON(),
|
|
272
|
+
tracks: out.tracks.map((candidate) =>
|
|
273
|
+
candidate.id === track.id
|
|
274
|
+
? { ...candidate, rhythm: kept.length > 0 ? kept : null }
|
|
275
|
+
: candidate,
|
|
276
|
+
),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** `kick E(4,16) · hat E(7,16,r2)` for a status line. */
|
|
283
|
+
export function rhythmSummary(track: Track): string {
|
|
284
|
+
return (track.rhythm ?? [])
|
|
285
|
+
.map((row) => `${row.voice} ${rowSummary(row)}`)
|
|
286
|
+
.join(" · ");
|
|
287
|
+
}
|