@hraness/dawg 0.5.0 → 0.6.1
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 +63 -0
- package/DAWG.md +592 -49
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +9 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +689 -2
- package/core/granular.ts +619 -0
- package/core/instruments.ts +442 -0
- package/core/keys.ts +955 -0
- package/core/resonators.ts +862 -0
- package/core/score.ts +529 -8
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +247 -16
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2417 -40
- package/core/sections.ts +44 -10
- package/core/strings.ts +1080 -0
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +5 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -2
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +13 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +42 -2
- package/src/agent/preview-tool.ts +7 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +726 -17
- package/src/agent/xcb-agent.ts +13 -0
- package/src/audio/arrange.ts +42 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +23 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +56 -6
- package/src/audio/fit.ts +447 -0
- package/src/audio/granular.ts +995 -0
- package/src/audio/instrument-check.ts +137 -0
- package/src/audio/instruments.ts +140 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +485 -0
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +476 -0
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +56 -0
- package/src/audio/live.ts +361 -15
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +17 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +295 -0
- package/src/audio/sampler.ts +328 -35
- package/src/audio/samples.ts +56 -8
- package/src/audio/strings/body.ts +263 -0
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +615 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +200 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +191 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +111 -23
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +35 -1
- package/src/commands/granular.ts +432 -0
- package/src/commands/help.ts +159 -5
- package/src/commands/keys.ts +629 -0
- package/src/commands/modal.ts +310 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +264 -0
- package/src/commands/sample.ts +52 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +201 -0
- package/src/commands/strum.ts +473 -0
- package/src/commands/time.ts +4 -1
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +339 -15
- package/src/project/check.ts +17 -0
- package/src/render.ts +49 -4
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +934 -9
- package/src/tui/modal-menu.ts +145 -0
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +70 -2
- package/src/tui/wind-menu.ts +144 -0
package/src/main.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
isUnknownInstrument,
|
|
4
|
+
plainSineAdvice,
|
|
5
|
+
} from "./audio/instrument-check.ts";
|
|
2
6
|
import { randomUUID } from "node:crypto";
|
|
3
7
|
import { readFileSync, writeSync } from "node:fs";
|
|
4
8
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
@@ -54,8 +58,47 @@ import {
|
|
|
54
58
|
applyExpressionCommand,
|
|
55
59
|
parseExpressionCommand,
|
|
56
60
|
} from "./commands/expression.ts";
|
|
57
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
applyFxCommand,
|
|
63
|
+
parseFxCommand,
|
|
64
|
+
unknownFxMessage,
|
|
65
|
+
} from "./commands/fx.ts";
|
|
66
|
+
import {
|
|
67
|
+
applyRigCommand,
|
|
68
|
+
parseRigCommand,
|
|
69
|
+
rigTrackFields,
|
|
70
|
+
rigWordPatch,
|
|
71
|
+
} from "./commands/rig.ts";
|
|
72
|
+
import {
|
|
73
|
+
applyGuitarCommand,
|
|
74
|
+
applyStrumCommand,
|
|
75
|
+
parseGuitarCommand,
|
|
76
|
+
parseStrumCommand,
|
|
77
|
+
} from "./commands/strum.ts";
|
|
78
|
+
import {
|
|
79
|
+
applyModalCommand,
|
|
80
|
+
modalListLines,
|
|
81
|
+
parseModalCommand,
|
|
82
|
+
} from "./commands/modal.ts";
|
|
83
|
+
import {
|
|
84
|
+
applyWindCommand,
|
|
85
|
+
parseWindCommand,
|
|
86
|
+
windListLines,
|
|
87
|
+
} from "./commands/wind.ts";
|
|
58
88
|
import { applySynthCommand, parseSynthCommand } from "./commands/synth.ts";
|
|
89
|
+
import { applyStringCommand, parseStringCommand } from "./commands/string.ts";
|
|
90
|
+
import { instrumentPatchForWord, isModalWord } from "../core/resonators.ts";
|
|
91
|
+
import {
|
|
92
|
+
applyGranularCommand,
|
|
93
|
+
granularTrackPreset,
|
|
94
|
+
grainSrcHint,
|
|
95
|
+
parseGranularCommand,
|
|
96
|
+
} from "./commands/granular.ts";
|
|
97
|
+
import {
|
|
98
|
+
applyKeysCommand,
|
|
99
|
+
newPianoTrack,
|
|
100
|
+
parseKeysCommand,
|
|
101
|
+
} from "./commands/keys.ts";
|
|
59
102
|
import {
|
|
60
103
|
applyTuningCommand,
|
|
61
104
|
importTuningFile,
|
|
@@ -94,9 +137,14 @@ import {
|
|
|
94
137
|
samplerTarget,
|
|
95
138
|
voiceNameFrom,
|
|
96
139
|
} from "./commands/sample.ts";
|
|
140
|
+
import { applyFitCommand, fitVoice, parseFitCommand } from "./commands/fit.ts";
|
|
141
|
+
import { applyShiftCommand, parseShiftCommand } from "./commands/shift.ts";
|
|
142
|
+
import { parseResampleCommand, runResample } from "./commands/resample.ts";
|
|
143
|
+
import { suggestFitMode } from "./audio/dsp/onset.ts";
|
|
97
144
|
import {
|
|
98
145
|
SampleLibrary,
|
|
99
146
|
hasSamplerTracks,
|
|
147
|
+
sampleKey,
|
|
100
148
|
type SampleBank,
|
|
101
149
|
type SampleProblem,
|
|
102
150
|
} from "./audio/samples.ts";
|
|
@@ -216,6 +264,7 @@ import {
|
|
|
216
264
|
addNote,
|
|
217
265
|
applyScoreOperation,
|
|
218
266
|
createScore,
|
|
267
|
+
isSamplerInstrument,
|
|
219
268
|
PACK_PREFIX,
|
|
220
269
|
scoreFromJSON,
|
|
221
270
|
SCORE_LIMITS,
|
|
@@ -260,6 +309,9 @@ function parsesLocally(text: string): boolean {
|
|
|
260
309
|
parseRhythmCommand,
|
|
261
310
|
parseFxCommand,
|
|
262
311
|
parseSynthCommand,
|
|
312
|
+
parseStringCommand,
|
|
313
|
+
parseGranularCommand,
|
|
314
|
+
parseKeysCommand,
|
|
263
315
|
parseExpressionCommand,
|
|
264
316
|
parseMasterCommand,
|
|
265
317
|
(value: string) => parseSectionCommand(value, score),
|
|
@@ -267,9 +319,17 @@ function parsesLocally(text: string): boolean {
|
|
|
267
319
|
parseKitCommand,
|
|
268
320
|
parsePackCommand,
|
|
269
321
|
parseSampleCommand,
|
|
322
|
+
parseFitCommand,
|
|
323
|
+
parseShiftCommand,
|
|
324
|
+
parseResampleCommand,
|
|
270
325
|
parseWavetableCommand,
|
|
271
326
|
parseTimeCommand,
|
|
272
327
|
parseTuningCommand,
|
|
328
|
+
parseRigCommand,
|
|
329
|
+
parseModalCommand,
|
|
330
|
+
parseGuitarCommand,
|
|
331
|
+
parseStrumCommand,
|
|
332
|
+
parseWindCommand,
|
|
273
333
|
].some((parse) => parse(text) !== undefined);
|
|
274
334
|
}
|
|
275
335
|
|
|
@@ -416,7 +476,13 @@ if (!demo && process.env.DAWG_AI !== "0" && stdout.isTTY)
|
|
|
416
476
|
|
|
417
477
|
const initial = createScore({
|
|
418
478
|
tracks: [
|
|
419
|
-
{
|
|
479
|
+
{
|
|
480
|
+
id: requestedTrack,
|
|
481
|
+
name: requestedTrack,
|
|
482
|
+
instrument: initialInstrument,
|
|
483
|
+
// `dawg jangle`: a guitar alias starts with its voice and rig.
|
|
484
|
+
...rigTrackFields(requestedTrack),
|
|
485
|
+
},
|
|
420
486
|
],
|
|
421
487
|
});
|
|
422
488
|
const sessionOptions: { sessionId?: string; setCurrent: boolean } = {
|
|
@@ -439,6 +505,22 @@ const freshWorkspace = !(await stat(join(process.cwd(), ".dawg")).then(
|
|
|
439
505
|
() => true,
|
|
440
506
|
() => false,
|
|
441
507
|
));
|
|
508
|
+
// Decode the import before a session exists, so a bad file is one line.
|
|
509
|
+
let importedScore: TrackScore | undefined;
|
|
510
|
+
if (importPath) {
|
|
511
|
+
try {
|
|
512
|
+
importedScore = decodeLoop(await readLoopFile(importPath));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
515
|
+
const midi = /\.midi?$/i.test(importPath)
|
|
516
|
+
? " · MIDI files are not imported; --import takes a .track.json loop"
|
|
517
|
+
: "";
|
|
518
|
+
process.stderr.write(
|
|
519
|
+
`dawg: cannot import ${importPath} · ${reason.split("\n")[0]!.slice(0, 160)}${midi}\n`,
|
|
520
|
+
);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
442
524
|
const session = await ensureSession(initial.toJSON(), sessionOptions);
|
|
443
525
|
// dawgd when connected, the file-lock path otherwise (see src/session/port.ts).
|
|
444
526
|
let port = await openSessionPort<ReturnType<TrackScore["toJSON"]>>({
|
|
@@ -451,9 +533,8 @@ let port = await openSessionPort<ReturnType<TrackScore["toJSON"]>>({
|
|
|
451
533
|
let record: SessionRecord<ReturnType<TrackScore["toJSON"]>> =
|
|
452
534
|
port.mode === "daemon" ? await port.load() : session.record;
|
|
453
535
|
let score = scoreFromJSON(record.composition);
|
|
454
|
-
if (importPath) {
|
|
455
|
-
|
|
456
|
-
score = imported;
|
|
536
|
+
if (importPath && importedScore) {
|
|
537
|
+
score = importedScore;
|
|
457
538
|
record = await port.append(
|
|
458
539
|
record,
|
|
459
540
|
{
|
|
@@ -1608,7 +1689,11 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1608
1689
|
}
|
|
1609
1690
|
const chordsCommand = command.match(/^\/chords(?:\s+(.+))?$/i);
|
|
1610
1691
|
if (chordsCommand) {
|
|
1611
|
-
const result = applyChordsCommand(
|
|
1692
|
+
const result = applyChordsCommand(
|
|
1693
|
+
chordSettings,
|
|
1694
|
+
chordsCommand[1] ?? "",
|
|
1695
|
+
score.tempoBpm,
|
|
1696
|
+
);
|
|
1612
1697
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1613
1698
|
}
|
|
1614
1699
|
const countIn = command.match(/^\/count-?in\s+([0-2])$/i);
|
|
@@ -1660,6 +1745,15 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1660
1745
|
/^\/?(?:add\s+)?track\s+([a-z0-9._-]{1,64})$/i,
|
|
1661
1746
|
);
|
|
1662
1747
|
if (trackCommand) return focusTrack(trackCommand[1]!.toLowerCase());
|
|
1748
|
+
// `track cloud grain hold`: focus (or create) a track and grain it.
|
|
1749
|
+
const grainTrack = command.match(
|
|
1750
|
+
/^\/?track\s+([a-z0-9._-]{1,64})\s+(grain\s+.+)$/i,
|
|
1751
|
+
);
|
|
1752
|
+
if (grainTrack && parseGranularCommand(grainTrack[2]!)) {
|
|
1753
|
+
const focused = await focusTrack(grainTrack[1]!.toLowerCase());
|
|
1754
|
+
if (requestedTrack !== grainTrack[1]!.toLowerCase()) return focused;
|
|
1755
|
+
return submit(grainTrack[2]!);
|
|
1756
|
+
}
|
|
1663
1757
|
// `/track piano b`: a name with spaces focuses the track of that name, or
|
|
1664
1758
|
// creates `piano-b` named "piano b".
|
|
1665
1759
|
const namedTrack = command.match(/^\/track\s+([a-z0-9._ -]{1,64})$/i);
|
|
@@ -1672,6 +1766,18 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1672
1766
|
}
|
|
1673
1767
|
const sample = parseSampleCommand(command);
|
|
1674
1768
|
if (sample) return sampleCommand(sample);
|
|
1769
|
+
const fit = parseFitCommand(command);
|
|
1770
|
+
if (fit) return fitCommand(fit);
|
|
1771
|
+
const shift = parseShiftCommand(command);
|
|
1772
|
+
if (shift) {
|
|
1773
|
+
const result = applyShiftCommand(score, requestedTrack, shift);
|
|
1774
|
+
if (!result.ok) return fail(result.message);
|
|
1775
|
+
await commitScore(result.next, "sample.set", { trackId: requestedTrack });
|
|
1776
|
+
await projectSync?.flushScore();
|
|
1777
|
+
return ok(result.message);
|
|
1778
|
+
}
|
|
1779
|
+
const resample = parseResampleCommand(command);
|
|
1780
|
+
if (resample) return resampleCommand(resample);
|
|
1675
1781
|
const pack = parsePackCommand(command);
|
|
1676
1782
|
if (pack) return packCommand(pack);
|
|
1677
1783
|
const pattern = parsePatternCommand(command);
|
|
@@ -1706,6 +1812,34 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1706
1812
|
await commitScore(result.next, result.kind, result.payload);
|
|
1707
1813
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1708
1814
|
}
|
|
1815
|
+
const guitar = parseGuitarCommand(command);
|
|
1816
|
+
if (guitar) {
|
|
1817
|
+
if (guitar.type !== "guitar-show" && guitar.type !== "guitar-hint")
|
|
1818
|
+
await materializeDraft();
|
|
1819
|
+
const result = applyGuitarCommand(score, requestedTrack, guitar);
|
|
1820
|
+
if (result.next && result.kind)
|
|
1821
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1822
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1823
|
+
}
|
|
1824
|
+
const strum = parseStrumCommand(command);
|
|
1825
|
+
if (strum) {
|
|
1826
|
+
if (strum.type === "strum") await materializeDraft();
|
|
1827
|
+
const result = applyStrumCommand(score, requestedTrack, strum, () =>
|
|
1828
|
+
randomUUID().slice(0, 12),
|
|
1829
|
+
);
|
|
1830
|
+
if (result.next && result.kind)
|
|
1831
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1832
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1833
|
+
}
|
|
1834
|
+
const rig = parseRigCommand(command);
|
|
1835
|
+
if (rig) {
|
|
1836
|
+
if (rig.type !== "rig-show" && rig.type !== "rig-hint")
|
|
1837
|
+
await materializeDraft();
|
|
1838
|
+
const result = applyRigCommand(score, requestedTrack, rig);
|
|
1839
|
+
if (result.next && result.kind)
|
|
1840
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1841
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1842
|
+
}
|
|
1709
1843
|
const fx = parseFxCommand(command);
|
|
1710
1844
|
if (fx) {
|
|
1711
1845
|
if (fx.type !== "fx-list") await materializeDraft();
|
|
@@ -1725,6 +1859,8 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1725
1859
|
await commitScore(result.next, result.kind, result.payload);
|
|
1726
1860
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1727
1861
|
}
|
|
1862
|
+
const unknownFx = unknownFxMessage(command);
|
|
1863
|
+
if (unknownFx) return fail(unknownFx);
|
|
1728
1864
|
const expression = parseExpressionCommand(command);
|
|
1729
1865
|
if (expression) {
|
|
1730
1866
|
if (expression.type !== "show" && expression.type !== "invalid")
|
|
@@ -1736,6 +1872,38 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1736
1872
|
}
|
|
1737
1873
|
const tuning = parseTuningCommand(command);
|
|
1738
1874
|
if (tuning) return tuningCommand(tuning);
|
|
1875
|
+
const keysCommand = parseKeysCommand(command);
|
|
1876
|
+
if (keysCommand) {
|
|
1877
|
+
const reads =
|
|
1878
|
+
keysCommand.type === "keys-list" || keysCommand.type === "keys-presets";
|
|
1879
|
+
if (!reads) await materializeDraft();
|
|
1880
|
+
const result = applyKeysCommand(score, requestedTrack, keysCommand);
|
|
1881
|
+
if (result.next && result.kind)
|
|
1882
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1883
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1884
|
+
}
|
|
1885
|
+
const modal = parseModalCommand(command);
|
|
1886
|
+
if (modal) {
|
|
1887
|
+
if (modal.type === "modal-list")
|
|
1888
|
+
tui.openText("modal presets", modalListLines());
|
|
1889
|
+
if (modal.type !== "modal-show" && modal.type !== "modal-list")
|
|
1890
|
+
await materializeDraft();
|
|
1891
|
+
const result = applyModalCommand(score, requestedTrack, modal);
|
|
1892
|
+
if (result.next && result.kind)
|
|
1893
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1894
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1895
|
+
}
|
|
1896
|
+
const windCommand = parseWindCommand(command);
|
|
1897
|
+
if (windCommand) {
|
|
1898
|
+
if (windCommand.type === "wind-list")
|
|
1899
|
+
tui.openText("wind presets", windListLines());
|
|
1900
|
+
if (windCommand.type !== "wind-show" && windCommand.type !== "wind-list")
|
|
1901
|
+
await materializeDraft();
|
|
1902
|
+
const result = applyWindCommand(score, requestedTrack, windCommand);
|
|
1903
|
+
if (result.next && result.kind)
|
|
1904
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1905
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1906
|
+
}
|
|
1739
1907
|
const synth = parseSynthCommand(command);
|
|
1740
1908
|
if (synth) {
|
|
1741
1909
|
if (synth.type !== "synth-list") await materializeDraft();
|
|
@@ -1744,6 +1912,29 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1744
1912
|
await commitScore(result.next, result.kind, result.payload);
|
|
1745
1913
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1746
1914
|
}
|
|
1915
|
+
const stringCommand = parseStringCommand(command);
|
|
1916
|
+
if (stringCommand) {
|
|
1917
|
+
if (
|
|
1918
|
+
stringCommand.type !== "string-list" &&
|
|
1919
|
+
stringCommand.type !== "string-presets"
|
|
1920
|
+
)
|
|
1921
|
+
await materializeDraft();
|
|
1922
|
+
const result = applyStringCommand(score, requestedTrack, stringCommand);
|
|
1923
|
+
if (result.next && result.kind)
|
|
1924
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1925
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1926
|
+
}
|
|
1927
|
+
const grainHint = grainSrcHint(command);
|
|
1928
|
+
if (grainHint) return fail(grainHint);
|
|
1929
|
+
const grain = parseGranularCommand(command);
|
|
1930
|
+
if (grain) {
|
|
1931
|
+
if (grain.type !== "grain-list" && grain.type !== "grain-presets")
|
|
1932
|
+
await materializeDraft();
|
|
1933
|
+
const result = applyGranularCommand(score, requestedTrack, grain);
|
|
1934
|
+
if (result.next && result.kind)
|
|
1935
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1936
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1937
|
+
}
|
|
1747
1938
|
const masterCommand = parseMasterCommand(command);
|
|
1748
1939
|
if (masterCommand) {
|
|
1749
1940
|
if (masterCommand.type === "master-measure")
|
|
@@ -1911,7 +2102,16 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1911
2102
|
const next = score.withTempo(parsed.tempoBpm);
|
|
1912
2103
|
await commitScore(next, "score.tempo", { tempoBpm: parsed.tempoBpm });
|
|
1913
2104
|
clock.follow(next);
|
|
1914
|
-
|
|
2105
|
+
// A bare `bpm <n>` stays song tempo; on a sampler track say where the
|
|
2106
|
+
// sample's own tempo lives (`/bpm`, 0.6 fit).
|
|
2107
|
+
const focused = next.tracks.find((t) => t.id === requestedTrack);
|
|
2108
|
+
const samplerHint =
|
|
2109
|
+
/^\s*bpm\b/i.test(prompt) &&
|
|
2110
|
+
focused?.sampler &&
|
|
2111
|
+
isSamplerInstrument(focused.instrument)
|
|
2112
|
+
? ` · song tempo · the sample's own tempo is /bpm ${parsed.tempoBpm}`
|
|
2113
|
+
: "";
|
|
2114
|
+
return `tempo · ${next.tempoBpm} BPM${samplerHint}`;
|
|
1915
2115
|
}
|
|
1916
2116
|
if (parsed.type === "add-track") return focusTrack(parsed.trackId);
|
|
1917
2117
|
if (parsed.type === "set-bars" || parsed.type === "extend-bars") {
|
|
@@ -1924,15 +2124,45 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1924
2124
|
return `bars · ${bars}`;
|
|
1925
2125
|
}
|
|
1926
2126
|
if (parsed.type === "track") {
|
|
2127
|
+
// An unknown word would store and play a plain sine: say so instead.
|
|
2128
|
+
const word = parsed.patch.instrument;
|
|
2129
|
+
if (word !== undefined && isUnknownInstrument(word)) {
|
|
2130
|
+
const advice = plainSineAdvice(word) ?? "";
|
|
2131
|
+
const near = advice.match(/did you mean (\S+)\?/)?.[1];
|
|
2132
|
+
return fail(
|
|
2133
|
+
`instrument ${truncateForCard(word)} · not a dawg instrument${near ? ` · did you mean ${near}?` : ""} · /menu sounds lists them`,
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
// `instrument jangle`: a guitar alias also loads its rig.
|
|
2137
|
+
const patch = parsed.word
|
|
2138
|
+
? {
|
|
2139
|
+
...parsed.patch,
|
|
2140
|
+
...rigWordPatch(
|
|
2141
|
+
parsed.word,
|
|
2142
|
+
score.tracks.find((t) => t.id === requestedTrack)?.fx,
|
|
2143
|
+
),
|
|
2144
|
+
}
|
|
2145
|
+
: parsed.patch;
|
|
1927
2146
|
const next = applyScoreOperation(score, {
|
|
1928
2147
|
type: "updateTrack",
|
|
1929
2148
|
trackId: requestedTrack,
|
|
1930
|
-
patch
|
|
2149
|
+
patch,
|
|
1931
2150
|
});
|
|
1932
2151
|
await commitScore(next, "score.track", {
|
|
1933
2152
|
trackId: requestedTrack,
|
|
1934
|
-
patch
|
|
2153
|
+
patch,
|
|
1935
2154
|
});
|
|
2155
|
+
// Plain `marimba` keeps the legacy tone; point at the mallet engine.
|
|
2156
|
+
if (parsed.patch.instrument === "marimba" && !("modal" in parsed.patch))
|
|
2157
|
+
return `track · ${requestedTrack} · marimba (legacy tone) · modal marimba for the mallet engine`;
|
|
2158
|
+
// Plain `wind` keeps the legacy tone; point at the wind engine.
|
|
2159
|
+
if (parsed.patch.instrument === "wind" && !("wind" in parsed.patch))
|
|
2160
|
+
return `track · ${requestedTrack} · wind (legacy tone) · wind flute (or sax, trumpet …) for the wind engine`;
|
|
2161
|
+
const sine =
|
|
2162
|
+
word !== undefined && !("string" in parsed.patch)
|
|
2163
|
+
? plainSineAdvice(word)
|
|
2164
|
+
: undefined;
|
|
2165
|
+
if (sine) return `track · ${requestedTrack} · ${sine}`;
|
|
1936
2166
|
return `track · ${requestedTrack}`;
|
|
1937
2167
|
}
|
|
1938
2168
|
if (parsed.type === "automation") {
|
|
@@ -2072,14 +2302,28 @@ async function focusTrack(trackId: string): Promise<Receipt> {
|
|
|
2072
2302
|
)
|
|
2073
2303
|
)
|
|
2074
2304
|
return warn(`${trackId} is open in another window`);
|
|
2075
|
-
const
|
|
2305
|
+
const existing = score.tracks.find((track) => track.id === trackId);
|
|
2306
|
+
const exists = existing !== undefined;
|
|
2307
|
+
// `track cloud`, `track hold-2`: a new granular track with that preset.
|
|
2308
|
+
const grainPreset = granularTrackPreset(trackId);
|
|
2076
2309
|
if (!exists) {
|
|
2077
2310
|
const next = applyScoreOperation(score, {
|
|
2078
2311
|
type: "addTrack",
|
|
2079
2312
|
track: {
|
|
2080
2313
|
id: trackId,
|
|
2081
2314
|
name: trackId,
|
|
2082
|
-
instrument:
|
|
2315
|
+
instrument: grainPreset
|
|
2316
|
+
? "granular"
|
|
2317
|
+
: isDrumInstrument(trackId)
|
|
2318
|
+
? "kit"
|
|
2319
|
+
: "sine",
|
|
2320
|
+
// `track jangle`, `track gtr-metal`…: a guitar voice and its rig.
|
|
2321
|
+
...rigTrackFields(trackId),
|
|
2322
|
+
...(grainPreset ? { granular: { preset: grainPreset } } : {}),
|
|
2323
|
+
// `/track piano` (or grand, felt…) starts on the modelled piano.
|
|
2324
|
+
...newPianoTrack(trackId),
|
|
2325
|
+
// `track vibes`: a 0.6 modal word names the track and its preset.
|
|
2326
|
+
...(isModalWord(trackId) ? instrumentPatchForWord(trackId) : {}),
|
|
2083
2327
|
},
|
|
2084
2328
|
});
|
|
2085
2329
|
await commitScore(next, "track.create", { trackId });
|
|
@@ -2087,7 +2331,17 @@ async function focusTrack(trackId: string): Promise<Receipt> {
|
|
|
2087
2331
|
await port.focus(trackId);
|
|
2088
2332
|
requestedTrack = trackId;
|
|
2089
2333
|
draftTrack = false;
|
|
2090
|
-
|
|
2334
|
+
if (exists && grainPreset && existing.instrument !== "granular")
|
|
2335
|
+
return warn(
|
|
2336
|
+
`track · ${trackId} is a ${existing.instrument} track · grain ${grainPreset} makes it granular`,
|
|
2337
|
+
);
|
|
2338
|
+
return ok(
|
|
2339
|
+
exists
|
|
2340
|
+
? `track · ${trackId}`
|
|
2341
|
+
: grainPreset
|
|
2342
|
+
? `track created · ${trackId} · granular ${grainPreset} · nothing to download`
|
|
2343
|
+
: `track created · ${trackId}`,
|
|
2344
|
+
);
|
|
2091
2345
|
}
|
|
2092
2346
|
|
|
2093
2347
|
/** Decode every sampler voice (cached) and return what failed to load. */
|
|
@@ -2173,6 +2427,64 @@ async function tuningCommand(command: TuningCommand): Promise<Receipt> {
|
|
|
2173
2427
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
2174
2428
|
}
|
|
2175
2429
|
|
|
2430
|
+
/** `/fitmode`, `/bpm`, `/len` on the focused sampler voice (0.6). */
|
|
2431
|
+
async function fitCommand(
|
|
2432
|
+
command: NonNullable<ReturnType<typeof parseFitCommand>>,
|
|
2433
|
+
): Promise<Receipt> {
|
|
2434
|
+
let suggested: "beats" | "tones" | undefined;
|
|
2435
|
+
if (command.control === "fitmode" && command.value === undefined) {
|
|
2436
|
+
const target = fitVoice(score, requestedTrack, command.voice);
|
|
2437
|
+
if ("error" in target) return fail(target.error);
|
|
2438
|
+
const decoded = liveSampleBank?.voices.get(
|
|
2439
|
+
sampleKey(requestedTrack, target.voice),
|
|
2440
|
+
);
|
|
2441
|
+
if (!decoded)
|
|
2442
|
+
return warn(
|
|
2443
|
+
"fit · the sample is still loading · fitmode repitch|beats|tones",
|
|
2444
|
+
);
|
|
2445
|
+
suggested = suggestFitMode(decoded.mono, decoded.sampleRate);
|
|
2446
|
+
}
|
|
2447
|
+
const result = applyFitCommand(score, requestedTrack, command, suggested);
|
|
2448
|
+
if (!result.ok) return fail(result.message);
|
|
2449
|
+
await commitScore(result.next, "sample.set", { trackId: requestedTrack });
|
|
2450
|
+
await projectSync?.flushScore();
|
|
2451
|
+
return ok(
|
|
2452
|
+
suggested
|
|
2453
|
+
? `${result.message} · suggested from the sound (${suggested === "beats" ? "hits" : "held tones"})`
|
|
2454
|
+
: result.message,
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
async function resampleCommand(
|
|
2459
|
+
command: NonNullable<ReturnType<typeof parseResampleCommand>>,
|
|
2460
|
+
): Promise<Receipt> {
|
|
2461
|
+
await materializeDraft();
|
|
2462
|
+
// The source's own samples must be loaded for the render.
|
|
2463
|
+
if (hasSamplerTracks(score)) await sampleProblems(score);
|
|
2464
|
+
const result = await runResample({
|
|
2465
|
+
projectRoot: process.cwd(),
|
|
2466
|
+
score,
|
|
2467
|
+
command,
|
|
2468
|
+
...(liveSampleBank ? { samples: liveSampleBank } : {}),
|
|
2469
|
+
});
|
|
2470
|
+
if (!result.ok) return fail(result.message);
|
|
2471
|
+
const problems = (await sampleProblems(result.next)).filter(
|
|
2472
|
+
(problem) =>
|
|
2473
|
+
problem.trackId === result.trackId && problem.level === "error",
|
|
2474
|
+
);
|
|
2475
|
+
if (problems.length > 0) return fail(`resample · ${problems[0]!.message}`);
|
|
2476
|
+
await commitScore(result.next, "resample", {
|
|
2477
|
+
trackId: result.trackId,
|
|
2478
|
+
src: result.src,
|
|
2479
|
+
sha256: result.sha256,
|
|
2480
|
+
});
|
|
2481
|
+
await port.focus(result.trackId);
|
|
2482
|
+
requestedTrack = result.trackId;
|
|
2483
|
+
draftTrack = false;
|
|
2484
|
+
await projectSync?.flushScore();
|
|
2485
|
+
return ok(result.message);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2176
2488
|
async function sampleCommand(
|
|
2177
2489
|
command: NonNullable<ReturnType<typeof parseSampleCommand>>,
|
|
2178
2490
|
): Promise<Receipt> {
|
|
@@ -2965,7 +3277,11 @@ function stagedChordSettings(): ChordSettings | undefined {
|
|
|
2965
3277
|
if (chords.length === 0) return undefined;
|
|
2966
3278
|
const settings = { ...chordSettings };
|
|
2967
3279
|
for (const command of chords)
|
|
2968
|
-
applyChordsCommand(
|
|
3280
|
+
applyChordsCommand(
|
|
3281
|
+
settings,
|
|
3282
|
+
command.match(CHORDS_COMMAND)![1]!,
|
|
3283
|
+
score.tempoBpm,
|
|
3284
|
+
);
|
|
2969
3285
|
return settings;
|
|
2970
3286
|
}
|
|
2971
3287
|
|
|
@@ -3022,7 +3338,11 @@ async function applyStaged(
|
|
|
3022
3338
|
const chords = command.trim().match(CHORDS_COMMAND);
|
|
3023
3339
|
if (chords) {
|
|
3024
3340
|
// Window state: checked on a copy; kept settings apply on Enter.
|
|
3025
|
-
const result = applyChordsCommand(
|
|
3341
|
+
const result = applyChordsCommand(
|
|
3342
|
+
{ ...chordSettings },
|
|
3343
|
+
chords[1]!,
|
|
3344
|
+
base.tempoBpm,
|
|
3345
|
+
);
|
|
3026
3346
|
return { next: base, ok: result.ok, message: result.message };
|
|
3027
3347
|
}
|
|
3028
3348
|
await prefetchStaged(base, command);
|
|
@@ -3215,7 +3535,11 @@ async function keepStaged(): Promise<Receipt> {
|
|
|
3215
3535
|
CHORDS_COMMAND.test(command),
|
|
3216
3536
|
);
|
|
3217
3537
|
for (const command of chordCommands)
|
|
3218
|
-
applyChordsCommand(
|
|
3538
|
+
applyChordsCommand(
|
|
3539
|
+
chordSettings,
|
|
3540
|
+
command.match(CHORDS_COMMAND)![1]!,
|
|
3541
|
+
score.tempoBpm,
|
|
3542
|
+
);
|
|
3219
3543
|
const scoreCommands = taken.commands.filter(
|
|
3220
3544
|
(command) => !CHORDS_COMMAND.test(command),
|
|
3221
3545
|
);
|
package/src/project/check.ts
CHANGED
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
formatDiagnostic,
|
|
9
9
|
type Diagnostic,
|
|
10
10
|
} from "../../core/sdk/eval.ts";
|
|
11
|
+
import {
|
|
12
|
+
legacyResonatorWarnings,
|
|
13
|
+
modalPairWarnings,
|
|
14
|
+
} from "../../core/resonators.ts";
|
|
15
|
+
import { plainSineWarnings } from "../audio/instrument-check.ts";
|
|
11
16
|
import { isProject } from "./init.ts";
|
|
12
17
|
import { typecheckProject } from "./typecheck.ts";
|
|
13
18
|
|
|
@@ -23,6 +28,8 @@ export type CheckReport = Readonly<{
|
|
|
23
28
|
evalMs: number;
|
|
24
29
|
tracks: number;
|
|
25
30
|
notes: number;
|
|
31
|
+
/** Advice that does not fail the check (0.6: legacy instrument words). */
|
|
32
|
+
warnings: readonly string[];
|
|
26
33
|
}>;
|
|
27
34
|
|
|
28
35
|
/** Runs both checks on `project`; never throws for project problems. */
|
|
@@ -45,6 +52,15 @@ export async function checkProject(
|
|
|
45
52
|
evalMs: evaluated.ms,
|
|
46
53
|
tracks: evaluated.ok ? evaluated.score.tracks.length : 0,
|
|
47
54
|
notes: evaluated.ok ? evaluated.score.notes.length : 0,
|
|
55
|
+
warnings: Object.freeze(
|
|
56
|
+
evaluated.ok
|
|
57
|
+
? [
|
|
58
|
+
...legacyResonatorWarnings(evaluated.score.tracks),
|
|
59
|
+
...modalPairWarnings(evaluated.score.tracks),
|
|
60
|
+
...plainSineWarnings(evaluated.score.tracks),
|
|
61
|
+
]
|
|
62
|
+
: [],
|
|
63
|
+
),
|
|
48
64
|
});
|
|
49
65
|
}
|
|
50
66
|
|
|
@@ -65,6 +81,7 @@ export async function runCheckCommand(
|
|
|
65
81
|
const report = await checkProject(project, deps);
|
|
66
82
|
for (const diagnostic of report.diagnostics)
|
|
67
83
|
stderr.write(`${formatDiagnostic(diagnostic)}\n`);
|
|
84
|
+
for (const warning of report.warnings) stderr.write(`warning: ${warning}\n`);
|
|
68
85
|
if (report.ok) {
|
|
69
86
|
stdout.write(
|
|
70
87
|
`ok · ${report.tracks} track${report.tracks === 1 ? "" : "s"}, ${report.notes} note${
|
package/src/render.ts
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
scoreFromJSON,
|
|
13
|
+
ScoreValidationError,
|
|
14
|
+
withNoteCap,
|
|
15
|
+
type TrackScore,
|
|
16
|
+
} from "../core/score.ts";
|
|
12
17
|
import { decodeLoop } from "../core/loop.ts";
|
|
13
18
|
import { scoreToMidi } from "../core/midi.ts";
|
|
14
19
|
import { RENDER_CHANNELS, encodeWav, withWavCues } from "./audio/wav.ts";
|
|
@@ -19,7 +24,12 @@ import {
|
|
|
19
24
|
measurementLine,
|
|
20
25
|
parseMasterCommand,
|
|
21
26
|
} from "./commands/master.ts";
|
|
22
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
BAKED_NOTE_CAP,
|
|
29
|
+
bakeTrackTime,
|
|
30
|
+
findSection,
|
|
31
|
+
sectionScore,
|
|
32
|
+
} from "../core/sections.ts";
|
|
23
33
|
import {
|
|
24
34
|
exportScore,
|
|
25
35
|
renderArrangedPcm,
|
|
@@ -109,7 +119,9 @@ export async function runRenderCommand(
|
|
|
109
119
|
return 1;
|
|
110
120
|
}
|
|
111
121
|
// One section as it plays when looped: its mutes and variations, no form.
|
|
112
|
-
score =
|
|
122
|
+
score = withNoteCap(BAKED_NOTE_CAP, () =>
|
|
123
|
+
sectionScore(bakeTrackTime(score), section),
|
|
124
|
+
);
|
|
113
125
|
}
|
|
114
126
|
if (/\.midi?$/i.test(target)) {
|
|
115
127
|
// MIDI carries notes, not audio: the master and loudness flags do not apply.
|
|
@@ -156,6 +168,16 @@ export async function runRenderCommand(
|
|
|
156
168
|
samples = await new SampleLibrary({ projectRoot: workspace }).load(score);
|
|
157
169
|
for (const problem of samples.problems)
|
|
158
170
|
stderr.write(`sample ${problem.level} · ${problem.message}\n`);
|
|
171
|
+
// A voice that cannot load (a missing file, a pin that no longer
|
|
172
|
+
// matches) would render as silence: fail rather than write a song with
|
|
173
|
+
// a part missing.
|
|
174
|
+
const failed = samples.problems.filter((p) => p.level === "error").length;
|
|
175
|
+
if (failed > 0) {
|
|
176
|
+
stderr.write(
|
|
177
|
+
`render failed · ${failed} sample ${failed === 1 ? "voice" : "voices"} could not load · fix or re-pick ${failed === 1 ? "it" : "them"} and render again\n`,
|
|
178
|
+
);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
159
181
|
}
|
|
160
182
|
// A song with a master is a deliverable: 48 kHz unless --rate says
|
|
161
183
|
// otherwise; a plain song keeps the engine's rate, as dawg 0.4 wrote it.
|
|
@@ -172,7 +194,14 @@ export async function runRenderCommand(
|
|
|
172
194
|
);
|
|
173
195
|
return 2;
|
|
174
196
|
}
|
|
175
|
-
|
|
197
|
+
let audio;
|
|
198
|
+
try {
|
|
199
|
+
audio = renderArrangedPcm(score, { samples, sampleRate });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (!(error instanceof ScoreValidationError)) throw error;
|
|
202
|
+
stderr.write(`render failed · ${error.message}\n`);
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
176
205
|
let wav = encodeWav(audio.pcm, audio.sampleRate, RENDER_CHANNELS);
|
|
177
206
|
// Section starts as cue points (a --section render is one section).
|
|
178
207
|
if (only === undefined)
|
|
@@ -200,6 +229,15 @@ export async function runRenderCommand(
|
|
|
200
229
|
`rendered · ${target} · ${seconds.toFixed(2)} s · ${wav.byteLength} bytes · ${sha}\n`,
|
|
201
230
|
);
|
|
202
231
|
if (credits) stdout.write(`credits · ${credits}\n`);
|
|
232
|
+
const clipped = clippedSamples(audio.pcm);
|
|
233
|
+
if (clipped > 0)
|
|
234
|
+
stderr.write(
|
|
235
|
+
`warning · ${clipped} ${clipped === 1 ? "sample clips" : "samples clip"} at full scale · ${
|
|
236
|
+
audio.master
|
|
237
|
+
? "add master limiter or lower the master gain"
|
|
238
|
+
: "lower track volumes or add a master with a limiter (master streaming, or master: { limiter: {} } in song.ts)"
|
|
239
|
+
}\n`,
|
|
240
|
+
);
|
|
203
241
|
if (options.has("--measure"))
|
|
204
242
|
stdout.write(
|
|
205
243
|
`loudness · ${measurementLine(measureRendered(audio, false).mix, audio.master)}\n`,
|
|
@@ -209,6 +247,13 @@ export async function runRenderCommand(
|
|
|
209
247
|
return 0;
|
|
210
248
|
}
|
|
211
249
|
|
|
250
|
+
/** Samples at or past 16-bit full scale: the mix bus hit the rails. */
|
|
251
|
+
export function clippedSamples(pcm: Int16Array): number {
|
|
252
|
+
let count = 0;
|
|
253
|
+
for (const value of pcm) if (value >= 32_767 || value <= -32_768) count += 1;
|
|
254
|
+
return count;
|
|
255
|
+
}
|
|
256
|
+
|
|
212
257
|
async function loadScore(
|
|
213
258
|
workspace: string,
|
|
214
259
|
options: ReadonlyMap<string, string>,
|