@hraness/dawg 0.7.0 → 0.8.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 +26 -0
- package/DAWG.md +117 -30
- package/README.md +7 -5
- package/core/diff.ts +2 -0
- package/core/ids.ts +52 -0
- package/core/loop.ts +3 -0
- package/core/range.ts +581 -0
- package/core/score.ts +123 -1
- package/core/sdk/print.ts +11 -2
- package/core/sdk/v1.ts +281 -2
- package/core/sections.ts +198 -19
- package/guides/arrange.md +9 -8
- package/guides/audio.md +31 -0
- package/guides/mix.md +3 -1
- package/guides/panes.md +32 -0
- package/guides/project.md +3 -2
- package/guides/rhythm.md +2 -1
- package/guides/tape.md +32 -0
- package/package.json +2 -1
- package/src/agent/agent.ts +4 -4
- package/src/agent/command-agent.ts +19 -10
- package/src/agent/ops.ts +15 -0
- package/src/agent/planner.ts +12 -0
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/show-me.ts +1 -1
- package/src/agent/tools.ts +2 -0
- package/src/agent/xcb-agent.ts +2 -3
- package/src/audio/arrange.ts +12 -2
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +84 -13
- package/src/audio/engine.ts +216 -28
- package/src/commands/arrange.ts +60 -25
- package/src/commands/grammar.ts +3 -1
- package/src/commands/help.ts +187 -9
- package/src/commands/history.ts +122 -11
- package/src/commands/keys.ts +17 -0
- package/src/commands/parses.ts +6 -1
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +2 -6
- package/src/commands/vocal-pitch.ts +2 -1
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +18 -0
- package/src/launch-args.ts +105 -1
- package/src/main.ts +1142 -86
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +196 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/origin.ts +20 -0
- package/src/session/port.ts +87 -10
- package/src/session/protocol.ts +360 -6
- package/src/session/rebase.ts +46 -8
- package/src/session/receipt.ts +7 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +61 -2
- package/src/tui/arrange-menu.ts +169 -1
- package/src/tui/euclid.ts +95 -14
- package/src/tui/fader.ts +55 -2
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu.ts +228 -2
- package/src/tui/play-session.ts +196 -21
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/tui/app.ts +217 -44
- package/tui/delight.ts +49 -0
- package/tui/drawer.ts +167 -9
- package/tui/grammar.ts +34 -5
- package/tui/guide.ts +8 -1
- package/tui/highway.ts +40 -5
- package/tui/hints.ts +5 -0
- package/tui/hits.ts +7 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +12 -0
- package/tui/screen.ts +7 -4
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +19 -1
package/core/sections.ts
CHANGED
|
@@ -535,9 +535,9 @@ export function sectionScore(score: TrackScore, section: Section): TrackScore {
|
|
|
535
535
|
// ---------------------------------------------------------------------------
|
|
536
536
|
// Automation over time edits
|
|
537
537
|
|
|
538
|
-
type Points = readonly AutomationPoint[];
|
|
538
|
+
export type Points = readonly AutomationPoint[];
|
|
539
539
|
|
|
540
|
-
const TRACK_LANES = [
|
|
540
|
+
export const TRACK_LANES = [
|
|
541
541
|
"volumeAutomation",
|
|
542
542
|
"panAutomation",
|
|
543
543
|
"filterAutomation",
|
|
@@ -548,7 +548,10 @@ const TRACK_LANES = [
|
|
|
548
548
|
] as const;
|
|
549
549
|
|
|
550
550
|
/** Applies `edit` to every automation lane of `track`. */
|
|
551
|
-
function mapAutomation(
|
|
551
|
+
export function mapAutomation(
|
|
552
|
+
track: Track,
|
|
553
|
+
edit: (points: Points) => Points,
|
|
554
|
+
): Track {
|
|
552
555
|
const out: Record<string, unknown> = { ...track };
|
|
553
556
|
let changed = false;
|
|
554
557
|
for (const field of TRACK_LANES) {
|
|
@@ -578,7 +581,7 @@ function mapAutomation(track: Track, edit: (points: Points) => Points): Track {
|
|
|
578
581
|
}
|
|
579
582
|
|
|
580
583
|
/** The lane value at `tick` (undefined before the first point). */
|
|
581
|
-
function valueAt(points: Points, tick: number): number | undefined {
|
|
584
|
+
export function valueAt(points: Points, tick: number): number | undefined {
|
|
582
585
|
const first = points[0];
|
|
583
586
|
if (!first || tick < first.tick) return undefined;
|
|
584
587
|
for (let index = 1; index < points.length; index += 1) {
|
|
@@ -593,7 +596,7 @@ function valueAt(points: Points, tick: number): number | undefined {
|
|
|
593
596
|
return points[points.length - 1]!.value;
|
|
594
597
|
}
|
|
595
598
|
|
|
596
|
-
function sortPoints(points: AutomationPoint[]): Points {
|
|
599
|
+
export function sortPoints(points: AutomationPoint[]): Points {
|
|
597
600
|
const byTick = new Map<number, AutomationPoint>();
|
|
598
601
|
for (const point of points) byTick.set(point.tick, point);
|
|
599
602
|
return [...byTick.values()].sort((a, b) => a.tick - b.tick);
|
|
@@ -682,7 +685,7 @@ function copyPoints(
|
|
|
682
685
|
// ---------------------------------------------------------------------------
|
|
683
686
|
// Ripple edits
|
|
684
687
|
|
|
685
|
-
function checkBars(bars: number): void {
|
|
688
|
+
export function checkBars(bars: number): void {
|
|
686
689
|
if (bars > SCORE_LIMITS.maxBars)
|
|
687
690
|
throw new ScoreValidationError(
|
|
688
691
|
`the song would be ${bars} bars; the limit is ${SCORE_LIMITS.maxBars}`,
|
|
@@ -806,6 +809,14 @@ export function insertBars(
|
|
|
806
809
|
? { ...section, bars: section.bars + count }
|
|
807
810
|
: section,
|
|
808
811
|
),
|
|
812
|
+
// The loop range moves (or grows) like a section.
|
|
813
|
+
loop: score.loop
|
|
814
|
+
? score.loop.startBar >= atBar
|
|
815
|
+
? { ...score.loop, startBar: score.loop.startBar + count }
|
|
816
|
+
: score.loop.startBar + score.loop.bars > atBar
|
|
817
|
+
? { ...score.loop, bars: score.loop.bars + count }
|
|
818
|
+
: score.loop
|
|
819
|
+
: null,
|
|
809
820
|
});
|
|
810
821
|
}
|
|
811
822
|
|
|
@@ -884,9 +895,27 @@ export function deleteBars(
|
|
|
884
895
|
notes,
|
|
885
896
|
sections,
|
|
886
897
|
form: score.form.filter((entry) => names.has(foldName(entry.section))),
|
|
898
|
+
loop: score.loop ? shrinkRange(score.loop, atBar, count) : null,
|
|
887
899
|
});
|
|
888
900
|
}
|
|
889
901
|
|
|
902
|
+
/** A bar range after bars `atBar..atBar+count` go (null when it goes too). */
|
|
903
|
+
function shrinkRange(
|
|
904
|
+
range: Readonly<{ startBar: number; bars: number }>,
|
|
905
|
+
atBar: number,
|
|
906
|
+
count: number,
|
|
907
|
+
): { startBar: number; bars: number } | null {
|
|
908
|
+
const until = range.startBar + range.bars;
|
|
909
|
+
if (until <= atBar) return range;
|
|
910
|
+
if (range.startBar >= atBar + count)
|
|
911
|
+
return { startBar: range.startBar - count, bars: range.bars };
|
|
912
|
+
const left = Math.max(0, atBar - range.startBar);
|
|
913
|
+
const right = Math.max(0, until - (atBar + count));
|
|
914
|
+
return left + right > 0
|
|
915
|
+
? { startBar: Math.min(range.startBar, atBar), bars: left + right }
|
|
916
|
+
: null;
|
|
917
|
+
}
|
|
918
|
+
|
|
890
919
|
/** Unused note ids `prefix1`, `prefix2`, ... */
|
|
891
920
|
function idMaker(score: TrackScore, prefix: string): () => string {
|
|
892
921
|
const used = new Set(score.notes.map((note) => note.id));
|
|
@@ -1088,24 +1117,49 @@ export function duplicateSection(
|
|
|
1088
1117
|
);
|
|
1089
1118
|
}
|
|
1090
1119
|
|
|
1091
|
-
/**
|
|
1120
|
+
/** Options for `copyBars`: which tracks, and whether the copy overdubs. */
|
|
1121
|
+
export type CopyBarsOptions = Readonly<{
|
|
1122
|
+
/** Only these tracks (default all). A track subset leaves tempo alone. */
|
|
1123
|
+
trackIds?: ReadonlySet<string>;
|
|
1124
|
+
/** Overdub: keep the destination's notes (default replaces them). */
|
|
1125
|
+
merge?: boolean;
|
|
1126
|
+
}>;
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Copy the music of bars `from..from+count` onto bars `to..` (replacing it,
|
|
1130
|
+
* or with `merge` overdubbing it). All tracks bring the song's tempo and
|
|
1131
|
+
* fermatas with them; a track subset copies only its own notes, clips,
|
|
1132
|
+
* pedal and automation.
|
|
1133
|
+
*/
|
|
1092
1134
|
export function copyBars(
|
|
1093
1135
|
score: TrackScore,
|
|
1094
1136
|
fromBar: number,
|
|
1095
1137
|
count: number,
|
|
1096
1138
|
toBar: number,
|
|
1097
1139
|
idPrefix = "copy-",
|
|
1140
|
+
options: CopyBarsOptions = {},
|
|
1098
1141
|
): TrackScore {
|
|
1099
1142
|
const ticks = barTicks(score);
|
|
1100
1143
|
const from = fromBar * ticks;
|
|
1101
1144
|
const length = count * ticks;
|
|
1102
1145
|
const to = toBar * ticks;
|
|
1103
1146
|
const nextId = idMaker(score, idPrefix);
|
|
1147
|
+
const picked = (trackId: string): boolean =>
|
|
1148
|
+
options.trackIds === undefined || options.trackIds.has(trackId);
|
|
1104
1149
|
const kept = score.notes.filter(
|
|
1105
|
-
(note) =>
|
|
1150
|
+
(note) =>
|
|
1151
|
+
options.merge ||
|
|
1152
|
+
!picked(note.trackId) ||
|
|
1153
|
+
note.startTick < to ||
|
|
1154
|
+
note.startTick >= to + length,
|
|
1106
1155
|
);
|
|
1107
1156
|
const copies = score.notes
|
|
1108
|
-
.filter(
|
|
1157
|
+
.filter(
|
|
1158
|
+
(note) =>
|
|
1159
|
+
picked(note.trackId) &&
|
|
1160
|
+
note.startTick >= from &&
|
|
1161
|
+
note.startTick < from + length,
|
|
1162
|
+
)
|
|
1109
1163
|
.map((note) => ({
|
|
1110
1164
|
...note,
|
|
1111
1165
|
id: nextId(),
|
|
@@ -1122,19 +1176,21 @@ export function copyBars(
|
|
|
1122
1176
|
{ from, to: from + length, offset: to },
|
|
1123
1177
|
{ from: to + length, to: Infinity, offset: to + length },
|
|
1124
1178
|
]);
|
|
1179
|
+
const song = options.trackIds === undefined;
|
|
1125
1180
|
return new TrackScore({
|
|
1126
1181
|
...score.toJSON(),
|
|
1127
|
-
tempoBpm: ripple.tempoBpm,
|
|
1128
|
-
time: ripple.time,
|
|
1182
|
+
...(song ? { tempoBpm: ripple.tempoBpm, time: ripple.time } : {}),
|
|
1129
1183
|
tracks: score.tracks.map((track) =>
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1184
|
+
picked(track.id)
|
|
1185
|
+
? copyClipBars(
|
|
1186
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
1187
|
+
copyPoints(points, from, length, to),
|
|
1188
|
+
),
|
|
1189
|
+
from,
|
|
1190
|
+
length,
|
|
1191
|
+
to,
|
|
1192
|
+
)
|
|
1193
|
+
: track,
|
|
1138
1194
|
),
|
|
1139
1195
|
notes: [...kept, ...copies],
|
|
1140
1196
|
});
|
|
@@ -1250,6 +1306,129 @@ export function resetSection(score: TrackScore, name: string): TrackScore {
|
|
|
1250
1306
|
});
|
|
1251
1307
|
}
|
|
1252
1308
|
|
|
1309
|
+
/**
|
|
1310
|
+
* Split a section at `atBar` (0-based, strictly inside it) into two: the
|
|
1311
|
+
* first keeps the name, the second is `<name> 2` (or `as`). Both keep the
|
|
1312
|
+
* mutes and variations; the form plays both halves wherever it played the
|
|
1313
|
+
* section, and a loop on it becomes the loop range over the same bars.
|
|
1314
|
+
*/
|
|
1315
|
+
export function splitSection(
|
|
1316
|
+
score: TrackScore,
|
|
1317
|
+
name: string,
|
|
1318
|
+
atBar: number,
|
|
1319
|
+
as?: string,
|
|
1320
|
+
): TrackScore {
|
|
1321
|
+
const section = requireSection(score, name);
|
|
1322
|
+
const end = section.startBar + section.bars;
|
|
1323
|
+
if (!Number.isInteger(atBar) || atBar <= section.startBar || atBar >= end)
|
|
1324
|
+
throw new ScoreValidationError(
|
|
1325
|
+
section.bars < 2
|
|
1326
|
+
? `${section.name} is one bar; nothing to split`
|
|
1327
|
+
: `split ${section.name} at a bar ${section.startBar + 2}..${end}`,
|
|
1328
|
+
);
|
|
1329
|
+
if (as !== undefined && findSection(score, as))
|
|
1330
|
+
throw new ScoreValidationError(`a section named ${as} exists`);
|
|
1331
|
+
const second = uniqueSectionName(score, as ?? `${section.name} 2`);
|
|
1332
|
+
const form: FormEntry[] = [];
|
|
1333
|
+
for (const entry of score.form) {
|
|
1334
|
+
if (foldName(entry.section) !== foldName(section.name)) {
|
|
1335
|
+
form.push(entry);
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
for (let pass = 0; pass < (entry.repeat ?? 1); pass += 1)
|
|
1339
|
+
form.push({ section: section.name }, { section: second });
|
|
1340
|
+
}
|
|
1341
|
+
if (form.length > SCORE_LIMITS.maxFormEntries)
|
|
1342
|
+
throw new ScoreValidationError(
|
|
1343
|
+
`the form would have ${form.length} entries; the limit is ${SCORE_LIMITS.maxFormEntries}`,
|
|
1344
|
+
"score-limit",
|
|
1345
|
+
);
|
|
1346
|
+
const looping = score.loopSection === section.name;
|
|
1347
|
+
const split = score.withSections(
|
|
1348
|
+
score.sections.flatMap((candidate) =>
|
|
1349
|
+
candidate === section
|
|
1350
|
+
? [
|
|
1351
|
+
{ ...section, bars: atBar - section.startBar },
|
|
1352
|
+
{ ...section, name: second, startBar: atBar, bars: end - atBar },
|
|
1353
|
+
]
|
|
1354
|
+
: [candidate],
|
|
1355
|
+
),
|
|
1356
|
+
form,
|
|
1357
|
+
looping ? null : score.loopSection,
|
|
1358
|
+
);
|
|
1359
|
+
return looping
|
|
1360
|
+
? split.withLoop({ startBar: section.startBar, bars: section.bars })
|
|
1361
|
+
: split;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Join a section with the one that starts where it ends; it keeps the
|
|
1366
|
+
* first one's name, mutes and variations. Refused when the two mute or vary
|
|
1367
|
+
* differently (joining would change the sound) unless `force`, or when
|
|
1368
|
+
* the form plays the second anywhere but right after the first.
|
|
1369
|
+
*/
|
|
1370
|
+
export function joinSection(
|
|
1371
|
+
score: TrackScore,
|
|
1372
|
+
name: string,
|
|
1373
|
+
options: Readonly<{ force?: boolean }> = {},
|
|
1374
|
+
): TrackScore {
|
|
1375
|
+
const section = requireSection(score, name);
|
|
1376
|
+
const end = section.startBar + section.bars;
|
|
1377
|
+
const next = score.sections.find((candidate) => candidate.startBar === end);
|
|
1378
|
+
if (!next)
|
|
1379
|
+
throw new ScoreValidationError(
|
|
1380
|
+
`no section starts at bar ${end + 1}, right after ${section.name}`,
|
|
1381
|
+
);
|
|
1382
|
+
const sameMute =
|
|
1383
|
+
JSON.stringify([...(section.mute ?? [])].sort()) ===
|
|
1384
|
+
JSON.stringify([...(next.mute ?? [])].sort());
|
|
1385
|
+
if (
|
|
1386
|
+
!options.force &&
|
|
1387
|
+
(!sameMute ||
|
|
1388
|
+
JSON.stringify(section.vary ?? {}) !== JSON.stringify(next.vary ?? {}))
|
|
1389
|
+
)
|
|
1390
|
+
throw new ScoreValidationError(
|
|
1391
|
+
`${section.name} and ${next.name} mute or vary differently · section join ${section.name} force`,
|
|
1392
|
+
);
|
|
1393
|
+
const form: FormEntry[] = [];
|
|
1394
|
+
const entries = score.form;
|
|
1395
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1396
|
+
const entry = entries[index]!;
|
|
1397
|
+
const isFirst = foldName(entry.section) === foldName(section.name);
|
|
1398
|
+
const isSecond = foldName(entry.section) === foldName(next.name);
|
|
1399
|
+
const following = entries[index + 1];
|
|
1400
|
+
if (
|
|
1401
|
+
isFirst &&
|
|
1402
|
+
(entry.repeat ?? 1) === 1 &&
|
|
1403
|
+
following &&
|
|
1404
|
+
foldName(following.section) === foldName(next.name) &&
|
|
1405
|
+
(following.repeat ?? 1) === 1
|
|
1406
|
+
) {
|
|
1407
|
+
form.push({ section: section.name });
|
|
1408
|
+
index += 1;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (isFirst || isSecond)
|
|
1412
|
+
throw new ScoreValidationError(
|
|
1413
|
+
`the form plays ${section.name} and ${next.name} apart · change the form first`,
|
|
1414
|
+
);
|
|
1415
|
+
form.push(entry);
|
|
1416
|
+
}
|
|
1417
|
+
const looping =
|
|
1418
|
+
score.loopSection === section.name || score.loopSection === next.name;
|
|
1419
|
+
return score.withSections(
|
|
1420
|
+
score.sections
|
|
1421
|
+
.filter((candidate) => candidate !== next)
|
|
1422
|
+
.map((candidate) =>
|
|
1423
|
+
candidate === section
|
|
1424
|
+
? { ...section, bars: section.bars + next.bars }
|
|
1425
|
+
: candidate,
|
|
1426
|
+
),
|
|
1427
|
+
form,
|
|
1428
|
+
looping ? section.name : score.loopSection,
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1253
1432
|
function omit<T extends object, K extends keyof T>(
|
|
1254
1433
|
value: T,
|
|
1255
1434
|
key: K,
|
package/guides/arrange.md
CHANGED
|
@@ -4,18 +4,19 @@ title: Arrange
|
|
|
4
4
|
order: 9
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Notes make tracks,
|
|
7
|
+
Notes make tracks, bars make sections, sections make the form. Name a
|
|
8
|
+
range of bars and loop, copy or move it.
|
|
8
9
|
|
|
9
10
|
## Ask
|
|
10
11
|
|
|
11
|
-
- "
|
|
12
|
+
- "loop bars 5 to 6" · "copy the bass in 5-6 to bar 7 twice"
|
|
12
13
|
|
|
13
14
|
## Type it yourself
|
|
14
15
|
|
|
15
|
-
- `
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
16
|
+
- `loop 5-6` · `loop chorus` · `loop next` · `loop off`
|
|
17
|
+
- `copy bass 5-6 to 7 x2` · `move bass 5-6 to 9` · `clear bass 5-6`
|
|
18
|
+
- `copy all 1-4` then `paste at 9` · `reverse bass 5-6` · `jump 5`
|
|
19
|
+
- `section chorus 9-16` · `form verse chorus` · `form print`
|
|
19
20
|
|
|
20
21
|
## Menu
|
|
21
22
|
|
|
@@ -23,8 +24,8 @@ Notes make tracks, tracks make sections, sections make the form.
|
|
|
23
24
|
|
|
24
25
|
## Keys
|
|
25
26
|
|
|
26
|
-
-
|
|
27
|
+
- Ctrl-T TAPE: every track across the bars · `\` loops the section
|
|
27
28
|
|
|
28
29
|
## Next
|
|
29
30
|
|
|
30
|
-
- `guide
|
|
31
|
+
- `guide tape` · `guide notes` · `guide tracks` · `guide panes`
|
package/guides/audio.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: audio
|
|
3
|
+
title: Audio output and input
|
|
4
|
+
parent: project
|
|
5
|
+
order: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
One output and one input, or the system default. Saved on this
|
|
9
|
+
machine, not in the song. Needs the native sink (`dawg doctor`).
|
|
10
|
+
|
|
11
|
+
## Ask
|
|
12
|
+
|
|
13
|
+
- "which audio output am I using?"
|
|
14
|
+
|
|
15
|
+
## Type it yourself
|
|
16
|
+
|
|
17
|
+
- `audio` · `audio out USB Audio Interface` · `audio out default`
|
|
18
|
+
- `audio in Built-in Microphone` · `audio test` (tone, input meter)
|
|
19
|
+
|
|
20
|
+
## Menu
|
|
21
|
+
|
|
22
|
+
- Ctrl-K › Project › audio › output · input · test
|
|
23
|
+
|
|
24
|
+
## Keys
|
|
25
|
+
|
|
26
|
+
- In a device list: arrows move, Enter picks (a soft blip plays), Esc
|
|
27
|
+
|
|
28
|
+
## Next
|
|
29
|
+
|
|
30
|
+
- Unplugged mid-song: playback moves to the default, said once
|
|
31
|
+
- `DAWG_AUDIO_DEVICE` overrides · `guide project`
|
package/guides/mix.md
CHANGED
|
@@ -11,7 +11,8 @@ order: 8
|
|
|
11
11
|
|
|
12
12
|
## Type it yourself
|
|
13
13
|
|
|
14
|
-
- `volume 0.7` (0..1) · `pan -0.4` (-1..1)
|
|
14
|
+
- `volume 0.7` (0..1) · `pan -0.4` (-1..1) · `volume drums 0.5`
|
|
15
|
+
- `knobs` four knobs for this track · `mix` the mixer page
|
|
15
16
|
- `mute` · `unmute` · `solo` · `unsolo`
|
|
16
17
|
- `fx orbit 2` send to a bus · `fx duck preset pump` on the kick
|
|
17
18
|
- `master streaming` · `master target -14` · `master measure`
|
|
@@ -23,6 +24,7 @@ order: 8
|
|
|
23
24
|
## Keys
|
|
24
25
|
|
|
25
26
|
- `volume` or `pan` alone opens a fader to drag or step
|
|
27
|
+
- Knobs: ↑↓ pick ● ▲ ■ ◆ · ←→ turn · ⇧ coarse · x reset · Tab more
|
|
26
28
|
|
|
27
29
|
## Next
|
|
28
30
|
|
package/guides/panes.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: panes
|
|
3
|
+
title: Panes
|
|
4
|
+
parent: project
|
|
5
|
+
order: 4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Open the same song in several terminals: one plays, one shows TAPE,
|
|
9
|
+
one the knobs. Each pane has a letter and its own undo.
|
|
10
|
+
|
|
11
|
+
## Ask
|
|
12
|
+
|
|
13
|
+
- "which panes are open?"
|
|
14
|
+
|
|
15
|
+
## Type it yourself
|
|
16
|
+
|
|
17
|
+
- `panes` · `pane tape` (copies `dawg pane tape` to paste elsewhere)
|
|
18
|
+
- `pin` keep this pane on its track · `unpin`
|
|
19
|
+
- `follow b` follow pane B's track · `follow` the latest · `unfollow`
|
|
20
|
+
- `undo` this pane's last edit · `undo all` anyone's
|
|
21
|
+
|
|
22
|
+
## Menu
|
|
23
|
+
|
|
24
|
+
- Ctrl-K › Project › session
|
|
25
|
+
|
|
26
|
+
## Keys
|
|
27
|
+
|
|
28
|
+
- In a shell: `dawg pane home|play|tape|sound|menu [track] [pin]`
|
|
29
|
+
|
|
30
|
+
## Next
|
|
31
|
+
|
|
32
|
+
- `guide sessions` · `guide tape` · `guide project`
|
package/guides/project.md
CHANGED
|
@@ -19,7 +19,7 @@ live here; windows on one folder stay in sync.
|
|
|
19
19
|
|
|
20
20
|
## Menu
|
|
21
21
|
|
|
22
|
-
- Ctrl-K › Project › play · tempo · export · session · agent
|
|
22
|
+
- Ctrl-K › Project › play · tempo · export · session · agent · audio
|
|
23
23
|
|
|
24
24
|
## Keys
|
|
25
25
|
|
|
@@ -27,4 +27,5 @@ live here; windows on one folder stay in sync.
|
|
|
27
27
|
|
|
28
28
|
## Next
|
|
29
29
|
|
|
30
|
-
- `guide files` · `guide sessions` · `guide agent`
|
|
30
|
+
- `guide files` · `guide sessions` · `guide agent` · `guide audio`
|
|
31
|
+
- `guide panes`
|
package/guides/rhythm.md
CHANGED
|
@@ -23,7 +23,8 @@ A drum track has one row per drum: kick, snare, hat and more.
|
|
|
23
23
|
|
|
24
24
|
## Keys
|
|
25
25
|
|
|
26
|
-
- In the euclid editor ↑↓
|
|
26
|
+
- In the euclid editor ↑↓ knob (● drum ▲ pulses ■ rotate
|
|
27
|
+
◆ velocity) · ←→ turn · Tab field · Space loop
|
|
27
28
|
|
|
28
29
|
## Next
|
|
29
30
|
|
package/guides/tape.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: tape
|
|
3
|
+
title: TAPE
|
|
4
|
+
parent: arrange
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Ctrl-T shows every track across the bars. Each key echoes the command
|
|
9
|
+
it runs, so the prompt teaches the typed form. Esc goes back.
|
|
10
|
+
|
|
11
|
+
## Ask
|
|
12
|
+
|
|
13
|
+
- "loop the chorus and copy its drums to bar 17"
|
|
14
|
+
|
|
15
|
+
## Type it yourself
|
|
16
|
+
|
|
17
|
+
- `tape` · `loop chorus` · `copy drums 9-16 to 17` · `jump 5.3`
|
|
18
|
+
|
|
19
|
+
## Menu
|
|
20
|
+
|
|
21
|
+
- Ctrl-K › Arrange › sections · form
|
|
22
|
+
|
|
23
|
+
## Keys
|
|
24
|
+
|
|
25
|
+
- ↑↓ pick a knob: ● playhead ▲ loop ■ tempo ◆ volume · ←→ turn it
|
|
26
|
+
- `\` loop the section here · `[` `]` `{` `}` `<` `>` shape the loop
|
|
27
|
+
- `c` copy · `x` cut · `v` paste (`V` inserts) · Delete clear · `~`
|
|
28
|
+
|
|
29
|
+
## Next
|
|
30
|
+
|
|
31
|
+
- Form repeats are shaded `░`; an edit there says which copies it hits
|
|
32
|
+
- `dawg pane tape` in another terminal · `guide arrange`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/dawg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"scripts": {
|
|
34
34
|
"dawg": "bun run src/main.ts",
|
|
35
35
|
"bench:perf": "bun bench/perf/run.ts",
|
|
36
|
+
"sizes": "bun bench/sizes/matrix.ts",
|
|
36
37
|
"test": "bun test",
|
|
37
38
|
"typecheck": "tsc --noEmit",
|
|
38
39
|
"check": "bun test && bun run typecheck && bun run format",
|
package/src/agent/agent.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { newId } from "../../core/ids.ts";
|
|
1
2
|
import type { CommandHost } from "./command-agent.ts";
|
|
2
3
|
import { GRANULAR_PARAMS } from "../../core/granular.ts";
|
|
3
4
|
import {
|
|
@@ -201,7 +202,7 @@ export type AgentTurnOptions = Readonly<{
|
|
|
201
202
|
signal?: AbortSignal;
|
|
202
203
|
budget?: AgentBudget;
|
|
203
204
|
tools?: readonly AgentTool[];
|
|
204
|
-
/** Note ID factory; defaults to
|
|
205
|
+
/** Note ID factory; defaults to `newId(track)` (core/ids.ts). */
|
|
205
206
|
newNoteId?: (trackId: string, revision: number, index: number) => string;
|
|
206
207
|
}>;
|
|
207
208
|
|
|
@@ -243,6 +244,7 @@ export const CONTENT_TOOLS: ReadonlySet<string> = new Set([
|
|
|
243
244
|
"set_automation",
|
|
244
245
|
"add_transition",
|
|
245
246
|
"edit_section",
|
|
247
|
+
"edit_range",
|
|
246
248
|
"set_form",
|
|
247
249
|
"extend_loop",
|
|
248
250
|
"edit_clip",
|
|
@@ -322,11 +324,9 @@ export async function runAgentTurn(
|
|
|
322
324
|
};
|
|
323
325
|
const tools = options.tools ?? AGENT_TOOLS;
|
|
324
326
|
const chatToolList = chatTools(tools);
|
|
325
|
-
const nonce = Math.random().toString(36).slice(2, 6);
|
|
326
327
|
const newNoteId =
|
|
327
328
|
options.newNoteId ??
|
|
328
|
-
((trackId: string,
|
|
329
|
-
`${trackId.slice(0, 40)}-${revision}-${nonce}${index}`);
|
|
329
|
+
((trackId: string, _revision: number, _index: number) => newId(trackId));
|
|
330
330
|
const deadline = turnDeadline(limits.timeoutMs);
|
|
331
331
|
const signal = options.signal
|
|
332
332
|
? AbortSignal.any([options.signal, deadline.signal])
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* sends about a ninth of the input tokens and finishes in about half the
|
|
11
11
|
* time (docs/show-me.md).
|
|
12
12
|
*/
|
|
13
|
+
import { newId } from "../../core/ids.ts";
|
|
13
14
|
import { HELP_SECTIONS } from "../commands/help.ts";
|
|
14
15
|
import { MEDIA_TOOLS } from "../media/tools.ts";
|
|
15
16
|
import {
|
|
@@ -64,15 +65,24 @@ export const COMMAND_MODE_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
|
64
65
|
/** Steps per command-mode turn: each extra one is a correction round. */
|
|
65
66
|
export const COMMAND_MAX_STEPS = 4;
|
|
66
67
|
|
|
68
|
+
/** Reference groups the command agent is not shown. */
|
|
69
|
+
const AGENT_SKIPS: ReadonlySet<string> = new Set([
|
|
70
|
+
"project · window",
|
|
71
|
+
"project · panes",
|
|
72
|
+
"project · audio",
|
|
73
|
+
"agent",
|
|
74
|
+
]);
|
|
75
|
+
|
|
67
76
|
function commandReference(): string {
|
|
68
|
-
// Song commands only
|
|
69
|
-
//
|
|
70
|
-
return HELP_SECTIONS.filter(
|
|
71
|
-
(section) =>
|
|
72
|
-
section.group !== "project · window" && section.group !== "agent",
|
|
73
|
-
)
|
|
77
|
+
// Song commands only. Panes, audio devices and the window groups change the person's window or
|
|
78
|
+
// machine; the clipboard row is a two-step buffer the agent never needs.
|
|
79
|
+
return HELP_SECTIONS.filter((section) => !AGENT_SKIPS.has(section.group))
|
|
74
80
|
.flatMap((section) =>
|
|
75
|
-
section.entries
|
|
81
|
+
section.entries
|
|
82
|
+
.filter(
|
|
83
|
+
(entry) => !/\bpaste\b/.test(`${entry.command} ${entry.summary}`),
|
|
84
|
+
)
|
|
85
|
+
.map((entry) => `${entry.command} — ${entry.summary}`),
|
|
76
86
|
)
|
|
77
87
|
.join("\n");
|
|
78
88
|
}
|
|
@@ -81,6 +91,7 @@ export const COMMAND_AGENT_PROMPT = [
|
|
|
81
91
|
"You are dawg, a loop composer inside a terminal music workstation, and you teach it by doing.",
|
|
82
92
|
"Act by writing dawg prompt commands, exactly as a human types them in the prompt bar, one per line. The user watches each command appear in their prompt bar as you write it, and each line runs the moment it ends, so they learn the commands by watching you.",
|
|
83
93
|
"Prefer the gestures a person would use: a parameter as one value command (fx reverb mix 0.4, volume 0.7, pan -0.3, which the user sees as a fader moving), notes one per line with add (add C4 at 0 for 0.5, played on the user's keyboard as you write them), drums with hit, pattern and euclid on a kit track, a whole groove with groove <name>.",
|
|
94
|
+
"To arrange, name bars: loop 5-6 loops them, copy bass 5-6 to 7 and move bass 5-6 to 9 place them in one step, clear bass 5-6 empties them. Never copy to the clipboard and paste.",
|
|
84
95
|
"Commands apply to the focused track. /track <name> focuses a track or creates it; instrument kit makes a drum track. Times are beats from 0: in 4/4 musicians' beats 2 and 4 are beats 1 and 3 here, and bar n starts at (n-1)×beats per bar.",
|
|
85
96
|
"Write commands first, with no numbering, quotes or code fences. Then write one short plain sentence (not a command) that says what changed and names the key command so the user can do it by hand next time.",
|
|
86
97
|
"If a command fails you get its error; correct it with another command or stop.",
|
|
@@ -132,11 +143,9 @@ export async function runCommandAgentTurn(
|
|
|
132
143
|
const offered = chatTools(
|
|
133
144
|
allTools.filter((tool) => COMMAND_MODE_TOOL_NAMES.has(tool.name)),
|
|
134
145
|
);
|
|
135
|
-
const nonce = Math.random().toString(36).slice(2, 6);
|
|
136
146
|
const newNoteId =
|
|
137
147
|
options.newNoteId ??
|
|
138
|
-
((trackId: string,
|
|
139
|
-
`${trackId.slice(0, 40)}-${revision}-${nonce}${index}`);
|
|
148
|
+
((trackId: string, _revision: number, _index: number) => newId(trackId));
|
|
140
149
|
const deadline = turnDeadline(limits.timeoutMs);
|
|
141
150
|
const signal = options.signal
|
|
142
151
|
? AbortSignal.any([options.signal, deadline.signal])
|
package/src/agent/ops.ts
CHANGED
|
@@ -52,6 +52,8 @@ export type AgentOperation =
|
|
|
52
52
|
};
|
|
53
53
|
/** The instrument word as typed (a guitar alias also loads its rig). */
|
|
54
54
|
word?: string;
|
|
55
|
+
/** `volume drums 0.5`: another track than the focused one. */
|
|
56
|
+
trackId?: string;
|
|
55
57
|
}
|
|
56
58
|
| {
|
|
57
59
|
type: "automation";
|
|
@@ -101,6 +103,19 @@ export function parsePrompt(prompt: string): AgentOperation | undefined {
|
|
|
101
103
|
return { type: "track", patch: { muted: false } };
|
|
102
104
|
const volume = text.match(/^(?:volume|vol)\s+(0(?:\.\d+)?|1(?:\.0+)?)$/);
|
|
103
105
|
if (volume) return { type: "track", patch: { volume: Number(volume[1]) } };
|
|
106
|
+
// `volume drums 0.5` / `pan bass -0.2`: the mixer page's rows name their
|
|
107
|
+
// track, so a level is set without moving the focus.
|
|
108
|
+
const level = text.match(
|
|
109
|
+
/^(volume|vol|pan)\s+([a-z0-9][a-z0-9._-]{0,63})\s+(-?1(?:\.0+)?|-?0(?:\.\d+)?)$/,
|
|
110
|
+
);
|
|
111
|
+
if (level && !(level[1] !== "pan" && level[3]!.startsWith("-"))) {
|
|
112
|
+
const value = Number(level[3]);
|
|
113
|
+
return {
|
|
114
|
+
type: "track",
|
|
115
|
+
patch: level[1] === "pan" ? { pan: value } : { volume: value },
|
|
116
|
+
trackId: level[2]!,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
104
119
|
const clearAutomation = text.match(
|
|
105
120
|
/^(?:clear|reset)\s+(?:(volume|pan)\s+)?automation$/,
|
|
106
121
|
);
|
package/src/agent/planner.ts
CHANGED
|
@@ -178,6 +178,18 @@ function parseOperation(value: unknown): ScoreOperation {
|
|
|
178
178
|
loopSection: (value.loopSection ?? null) as string | null,
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
+
if (
|
|
182
|
+
value.type === "setLoop" &&
|
|
183
|
+
(value.loop === null ||
|
|
184
|
+
(isRecord(value.loop) &&
|
|
185
|
+
typeof value.loop.startBar === "number" &&
|
|
186
|
+
typeof value.loop.bars === "number"))
|
|
187
|
+
)
|
|
188
|
+
// The reducer checks the range (TrackScore normalizeLoopRange).
|
|
189
|
+
return {
|
|
190
|
+
type: "setLoop",
|
|
191
|
+
loop: value.loop as { startBar: number; bars: number } | null,
|
|
192
|
+
};
|
|
181
193
|
if (
|
|
182
194
|
value.type === "updateTrack" &&
|
|
183
195
|
typeof value.trackId === "string" &&
|