@hraness/dawg 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- 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/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- 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 +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -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 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -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 +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -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 +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- 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/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -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/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- 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 +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- 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 +82 -32
- 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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- 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 +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- 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 +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- 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 +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- 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 +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAPE's keys (op1-ux §6.2): each gesture becomes the typed command a person
|
|
3
|
+
* or the agent would write, so every key echoes and runs through `submit`
|
|
4
|
+
* (one receipt, one undo step, the same path as typing). Like play-mode.ts
|
|
5
|
+
* COMMAND_KEYS, this module only maps keys; src/main.ts runs the result.
|
|
6
|
+
*
|
|
7
|
+
* The range a gesture acts on is core/range.ts `rangeOf`: the loop, else
|
|
8
|
+
* the section under the playhead, else that bar. The four knobs are the
|
|
9
|
+
* screen-independent tui/knobs.ts strip on the `tape` page of KNOB_MAPS.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
barsWord,
|
|
13
|
+
rangeOf,
|
|
14
|
+
type BarRange,
|
|
15
|
+
type ResolvedRange,
|
|
16
|
+
} from "../../core/range.ts";
|
|
17
|
+
import {
|
|
18
|
+
findSection,
|
|
19
|
+
formSegments,
|
|
20
|
+
sectionAtBar,
|
|
21
|
+
} from "../../core/sections.ts";
|
|
22
|
+
import { TrackScore } from "../../core/score.ts";
|
|
23
|
+
import { barAt, barStartTick } from "../../core/tempo.ts";
|
|
24
|
+
import type { KnobSlot, KnobSlots } from "../../tui/knobs.ts";
|
|
25
|
+
import { knobMap } from "./knob-map.ts";
|
|
26
|
+
|
|
27
|
+
/** What TAPE knows when a key arrives. */
|
|
28
|
+
export type TapeContext = Readonly<{
|
|
29
|
+
score: TrackScore;
|
|
30
|
+
/** The focused track id. */
|
|
31
|
+
trackId: string;
|
|
32
|
+
/** Score beat under the playhead (score time, not transport time). */
|
|
33
|
+
beat: number;
|
|
34
|
+
/** This pane's clipboard: its source track (or `all`) and range. */
|
|
35
|
+
clipboard?: Readonly<{ source: string; range: BarRange }> | undefined;
|
|
36
|
+
/** The clipboard came from a cut whose clear is still the last edit. */
|
|
37
|
+
cut?: boolean | undefined;
|
|
38
|
+
/** The clipboard's source bars changed since the copy. */
|
|
39
|
+
stale?: boolean | undefined;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
export type TapeAction =
|
|
43
|
+
/** Run these typed commands in order (each echoes, as if typed). */
|
|
44
|
+
| Readonly<{ type: "run"; commands: readonly string[]; cut?: boolean }>
|
|
45
|
+
/**
|
|
46
|
+
* Paste: run `command`, then move the playhead to bar `end` (tiling).
|
|
47
|
+
* `fold`: the first paste after a cut, whose `move` reads the bars from
|
|
48
|
+
* the score before the cut's clear, so cut and paste land as one move.
|
|
49
|
+
*/
|
|
50
|
+
| Readonly<{ type: "paste"; command: string; end: number; fold?: boolean }>
|
|
51
|
+
/** A note that runs nothing (`no section under the playhead`). */
|
|
52
|
+
| Readonly<{ type: "note"; message: string }>
|
|
53
|
+
| Readonly<{ type: "zoom"; direction: 1 | -1 }>
|
|
54
|
+
| Readonly<{ type: "focus"; row: number }>
|
|
55
|
+
| Readonly<{ type: "transport" }>
|
|
56
|
+
| Readonly<{ type: "keys" }>
|
|
57
|
+
| Readonly<{ type: "exit" }>
|
|
58
|
+
/** Not a TAPE key: the prompt, arrows for the knobs, ctrl keys. */
|
|
59
|
+
| Readonly<{ type: "pass" }>;
|
|
60
|
+
|
|
61
|
+
/** The hint row: six gestures, the rest are in `?` (KEYS.tape). */
|
|
62
|
+
export const TAPE_HINT =
|
|
63
|
+
"c copy · x cut · v paste · \\ loop · r record · 1-9 track · ? keys";
|
|
64
|
+
|
|
65
|
+
/** `5-6` or `5`, as typed (1-based, ASCII hyphen). */
|
|
66
|
+
export function typedRange(range: BarRange): string {
|
|
67
|
+
const first = range.startBar + 1;
|
|
68
|
+
const last = range.startBar + range.bars;
|
|
69
|
+
return first === last ? `${first}` : `${first}-${last}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The bar (0-based) under a score beat. */
|
|
73
|
+
export function barOfBeat(score: TrackScore, beat: number): number {
|
|
74
|
+
const tick = Math.max(0, Math.round(beat * score.ticksPerBeat));
|
|
75
|
+
return Math.min(score.bars - 1, barAt(score, tick).bar);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `5.3` for a score beat (1-based bar.beat). */
|
|
79
|
+
export function barBeatOf(score: TrackScore, beat: number): string {
|
|
80
|
+
const tick = Math.max(0, Math.round(beat * score.ticksPerBeat));
|
|
81
|
+
const at = barAt(score, tick);
|
|
82
|
+
const beatTicks = (at.barTicks / at.beatsPerBar) | 0;
|
|
83
|
+
return `${at.bar + 1}.${Math.floor(at.offset / Math.max(1, beatTicks)) + 1}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The loop as bars: `score.loop`, else the looped section, else none. */
|
|
87
|
+
export function loopRange(score: TrackScore): BarRange | undefined {
|
|
88
|
+
if (score.loop) return score.loop;
|
|
89
|
+
if (score.loopSection === undefined) return undefined;
|
|
90
|
+
const section = findSection(score, score.loopSection);
|
|
91
|
+
return section
|
|
92
|
+
? { startBar: section.startBar, bars: section.bars }
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The range a gesture acts on now. */
|
|
97
|
+
export function tapeRange(context: TapeContext): ResolvedRange {
|
|
98
|
+
return rangeOf(context.score, barOfBeat(context.score, context.beat));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function trackName(score: TrackScore, id: string): string {
|
|
102
|
+
return score.tracks.find((track) => track.id === id)?.name ?? id;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The status line: `range: bass · bars 5–6 (loop)`. */
|
|
106
|
+
export function rangeLine(context: TapeContext): string {
|
|
107
|
+
const resolved = tapeRange(context);
|
|
108
|
+
const from =
|
|
109
|
+
resolved.source === "loop"
|
|
110
|
+
? "loop"
|
|
111
|
+
: resolved.source === "section"
|
|
112
|
+
? resolved.section
|
|
113
|
+
: "playhead";
|
|
114
|
+
const line = `range: ${trackName(context.score, context.trackId)} · ${barsWord(resolved.range)} (${from})`;
|
|
115
|
+
const repeat = formRepeat(context.score, resolved.range.startBar);
|
|
116
|
+
return repeat ? `${line} · ${repeat}` : line;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** How many times the form plays each section; empty without a form. */
|
|
120
|
+
export function formPasses(score: TrackScore): Map<string, number> {
|
|
121
|
+
const passes = new Map<string, number>();
|
|
122
|
+
for (const segment of formSegments(score))
|
|
123
|
+
passes.set(
|
|
124
|
+
segment.section.name,
|
|
125
|
+
(passes.get(segment.section.name) ?? 0) + 1,
|
|
126
|
+
);
|
|
127
|
+
return passes;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `edits chorus (plays 2×)` when `bar` is in a section the form plays more
|
|
132
|
+
* than once (op1-ux §4): an edit there lands on every pass.
|
|
133
|
+
*/
|
|
134
|
+
export function formRepeat(score: TrackScore, bar: number): string | undefined {
|
|
135
|
+
if (score.form.length === 0) return undefined;
|
|
136
|
+
const section = sectionAtBar(score, bar);
|
|
137
|
+
if (!section) return undefined;
|
|
138
|
+
const times = formPasses(score).get(section.name) ?? 0;
|
|
139
|
+
return times > 1 ? `edits ${section.name} (plays ${times}×)` : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `clipboard: drums · 2 bars`, for the chip. */
|
|
143
|
+
export function clipboardLabel(
|
|
144
|
+
score: TrackScore,
|
|
145
|
+
clipboard: Readonly<{ source: string; range: BarRange }> | undefined,
|
|
146
|
+
): string | undefined {
|
|
147
|
+
if (!clipboard) return undefined;
|
|
148
|
+
const who =
|
|
149
|
+
clipboard.source === "all"
|
|
150
|
+
? "all tracks"
|
|
151
|
+
: trackName(score, clipboard.source);
|
|
152
|
+
const bars = clipboard.range.bars;
|
|
153
|
+
return `clipboard: ${who} · ${bars} ${bars === 1 ? "bar" : "bars"}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sectionsInOrder(score: TrackScore) {
|
|
157
|
+
return [...score.sections].sort((a, b) => a.startBar - b.startBar);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function loopCommand(score: TrackScore, range: BarRange): string | undefined {
|
|
161
|
+
const startBar = Math.max(0, range.startBar);
|
|
162
|
+
const bars = Math.min(range.bars, score.bars - startBar);
|
|
163
|
+
if (bars < 1 || startBar >= score.bars) return undefined;
|
|
164
|
+
return `loop ${typedRange({ startBar, bars })}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The loop a `[ ] { } < >` key starts from: the loop, else the range. */
|
|
168
|
+
function loopOrRange(context: TapeContext): BarRange {
|
|
169
|
+
return loopRange(context.score) ?? tapeRange(context).range;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Keys that run one fixed typed command (with the range filled in). */
|
|
173
|
+
function rangeVerb(
|
|
174
|
+
context: TapeContext,
|
|
175
|
+
verb: "copy" | "clear" | "reverse",
|
|
176
|
+
all: boolean,
|
|
177
|
+
): string {
|
|
178
|
+
const who = all ? "all" : context.trackId;
|
|
179
|
+
return `${verb} ${who} ${typedRange(tapeRange(context).range)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function paste(context: TapeContext, insert: boolean): TapeAction {
|
|
183
|
+
const board = context.clipboard;
|
|
184
|
+
if (!board)
|
|
185
|
+
return {
|
|
186
|
+
type: "note",
|
|
187
|
+
message: "paste · the clipboard is empty · c copies the range",
|
|
188
|
+
};
|
|
189
|
+
const score = context.score;
|
|
190
|
+
const at = barOfBeat(score, context.beat);
|
|
191
|
+
const tail = insert ? " insert" : "";
|
|
192
|
+
const end = at + board.range.bars;
|
|
193
|
+
// A one-track clipboard lands on the focused track: the typed form names
|
|
194
|
+
// its source, so it only echoes `copy`/`move` when they mean the same.
|
|
195
|
+
const sameTrack = board.source === "all" || board.source === context.trackId;
|
|
196
|
+
if (context.stale || !sameTrack)
|
|
197
|
+
return { type: "paste", command: `paste at ${at + 1}${tail}`, end };
|
|
198
|
+
// The first paste after a cut folds into `move` (§6.2): one revision.
|
|
199
|
+
const verb = context.cut ? "move" : "copy";
|
|
200
|
+
return {
|
|
201
|
+
type: "paste",
|
|
202
|
+
command: `${verb} ${board.source} ${typedRange(board.range)} to ${at + 1}${tail}`,
|
|
203
|
+
...(context.cut ? { fold: true } : {}),
|
|
204
|
+
end,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One key on TAPE (the prompt is empty and no overlay is up). */
|
|
209
|
+
export function tapeKey(context: TapeContext, value: string): TapeAction {
|
|
210
|
+
const score = context.score;
|
|
211
|
+
const bar = barOfBeat(score, context.beat);
|
|
212
|
+
const track = score.tracks.find((item) => item.id === context.trackId);
|
|
213
|
+
switch (value) {
|
|
214
|
+
case "\u001b":
|
|
215
|
+
case "\u0014":
|
|
216
|
+
return { type: "exit" };
|
|
217
|
+
case " ":
|
|
218
|
+
return { type: "transport" };
|
|
219
|
+
case "?":
|
|
220
|
+
return { type: "keys" };
|
|
221
|
+
case "-":
|
|
222
|
+
return { type: "zoom", direction: -1 };
|
|
223
|
+
case "=":
|
|
224
|
+
case "+":
|
|
225
|
+
return { type: "zoom", direction: 1 };
|
|
226
|
+
case "\t": {
|
|
227
|
+
const index = score.tracks.findIndex(
|
|
228
|
+
(item) => item.id === context.trackId,
|
|
229
|
+
);
|
|
230
|
+
return { type: "focus", row: (index + 1) % score.tracks.length };
|
|
231
|
+
}
|
|
232
|
+
case "c":
|
|
233
|
+
case "C":
|
|
234
|
+
return {
|
|
235
|
+
type: "run",
|
|
236
|
+
commands: [rangeVerb(context, "copy", value === "C")],
|
|
237
|
+
};
|
|
238
|
+
case "x":
|
|
239
|
+
case "X": {
|
|
240
|
+
const all = value === "X";
|
|
241
|
+
return {
|
|
242
|
+
type: "run",
|
|
243
|
+
commands: [
|
|
244
|
+
rangeVerb(context, "copy", all),
|
|
245
|
+
rangeVerb(context, "clear", all),
|
|
246
|
+
],
|
|
247
|
+
cut: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
case "v":
|
|
251
|
+
case "V":
|
|
252
|
+
return paste(context, value === "V");
|
|
253
|
+
case "\u001b[3~":
|
|
254
|
+
case "\u007f":
|
|
255
|
+
return { type: "run", commands: [rangeVerb(context, "clear", false)] };
|
|
256
|
+
case "~":
|
|
257
|
+
return { type: "run", commands: [rangeVerb(context, "reverse", false)] };
|
|
258
|
+
case "s":
|
|
259
|
+
case "S": {
|
|
260
|
+
const section = sectionAtBar(score, bar);
|
|
261
|
+
if (!section)
|
|
262
|
+
return {
|
|
263
|
+
type: "note",
|
|
264
|
+
message: "no section under the playhead · section add verse 1-4",
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
type: "run",
|
|
268
|
+
commands: [
|
|
269
|
+
value === "s"
|
|
270
|
+
? `section split ${section.name} at ${bar + 1}`
|
|
271
|
+
: `section join ${joinTarget(score, section).name}`,
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
case "h":
|
|
276
|
+
if (!track) return { type: "pass" };
|
|
277
|
+
return {
|
|
278
|
+
type: "run",
|
|
279
|
+
commands: [`${track.muted ? "unmute" : "mute"} ${track.id}`],
|
|
280
|
+
};
|
|
281
|
+
case "H":
|
|
282
|
+
if (!track) return { type: "pass" };
|
|
283
|
+
return {
|
|
284
|
+
type: "run",
|
|
285
|
+
// `solo` acts on the focused track, which is this row.
|
|
286
|
+
commands: [track.solo ? "unsolo" : "solo"],
|
|
287
|
+
};
|
|
288
|
+
case "m":
|
|
289
|
+
return { type: "run", commands: ["/click"] };
|
|
290
|
+
case "b":
|
|
291
|
+
return { type: "run", commands: ["stop"] };
|
|
292
|
+
case ",":
|
|
293
|
+
case ".": {
|
|
294
|
+
// `.` the next section start after the playhead; `,` the last one
|
|
295
|
+
// before it (the current section's start when inside it).
|
|
296
|
+
const starts = sectionsInOrder(score).map((section) => ({
|
|
297
|
+
section,
|
|
298
|
+
beat: beatOfBar(score, section.startBar),
|
|
299
|
+
}));
|
|
300
|
+
const found =
|
|
301
|
+
value === "."
|
|
302
|
+
? starts.find((item) => item.beat > context.beat + 1e-6)
|
|
303
|
+
: [...starts]
|
|
304
|
+
.reverse()
|
|
305
|
+
.find((item) => item.beat < context.beat - 1e-6);
|
|
306
|
+
const target = found?.section;
|
|
307
|
+
if (!target)
|
|
308
|
+
return {
|
|
309
|
+
type: "note",
|
|
310
|
+
message: `no ${value === "." ? "next" : "previous"} section`,
|
|
311
|
+
};
|
|
312
|
+
return { type: "run", commands: [`jump ${target.name}`] };
|
|
313
|
+
}
|
|
314
|
+
case "<":
|
|
315
|
+
case ">": {
|
|
316
|
+
const loop = loopOrRange(context);
|
|
317
|
+
const direction = value === ">" ? 1 : -1;
|
|
318
|
+
const command = loopCommand(score, {
|
|
319
|
+
startBar: loop.startBar + direction * loop.bars,
|
|
320
|
+
bars: loop.bars,
|
|
321
|
+
});
|
|
322
|
+
return command
|
|
323
|
+
? { type: "run", commands: [command] }
|
|
324
|
+
: { type: "note", message: "loop · at the edge of the song" };
|
|
325
|
+
}
|
|
326
|
+
case "[":
|
|
327
|
+
case "]": {
|
|
328
|
+
const loop = loopOrRange(context);
|
|
329
|
+
const bars = loop.bars + (value === "]" ? 1 : -1);
|
|
330
|
+
const command =
|
|
331
|
+
bars >= 1 && loop.startBar + bars <= score.bars
|
|
332
|
+
? loopCommand(score, { startBar: loop.startBar, bars })
|
|
333
|
+
: undefined;
|
|
334
|
+
return command
|
|
335
|
+
? { type: "run", commands: [command] }
|
|
336
|
+
: { type: "note", message: "loop · cannot size it further" };
|
|
337
|
+
}
|
|
338
|
+
case "{":
|
|
339
|
+
case "}": {
|
|
340
|
+
const loop = loopOrRange(context);
|
|
341
|
+
const shift = value === "}" ? 1 : -1;
|
|
342
|
+
const command =
|
|
343
|
+
loop.bars - shift >= 1 && loop.startBar + shift >= 0
|
|
344
|
+
? loopCommand(score, {
|
|
345
|
+
startBar: loop.startBar + shift,
|
|
346
|
+
bars: loop.bars - shift,
|
|
347
|
+
})
|
|
348
|
+
: undefined;
|
|
349
|
+
return command
|
|
350
|
+
? { type: "run", commands: [command] }
|
|
351
|
+
: { type: "note", message: "loop · cannot move its start further" };
|
|
352
|
+
}
|
|
353
|
+
// §4.7: r / R record the loop in PLAY, one undo step per pass.
|
|
354
|
+
case "r":
|
|
355
|
+
return { type: "run", commands: ["keys record"] };
|
|
356
|
+
case "R":
|
|
357
|
+
return { type: "run", commands: ["keys record replace"] };
|
|
358
|
+
case "\\": {
|
|
359
|
+
if (loopRange(score)) return { type: "run", commands: ["loop off"] };
|
|
360
|
+
const section = sectionAtBar(score, bar);
|
|
361
|
+
return {
|
|
362
|
+
type: "run",
|
|
363
|
+
commands: [section ? `loop ${section.name}` : `loop ${bar + 1}`],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
default:
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (/^[1-9]$/.test(value)) {
|
|
370
|
+
const row = Number(value) - 1;
|
|
371
|
+
if (row >= score.tracks.length)
|
|
372
|
+
return {
|
|
373
|
+
type: "note",
|
|
374
|
+
message: `no track ${value} · ${score.tracks.length} tracks`,
|
|
375
|
+
};
|
|
376
|
+
return { type: "focus", row };
|
|
377
|
+
}
|
|
378
|
+
return { type: "pass" };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** `track drums`: the command a row focus runs. */
|
|
382
|
+
export function focusCommand(
|
|
383
|
+
score: TrackScore,
|
|
384
|
+
row: number,
|
|
385
|
+
): string | undefined {
|
|
386
|
+
const track = score.tracks[row];
|
|
387
|
+
return track ? `track ${track.id}` : undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** The score beat where `bar` starts (meter-aware). */
|
|
391
|
+
export function beatOfBar(score: TrackScore, bar: number): number {
|
|
392
|
+
return barStartTick(score, bar) / score.ticksPerBeat;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Beats in `bar` (meter-aware). */
|
|
396
|
+
function beatsIn(score: TrackScore, bar: number): number {
|
|
397
|
+
return (
|
|
398
|
+
(barStartTick(score, bar + 1) - barStartTick(score, bar)) /
|
|
399
|
+
score.ticksPerBeat
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** `jump 5` / `jump 5.3` for a 0-based bar and beat. */
|
|
404
|
+
function jumpCommand(bar: number, beat: number): string {
|
|
405
|
+
return beat > 0 ? `jump ${bar + 1}.${beat + 1}` : `jump ${bar + 1}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function round(value: number, digits: number): number {
|
|
409
|
+
const scale = 10 ** digits;
|
|
410
|
+
return Math.round(value * scale) / scale;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** `−4.9 dB` for a linear volume. */
|
|
414
|
+
export function decibels(volume: number): string {
|
|
415
|
+
if (volume <= 0) return "-inf dB";
|
|
416
|
+
const db = 20 * Math.log10(volume);
|
|
417
|
+
return `${db < 0 ? "−" : ""}${Math.abs(db).toFixed(1)} dB`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The tempo segment under a tick: its bar (undefined = the start tempo). */
|
|
421
|
+
function tempoSegment(
|
|
422
|
+
score: TrackScore,
|
|
423
|
+
tick: number,
|
|
424
|
+
): { bar?: number; bpm: number } {
|
|
425
|
+
const events = [...(score.time?.tempo ?? [])]
|
|
426
|
+
.filter((event) => event.tick <= tick && event.tick > 0)
|
|
427
|
+
.sort((a, b) => a.tick - b.tick);
|
|
428
|
+
const last = events.at(-1);
|
|
429
|
+
if (!last) return { bpm: score.tempoBpm };
|
|
430
|
+
return { bar: barAt(score, last.tick).bar, bpm: last.bpm };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Labels of the `tape` knob page (KNOB_MAPS.tape). */
|
|
434
|
+
function knobLabel(index: number, fallback: string): string {
|
|
435
|
+
return knobMap("tape")?.[index] ?? fallback;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* TAPE's four knobs (§7.3): blue the playhead (a beat; ⇧ a bar), green the
|
|
440
|
+
* loop length in bars, white the tempo of the segment under the playhead,
|
|
441
|
+
* orange the focused track's volume. Each turn is a typed command.
|
|
442
|
+
*/
|
|
443
|
+
export function tapeKnobs(context: TapeContext): KnobSlots {
|
|
444
|
+
const score = context.score;
|
|
445
|
+
const bar = barOfBeat(score, context.beat);
|
|
446
|
+
const tick = Math.max(0, Math.round(context.beat * score.ticksPerBeat));
|
|
447
|
+
const beatInBar = Math.floor(context.beat - beatOfBar(score, bar) + 1e-6);
|
|
448
|
+
const totalBeats = beatOfBar(score, score.bars);
|
|
449
|
+
const playhead: KnobSlot = {
|
|
450
|
+
label: knobLabel(0, "playhead"),
|
|
451
|
+
text: barBeatOf(score, context.beat),
|
|
452
|
+
position: totalBeats > 0 ? Math.min(1, context.beat / totalBeats) : 0,
|
|
453
|
+
turn: (direction, coarse) => {
|
|
454
|
+
if (coarse) {
|
|
455
|
+
const next = bar + direction;
|
|
456
|
+
return next < 0 || next >= score.bars
|
|
457
|
+
? undefined
|
|
458
|
+
: jumpCommand(next, 0);
|
|
459
|
+
}
|
|
460
|
+
let nextBar = bar;
|
|
461
|
+
let nextBeat = beatInBar + direction;
|
|
462
|
+
if (nextBeat < 0) {
|
|
463
|
+
nextBar -= 1;
|
|
464
|
+
if (nextBar < 0) return undefined;
|
|
465
|
+
nextBeat = beatsIn(score, nextBar) - 1;
|
|
466
|
+
} else if (nextBeat >= beatsIn(score, bar)) {
|
|
467
|
+
nextBar += 1;
|
|
468
|
+
nextBeat = 0;
|
|
469
|
+
if (nextBar >= score.bars) return undefined;
|
|
470
|
+
}
|
|
471
|
+
return jumpCommand(nextBar, nextBeat);
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
const loop = loopRange(score);
|
|
475
|
+
const loopSlot: KnobSlot = {
|
|
476
|
+
label: knobLabel(1, "loop"),
|
|
477
|
+
text: loop ? `${loop.bars} ${loop.bars === 1 ? "bar" : "bars"}` : "off",
|
|
478
|
+
position: loop ? loop.bars / Math.max(1, score.bars) : 0,
|
|
479
|
+
turn: (direction, coarse) => {
|
|
480
|
+
const from = loop ?? { startBar: bar, bars: 0 };
|
|
481
|
+
const step = coarse ? 4 : 1;
|
|
482
|
+
const bars = Math.max(
|
|
483
|
+
1,
|
|
484
|
+
Math.min(score.bars - from.startBar, from.bars + direction * step),
|
|
485
|
+
);
|
|
486
|
+
if (loop && bars === loop.bars) return undefined;
|
|
487
|
+
return loopCommand(score, { startBar: from.startBar, bars });
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
const segment = tempoSegment(score, tick);
|
|
491
|
+
const tempo: KnobSlot = {
|
|
492
|
+
label: knobLabel(2, "tempo"),
|
|
493
|
+
text: `${round(segment.bpm, 1)} BPM`,
|
|
494
|
+
position: (segment.bpm - 20) / 280,
|
|
495
|
+
turn: (direction, coarse) => {
|
|
496
|
+
const next = Math.round(segment.bpm) + direction * (coarse ? 10 : 1);
|
|
497
|
+
if (next < 20 || next > 300) return undefined;
|
|
498
|
+
return segment.bar === undefined
|
|
499
|
+
? `tempo ${next}`
|
|
500
|
+
: `tempo ${next} at bar ${segment.bar + 1}`;
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
const track = score.tracks.find((item) => item.id === context.trackId);
|
|
504
|
+
const level: KnobSlot | undefined = track
|
|
505
|
+
? {
|
|
506
|
+
label: knobLabel(3, "volume"),
|
|
507
|
+
text: decibels(track.volume),
|
|
508
|
+
position: track.volume,
|
|
509
|
+
turn: (direction, coarse) => {
|
|
510
|
+
const next = round(
|
|
511
|
+
Math.max(
|
|
512
|
+
0,
|
|
513
|
+
Math.min(1, track.volume + direction * (coarse ? 0.1 : 0.05)),
|
|
514
|
+
),
|
|
515
|
+
2,
|
|
516
|
+
);
|
|
517
|
+
return next === track.volume
|
|
518
|
+
? undefined
|
|
519
|
+
: `volume ${track.id} ${next}`;
|
|
520
|
+
},
|
|
521
|
+
}
|
|
522
|
+
: undefined;
|
|
523
|
+
return [playhead, loopSlot, tempo, level];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** What enter opens for each knob: its noun, as typed. */
|
|
527
|
+
export function knobNoun(context: TapeContext, index: number): string {
|
|
528
|
+
switch (index) {
|
|
529
|
+
case 0:
|
|
530
|
+
return "jump";
|
|
531
|
+
case 1:
|
|
532
|
+
return "loop";
|
|
533
|
+
case 2:
|
|
534
|
+
return "tempo";
|
|
535
|
+
default:
|
|
536
|
+
return `volume ${context.trackId}`;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function withoutLoop(score: TrackScore): string {
|
|
541
|
+
const { loop: _loop, loopSection: _section, ...rest } = score.toJSON();
|
|
542
|
+
return JSON.stringify(rest);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Whether a cut is still waiting for its paste: nothing but the loop has
|
|
547
|
+
* changed since (a `jump` that carries the loop along keeps it armed).
|
|
548
|
+
*/
|
|
549
|
+
export function cutArmed(
|
|
550
|
+
after: TrackScore | undefined,
|
|
551
|
+
now: TrackScore,
|
|
552
|
+
): boolean {
|
|
553
|
+
if (!after) return false;
|
|
554
|
+
return after === now || withoutLoop(after) === withoutLoop(now);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** The score a folded `move` reads: before the cut, with today's loop. */
|
|
558
|
+
export function foldBase(before: TrackScore, now: TrackScore): TrackScore {
|
|
559
|
+
return new TrackScore({
|
|
560
|
+
...before.toJSON(),
|
|
561
|
+
loop: now.loop ?? null,
|
|
562
|
+
loopSection: now.loop ? null : (now.loopSection ?? null),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* `S` joins the section under the playhead with its neighbour: the one
|
|
568
|
+
* that ends where it starts (undoing an `s`), else the one after it.
|
|
569
|
+
* `section join <name>` joins a section with the next.
|
|
570
|
+
*/
|
|
571
|
+
function joinTarget(
|
|
572
|
+
score: TrackScore,
|
|
573
|
+
section: NonNullable<ReturnType<typeof sectionAtBar>>,
|
|
574
|
+
) {
|
|
575
|
+
const before = score.sections.find(
|
|
576
|
+
(item) =>
|
|
577
|
+
item !== section && item.startBar + item.bars === section.startBar,
|
|
578
|
+
);
|
|
579
|
+
return before ?? section;
|
|
580
|
+
}
|