@hraness/dawg 0.6.0 → 0.7.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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sing` prompt command (0.7): the built-in singing voice on the
|
|
3
|
+
* focused track (core/sing.ts). The menu and the `set_sing` / `set_vowels`
|
|
4
|
+
* agent tools build the same edits.
|
|
5
|
+
*
|
|
6
|
+
* sing show the track's preset and overrides
|
|
7
|
+
* sing <preset> aah ooh choir oohchoir chorale airy
|
|
8
|
+
* glass lament soprano basso drone
|
|
9
|
+
* khoomei sygyt kargyraa
|
|
10
|
+
* sing <preset> <param> <value>… a preset with overrides
|
|
11
|
+
* sing <param> <value> [<param> <value>…] vowel o, voices 6, voice bass
|
|
12
|
+
* sing drone <D3|50> throat mode on the current preset
|
|
13
|
+
* sing harmonics 6-12 the overtone range a throat track picks
|
|
14
|
+
* sing <param> off back to the preset's value
|
|
15
|
+
* sing vowels a e i o>u … the track's notes' vowels, in order, cycled
|
|
16
|
+
* sing reset | sing off | sing list
|
|
17
|
+
* note vowel <a|a>u|off> [target] the sung vowel of some notes
|
|
18
|
+
*
|
|
19
|
+
* Any edit makes the track a sing track (`instrument "sing"` plus a `sing`
|
|
20
|
+
* field). One `updateTrack` revision and one undo step per command.
|
|
21
|
+
*/
|
|
22
|
+
import { isGuideInstrument } from "../../core/clips.ts";
|
|
23
|
+
import { parseKey } from "../../core/chords.ts";
|
|
24
|
+
import { FxValidationError } from "../../core/params.ts";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_SING_PRESET,
|
|
27
|
+
isSingPreset,
|
|
28
|
+
normalizeSing,
|
|
29
|
+
normalizeVowel,
|
|
30
|
+
parseDrone,
|
|
31
|
+
resolveSing,
|
|
32
|
+
SING_INSTRUMENT,
|
|
33
|
+
SING_PARAMS,
|
|
34
|
+
SING_PRESETS,
|
|
35
|
+
SING_PRESET_NAMES,
|
|
36
|
+
singNoteName,
|
|
37
|
+
singParamName,
|
|
38
|
+
singSummary,
|
|
39
|
+
type SingPreset,
|
|
40
|
+
type TrackSing,
|
|
41
|
+
} from "../../core/sing.ts";
|
|
42
|
+
import {
|
|
43
|
+
addNote,
|
|
44
|
+
ScoreValidationError,
|
|
45
|
+
updateTrack,
|
|
46
|
+
type TrackScore,
|
|
47
|
+
} from "../../core/score.ts";
|
|
48
|
+
import { nearest } from "./nearest.ts";
|
|
49
|
+
import {
|
|
50
|
+
describeTarget,
|
|
51
|
+
parseNoteTarget,
|
|
52
|
+
patchNotes,
|
|
53
|
+
targetNotes,
|
|
54
|
+
type NoteTarget,
|
|
55
|
+
} from "./expression.ts";
|
|
56
|
+
import { parseParamValue } from "./fx.ts";
|
|
57
|
+
|
|
58
|
+
export type SingValue = number | string | readonly [number, number] | null;
|
|
59
|
+
|
|
60
|
+
export type SingCommand =
|
|
61
|
+
| { type: "sing-show" }
|
|
62
|
+
| { type: "sing-list" }
|
|
63
|
+
| { type: "sing-reset" }
|
|
64
|
+
| { type: "sing-off" }
|
|
65
|
+
| { type: "sing-usage"; message: string }
|
|
66
|
+
| { type: "sing-vowels"; vowels: readonly string[] }
|
|
67
|
+
| { type: "note-vowel"; vowel: string | null; target: NoteTarget }
|
|
68
|
+
| {
|
|
69
|
+
type: "sing-set";
|
|
70
|
+
/** A preset applied before the values (`sing khoomei drone D3`). */
|
|
71
|
+
preset?: SingPreset;
|
|
72
|
+
/** `null` returns a parameter to the preset's value. */
|
|
73
|
+
values: Readonly<Record<string, SingValue>>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const SING_USAGE =
|
|
77
|
+
"sing <preset> | sing <param> <value> | sing drone <D3> | sing vowels a e i … | sing reset | sing off | sing list";
|
|
78
|
+
|
|
79
|
+
const PARAM_NAMES = [...Object.keys(SING_PARAMS), "harmonics"];
|
|
80
|
+
|
|
81
|
+
function rangeOf(name: string): string {
|
|
82
|
+
if (name === "vowel") return "vowel is a e i o u or a morph like a>o";
|
|
83
|
+
if (name === "drone")
|
|
84
|
+
return "drone is a note name C2..G4 (D3) or MIDI 36..67";
|
|
85
|
+
if (name === "harmonics") return "harmonics is a range like 6-12 (2..24)";
|
|
86
|
+
const spec = SING_PARAMS[name]!;
|
|
87
|
+
if (spec.kind === "number") return `${name} is ${spec.min}..${spec.max}`;
|
|
88
|
+
if (spec.kind === "enum") return `${name} is one of ${spec.values.join(" ")}`;
|
|
89
|
+
return `${name} is on or off`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function presetFor(word: string): SingPreset | undefined {
|
|
93
|
+
const lower = word.toLowerCase();
|
|
94
|
+
return isSingPreset(lower) ? lower : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One value word for `name`; undefined when it is out of range. */
|
|
98
|
+
function valueOf(name: string, word: string): SingValue | undefined {
|
|
99
|
+
try {
|
|
100
|
+
if (name === "vowel") return normalizeVowel(word);
|
|
101
|
+
if (name === "drone") return parseDrone(word);
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (name === "harmonics") {
|
|
106
|
+
const match = /^(\d+)[-,:](\d+)$/.exec(word);
|
|
107
|
+
if (!match) return undefined;
|
|
108
|
+
const lo = Number(match[1]);
|
|
109
|
+
const hi = Number(match[2]);
|
|
110
|
+
return lo >= 2 && hi <= 24 && lo < hi
|
|
111
|
+
? (Object.freeze([lo, hi]) as readonly [number, number])
|
|
112
|
+
: undefined;
|
|
113
|
+
}
|
|
114
|
+
const spec = SING_PARAMS[name];
|
|
115
|
+
if (!spec) return undefined;
|
|
116
|
+
const value = parseParamValue(spec, word);
|
|
117
|
+
return typeof value === "boolean" ? undefined : value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseNoteVowel(words: readonly string[]): SingCommand | undefined {
|
|
121
|
+
const [first, ...rest] = words;
|
|
122
|
+
if (!first)
|
|
123
|
+
return { type: "sing-usage", message: "note vowel <a|a>u|off> [target]" };
|
|
124
|
+
let vowel: string | null;
|
|
125
|
+
if (first === "off" || first === "unset") vowel = null;
|
|
126
|
+
else {
|
|
127
|
+
try {
|
|
128
|
+
vowel = normalizeVowel(first);
|
|
129
|
+
} catch {
|
|
130
|
+
return {
|
|
131
|
+
type: "sing-usage",
|
|
132
|
+
message: "note vowel is a e i o u or a morph like a>u",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const target = parseNoteTarget(rest);
|
|
137
|
+
if (!target)
|
|
138
|
+
return {
|
|
139
|
+
type: "sing-usage",
|
|
140
|
+
message: "note vowel <v> [all|last|bar <n>|bars <a>-<b>|<ids>]",
|
|
141
|
+
};
|
|
142
|
+
return { type: "note-vowel", vowel, target };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function parseSingCommand(prompt: string): SingCommand | undefined {
|
|
146
|
+
const words = prompt.trim().replace(/^\//, "").toLowerCase().split(/\s+/);
|
|
147
|
+
if (words[0] === "note" && words[1] === "vowel") {
|
|
148
|
+
if (prompt.length > 1_024) return undefined;
|
|
149
|
+
return parseNoteVowel(words.slice(2));
|
|
150
|
+
}
|
|
151
|
+
if (words[0] === "presets" && words[1] === "sing" && words.length === 2)
|
|
152
|
+
return { type: "sing-list" };
|
|
153
|
+
if (words[0] !== "sing") return undefined;
|
|
154
|
+
if (words.length === 1) return { type: "sing-show" };
|
|
155
|
+
if (prompt.length > 1_024) return undefined;
|
|
156
|
+
const rest = words.slice(1);
|
|
157
|
+
if (rest.length === 1 && rest[0] === "reset") return { type: "sing-reset" };
|
|
158
|
+
if (rest.length === 1 && rest[0] === "off") return { type: "sing-off" };
|
|
159
|
+
if (rest.length === 1 && (rest[0] === "list" || rest[0] === "presets"))
|
|
160
|
+
return { type: "sing-list" };
|
|
161
|
+
if (rest[0] === "vowels") {
|
|
162
|
+
const vowels: string[] = [];
|
|
163
|
+
for (const word of rest.slice(1)) {
|
|
164
|
+
try {
|
|
165
|
+
vowels.push(normalizeVowel(word));
|
|
166
|
+
} catch {
|
|
167
|
+
return {
|
|
168
|
+
type: "sing-usage",
|
|
169
|
+
message: `sing vowels · ${word.slice(0, 16)} is not a e i o u or a morph like a>o`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return vowels.length > 0
|
|
174
|
+
? { type: "sing-vowels", vowels }
|
|
175
|
+
: { type: "sing-usage", message: "sing vowels a e i o u (cycled)" };
|
|
176
|
+
}
|
|
177
|
+
if (rest[0] === "preset") {
|
|
178
|
+
const preset = rest.length === 2 ? presetFor(rest[1]!) : undefined;
|
|
179
|
+
return preset
|
|
180
|
+
? { type: "sing-set", preset, values: {} }
|
|
181
|
+
: {
|
|
182
|
+
type: "sing-usage",
|
|
183
|
+
message: `sing preset is one of ${SING_PRESET_NAMES.join(" ")}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (rest.length === 1) {
|
|
187
|
+
const preset = presetFor(rest[0]!);
|
|
188
|
+
if (preset) return { type: "sing-set", preset, values: {} };
|
|
189
|
+
const near = nearest(rest[0]!.slice(0, 24), SING_PRESET_NAMES);
|
|
190
|
+
return {
|
|
191
|
+
type: "sing-usage",
|
|
192
|
+
message: near ? `sing · did you mean ${near}?` : SING_USAGE,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// `harmonics 6 12` is the range `harmonics 6-12`.
|
|
196
|
+
for (let index = 0; index + 2 < rest.length; index += 1)
|
|
197
|
+
if (
|
|
198
|
+
rest[index] === "harmonics" &&
|
|
199
|
+
/^\d+$/.test(rest[index + 1]!) &&
|
|
200
|
+
/^\d+$/.test(rest[index + 2]!)
|
|
201
|
+
)
|
|
202
|
+
rest.splice(index + 1, 2, `${rest[index + 1]}-${rest[index + 2]}`);
|
|
203
|
+
// `sing <preset> <param> <value>…`: the preset, then its overrides.
|
|
204
|
+
const lead = rest.length % 2 === 1 ? presetFor(rest[0]!) : undefined;
|
|
205
|
+
if (lead) rest.shift();
|
|
206
|
+
if (rest.length % 2 !== 0) return { type: "sing-usage", message: SING_USAGE };
|
|
207
|
+
const values: Record<string, SingValue> = {};
|
|
208
|
+
for (let index = 0; index < rest.length; index += 2) {
|
|
209
|
+
const name = singParamName(rest[index]!);
|
|
210
|
+
if (!name) {
|
|
211
|
+
const word = rest[index]!.slice(0, 24);
|
|
212
|
+
const near = nearest(word, PARAM_NAMES);
|
|
213
|
+
return {
|
|
214
|
+
type: "sing-usage",
|
|
215
|
+
message: `sing has no parameter ${word}${near ? ` · did you mean ${near}?` : ""} · vowel voices bright breath vib drone …`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const word = rest[index + 1]!;
|
|
219
|
+
if (word === "off" || word === "unset") {
|
|
220
|
+
values[name] = null;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const value = valueOf(name, word);
|
|
224
|
+
if (value === undefined)
|
|
225
|
+
return { type: "sing-usage", message: rangeOf(name) };
|
|
226
|
+
values[name] = value;
|
|
227
|
+
}
|
|
228
|
+
return lead
|
|
229
|
+
? { type: "sing-set", preset: lead, values }
|
|
230
|
+
: { type: "sing-set", values };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** `preset khoomei · drone D3 · voices 4`, overrides in canonical order. */
|
|
234
|
+
export function describeSing(sing: TrackSing | undefined): string {
|
|
235
|
+
if (!sing) return "not a sing track";
|
|
236
|
+
const preset = sing.preset ?? DEFAULT_SING_PRESET;
|
|
237
|
+
const overrides = Object.entries(sing)
|
|
238
|
+
.filter(([key]) => key !== "preset")
|
|
239
|
+
.map(([key, value]) =>
|
|
240
|
+
key === "drone"
|
|
241
|
+
? `drone ${singNoteName(value as number)}`
|
|
242
|
+
: Array.isArray(value)
|
|
243
|
+
? `${key} ${value.join("-")}`
|
|
244
|
+
: `${key} ${value}`,
|
|
245
|
+
);
|
|
246
|
+
return [`preset ${preset}`, ...overrides].join(" · ");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** One line per preset, for `sing list` and the agent brief. */
|
|
250
|
+
export function singListLines(): string[] {
|
|
251
|
+
return SING_PRESET_NAMES.map(
|
|
252
|
+
(name) => `${name.padEnd(9)} ${SING_PRESETS[name].doc}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type SingResult = Readonly<{
|
|
257
|
+
ok: boolean;
|
|
258
|
+
message: string;
|
|
259
|
+
next?: TrackScore;
|
|
260
|
+
kind?: string;
|
|
261
|
+
payload?: Record<string, unknown>;
|
|
262
|
+
}>;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The sing field a command leaves on a track (pure; the agent tool shares
|
|
266
|
+
* it). A preset switch keeps the overrides; `reset` keeps the preset.
|
|
267
|
+
*/
|
|
268
|
+
export function nextSing(
|
|
269
|
+
current: TrackSing | undefined,
|
|
270
|
+
command: Extract<SingCommand, { type: "sing-reset" | "sing-set" }>,
|
|
271
|
+
): TrackSing {
|
|
272
|
+
if (command.type === "sing-reset")
|
|
273
|
+
return normalizeSing(current?.preset ? { preset: current.preset } : {})!;
|
|
274
|
+
const base: Record<string, unknown> = { ...current };
|
|
275
|
+
if (command.preset) base.preset = command.preset;
|
|
276
|
+
for (const [key, value] of Object.entries(command.values)) {
|
|
277
|
+
if (value === null) delete base[key];
|
|
278
|
+
else base[key] = value;
|
|
279
|
+
}
|
|
280
|
+
return normalizeSing(base)!;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Set the vowels of a track's notes in time order, cycling `vowels`
|
|
285
|
+
* (`sing vowels a e i o u`). Pure; the `set_vowels` tool shares it.
|
|
286
|
+
* `noteIds` limits it to those notes (still in time order).
|
|
287
|
+
*/
|
|
288
|
+
export function assignVowels(
|
|
289
|
+
score: TrackScore,
|
|
290
|
+
trackId: string,
|
|
291
|
+
vowels: readonly string[],
|
|
292
|
+
noteIds?: readonly string[],
|
|
293
|
+
): { next: TrackScore; count: number } {
|
|
294
|
+
const ids = noteIds ? new Set(noteIds) : undefined;
|
|
295
|
+
const notes = score.notes
|
|
296
|
+
.filter((note) => (ids ? ids.has(note.id) : note.trackId === trackId))
|
|
297
|
+
.sort((a, b) => a.startTick - b.startTick || a.pitch - b.pitch);
|
|
298
|
+
if (notes.length === 0 || vowels.length === 0)
|
|
299
|
+
return { next: score, count: 0 };
|
|
300
|
+
const canonical = vowels.map((vowel) => normalizeVowel(vowel));
|
|
301
|
+
let next = score;
|
|
302
|
+
// Group notes by vowel so one patchNotes call per vowel builds the score.
|
|
303
|
+
const byVowel = new Map<string, Set<string>>();
|
|
304
|
+
notes.forEach((note, index) => {
|
|
305
|
+
const vowel = canonical[index % canonical.length]!;
|
|
306
|
+
const set = byVowel.get(vowel) ?? new Set<string>();
|
|
307
|
+
set.add(note.id);
|
|
308
|
+
byVowel.set(vowel, set);
|
|
309
|
+
});
|
|
310
|
+
for (const [vowel, set] of byVowel) next = patchNotes(next, set, { vowel });
|
|
311
|
+
return { next, count: notes.length };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The throat demo line in semitones over the drone (formant.md 4.4). */
|
|
315
|
+
export const THROAT_DEMO_STEPS = Object.freeze([
|
|
316
|
+
12, 14, 16, 19, 21, 19, 16, 14,
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Write the throat demo melody on an empty throat track: one note per
|
|
321
|
+
* two beats, one octave above the drone that plays, inside the song.
|
|
322
|
+
* Undefined when the voice has no drone. Pure.
|
|
323
|
+
*/
|
|
324
|
+
export function throatDemo(
|
|
325
|
+
score: TrackScore,
|
|
326
|
+
trackId: string,
|
|
327
|
+
sing: TrackSing,
|
|
328
|
+
): { next: TrackScore; count: number } | undefined {
|
|
329
|
+
const drone = resolveSing(sing, keyRootOf(score)).drone;
|
|
330
|
+
if (drone === undefined) return undefined;
|
|
331
|
+
const step = score.ticksPerBeat * 2;
|
|
332
|
+
const total = score.ticksPerBeat * score.beatsPerBar * score.bars;
|
|
333
|
+
const ids = new Set(score.notes.map((note) => note.id));
|
|
334
|
+
let next = score;
|
|
335
|
+
let count = 0;
|
|
336
|
+
for (const [index, offset] of THROAT_DEMO_STEPS.entries()) {
|
|
337
|
+
const startTick = index * step;
|
|
338
|
+
if (startTick + step > total) break;
|
|
339
|
+
let id = `${trackId}-demo-${index + 1}`;
|
|
340
|
+
for (let n = 2; ids.has(id); n += 1)
|
|
341
|
+
id = `${trackId}-demo-${index + 1}-${n}`;
|
|
342
|
+
ids.add(id);
|
|
343
|
+
next = addNote(next, {
|
|
344
|
+
id,
|
|
345
|
+
trackId,
|
|
346
|
+
startTick,
|
|
347
|
+
durationTicks: step,
|
|
348
|
+
pitch: Math.min(127, drone + offset),
|
|
349
|
+
velocity: 0.8,
|
|
350
|
+
});
|
|
351
|
+
count += 1;
|
|
352
|
+
}
|
|
353
|
+
return count > 0 ? { next, count } : undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const keyRootOf = (score: TrackScore): number | undefined =>
|
|
357
|
+
parseKey(score.key)?.tonic;
|
|
358
|
+
|
|
359
|
+
/** The instrument a track plays after `sing off` (the default track voice). */
|
|
360
|
+
export const SING_OFF_INSTRUMENT = "sine";
|
|
361
|
+
|
|
362
|
+
export function applySingCommand(
|
|
363
|
+
score: TrackScore,
|
|
364
|
+
trackId: string,
|
|
365
|
+
command: SingCommand,
|
|
366
|
+
): SingResult {
|
|
367
|
+
if (command.type === "sing-usage")
|
|
368
|
+
return {
|
|
369
|
+
ok: false,
|
|
370
|
+
message: command.message.startsWith("note")
|
|
371
|
+
? command.message
|
|
372
|
+
: `sing · ${command.message}`,
|
|
373
|
+
};
|
|
374
|
+
if (command.type === "sing-list")
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
message: `sing presets · ${SING_PRESET_NAMES.join(" ")}`,
|
|
378
|
+
};
|
|
379
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
380
|
+
if (!track) return { ok: false, message: `no track · ${trackId}` };
|
|
381
|
+
if (command.type === "note-vowel") {
|
|
382
|
+
const notes = targetNotes(score, trackId, command.target);
|
|
383
|
+
if (notes.length === 0)
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
message: `note vowel · no notes in ${describeTarget(command.target)}`,
|
|
387
|
+
};
|
|
388
|
+
const next = patchNotes(score, new Set(notes.map((note) => note.id)), {
|
|
389
|
+
vowel: command.vowel,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
ok: true,
|
|
393
|
+
message: `note vowel · ${command.vowel ?? "off"} · ${notes.length} note${notes.length === 1 ? "" : "s"}`,
|
|
394
|
+
next,
|
|
395
|
+
kind: "score.notes.vowel",
|
|
396
|
+
payload: {
|
|
397
|
+
trackId,
|
|
398
|
+
vowel: command.vowel,
|
|
399
|
+
noteIds: notes.map((note) => note.id),
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (command.type === "sing-vowels") {
|
|
404
|
+
const { next, count } = assignVowels(score, trackId, command.vowels);
|
|
405
|
+
if (count === 0)
|
|
406
|
+
return { ok: false, message: `sing vowels · ${trackId} has no notes` };
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
message: `sing vowels · ${command.vowels.join(" ")} over ${count} note${count === 1 ? "" : "s"}`,
|
|
410
|
+
next,
|
|
411
|
+
kind: "score.notes.vowel",
|
|
412
|
+
payload: { trackId, vowels: command.vowels },
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const current = track.instrument === SING_INSTRUMENT ? track.sing : undefined;
|
|
416
|
+
if (command.type === "sing-show")
|
|
417
|
+
return {
|
|
418
|
+
ok: true,
|
|
419
|
+
message: current
|
|
420
|
+
? `sing · ${singSummary(current, keyRootOf(score))} · ${describeSing(current)}`
|
|
421
|
+
: track.instrument === SING_INSTRUMENT
|
|
422
|
+
? `sing · off · sing <preset> turns it on (${SING_PRESET_NAMES.join(" ")})`
|
|
423
|
+
: isGuideInstrument(track.instrument)
|
|
424
|
+
? `${trackId} sings with the vocal guide · try sing choir`
|
|
425
|
+
: `sing · ${trackId} plays ${track.instrument} · sing <preset> to switch (${SING_PRESET_NAMES.join(" ")})`,
|
|
426
|
+
};
|
|
427
|
+
if (track.sampler || track.instrument === "kit")
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
message: `sing · ${trackId} is a ${track.sampler ? "sampler" : "drum"} track`,
|
|
431
|
+
};
|
|
432
|
+
if (command.type === "sing-off") {
|
|
433
|
+
if (!current) return { ok: true, message: "sing · already off" };
|
|
434
|
+
// Off is a plain sine, never instrument "sing" without its field: the
|
|
435
|
+
// word `sing` reads back as the default voice, so a field-less sing
|
|
436
|
+
// track would sound again after a save and reload.
|
|
437
|
+
const next = updateTrack(score, trackId, {
|
|
438
|
+
instrument: SING_OFF_INSTRUMENT,
|
|
439
|
+
sing: null,
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
ok: true,
|
|
443
|
+
message: `sing · off (${SING_OFF_INSTRUMENT}) · sing ${current.preset ?? DEFAULT_SING_PRESET} turns it back on`,
|
|
444
|
+
next,
|
|
445
|
+
kind: "score.sing",
|
|
446
|
+
payload: { trackId, instrument: SING_OFF_INSTRUMENT, sing: null },
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
let sing: TrackSing;
|
|
450
|
+
let next: TrackScore;
|
|
451
|
+
try {
|
|
452
|
+
sing = nextSing(current, command);
|
|
453
|
+
next = updateTrack(score, trackId, { instrument: SING_INSTRUMENT, sing });
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (
|
|
456
|
+
error instanceof ScoreValidationError ||
|
|
457
|
+
error instanceof FxValidationError
|
|
458
|
+
)
|
|
459
|
+
return { ok: false, message: `sing · ${error.message}` };
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
// A throat style needs a run of notes to steer the drone's overtones:
|
|
463
|
+
// on an empty track, write a short demo line so one step is audible.
|
|
464
|
+
const demo =
|
|
465
|
+
command.type === "sing-set" &&
|
|
466
|
+
command.preset !== undefined &&
|
|
467
|
+
!score.notes.some((note) => note.trackId === trackId)
|
|
468
|
+
? throatDemo(next, trackId, sing)
|
|
469
|
+
: undefined;
|
|
470
|
+
if (demo) next = demo.next;
|
|
471
|
+
return {
|
|
472
|
+
ok: true,
|
|
473
|
+
message: `sing · ${singSummary(sing, keyRootOf(next))}${demo ? ` · ${demo.count} demo notes (^z removes them; clear before writing your own)` : ""}`,
|
|
474
|
+
next,
|
|
475
|
+
kind: "score.sing",
|
|
476
|
+
payload: { trackId, instrument: SING_INSTRUMENT, sing },
|
|
477
|
+
};
|
|
478
|
+
}
|
package/src/commands/string.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* string <param> off back to the preset's value
|
|
10
10
|
* string reset drop the overrides, keep the preset
|
|
11
11
|
* string off back to the track's synth voice
|
|
12
|
+
* bowed play the cello preset (bowed strings)
|
|
13
|
+
* bowed <preset> a bowed preset (violin viola cello …)
|
|
14
|
+
* bowed presets the bowed presets
|
|
12
15
|
*
|
|
13
16
|
* `string sitar`, `string buzz 0.8 sym 0.5`, `string ring 6`. Each command
|
|
14
17
|
* is one `updateTrack` revision and one undo step.
|
|
@@ -20,6 +23,8 @@ import {
|
|
|
20
23
|
type TrackScore,
|
|
21
24
|
} from "../../core/score.ts";
|
|
22
25
|
import {
|
|
26
|
+
BOWED_PRESET_NAMES,
|
|
27
|
+
DEFAULT_BOWED_PRESET,
|
|
23
28
|
DEFAULT_STRING_PRESET,
|
|
24
29
|
isStringTrack,
|
|
25
30
|
normalizeString,
|
|
@@ -37,7 +42,7 @@ import { parseParamValue } from "./fx.ts";
|
|
|
37
42
|
|
|
38
43
|
export type StringCommand =
|
|
39
44
|
| { type: "string-list" }
|
|
40
|
-
| { type: "string-presets" }
|
|
45
|
+
| { type: "string-presets"; bowed?: boolean }
|
|
41
46
|
| { type: "string-reset" }
|
|
42
47
|
| { type: "string-off" }
|
|
43
48
|
| { type: "string-preset"; preset: string }
|
|
@@ -49,6 +54,7 @@ export type StringCommand =
|
|
|
49
54
|
|
|
50
55
|
export function parseStringCommand(prompt: string): StringCommand | undefined {
|
|
51
56
|
const words = prompt.trim().split(/\s+/);
|
|
57
|
+
if (words[0]?.toLowerCase() === "bowed") return parseBowed(words.slice(1));
|
|
52
58
|
if (words[0]?.toLowerCase() !== "string") return undefined;
|
|
53
59
|
if (words.length === 1) return { type: "string-list" };
|
|
54
60
|
if (prompt.length > 1_024) return undefined;
|
|
@@ -82,6 +88,26 @@ export function parseStringCommand(prompt: string): StringCommand | undefined {
|
|
|
82
88
|
return { type: "string-set", values };
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* `bowed` (0.6.1): the string engine's bowed family. `bowed` plays cello,
|
|
93
|
+
* `bowed <preset>` a bowed preset, `bowed presets` lists them, and
|
|
94
|
+
* `bowed <param> <value>…` is `string <param> <value>…`.
|
|
95
|
+
*/
|
|
96
|
+
function parseBowed(words: string[]): StringCommand | undefined {
|
|
97
|
+
if (words.length === 0)
|
|
98
|
+
return { type: "string-preset", preset: DEFAULT_BOWED_PRESET };
|
|
99
|
+
const rest = words.map((word) => word.toLowerCase());
|
|
100
|
+
if (rest.length === 1 && (rest[0] === "presets" || rest[0] === "list"))
|
|
101
|
+
return { type: "string-presets", bowed: true };
|
|
102
|
+
if (rest.length === 1) {
|
|
103
|
+
const preset = stringPresetName(rest[0]!);
|
|
104
|
+
return preset && BOWED_PRESET_NAMES.includes(preset)
|
|
105
|
+
? { type: "string-preset", preset }
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
return parseStringCommand(`string ${rest.join(" ")}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
/** `sitar · buzz 0.8 · sym 0.5`, or `nylon` for a bare preset. */
|
|
86
112
|
export function describeString(settings: TrackString | undefined): string {
|
|
87
113
|
const parts = [stringPresetOf(settings)];
|
|
@@ -108,7 +134,7 @@ export function applyStringCommand(
|
|
|
108
134
|
if (command.type === "string-presets")
|
|
109
135
|
return {
|
|
110
136
|
ok: true,
|
|
111
|
-
message:
|
|
137
|
+
message: `${command.bowed ? "bowed" : "string"} presets · ${(command.bowed ? BOWED_PRESET_NAMES : STRING_PRESET_NAMES).map((name) => `${name} (${STRING_PRESETS[name]!.doc})`).join(" · ")}`,
|
|
112
138
|
};
|
|
113
139
|
if (track.sampler || track.kit)
|
|
114
140
|
return {
|