@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
package/src/commands/drums.ts
CHANGED
|
@@ -54,12 +54,13 @@ export function parsePatternCommand(
|
|
|
54
54
|
const match = command
|
|
55
55
|
.trim()
|
|
56
56
|
.match(
|
|
57
|
-
|
|
57
|
+
/^\/?(?:patterns?|grooves?)(?:\s+([a-z0-9][\w-]{0,40}))?(?:\s+(keep-tempo|keep|tempo))?\s*$/i,
|
|
58
58
|
);
|
|
59
59
|
if (!match) return undefined;
|
|
60
60
|
const name = match[1]?.toLowerCase();
|
|
61
61
|
if (!name) return { kind: "browse" };
|
|
62
|
-
if (name === "list" || name === "ls"
|
|
62
|
+
if (name === "list" || name === "ls" || name === "presets")
|
|
63
|
+
return { kind: "list" };
|
|
63
64
|
const flag = match[2]?.toLowerCase();
|
|
64
65
|
return {
|
|
65
66
|
kind: "apply",
|
package/src/commands/edit.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* score and the session event, leaving persistence to the caller.
|
|
14
14
|
*/
|
|
15
15
|
import {
|
|
16
|
+
AUTOMATION_PARAMETERS,
|
|
16
17
|
automationPoints,
|
|
17
18
|
automationRange,
|
|
18
19
|
SCORE_LIMITS,
|
|
@@ -40,7 +41,13 @@ export function parseLane(name: string): AutomationParameter | undefined {
|
|
|
40
41
|
const lower = name.toLowerCase();
|
|
41
42
|
if (lower === "volume" || lower === "vol") return "volume";
|
|
42
43
|
if (lower === "pan") return "pan";
|
|
43
|
-
|
|
44
|
+
// Lanes are typed in any case: `synth-pitchjump` is `synth-pitchJump`.
|
|
45
|
+
return (
|
|
46
|
+
parseEffectLane(lower) ??
|
|
47
|
+
parseEffectLane(
|
|
48
|
+
AUTOMATION_PARAMETERS.find((lane) => lane.toLowerCase() === lower) ?? "",
|
|
49
|
+
)
|
|
50
|
+
);
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
const NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
@@ -69,7 +76,7 @@ export function parseEditCommand(prompt: string): EditCommand | undefined {
|
|
|
69
76
|
const parsed = parseKey(value);
|
|
70
77
|
return parsed ? { type: "key", key: keyName(parsed) } : undefined;
|
|
71
78
|
}
|
|
72
|
-
const points = text.match(/^automate ([a-
|
|
79
|
+
const points = text.match(/^automate ([a-z0-9-]+) points((?: \S+)+)$/i);
|
|
73
80
|
if (points) {
|
|
74
81
|
const parameter = parseLane(points[1]!);
|
|
75
82
|
if (!parameter) return undefined;
|
|
@@ -89,7 +96,7 @@ export function parseEditCommand(prompt: string): EditCommand | undefined {
|
|
|
89
96
|
return undefined;
|
|
90
97
|
return { type: "automation-points", parameter, points: parsed };
|
|
91
98
|
}
|
|
92
|
-
const remove = text.match(/^automate ([a-
|
|
99
|
+
const remove = text.match(/^automate ([a-z0-9-]+) remove (\d+(?:\.\d+)?)$/i);
|
|
93
100
|
if (remove) {
|
|
94
101
|
const parameter = parseLane(remove[1]!);
|
|
95
102
|
return parameter
|
|
@@ -174,7 +181,7 @@ export function applyEditCommand(
|
|
|
174
181
|
if (points.length === current.length)
|
|
175
182
|
return {
|
|
176
183
|
ok: false,
|
|
177
|
-
message: `no ${command.parameter} point at beat ${command.beat}`,
|
|
184
|
+
message: `no ${command.parameter} point at beat ${command.beat} · the ${command.parameter} lane shows them · help automate`,
|
|
178
185
|
};
|
|
179
186
|
}
|
|
180
187
|
return {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* glide <ms>|off <target> portamento into each note
|
|
9
9
|
* glide <ms>|0|off [legato|mono|poly] the track's glide (`glide mono`)
|
|
10
10
|
* pedal <beat>-<beat>…|bars [<bar>-<bar>]|down|half|up <beat>|off
|
|
11
|
+
* pedal soft|sost <same forms> una corda and sostenuto (0.6.1)
|
|
11
12
|
* velcurve linear|soft|hard|fixed [<v>]
|
|
12
13
|
* humanize <ms> [<vel%> [<len%>]] [seed <n>]|on|off|reseed|seed <n>
|
|
13
14
|
* humanize <ms> [<vel%> [<len%>]]|exact|off <target> per-note amounts
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
normalizeBend,
|
|
33
34
|
normalizeHumanize,
|
|
34
35
|
normalizePedal,
|
|
36
|
+
SOSTENUTO_STATES,
|
|
35
37
|
normalizeTrackGlide,
|
|
36
38
|
normalizeVelocityCurve,
|
|
37
39
|
normalizeVibrato,
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
type Note,
|
|
54
56
|
type Track,
|
|
55
57
|
} from "../../core/score.ts";
|
|
58
|
+
import { softPedalNote } from "../../core/keys.ts";
|
|
56
59
|
import { barStartTick, loopTicksOf } from "../../core/tempo.ts";
|
|
57
60
|
|
|
58
61
|
export type NoteTarget =
|
|
@@ -73,11 +76,15 @@ export type ExpressionCommand =
|
|
|
73
76
|
| { type: "vibrato"; vibrato: NoteVibrato | null; target: NoteTarget }
|
|
74
77
|
| { type: "note-glide"; glide: number | null; target: NoteTarget }
|
|
75
78
|
| { type: "track-glide"; time?: number | null; mode?: GlideMode }
|
|
76
|
-
| {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
| {
|
|
80
|
+
type: "pedal-spans";
|
|
81
|
+
spans: readonly { from: number; to: number }[];
|
|
82
|
+
lane?: PedalLane;
|
|
83
|
+
}
|
|
84
|
+
| { type: "pedal-bars"; from?: number; to?: number; lane?: PedalLane }
|
|
85
|
+
| { type: "pedal-list"; lane?: PedalLane }
|
|
86
|
+
| { type: "pedal-event"; state: PedalState; beat: number; lane?: PedalLane }
|
|
87
|
+
| { type: "pedal-off"; lane?: PedalLane }
|
|
81
88
|
| { type: "velcurve"; curve: VelocityCurveName; fixed?: number }
|
|
82
89
|
| {
|
|
83
90
|
type: "humanize";
|
|
@@ -299,7 +306,31 @@ function nearWord(text: string, word: string): boolean {
|
|
|
299
306
|
return previous[word.length]! <= 2;
|
|
300
307
|
}
|
|
301
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Which pedal a `pedal` command edits (0.6.1): absent is the sustain
|
|
311
|
+
* pedal, `soft` the una corda (Track.softPedal), `sost` the sostenuto.
|
|
312
|
+
*/
|
|
313
|
+
export type PedalLane = "soft" | "sost";
|
|
314
|
+
|
|
315
|
+
const PEDAL_LANES: Readonly<Record<string, PedalLane>> = {
|
|
316
|
+
soft: "soft",
|
|
317
|
+
unacorda: "soft",
|
|
318
|
+
sost: "sost",
|
|
319
|
+
sostenuto: "sost",
|
|
320
|
+
};
|
|
321
|
+
|
|
302
322
|
function parsePedal(words: string[]): ExpressionCommand | undefined {
|
|
323
|
+
const lane = words[0] ? PEDAL_LANES[words[0]] : undefined;
|
|
324
|
+
if (lane) {
|
|
325
|
+
const command = parsePedalLane(words.slice(1));
|
|
326
|
+
return command && command.type.startsWith("pedal-")
|
|
327
|
+
? ({ ...command, lane } as ExpressionCommand)
|
|
328
|
+
: undefined;
|
|
329
|
+
}
|
|
330
|
+
return parsePedalLane(words);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parsePedalLane(words: string[]): ExpressionCommand | undefined {
|
|
303
334
|
const [first, ...rest] = words;
|
|
304
335
|
if (!first) return { type: "pedal-list" };
|
|
305
336
|
if (OFF.test(first) && rest.length === 0) return { type: "pedal-off" };
|
|
@@ -505,7 +536,7 @@ export function describeTarget(target: NoteTarget): string {
|
|
|
505
536
|
export function patchNotes(
|
|
506
537
|
score: TrackScore,
|
|
507
538
|
noteIds: ReadonlySet<string>,
|
|
508
|
-
patch: NoteExpressionPatch
|
|
539
|
+
patch: NoteExpressionPatch & Readonly<{ vowel?: string | null }>,
|
|
509
540
|
): TrackScore {
|
|
510
541
|
if (noteIds.size === 0) return score;
|
|
511
542
|
return new TrackScore({
|
|
@@ -564,6 +595,16 @@ export function describePerformance(score: TrackScore, track: Track): string {
|
|
|
564
595
|
const parts = [
|
|
565
596
|
`glide ${track.glide ? `${ms(track.glide.time)} ${track.glide.mode}` : "off"}`,
|
|
566
597
|
`pedal ${track.pedal ? `${track.pedal.length} event${track.pedal.length === 1 ? "" : "s"}` : "off"}`,
|
|
598
|
+
...(track.softPedal
|
|
599
|
+
? [
|
|
600
|
+
`soft ${track.softPedal.length} event${track.softPedal.length === 1 ? "" : "s"}`,
|
|
601
|
+
]
|
|
602
|
+
: []),
|
|
603
|
+
...(track.sostenuto
|
|
604
|
+
? [
|
|
605
|
+
`sost ${track.sostenuto.length} event${track.sostenuto.length === 1 ? "" : "s"}`,
|
|
606
|
+
]
|
|
607
|
+
: []),
|
|
567
608
|
`velcurve ${track.velocityCurve ? `${track.velocityCurve.curve}${track.velocityCurve.fixed !== undefined ? ` ${track.velocityCurve.fixed}` : ""}` : "linear"}`,
|
|
568
609
|
`humanize ${describeHumanize(track.humanize)}`,
|
|
569
610
|
];
|
|
@@ -680,10 +721,24 @@ function mergePedal(
|
|
|
680
721
|
current: readonly PedalEvent[] | undefined,
|
|
681
722
|
added: readonly PedalEvent[],
|
|
682
723
|
maxTick: number,
|
|
724
|
+
label?: string,
|
|
725
|
+
states?: readonly PedalState[],
|
|
683
726
|
): readonly PedalEvent[] | undefined {
|
|
684
727
|
const byTick = new Map((current ?? []).map((event) => [event.tick, event]));
|
|
685
728
|
for (const event of added) byTick.set(event.tick, event);
|
|
686
|
-
return normalizePedal([...byTick.values()], maxTick);
|
|
729
|
+
return normalizePedal([...byTick.values()], maxTick, label, states);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** The Track field, message label and allowed states of a pedal lane. */
|
|
733
|
+
function pedalLaneOf(lane: PedalLane | undefined): {
|
|
734
|
+
field: "pedal" | "softPedal" | "sostenuto";
|
|
735
|
+
label: string;
|
|
736
|
+
states?: readonly PedalState[];
|
|
737
|
+
} {
|
|
738
|
+
if (lane === "soft") return { field: "softPedal", label: "soft pedal" };
|
|
739
|
+
if (lane === "sost")
|
|
740
|
+
return { field: "sostenuto", label: "sostenuto", states: SOSTENUTO_STATES };
|
|
741
|
+
return { field: "pedal", label: "pedal" };
|
|
687
742
|
}
|
|
688
743
|
|
|
689
744
|
/** Apply `command` to the focused track `trackId`. */
|
|
@@ -771,11 +826,12 @@ export function applyExpressionCommand(
|
|
|
771
826
|
);
|
|
772
827
|
}
|
|
773
828
|
case "pedal-list": {
|
|
774
|
-
const
|
|
829
|
+
const { field, label } = pedalLaneOf(command.lane);
|
|
830
|
+
const events = track[field] ?? [];
|
|
775
831
|
if (events.length === 0)
|
|
776
832
|
return {
|
|
777
833
|
ok: true,
|
|
778
|
-
message:
|
|
834
|
+
message: `${label} · none on ${trackId} · ${command.lane ? `pedal ${command.lane} 0-4` : "pedal bars"} to add`,
|
|
779
835
|
};
|
|
780
836
|
const shown = events
|
|
781
837
|
.slice(0, 12)
|
|
@@ -784,24 +840,45 @@ export function applyExpressionCommand(
|
|
|
784
840
|
`${Math.round((event.tick / score.ticksPerBeat) * 1000) / 1000} ${event.state}`,
|
|
785
841
|
);
|
|
786
842
|
if (events.length > 12) shown.push(`+${events.length - 12} more`);
|
|
787
|
-
return {
|
|
843
|
+
return {
|
|
844
|
+
ok: true,
|
|
845
|
+
message: `${label} · ${shown.join(" · ")} · ${trackId}`,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
case "pedal-off": {
|
|
849
|
+
const { field, label } = pedalLaneOf(command.lane);
|
|
850
|
+
return track[field]
|
|
851
|
+
? trackResult(
|
|
852
|
+
score,
|
|
853
|
+
track,
|
|
854
|
+
{ [field]: null },
|
|
855
|
+
`${label} · off · ${trackId}`,
|
|
856
|
+
)
|
|
857
|
+
: { ok: true, message: `${label} · already off · ${trackId}` };
|
|
788
858
|
}
|
|
789
|
-
case "pedal-off":
|
|
790
|
-
return track.pedal
|
|
791
|
-
? trackResult(score, track, { pedal: null }, `pedal · off · ${trackId}`)
|
|
792
|
-
: { ok: true, message: `pedal · already off · ${trackId}` };
|
|
793
859
|
case "pedal-spans":
|
|
794
860
|
case "pedal-bars":
|
|
795
861
|
case "pedal-event": {
|
|
862
|
+
const { field, label, states } = pedalLaneOf(command.lane);
|
|
796
863
|
let added: PedalEvent[];
|
|
797
864
|
if (command.type === "pedal-bars") {
|
|
798
865
|
const from = command.from ?? 1;
|
|
799
866
|
if (from > score.bars)
|
|
800
867
|
return {
|
|
801
868
|
ok: false,
|
|
802
|
-
message:
|
|
869
|
+
message: `${label} · the loop has ${score.bars} bar${score.bars === 1 ? "" : "s"}`,
|
|
803
870
|
};
|
|
804
|
-
|
|
871
|
+
const to = Math.min(command.to ?? score.bars, score.bars);
|
|
872
|
+
// Soft and sostenuto hold through the bars (no re-pedalling).
|
|
873
|
+
added = command.lane
|
|
874
|
+
? [
|
|
875
|
+
{ tick: barStartTick(score, from - 1), state: "down" },
|
|
876
|
+
{
|
|
877
|
+
tick: Math.min(barStartTick(score, to), maxTick),
|
|
878
|
+
state: "up",
|
|
879
|
+
},
|
|
880
|
+
]
|
|
881
|
+
: barPedal(score, from, to);
|
|
805
882
|
} else if (command.type === "pedal-event") {
|
|
806
883
|
added = [
|
|
807
884
|
{ tick: Math.round(command.beat * tpb), state: command.state },
|
|
@@ -815,7 +892,7 @@ export function applyExpressionCommand(
|
|
|
815
892
|
if (added.some((event) => event.tick > maxTick))
|
|
816
893
|
return {
|
|
817
894
|
ok: false,
|
|
818
|
-
message:
|
|
895
|
+
message: `${label} · beats run 0..${maxTick / tpb} in this loop`,
|
|
819
896
|
};
|
|
820
897
|
let pedal: readonly PedalEvent[] | undefined;
|
|
821
898
|
try {
|
|
@@ -825,11 +902,11 @@ export function applyExpressionCommand(
|
|
|
825
902
|
command.type === "pedal-bars" &&
|
|
826
903
|
command.from === undefined &&
|
|
827
904
|
command.to === undefined
|
|
828
|
-
? normalizePedal(added, maxTick)
|
|
829
|
-
: mergePedal(track
|
|
905
|
+
? normalizePedal(added, maxTick, field, states)
|
|
906
|
+
: mergePedal(track[field], added, maxTick, field, states);
|
|
830
907
|
} catch (error) {
|
|
831
908
|
if (error instanceof ExpressionValidationError)
|
|
832
|
-
return { ok: false, message:
|
|
909
|
+
return { ok: false, message: `${label} · ${error.message}` };
|
|
833
910
|
throw error;
|
|
834
911
|
}
|
|
835
912
|
const downs = (pedal ?? []).filter((event) => event.state === "down");
|
|
@@ -840,11 +917,15 @@ export function applyExpressionCommand(
|
|
|
840
917
|
: []),
|
|
841
918
|
...(halves.length > 0 ? [`${halves.length} half`] : []),
|
|
842
919
|
].join(", ");
|
|
920
|
+
// Una corda is stored anyway (an instrument change may follow), but
|
|
921
|
+
// say when nothing will sound different.
|
|
922
|
+
const silent =
|
|
923
|
+
field === "softPedal" && pedal ? softPedalNote(track) : undefined;
|
|
843
924
|
return trackResult(
|
|
844
925
|
score,
|
|
845
926
|
track,
|
|
846
|
-
{
|
|
847
|
-
|
|
927
|
+
{ [field]: pedal ?? null },
|
|
928
|
+
`${label} · ${pedal?.length ?? 0} event${pedal?.length === 1 ? "" : "s"} (${held}) · ${trackId}${silent ? ` · no effect: ${silent}` : ""}`,
|
|
848
929
|
);
|
|
849
930
|
}
|
|
850
931
|
case "velcurve": {
|
|
@@ -933,7 +1014,7 @@ export const EXPRESSION_USAGE = Object.freeze({
|
|
|
933
1014
|
glide:
|
|
934
1015
|
"glide <ms>|<s>s|off [legato|mono|poly] (track) · glide <ms>|off <target> (notes) · glide 60 mono · SDK glide: 0.06 (s)",
|
|
935
1016
|
pedal:
|
|
936
|
-
"pedal <beat>-<beat>…|bars [<bar>-<bar>]|down|half|up <beat>|off · pedal 0-3.5 4-7.5 · pedal bars",
|
|
1017
|
+
"pedal [soft|sost] <beat>-<beat>…|bars [<bar>-<bar>]|down|half|up <beat>|off · pedal 0-3.5 4-7.5 · pedal bars · pedal soft 0-8 (una corda) · pedal sost 0-4 (holds keys down at 0)",
|
|
937
1018
|
velcurve:
|
|
938
1019
|
"velcurve linear|soft|hard|fixed [<v 0..1>] · velcurve soft · velcurve fixed 0.8",
|
|
939
1020
|
humanize:
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/formant` and `/vowel` (0.7): short forms of `fx formant` and `fx vowel`
|
|
3
|
+
* on the focused track. Both build the same `FxCommand` as `fx`, so the
|
|
4
|
+
* edit, validation and undo step are the fx command's.
|
|
5
|
+
*
|
|
6
|
+
* formant show the shift and mix
|
|
7
|
+
* formant <-12..12> [mix] shift the formants (pitch stays): -4 deeper
|
|
8
|
+
* formant <preset> deep giant bright tiny
|
|
9
|
+
* formant shift <st> mix <0..1> spelled out
|
|
10
|
+
* formant on | off | reset turn it on | remove it | back to 0 st, mix 1
|
|
11
|
+
*
|
|
12
|
+
* vowel show the vowel filter
|
|
13
|
+
* vowel <v> [<to> [<morph>]] a, or a morphing toward o (morph 0.5);
|
|
14
|
+
* keeps the mix you set
|
|
15
|
+
* vowel <preset> ee (also a, o)
|
|
16
|
+
* vowel to <v> | morph <0..1> change one part of the morph
|
|
17
|
+
* vowel to off drop the morph target (keeps vowel and mix)
|
|
18
|
+
* vowel mix <0..1> | off
|
|
19
|
+
*
|
|
20
|
+
* `/vocal formant …` runs the same `/formant` grammar.
|
|
21
|
+
*/
|
|
22
|
+
import { FX_PRESETS, VOWEL_VALUES } from "../../core/fx.ts";
|
|
23
|
+
import type { TrackScore } from "../../core/score.ts";
|
|
24
|
+
import {
|
|
25
|
+
applyFxCommand,
|
|
26
|
+
effectValues,
|
|
27
|
+
FORMANT_VOWEL_HINT,
|
|
28
|
+
parseFxCommand,
|
|
29
|
+
type FxCommand,
|
|
30
|
+
type FxResult,
|
|
31
|
+
} from "./fx.ts";
|
|
32
|
+
|
|
33
|
+
export const FORMANT_USAGE =
|
|
34
|
+
"formant <-12..12> [mix] | <deep|giant|bright|tiny> | on | off";
|
|
35
|
+
export const VOWEL_USAGE =
|
|
36
|
+
"vowel <a|e|i|o|u…> [<to> [<morph 0..1>]] | ee | to off | off";
|
|
37
|
+
|
|
38
|
+
export type FormantCommand =
|
|
39
|
+
| Readonly<{ type: "show"; effect: "formant" | "vowel" }>
|
|
40
|
+
| Readonly<{ type: "fx"; command: FxCommand }>
|
|
41
|
+
| Readonly<{ type: "error"; message: string }>;
|
|
42
|
+
|
|
43
|
+
const isVowel = (word: string): boolean =>
|
|
44
|
+
(VOWEL_VALUES as readonly string[]).includes(word);
|
|
45
|
+
|
|
46
|
+
/** A number word such as `-3`, `+2.5` or `-4st`. */
|
|
47
|
+
function numberWord(word: string): number | undefined {
|
|
48
|
+
const match = word.match(/^([+-]?\d+(?:\.\d+)?|[+-]?\.\d+)(?:st)?$/i);
|
|
49
|
+
return match ? Number(match[1]) : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parse `/formant …` (the slash is optional only with arguments). */
|
|
53
|
+
export function parseFormantCommand(
|
|
54
|
+
command: string,
|
|
55
|
+
): FormantCommand | undefined {
|
|
56
|
+
const match = command.trim().match(/^\/formant(?:\s+(.*))?$/i);
|
|
57
|
+
if (!match) return undefined;
|
|
58
|
+
return formantArgs(match[1] ?? "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The words after `/formant` (shared with `/vocal formant`). */
|
|
62
|
+
export function formantArgs(args: string): FormantCommand {
|
|
63
|
+
const words = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
64
|
+
if (words.length === 0) return { type: "show", effect: "formant" };
|
|
65
|
+
const [first, ...rest] = words as [string, ...string[]];
|
|
66
|
+
if (rest.length === 0 && (first === "off" || first === "reset"))
|
|
67
|
+
return {
|
|
68
|
+
type: "fx",
|
|
69
|
+
command: {
|
|
70
|
+
type: first === "off" ? "fx-off" : "fx-reset",
|
|
71
|
+
effect: "formant",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
// `on` is also a nasal vowel word; as the only word it is the toggle.
|
|
75
|
+
if (rest.length === 0 && ["on", "true", "yes"].includes(first))
|
|
76
|
+
return { type: "fx", command: { type: "fx-on", effect: "formant" } };
|
|
77
|
+
if (rest.length === 0 && FX_PRESETS.formant?.[first])
|
|
78
|
+
return {
|
|
79
|
+
type: "fx",
|
|
80
|
+
command: { type: "fx-preset", effect: "formant", preset: first },
|
|
81
|
+
};
|
|
82
|
+
const shift = numberWord(first);
|
|
83
|
+
// `-4`, `-4 0.5` or `-4 mix 0.5`.
|
|
84
|
+
const mixWords = rest[0] === "mix" ? rest.slice(1) : rest;
|
|
85
|
+
if (shift !== undefined && mixWords.length <= 1 && rest.length <= 2) {
|
|
86
|
+
const mix = mixWords[0] === undefined ? undefined : numberWord(mixWords[0]);
|
|
87
|
+
if (rest.length > 0 && mix === undefined)
|
|
88
|
+
return { type: "error", message: `usage: /${FORMANT_USAGE}` };
|
|
89
|
+
return {
|
|
90
|
+
type: "fx",
|
|
91
|
+
command: {
|
|
92
|
+
type: "fx-set",
|
|
93
|
+
effect: "formant",
|
|
94
|
+
values: { shift, ...(mix === undefined ? {} : { mix }) },
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (isVowel(first) || words.includes("vowel"))
|
|
99
|
+
return {
|
|
100
|
+
type: "error",
|
|
101
|
+
message: `${FORMANT_VOWEL_HINT} · /vowel ${isVowel(first) ? first : "a"}`,
|
|
102
|
+
};
|
|
103
|
+
const generic = parseFxCommand(`fx formant ${words.join(" ")}`);
|
|
104
|
+
if (generic) return { type: "fx", command: generic };
|
|
105
|
+
return { type: "error", message: `usage: /${FORMANT_USAGE}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Parse `/vowel …`. */
|
|
109
|
+
export function parseVowelCommand(command: string): FormantCommand | undefined {
|
|
110
|
+
const match = command.trim().match(/^\/vowel(?:\s+(.*))?$/i);
|
|
111
|
+
if (!match) return undefined;
|
|
112
|
+
const words = (match[1] ?? "")
|
|
113
|
+
.trim()
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.split(/\s+/)
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
if (words.length === 0) return { type: "show", effect: "vowel" };
|
|
118
|
+
const [first, second, third] = words;
|
|
119
|
+
if (words.length === 1 && (first === "off" || first === "reset"))
|
|
120
|
+
return {
|
|
121
|
+
type: "fx",
|
|
122
|
+
command: {
|
|
123
|
+
type: first === "off" ? "fx-off" : "fx-reset",
|
|
124
|
+
effect: "vowel",
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
if (words.length === 2 && first === "to" && second === "off")
|
|
128
|
+
return {
|
|
129
|
+
type: "fx",
|
|
130
|
+
command: { type: "fx-replace", effect: "vowel", values: {} },
|
|
131
|
+
};
|
|
132
|
+
if (
|
|
133
|
+
words.length === 1 &&
|
|
134
|
+
!isVowel(first!) &&
|
|
135
|
+
FX_PRESETS.vowel?.[first!] !== undefined
|
|
136
|
+
)
|
|
137
|
+
return {
|
|
138
|
+
type: "fx",
|
|
139
|
+
command: { type: "fx-preset", effect: "vowel", preset: first! },
|
|
140
|
+
};
|
|
141
|
+
if (isVowel(first!) && words.length <= 3) {
|
|
142
|
+
// `mix` is filled from the stored vowel when the command is applied.
|
|
143
|
+
if (second === undefined)
|
|
144
|
+
return {
|
|
145
|
+
type: "fx",
|
|
146
|
+
command: {
|
|
147
|
+
type: "fx-replace",
|
|
148
|
+
effect: "vowel",
|
|
149
|
+
values: { vowel: first! },
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const morph = third === undefined ? 0.5 : numberWord(third);
|
|
153
|
+
if (!isVowel(second) || morph === undefined)
|
|
154
|
+
return { type: "error", message: `usage: /${VOWEL_USAGE}` };
|
|
155
|
+
return {
|
|
156
|
+
type: "fx",
|
|
157
|
+
command: {
|
|
158
|
+
type: "fx-replace",
|
|
159
|
+
effect: "vowel",
|
|
160
|
+
values: { vowel: first!, to: second, morph },
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const generic = parseFxCommand(`fx vowel ${words.join(" ")}`);
|
|
165
|
+
if (generic && generic.type === "fx-set")
|
|
166
|
+
return {
|
|
167
|
+
type: "fx",
|
|
168
|
+
// `/vowel to o` alone starts halfway so the morph is audible.
|
|
169
|
+
command:
|
|
170
|
+
generic.values.to !== undefined && generic.values.morph === undefined
|
|
171
|
+
? { ...generic, values: { morph: 0.5, ...generic.values } }
|
|
172
|
+
: generic,
|
|
173
|
+
};
|
|
174
|
+
return { type: "error", message: `usage: /${VOWEL_USAGE}` };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Run a parsed `/formant` or `/vowel` on the focused track. */
|
|
178
|
+
export function applyFormantCommand(
|
|
179
|
+
score: TrackScore,
|
|
180
|
+
trackId: string,
|
|
181
|
+
command: FormantCommand,
|
|
182
|
+
): FxResult {
|
|
183
|
+
if (command.type === "error") return { ok: false, message: command.message };
|
|
184
|
+
if (command.type === "fx") {
|
|
185
|
+
// `/vowel <v> [...]` replaces the vowel and its morph but keeps the
|
|
186
|
+
// stored mix; `/vowel to off` keeps vowel and mix and drops the morph.
|
|
187
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
188
|
+
const fx = command.command;
|
|
189
|
+
if (fx.type === "fx-replace" && fx.effect === "vowel") {
|
|
190
|
+
const current = effectValues(track, "vowel");
|
|
191
|
+
const { vowel: vowelWord, ...morphPart } = fx.values;
|
|
192
|
+
if (vowelWord === undefined && !current)
|
|
193
|
+
return { ok: true, message: "vowel · off" };
|
|
194
|
+
return applyFxCommand(score, trackId, {
|
|
195
|
+
...fx,
|
|
196
|
+
values: {
|
|
197
|
+
vowel: vowelWord ?? current?.vowel ?? "a",
|
|
198
|
+
mix: current?.mix ?? 1,
|
|
199
|
+
...morphPart,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return applyFxCommand(score, trackId, fx);
|
|
204
|
+
}
|
|
205
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
206
|
+
if (!track) return { ok: false, message: `no track · ${trackId}` };
|
|
207
|
+
const values = effectValues(track, command.effect);
|
|
208
|
+
if (command.effect === "formant")
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
message: values
|
|
212
|
+
? `formant · shift ${values.shift} st · mix ${values.mix}`
|
|
213
|
+
: `formant · off · /${FORMANT_USAGE}`,
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
message: values
|
|
218
|
+
? `vowel · ${values.vowel}${values.to !== undefined ? ` → ${values.to} · morph ${values.morph ?? 0}` : ""} · mix ${values.mix}`
|
|
219
|
+
: `vowel · off · /${VOWEL_USAGE}`,
|
|
220
|
+
};
|
|
221
|
+
}
|