@hraness/dawg 0.5.0 → 0.6.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 +29 -0
- package/DAWG.md +269 -13
- package/README.md +4 -4
- package/core/diff.ts +4 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/resonators.ts +574 -0
- package/core/score.ts +181 -6
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +169 -13
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +1114 -21
- package/core/sections.ts +34 -8
- package/core/strings.ts +845 -0
- package/guides/effects.md +3 -1
- package/guides/sounds.md +4 -0
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +7 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +39 -2
- package/src/agent/preview-tool.ts +6 -1
- package/src/agent/tools.ts +538 -14
- package/src/agent/xcb-agent.ts +7 -0
- package/src/audio/arrange.ts +21 -3
- package/src/audio/dsp/bank.ts +233 -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/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +6 -1
- 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 +10 -1
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +231 -12
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +2 -0
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +163 -17
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +78 -6
- package/src/commands/expression.ts +9 -2
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +74 -3
- package/src/commands/keys.ts +353 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/time.ts +4 -1
- package/src/main.ts +210 -8
- package/src/project/check.ts +13 -0
- package/src/render.ts +33 -4
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +332 -9
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/play-session.ts +45 -2
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,36 @@ 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
|
+
applyModalCommand,
|
|
74
|
+
modalListLines,
|
|
75
|
+
parseModalCommand,
|
|
76
|
+
} from "./commands/modal.ts";
|
|
58
77
|
import { applySynthCommand, parseSynthCommand } from "./commands/synth.ts";
|
|
78
|
+
import { applyStringCommand, parseStringCommand } from "./commands/string.ts";
|
|
79
|
+
import { instrumentPatchForWord, isModalWord } from "../core/resonators.ts";
|
|
80
|
+
import {
|
|
81
|
+
applyGranularCommand,
|
|
82
|
+
granularTrackPreset,
|
|
83
|
+
grainSrcHint,
|
|
84
|
+
parseGranularCommand,
|
|
85
|
+
} from "./commands/granular.ts";
|
|
86
|
+
import {
|
|
87
|
+
applyKeysCommand,
|
|
88
|
+
newPianoTrack,
|
|
89
|
+
parseKeysCommand,
|
|
90
|
+
} from "./commands/keys.ts";
|
|
59
91
|
import {
|
|
60
92
|
applyTuningCommand,
|
|
61
93
|
importTuningFile,
|
|
@@ -94,9 +126,12 @@ import {
|
|
|
94
126
|
samplerTarget,
|
|
95
127
|
voiceNameFrom,
|
|
96
128
|
} from "./commands/sample.ts";
|
|
129
|
+
import { applyFitCommand, fitVoice, parseFitCommand } from "./commands/fit.ts";
|
|
130
|
+
import { suggestFitMode } from "./audio/dsp/onset.ts";
|
|
97
131
|
import {
|
|
98
132
|
SampleLibrary,
|
|
99
133
|
hasSamplerTracks,
|
|
134
|
+
sampleKey,
|
|
100
135
|
type SampleBank,
|
|
101
136
|
type SampleProblem,
|
|
102
137
|
} from "./audio/samples.ts";
|
|
@@ -216,6 +251,7 @@ import {
|
|
|
216
251
|
addNote,
|
|
217
252
|
applyScoreOperation,
|
|
218
253
|
createScore,
|
|
254
|
+
isSamplerInstrument,
|
|
219
255
|
PACK_PREFIX,
|
|
220
256
|
scoreFromJSON,
|
|
221
257
|
SCORE_LIMITS,
|
|
@@ -260,6 +296,9 @@ function parsesLocally(text: string): boolean {
|
|
|
260
296
|
parseRhythmCommand,
|
|
261
297
|
parseFxCommand,
|
|
262
298
|
parseSynthCommand,
|
|
299
|
+
parseStringCommand,
|
|
300
|
+
parseGranularCommand,
|
|
301
|
+
parseKeysCommand,
|
|
263
302
|
parseExpressionCommand,
|
|
264
303
|
parseMasterCommand,
|
|
265
304
|
(value: string) => parseSectionCommand(value, score),
|
|
@@ -267,9 +306,12 @@ function parsesLocally(text: string): boolean {
|
|
|
267
306
|
parseKitCommand,
|
|
268
307
|
parsePackCommand,
|
|
269
308
|
parseSampleCommand,
|
|
309
|
+
parseFitCommand,
|
|
270
310
|
parseWavetableCommand,
|
|
271
311
|
parseTimeCommand,
|
|
272
312
|
parseTuningCommand,
|
|
313
|
+
parseRigCommand,
|
|
314
|
+
parseModalCommand,
|
|
273
315
|
].some((parse) => parse(text) !== undefined);
|
|
274
316
|
}
|
|
275
317
|
|
|
@@ -416,7 +458,13 @@ if (!demo && process.env.DAWG_AI !== "0" && stdout.isTTY)
|
|
|
416
458
|
|
|
417
459
|
const initial = createScore({
|
|
418
460
|
tracks: [
|
|
419
|
-
{
|
|
461
|
+
{
|
|
462
|
+
id: requestedTrack,
|
|
463
|
+
name: requestedTrack,
|
|
464
|
+
instrument: initialInstrument,
|
|
465
|
+
// `dawg jangle`: a guitar alias starts with its voice and rig.
|
|
466
|
+
...rigTrackFields(requestedTrack),
|
|
467
|
+
},
|
|
420
468
|
],
|
|
421
469
|
});
|
|
422
470
|
const sessionOptions: { sessionId?: string; setCurrent: boolean } = {
|
|
@@ -1660,6 +1708,15 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1660
1708
|
/^\/?(?:add\s+)?track\s+([a-z0-9._-]{1,64})$/i,
|
|
1661
1709
|
);
|
|
1662
1710
|
if (trackCommand) return focusTrack(trackCommand[1]!.toLowerCase());
|
|
1711
|
+
// `track cloud grain hold`: focus (or create) a track and grain it.
|
|
1712
|
+
const grainTrack = command.match(
|
|
1713
|
+
/^\/?track\s+([a-z0-9._-]{1,64})\s+(grain\s+.+)$/i,
|
|
1714
|
+
);
|
|
1715
|
+
if (grainTrack && parseGranularCommand(grainTrack[2]!)) {
|
|
1716
|
+
const focused = await focusTrack(grainTrack[1]!.toLowerCase());
|
|
1717
|
+
if (requestedTrack !== grainTrack[1]!.toLowerCase()) return focused;
|
|
1718
|
+
return submit(grainTrack[2]!);
|
|
1719
|
+
}
|
|
1663
1720
|
// `/track piano b`: a name with spaces focuses the track of that name, or
|
|
1664
1721
|
// creates `piano-b` named "piano b".
|
|
1665
1722
|
const namedTrack = command.match(/^\/track\s+([a-z0-9._ -]{1,64})$/i);
|
|
@@ -1672,6 +1729,8 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1672
1729
|
}
|
|
1673
1730
|
const sample = parseSampleCommand(command);
|
|
1674
1731
|
if (sample) return sampleCommand(sample);
|
|
1732
|
+
const fit = parseFitCommand(command);
|
|
1733
|
+
if (fit) return fitCommand(fit);
|
|
1675
1734
|
const pack = parsePackCommand(command);
|
|
1676
1735
|
if (pack) return packCommand(pack);
|
|
1677
1736
|
const pattern = parsePatternCommand(command);
|
|
@@ -1706,6 +1765,15 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1706
1765
|
await commitScore(result.next, result.kind, result.payload);
|
|
1707
1766
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1708
1767
|
}
|
|
1768
|
+
const rig = parseRigCommand(command);
|
|
1769
|
+
if (rig) {
|
|
1770
|
+
if (rig.type !== "rig-show" && rig.type !== "rig-hint")
|
|
1771
|
+
await materializeDraft();
|
|
1772
|
+
const result = applyRigCommand(score, requestedTrack, rig);
|
|
1773
|
+
if (result.next && result.kind)
|
|
1774
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1775
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1776
|
+
}
|
|
1709
1777
|
const fx = parseFxCommand(command);
|
|
1710
1778
|
if (fx) {
|
|
1711
1779
|
if (fx.type !== "fx-list") await materializeDraft();
|
|
@@ -1725,6 +1793,8 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1725
1793
|
await commitScore(result.next, result.kind, result.payload);
|
|
1726
1794
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1727
1795
|
}
|
|
1796
|
+
const unknownFx = unknownFxMessage(command);
|
|
1797
|
+
if (unknownFx) return fail(unknownFx);
|
|
1728
1798
|
const expression = parseExpressionCommand(command);
|
|
1729
1799
|
if (expression) {
|
|
1730
1800
|
if (expression.type !== "show" && expression.type !== "invalid")
|
|
@@ -1736,6 +1806,27 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1736
1806
|
}
|
|
1737
1807
|
const tuning = parseTuningCommand(command);
|
|
1738
1808
|
if (tuning) return tuningCommand(tuning);
|
|
1809
|
+
const keysCommand = parseKeysCommand(command);
|
|
1810
|
+
if (keysCommand) {
|
|
1811
|
+
const reads =
|
|
1812
|
+
keysCommand.type === "keys-list" || keysCommand.type === "keys-presets";
|
|
1813
|
+
if (!reads) await materializeDraft();
|
|
1814
|
+
const result = applyKeysCommand(score, requestedTrack, keysCommand);
|
|
1815
|
+
if (result.next && result.kind)
|
|
1816
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1817
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1818
|
+
}
|
|
1819
|
+
const modal = parseModalCommand(command);
|
|
1820
|
+
if (modal) {
|
|
1821
|
+
if (modal.type === "modal-list")
|
|
1822
|
+
tui.openText("modal presets", modalListLines());
|
|
1823
|
+
if (modal.type !== "modal-show" && modal.type !== "modal-list")
|
|
1824
|
+
await materializeDraft();
|
|
1825
|
+
const result = applyModalCommand(score, requestedTrack, modal);
|
|
1826
|
+
if (result.next && result.kind)
|
|
1827
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1828
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1829
|
+
}
|
|
1739
1830
|
const synth = parseSynthCommand(command);
|
|
1740
1831
|
if (synth) {
|
|
1741
1832
|
if (synth.type !== "synth-list") await materializeDraft();
|
|
@@ -1744,6 +1835,29 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1744
1835
|
await commitScore(result.next, result.kind, result.payload);
|
|
1745
1836
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1746
1837
|
}
|
|
1838
|
+
const stringCommand = parseStringCommand(command);
|
|
1839
|
+
if (stringCommand) {
|
|
1840
|
+
if (
|
|
1841
|
+
stringCommand.type !== "string-list" &&
|
|
1842
|
+
stringCommand.type !== "string-presets"
|
|
1843
|
+
)
|
|
1844
|
+
await materializeDraft();
|
|
1845
|
+
const result = applyStringCommand(score, requestedTrack, stringCommand);
|
|
1846
|
+
if (result.next && result.kind)
|
|
1847
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1848
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1849
|
+
}
|
|
1850
|
+
const grainHint = grainSrcHint(command);
|
|
1851
|
+
if (grainHint) return fail(grainHint);
|
|
1852
|
+
const grain = parseGranularCommand(command);
|
|
1853
|
+
if (grain) {
|
|
1854
|
+
if (grain.type !== "grain-list" && grain.type !== "grain-presets")
|
|
1855
|
+
await materializeDraft();
|
|
1856
|
+
const result = applyGranularCommand(score, requestedTrack, grain);
|
|
1857
|
+
if (result.next && result.kind)
|
|
1858
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
1859
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
1860
|
+
}
|
|
1747
1861
|
const masterCommand = parseMasterCommand(command);
|
|
1748
1862
|
if (masterCommand) {
|
|
1749
1863
|
if (masterCommand.type === "master-measure")
|
|
@@ -1911,7 +2025,16 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1911
2025
|
const next = score.withTempo(parsed.tempoBpm);
|
|
1912
2026
|
await commitScore(next, "score.tempo", { tempoBpm: parsed.tempoBpm });
|
|
1913
2027
|
clock.follow(next);
|
|
1914
|
-
|
|
2028
|
+
// A bare `bpm <n>` stays song tempo; on a sampler track say where the
|
|
2029
|
+
// sample's own tempo lives (`/bpm`, 0.6 fit).
|
|
2030
|
+
const focused = next.tracks.find((t) => t.id === requestedTrack);
|
|
2031
|
+
const samplerHint =
|
|
2032
|
+
/^\s*bpm\b/i.test(prompt) &&
|
|
2033
|
+
focused?.sampler &&
|
|
2034
|
+
isSamplerInstrument(focused.instrument)
|
|
2035
|
+
? ` · song tempo · the sample's own tempo is /bpm ${parsed.tempoBpm}`
|
|
2036
|
+
: "";
|
|
2037
|
+
return `tempo · ${next.tempoBpm} BPM${samplerHint}`;
|
|
1915
2038
|
}
|
|
1916
2039
|
if (parsed.type === "add-track") return focusTrack(parsed.trackId);
|
|
1917
2040
|
if (parsed.type === "set-bars" || parsed.type === "extend-bars") {
|
|
@@ -1924,15 +2047,42 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1924
2047
|
return `bars · ${bars}`;
|
|
1925
2048
|
}
|
|
1926
2049
|
if (parsed.type === "track") {
|
|
2050
|
+
// An unknown word would store and play a plain sine: say so instead.
|
|
2051
|
+
const word = parsed.patch.instrument;
|
|
2052
|
+
if (word !== undefined && isUnknownInstrument(word)) {
|
|
2053
|
+
const advice = plainSineAdvice(word) ?? "";
|
|
2054
|
+
const near = advice.match(/did you mean (\S+)\?/)?.[1];
|
|
2055
|
+
return fail(
|
|
2056
|
+
`instrument ${truncateForCard(word)} · not a dawg instrument${near ? ` · did you mean ${near}?` : ""} · /menu sounds lists them`,
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
// `instrument jangle`: a guitar alias also loads its rig.
|
|
2060
|
+
const patch = parsed.word
|
|
2061
|
+
? {
|
|
2062
|
+
...parsed.patch,
|
|
2063
|
+
...rigWordPatch(
|
|
2064
|
+
parsed.word,
|
|
2065
|
+
score.tracks.find((t) => t.id === requestedTrack)?.fx,
|
|
2066
|
+
),
|
|
2067
|
+
}
|
|
2068
|
+
: parsed.patch;
|
|
1927
2069
|
const next = applyScoreOperation(score, {
|
|
1928
2070
|
type: "updateTrack",
|
|
1929
2071
|
trackId: requestedTrack,
|
|
1930
|
-
patch
|
|
2072
|
+
patch,
|
|
1931
2073
|
});
|
|
1932
2074
|
await commitScore(next, "score.track", {
|
|
1933
2075
|
trackId: requestedTrack,
|
|
1934
|
-
patch
|
|
2076
|
+
patch,
|
|
1935
2077
|
});
|
|
2078
|
+
// Plain `marimba` keeps the legacy tone; point at the mallet engine.
|
|
2079
|
+
if (parsed.patch.instrument === "marimba" && !("modal" in parsed.patch))
|
|
2080
|
+
return `track · ${requestedTrack} · marimba (legacy tone) · modal marimba for the mallet engine`;
|
|
2081
|
+
const sine =
|
|
2082
|
+
word !== undefined && !("string" in parsed.patch)
|
|
2083
|
+
? plainSineAdvice(word)
|
|
2084
|
+
: undefined;
|
|
2085
|
+
if (sine) return `track · ${requestedTrack} · ${sine}`;
|
|
1936
2086
|
return `track · ${requestedTrack}`;
|
|
1937
2087
|
}
|
|
1938
2088
|
if (parsed.type === "automation") {
|
|
@@ -2072,14 +2222,28 @@ async function focusTrack(trackId: string): Promise<Receipt> {
|
|
|
2072
2222
|
)
|
|
2073
2223
|
)
|
|
2074
2224
|
return warn(`${trackId} is open in another window`);
|
|
2075
|
-
const
|
|
2225
|
+
const existing = score.tracks.find((track) => track.id === trackId);
|
|
2226
|
+
const exists = existing !== undefined;
|
|
2227
|
+
// `track cloud`, `track hold-2`: a new granular track with that preset.
|
|
2228
|
+
const grainPreset = granularTrackPreset(trackId);
|
|
2076
2229
|
if (!exists) {
|
|
2077
2230
|
const next = applyScoreOperation(score, {
|
|
2078
2231
|
type: "addTrack",
|
|
2079
2232
|
track: {
|
|
2080
2233
|
id: trackId,
|
|
2081
2234
|
name: trackId,
|
|
2082
|
-
instrument:
|
|
2235
|
+
instrument: grainPreset
|
|
2236
|
+
? "granular"
|
|
2237
|
+
: isDrumInstrument(trackId)
|
|
2238
|
+
? "kit"
|
|
2239
|
+
: "sine",
|
|
2240
|
+
// `track jangle`, `track gtr-metal`…: a guitar voice and its rig.
|
|
2241
|
+
...rigTrackFields(trackId),
|
|
2242
|
+
...(grainPreset ? { granular: { preset: grainPreset } } : {}),
|
|
2243
|
+
// `/track piano` (or grand, felt…) starts on the modelled piano.
|
|
2244
|
+
...newPianoTrack(trackId),
|
|
2245
|
+
// `track vibes`: a 0.6 modal word names the track and its preset.
|
|
2246
|
+
...(isModalWord(trackId) ? instrumentPatchForWord(trackId) : {}),
|
|
2083
2247
|
},
|
|
2084
2248
|
});
|
|
2085
2249
|
await commitScore(next, "track.create", { trackId });
|
|
@@ -2087,7 +2251,17 @@ async function focusTrack(trackId: string): Promise<Receipt> {
|
|
|
2087
2251
|
await port.focus(trackId);
|
|
2088
2252
|
requestedTrack = trackId;
|
|
2089
2253
|
draftTrack = false;
|
|
2090
|
-
|
|
2254
|
+
if (exists && grainPreset && existing.instrument !== "granular")
|
|
2255
|
+
return warn(
|
|
2256
|
+
`track · ${trackId} is a ${existing.instrument} track · grain ${grainPreset} makes it granular`,
|
|
2257
|
+
);
|
|
2258
|
+
return ok(
|
|
2259
|
+
exists
|
|
2260
|
+
? `track · ${trackId}`
|
|
2261
|
+
: grainPreset
|
|
2262
|
+
? `track created · ${trackId} · granular ${grainPreset} · nothing to download`
|
|
2263
|
+
: `track created · ${trackId}`,
|
|
2264
|
+
);
|
|
2091
2265
|
}
|
|
2092
2266
|
|
|
2093
2267
|
/** Decode every sampler voice (cached) and return what failed to load. */
|
|
@@ -2173,6 +2347,34 @@ async function tuningCommand(command: TuningCommand): Promise<Receipt> {
|
|
|
2173
2347
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
2174
2348
|
}
|
|
2175
2349
|
|
|
2350
|
+
/** `/fitmode`, `/bpm`, `/len` on the focused sampler voice (0.6). */
|
|
2351
|
+
async function fitCommand(
|
|
2352
|
+
command: NonNullable<ReturnType<typeof parseFitCommand>>,
|
|
2353
|
+
): Promise<Receipt> {
|
|
2354
|
+
let suggested: "beats" | "tones" | undefined;
|
|
2355
|
+
if (command.control === "fitmode" && command.value === undefined) {
|
|
2356
|
+
const target = fitVoice(score, requestedTrack, command.voice);
|
|
2357
|
+
if ("error" in target) return fail(target.error);
|
|
2358
|
+
const decoded = liveSampleBank?.voices.get(
|
|
2359
|
+
sampleKey(requestedTrack, target.voice),
|
|
2360
|
+
);
|
|
2361
|
+
if (!decoded)
|
|
2362
|
+
return warn(
|
|
2363
|
+
"fit · the sample is still loading · fitmode repitch|beats|tones",
|
|
2364
|
+
);
|
|
2365
|
+
suggested = suggestFitMode(decoded.mono, decoded.sampleRate);
|
|
2366
|
+
}
|
|
2367
|
+
const result = applyFitCommand(score, requestedTrack, command, suggested);
|
|
2368
|
+
if (!result.ok) return fail(result.message);
|
|
2369
|
+
await commitScore(result.next, "sample.set", { trackId: requestedTrack });
|
|
2370
|
+
await projectSync?.flushScore();
|
|
2371
|
+
return ok(
|
|
2372
|
+
suggested
|
|
2373
|
+
? `${result.message} · suggested from the sound (${suggested === "beats" ? "hits" : "held tones"})`
|
|
2374
|
+
: result.message,
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2176
2378
|
async function sampleCommand(
|
|
2177
2379
|
command: NonNullable<ReturnType<typeof parseSampleCommand>>,
|
|
2178
2380
|
): Promise<Receipt> {
|
package/src/project/check.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
formatDiagnostic,
|
|
9
9
|
type Diagnostic,
|
|
10
10
|
} from "../../core/sdk/eval.ts";
|
|
11
|
+
import { legacyResonatorWarnings } from "../../core/resonators.ts";
|
|
12
|
+
import { plainSineWarnings } from "../audio/instrument-check.ts";
|
|
11
13
|
import { isProject } from "./init.ts";
|
|
12
14
|
import { typecheckProject } from "./typecheck.ts";
|
|
13
15
|
|
|
@@ -23,6 +25,8 @@ export type CheckReport = Readonly<{
|
|
|
23
25
|
evalMs: number;
|
|
24
26
|
tracks: number;
|
|
25
27
|
notes: number;
|
|
28
|
+
/** Advice that does not fail the check (0.6: legacy instrument words). */
|
|
29
|
+
warnings: readonly string[];
|
|
26
30
|
}>;
|
|
27
31
|
|
|
28
32
|
/** Runs both checks on `project`; never throws for project problems. */
|
|
@@ -45,6 +49,14 @@ export async function checkProject(
|
|
|
45
49
|
evalMs: evaluated.ms,
|
|
46
50
|
tracks: evaluated.ok ? evaluated.score.tracks.length : 0,
|
|
47
51
|
notes: evaluated.ok ? evaluated.score.notes.length : 0,
|
|
52
|
+
warnings: Object.freeze(
|
|
53
|
+
evaluated.ok
|
|
54
|
+
? [
|
|
55
|
+
...legacyResonatorWarnings(evaluated.score.tracks),
|
|
56
|
+
...plainSineWarnings(evaluated.score.tracks),
|
|
57
|
+
]
|
|
58
|
+
: [],
|
|
59
|
+
),
|
|
48
60
|
});
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -65,6 +77,7 @@ export async function runCheckCommand(
|
|
|
65
77
|
const report = await checkProject(project, deps);
|
|
66
78
|
for (const diagnostic of report.diagnostics)
|
|
67
79
|
stderr.write(`${formatDiagnostic(diagnostic)}\n`);
|
|
80
|
+
for (const warning of report.warnings) stderr.write(`warning: ${warning}\n`);
|
|
68
81
|
if (report.ok) {
|
|
69
82
|
stdout.write(
|
|
70
83
|
`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)
|
package/src/tui/audition.ts
CHANGED
|
@@ -528,9 +528,9 @@ export function auditionKey(value: string): AuditionKey | undefined {
|
|
|
528
528
|
* before.
|
|
529
529
|
*/
|
|
530
530
|
const STAGEABLE =
|
|
531
|
-
/^\/?(?:(fx|effects|filter|lowpass|synth|wt|wavetable|kit|instrument|vol|volume|pan|gain|speed|warpmode|root|pattern|euclid|art|articulation|bend|vibrato|glide|portamento|velcurve|vel-curve|pedal|sustain|humanize|tuning|tune|master)\s+\S|pack\s+use\s+\S)/i;
|
|
531
|
+
/^\/?(?:(fx|effects|filter|lowpass|synth|string|modal|wt|wavetable|grain|kit|instrument|vol|volume|pan|gain|speed|warpmode|root|pattern|euclid|art|articulation|bend|vibrato|glide|portamento|velcurve|vel-curve|pedal|sustain|humanize|tuning|tune|master|keys|piano)\s+\S|pack\s+use\s+\S|(?:piano|grand|upright|felt|honkytonk|prepared|ballad)(?:\s+piano)?\s*$)/i;
|
|
532
532
|
/** Subcommands that list or show instead of changing the sound. */
|
|
533
|
-
const READ_ONLY = /^\/?\S+\s+(list|show|info|help|measure|meter)\s*$/i;
|
|
533
|
+
const READ_ONLY = /^\/?\S+\s+(list|show|info|help|measure|meter|presets?)\s*$/i;
|
|
534
534
|
/** `master <unit>` alone only shows the unit. */
|
|
535
535
|
const MASTER_SHOW = /^\/?master\s+(eq|glue|tape|width|limiter)\s*$/i;
|
|
536
536
|
const MASTER_COMMAND = /^\/?master\s/i;
|