@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
package/tui/highway.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* lane per voice with a legend.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { emptyHint } from "./hints.ts";
|
|
13
14
|
import { CellBuffer } from "./screen.ts";
|
|
14
15
|
import { truncate } from "./text.ts";
|
|
15
16
|
import {
|
|
@@ -44,6 +45,24 @@ export interface NoteSnapshot {
|
|
|
44
45
|
* from (`D` for a `D−47` tag), since the lane is the key, not the sound.
|
|
45
46
|
*/
|
|
46
47
|
centsFrom?: string | undefined;
|
|
48
|
+
/** Sung syllable (`Note.lyric`), drawn beside the head like the cents tag. */
|
|
49
|
+
lyric?: string | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** An audio clip on the focused track (0.7), drawn in the clip row. */
|
|
53
|
+
export interface ClipSnapshot {
|
|
54
|
+
id: string;
|
|
55
|
+
/** Song beats where the clip sounds. */
|
|
56
|
+
startBeat: number;
|
|
57
|
+
durationBeats: number;
|
|
58
|
+
/** Normalised RMS over the clip's window (0..1), oldest first. */
|
|
59
|
+
peaks?: readonly number[] | undefined;
|
|
60
|
+
/** Take name and clip text, shown after the id. */
|
|
61
|
+
take?: string | undefined;
|
|
62
|
+
text?: string | undefined;
|
|
63
|
+
muted?: boolean | undefined;
|
|
64
|
+
/** The clip starts where another ends: a comp seam. */
|
|
65
|
+
seam?: boolean | undefined;
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
export interface TrackScoreSnapshot {
|
|
@@ -61,6 +80,8 @@ export interface TrackScoreSnapshot {
|
|
|
61
80
|
*/
|
|
62
81
|
barBeats?: readonly number[] | undefined;
|
|
63
82
|
loopBeats?: number | undefined;
|
|
83
|
+
/** The score's loop range (0-based start bar), for the header's `↻ 5–6`. */
|
|
84
|
+
loopRange?: Readonly<{ startBar: number; bars: number }> | undefined;
|
|
64
85
|
laneCount?: number | undefined;
|
|
65
86
|
/** Optional lane legend (drum voices) drawn just below the hit line. */
|
|
66
87
|
laneLabels?: readonly string[] | undefined;
|
|
@@ -83,6 +104,49 @@ export interface TrackScoreSnapshot {
|
|
|
83
104
|
* accent. Empty or omitted in focus view.
|
|
84
105
|
*/
|
|
85
106
|
layers?: readonly HighwayLayer[] | undefined;
|
|
107
|
+
/** The focused track sings (instrument `vocal`): the empty hint says how. */
|
|
108
|
+
vocal?: boolean | undefined;
|
|
109
|
+
/** Audio clips on the focused track (0.7): the right-edge clip row. */
|
|
110
|
+
clips?: readonly ClipSnapshot[] | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* Detected pitch of the focused track's audio (0.7 `/vocal pitch trace`),
|
|
113
|
+
* sorted by beat. Drawn as a dotted line under the notes, in the warning
|
|
114
|
+
* role where it sits more than 15 cents off the nearest semitone; hidden
|
|
115
|
+
* while one row spans more than a beat.
|
|
116
|
+
*/
|
|
117
|
+
pitchTrace?: readonly PitchTracePoint[] | undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One point of a pitch trace: score beats and fractional MIDI pitch. */
|
|
121
|
+
export interface PitchTracePoint {
|
|
122
|
+
beat: number;
|
|
123
|
+
pitch: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Cents a trace point sits away from its nearest semitone before it warns. */
|
|
127
|
+
export const TRACE_WARN_CENTS = 15;
|
|
128
|
+
|
|
129
|
+
/** The trace point nearest `beat` within `half` beats, by binary search. */
|
|
130
|
+
export function tracePointAt(
|
|
131
|
+
trace: readonly PitchTracePoint[],
|
|
132
|
+
beat: number,
|
|
133
|
+
half: number,
|
|
134
|
+
): PitchTracePoint | undefined {
|
|
135
|
+
let lo = 0;
|
|
136
|
+
let hi = trace.length;
|
|
137
|
+
while (lo < hi) {
|
|
138
|
+
const mid = (lo + hi) >> 1;
|
|
139
|
+
if (trace[mid]!.beat < beat) lo = mid + 1;
|
|
140
|
+
else hi = mid;
|
|
141
|
+
}
|
|
142
|
+
let best: PitchTracePoint | undefined;
|
|
143
|
+
for (const i of [lo - 1, lo]) {
|
|
144
|
+
const point = trace[i];
|
|
145
|
+
if (!point || Math.abs(point.beat - beat) > half) continue;
|
|
146
|
+
if (!best || Math.abs(point.beat - beat) < Math.abs(best.beat - beat))
|
|
147
|
+
best = point;
|
|
148
|
+
}
|
|
149
|
+
return best;
|
|
86
150
|
}
|
|
87
151
|
|
|
88
152
|
/** One background track overlaid on the highway. */
|
|
@@ -216,7 +280,10 @@ export function pitchProjection(
|
|
|
216
280
|
}
|
|
217
281
|
|
|
218
282
|
/** Choose the projection a snapshot implies. */
|
|
219
|
-
export function projectionFor(
|
|
283
|
+
export function projectionFor(
|
|
284
|
+
score: TrackScoreSnapshot,
|
|
285
|
+
minimumSpan = 12,
|
|
286
|
+
): LaneProjection {
|
|
220
287
|
if (score.projection) return score.projection;
|
|
221
288
|
const explicit =
|
|
222
289
|
score.laneLabels !== undefined ||
|
|
@@ -233,7 +300,7 @@ export function projectionFor(score: TrackScoreSnapshot): LaneProjection {
|
|
|
233
300
|
.flatMap((layer) => layer.notes);
|
|
234
301
|
return pitchProjection(
|
|
235
302
|
melodic.length ? [...score.notes, ...melodic] : score.notes,
|
|
236
|
-
|
|
303
|
+
minimumSpan,
|
|
237
304
|
score.tuningPeriod,
|
|
238
305
|
);
|
|
239
306
|
}
|
|
@@ -413,6 +480,12 @@ export interface HighwayOptions {
|
|
|
413
480
|
lookaheadBeats?: number;
|
|
414
481
|
/** Background style painted under the highway. */
|
|
415
482
|
background?: Style;
|
|
483
|
+
/** Seed and agent presence for the empty-state hint (tui/hints.ts). */
|
|
484
|
+
hint?: Readonly<{ seed: string; agent: boolean }> | undefined;
|
|
485
|
+
/** The first-loop sweep's head, 0..1 across the hit line (tui/delight.ts). */
|
|
486
|
+
sweep?: number | undefined;
|
|
487
|
+
/** Play keys sounding without recording: their lanes glow on the hit line. */
|
|
488
|
+
glows?: readonly Readonly<{ pitch: number; strength: number }>[] | undefined;
|
|
416
489
|
}
|
|
417
490
|
|
|
418
491
|
export interface HighwayLayout {
|
|
@@ -426,6 +499,28 @@ export interface HighwayLayout {
|
|
|
426
499
|
legendRow: number | undefined;
|
|
427
500
|
}
|
|
428
501
|
|
|
502
|
+
/**
|
|
503
|
+
* The widest a lane gets. A wider terminal shows more pitches (up to
|
|
504
|
+
* `MAX_PITCH_SPAN`) instead of fatter tiles, and past that the lanes stay
|
|
505
|
+
* this wide, centred.
|
|
506
|
+
*/
|
|
507
|
+
export const MAX_LANE_WIDTH = 10;
|
|
508
|
+
/** The most semitones a melodic highway spreads across on a wide terminal. */
|
|
509
|
+
export const MAX_PITCH_SPAN = 36;
|
|
510
|
+
/**
|
|
511
|
+
* The tallest a beat gets. A taller terminal looks further ahead (more bars
|
|
512
|
+
* coming) instead of stretching each note.
|
|
513
|
+
*/
|
|
514
|
+
export const MAX_ROWS_PER_BEAT = 4;
|
|
515
|
+
|
|
516
|
+
/** The pitch span a highway `width` columns wide fits: one to three octaves. */
|
|
517
|
+
export function pitchSpanFor(width: number): number {
|
|
518
|
+
return Math.max(
|
|
519
|
+
12,
|
|
520
|
+
Math.min(MAX_PITCH_SPAN, Math.floor((width - 3) / MAX_LANE_WIDTH)),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
429
524
|
export function highwayLayout(
|
|
430
525
|
region: HighwayRegion,
|
|
431
526
|
projection: LaneProjection,
|
|
@@ -437,13 +532,21 @@ export function highwayLayout(
|
|
|
437
532
|
const gutter = region.width >= 40 ? 3 : 0;
|
|
438
533
|
const area = Math.max(1, region.width - gutter);
|
|
439
534
|
const laneCount = projection.laneCount;
|
|
440
|
-
const laneWidth =
|
|
535
|
+
const laneWidth =
|
|
536
|
+
laneCount <= area
|
|
537
|
+
? Math.min(MAX_LANE_WIDTH, Math.floor(area / laneCount))
|
|
538
|
+
: 0;
|
|
441
539
|
const used = laneWidth > 0 ? laneWidth * laneCount : area;
|
|
442
540
|
const laneLeft = gutter + Math.floor((area - used) / 2);
|
|
443
541
|
const tileWidth =
|
|
444
542
|
laneWidth >= 4 ? laneWidth - 1 : Math.max(1, laneWidth || 1);
|
|
445
543
|
const lookahead =
|
|
446
|
-
lookaheadBeats ??
|
|
544
|
+
lookaheadBeats ??
|
|
545
|
+
Math.max(
|
|
546
|
+
2,
|
|
547
|
+
Math.min(8, Math.round(hitRow / 2.5)),
|
|
548
|
+
Math.floor(hitRow / MAX_ROWS_PER_BEAT),
|
|
549
|
+
);
|
|
447
550
|
return {
|
|
448
551
|
hitRow,
|
|
449
552
|
gutter,
|
|
@@ -473,6 +576,7 @@ interface Painter {
|
|
|
473
576
|
put(x: number, y: number, ch: string, style: Style): void;
|
|
474
577
|
restyle(x: number, y: number, style: Style): void;
|
|
475
578
|
isBlank(x: number, y: number): boolean;
|
|
579
|
+
charAt(x: number, y: number): string | undefined;
|
|
476
580
|
}
|
|
477
581
|
|
|
478
582
|
/**
|
|
@@ -492,7 +596,7 @@ export function paintHighway(
|
|
|
492
596
|
const glyphs = glyphsFor(capabilities);
|
|
493
597
|
const background = options.background ?? roles.canvas;
|
|
494
598
|
const bg = (style: Style): Style => onBackground(style, background);
|
|
495
|
-
const projection = projectionFor(score);
|
|
599
|
+
const projection = projectionFor(score, pitchSpanFor(region.width));
|
|
496
600
|
const layout = highwayLayout(region, projection, options.lookaheadBeats);
|
|
497
601
|
const { hitRow, rowsPerBeat, gutter } = layout;
|
|
498
602
|
const areaWidth = Math.max(1, region.width - gutter);
|
|
@@ -516,6 +620,9 @@ export function paintHighway(
|
|
|
516
620
|
isBlank(x, y) {
|
|
517
621
|
return buffer.get(region.x + x, region.y + y)?.ch === " ";
|
|
518
622
|
},
|
|
623
|
+
charAt(x, y) {
|
|
624
|
+
return buffer.get(region.x + x, region.y + y)?.ch;
|
|
625
|
+
},
|
|
519
626
|
};
|
|
520
627
|
|
|
521
628
|
const rowBeat = (row: number): number => beat + (hitRow - row) / rowsPerBeat;
|
|
@@ -525,13 +632,17 @@ export function paintHighway(
|
|
|
525
632
|
// centred hint says how to start. The hit line and grid still draw.
|
|
526
633
|
const empty =
|
|
527
634
|
score.notes.length === 0 &&
|
|
635
|
+
(score.clips ?? []).length === 0 &&
|
|
528
636
|
!(score.layers ?? []).some((layer) => layer.notes.length > 0);
|
|
529
637
|
|
|
530
638
|
// Beat, bar, and loop rules of increasing strength.
|
|
531
639
|
const barBeats =
|
|
532
640
|
score.barBeats && score.barBeats.length > 0 ? score.barBeats : undefined;
|
|
641
|
+
// An empty highway reserves one row for its hint: no rule scrolls
|
|
642
|
+
// through it, so the words never sit on the ruler, playing or paused.
|
|
643
|
+
const hintRow = empty ? Math.max(0, Math.floor(hitRow / 2)) : undefined;
|
|
533
644
|
for (let row = 0; row <= lastNoteRow; row += 1) {
|
|
534
|
-
if (row === hitRow) continue;
|
|
645
|
+
if (row === hitRow || row === hintRow) continue;
|
|
535
646
|
const center = rowBeat(row);
|
|
536
647
|
const half = 0.5 / rowsPerBeat;
|
|
537
648
|
let k = Math.ceil(center - half - 1e-9);
|
|
@@ -613,37 +724,40 @@ export function paintHighway(
|
|
|
613
724
|
);
|
|
614
725
|
}
|
|
615
726
|
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
if (
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
shade(roles.loopRule, 0.5 - offset * 0.1),
|
|
630
|
-
);
|
|
631
|
-
}
|
|
727
|
+
// First-loop sweep: once per song, a faint band crosses the hit line
|
|
728
|
+
// when the loop first wraps (tui/delight.ts decides when).
|
|
729
|
+
if (options.sweep !== undefined && !reduced) {
|
|
730
|
+
const head = Math.round(gutter + options.sweep * areaWidth);
|
|
731
|
+
for (let offset = 0; offset < 6; offset += 1) {
|
|
732
|
+
const column = head - offset;
|
|
733
|
+
if (column < gutter || column >= region.width) continue;
|
|
734
|
+
painter.put(
|
|
735
|
+
column,
|
|
736
|
+
hitRow,
|
|
737
|
+
glyphs.loopRule,
|
|
738
|
+
shade(roles.loopRule, 0.5 - offset * 0.1),
|
|
739
|
+
);
|
|
632
740
|
}
|
|
633
741
|
}
|
|
634
742
|
|
|
635
743
|
if (empty) {
|
|
636
744
|
const name = score.trackName ?? score.trackId ?? "track";
|
|
637
|
-
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
745
|
+
// One seeded line names a way in that works in this session: a
|
|
746
|
+
// request with an agent, a command without one (tui/hints.ts).
|
|
747
|
+
const text = emptyHint(
|
|
748
|
+
{
|
|
749
|
+
seed: options.hint?.seed ?? score.sessionId ?? "dawg",
|
|
750
|
+
agent: options.hint?.agent ?? true,
|
|
751
|
+
filled: false,
|
|
752
|
+
drums: projection.kind !== "pitch",
|
|
753
|
+
vocal: score.vocal === true,
|
|
754
|
+
playing: score.playing === true,
|
|
755
|
+
},
|
|
756
|
+
name,
|
|
643
757
|
areaWidth,
|
|
644
758
|
);
|
|
645
759
|
const x = gutter + Math.max(0, Math.floor((areaWidth - text.length) / 2));
|
|
646
|
-
const y =
|
|
760
|
+
const y = hintRow ?? 0;
|
|
647
761
|
for (let index = 0; index < text.length; index += 1)
|
|
648
762
|
painter.put(x + index, y, text[index]!, roles.muted);
|
|
649
763
|
}
|
|
@@ -668,6 +782,34 @@ export function paintHighway(
|
|
|
668
782
|
}
|
|
669
783
|
}
|
|
670
784
|
|
|
785
|
+
// 0.7 pitch trace: one dot per row under the notes, at the fractional
|
|
786
|
+
// lane of the detected pitch. Rows wider than a beat hide it.
|
|
787
|
+
const trace = score.pitchTrace;
|
|
788
|
+
if (trace && trace.length > 0 && rowsPerBeat >= 1) {
|
|
789
|
+
const half = 0.5 / rowsPerBeat;
|
|
790
|
+
const width = Math.max(1, layout.laneWidth || 1);
|
|
791
|
+
for (let row = 0; row <= lastNoteRow; row += 1) {
|
|
792
|
+
if (row === hitRow) continue;
|
|
793
|
+
let at = rowBeat(row);
|
|
794
|
+
if (loop) at = ((at % loop) + loop) % loop;
|
|
795
|
+
const point = tracePointAt(trace, at, half);
|
|
796
|
+
if (!point) continue;
|
|
797
|
+
const near = Math.round(point.pitch);
|
|
798
|
+
const lane = projection.laneOf({ startBeat: point.beat, pitch: near });
|
|
799
|
+
if (lane === undefined) continue;
|
|
800
|
+
const off = point.pitch - near;
|
|
801
|
+
const x =
|
|
802
|
+
laneX(layout, lane, areaWidth) +
|
|
803
|
+
Math.max(0, Math.min(width - 1, Math.floor((off + 0.5) * width)));
|
|
804
|
+
painter.put(
|
|
805
|
+
x,
|
|
806
|
+
row,
|
|
807
|
+
glyphs.muted,
|
|
808
|
+
Math.abs(off) * 100 > TRACE_WARN_CENTS ? roles.warning : roles.muted,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
671
813
|
// Note occurrences across loop repeats, ordered so active notes draw last.
|
|
672
814
|
const belowBeats = (lastNoteRow - hitRow + 1) / rowsPerBeat;
|
|
673
815
|
const aheadBeats = hitRow / rowsPerBeat + 1 / rowsPerBeat;
|
|
@@ -835,11 +977,13 @@ export function paintHighway(
|
|
|
835
977
|
: shade(base, 0.45 * glow);
|
|
836
978
|
if (headRow <= hitRow - 1) {
|
|
837
979
|
tile(headRow, headGlyph, style);
|
|
838
|
-
const
|
|
980
|
+
const cents =
|
|
839
981
|
(note.cents !== undefined || note.centsFrom !== undefined) &&
|
|
840
982
|
!note.muted
|
|
841
983
|
? centsTag(note.cents, note.centsFrom)
|
|
842
984
|
: "";
|
|
985
|
+
const lyric = note.lyric && !note.muted ? note.lyric.slice(0, 8) : "";
|
|
986
|
+
const tag = [cents, lyric].filter(Boolean).join(" ");
|
|
843
987
|
if (tag && x + width + tag.length <= region.width)
|
|
844
988
|
for (let index = 0; index < tag.length; index += 1)
|
|
845
989
|
painter.put(x + width + index, headRow, tag[index]!, roles.muted);
|
|
@@ -938,9 +1082,144 @@ export function paintHighway(
|
|
|
938
1082
|
}
|
|
939
1083
|
}
|
|
940
1084
|
}
|
|
1085
|
+
// Ghost notes: a play key that is not recording lights its lane on the
|
|
1086
|
+
// hit line for a moment, so the hand sees where the music would land.
|
|
1087
|
+
if (options.glows && !reduced) {
|
|
1088
|
+
const width = Math.max(1, layout.laneWidth || 1);
|
|
1089
|
+
for (const glow of options.glows) {
|
|
1090
|
+
const lane = projection.laneOf({ startBeat: beat, pitch: glow.pitch });
|
|
1091
|
+
if (lane === undefined) continue;
|
|
1092
|
+
const x = laneX(layout, lane, areaWidth);
|
|
1093
|
+
const style = {
|
|
1094
|
+
...shade(accent, 0.45 * glow.strength),
|
|
1095
|
+
bold: glow.strength > 0.5,
|
|
1096
|
+
};
|
|
1097
|
+
for (let offset = 0; offset < width; offset += 1)
|
|
1098
|
+
painter.put(x + offset, hitRow, glyphs.hitStrong, style);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (score.clips && score.clips.length > 0 && region.width >= 24)
|
|
1102
|
+
paintClipRow(painter, score, {
|
|
1103
|
+
beat,
|
|
1104
|
+
hitRow,
|
|
1105
|
+
lastNoteRow,
|
|
1106
|
+
rowsPerBeat,
|
|
1107
|
+
width: region.width,
|
|
1108
|
+
loop,
|
|
1109
|
+
glyphs,
|
|
1110
|
+
accent,
|
|
1111
|
+
roles,
|
|
1112
|
+
});
|
|
941
1113
|
return layout;
|
|
942
1114
|
}
|
|
943
1115
|
|
|
1116
|
+
function rowHasOnlyRule(
|
|
1117
|
+
painter: Painter,
|
|
1118
|
+
x: number,
|
|
1119
|
+
length: number,
|
|
1120
|
+
row: number,
|
|
1121
|
+
glyphs: HighwayGlyphs,
|
|
1122
|
+
): boolean {
|
|
1123
|
+
for (let index = 0; index < length; index += 1) {
|
|
1124
|
+
const ch = painter.charAt(x + index, row);
|
|
1125
|
+
if (ch !== " " && ch !== glyphs.beatRule) return false;
|
|
1126
|
+
}
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/** Shading for a clip's level, quiet to loud. */
|
|
1131
|
+
const CLIP_LEVELS_UNICODE = ["▏", "░", "▒", "▓", "█"];
|
|
1132
|
+
const CLIP_LEVELS_ASCII = [":", ".", "o", "O", "#"];
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* The clip row: the rightmost column carries each clip's level as it
|
|
1136
|
+
* falls toward the hit line, a seam (`┄`) where one clip continues
|
|
1137
|
+
* another, and `id take · text` beside the clip's first row.
|
|
1138
|
+
*/
|
|
1139
|
+
function paintClipRow(
|
|
1140
|
+
painter: Painter,
|
|
1141
|
+
score: TrackScoreSnapshot,
|
|
1142
|
+
at: {
|
|
1143
|
+
beat: number;
|
|
1144
|
+
hitRow: number;
|
|
1145
|
+
lastNoteRow: number;
|
|
1146
|
+
rowsPerBeat: number;
|
|
1147
|
+
width: number;
|
|
1148
|
+
loop: number | undefined;
|
|
1149
|
+
glyphs: HighwayGlyphs;
|
|
1150
|
+
accent: Style;
|
|
1151
|
+
roles: Theme["roles"];
|
|
1152
|
+
},
|
|
1153
|
+
): void {
|
|
1154
|
+
const column = at.width - 1;
|
|
1155
|
+
const levels =
|
|
1156
|
+
at.glyphs === ASCII_GLYPHS ? CLIP_LEVELS_ASCII : CLIP_LEVELS_UNICODE;
|
|
1157
|
+
const seamGlyph = at.glyphs === ASCII_GLYPHS ? "-" : "┄";
|
|
1158
|
+
const rowBeat = (row: number) => at.beat + (at.hitRow - row) / at.rowsPerBeat;
|
|
1159
|
+
const half = 0.5 / at.rowsPerBeat;
|
|
1160
|
+
const occurrences = (clip: ClipSnapshot): number[] => {
|
|
1161
|
+
if (!at.loop) return [clip.startBeat];
|
|
1162
|
+
const base = Math.floor(at.beat / at.loop) * at.loop;
|
|
1163
|
+
return [base - at.loop, base, base + at.loop].map(
|
|
1164
|
+
(offset) => offset + clip.startBeat,
|
|
1165
|
+
);
|
|
1166
|
+
};
|
|
1167
|
+
for (const clip of score.clips ?? []) {
|
|
1168
|
+
const style = clip.muted ? at.roles.mutedNote : at.accent;
|
|
1169
|
+
for (const start of occurrences(clip)) {
|
|
1170
|
+
const end = start + clip.durationBeats;
|
|
1171
|
+
for (let row = 0; row <= at.lastNoteRow; row += 1) {
|
|
1172
|
+
if (row === at.hitRow) continue;
|
|
1173
|
+
const center = rowBeat(row);
|
|
1174
|
+
if (center + half <= start || center - half >= end) continue;
|
|
1175
|
+
const isStart = start >= center - half && start < center + half;
|
|
1176
|
+
if (isStart && clip.seam) {
|
|
1177
|
+
painter.put(column, row, seamGlyph, at.roles.muted);
|
|
1178
|
+
} else {
|
|
1179
|
+
const peaks = clip.peaks;
|
|
1180
|
+
const fraction = Math.max(
|
|
1181
|
+
0,
|
|
1182
|
+
Math.min(
|
|
1183
|
+
0.999,
|
|
1184
|
+
(center - start) / Math.max(1e-9, clip.durationBeats),
|
|
1185
|
+
),
|
|
1186
|
+
);
|
|
1187
|
+
const level =
|
|
1188
|
+
peaks && peaks.length > 0
|
|
1189
|
+
? peaks[Math.floor(fraction * peaks.length)]!
|
|
1190
|
+
: 0.6;
|
|
1191
|
+
const index = clip.muted
|
|
1192
|
+
? 0
|
|
1193
|
+
: Math.min(
|
|
1194
|
+
levels.length - 1,
|
|
1195
|
+
Math.round(level * (levels.length - 1)),
|
|
1196
|
+
);
|
|
1197
|
+
painter.put(column, row, levels[index]!, style);
|
|
1198
|
+
}
|
|
1199
|
+
if (isStart && row < at.hitRow) {
|
|
1200
|
+
const label = [clip.id, clip.take, clip.text ? `· ${clip.text}` : ""]
|
|
1201
|
+
.filter(Boolean)
|
|
1202
|
+
.join(" ")
|
|
1203
|
+
.slice(0, Math.max(0, Math.min(24, at.width - 6)));
|
|
1204
|
+
const x = column - 1 - label.length;
|
|
1205
|
+
// Over a beat rule the whole label draws; over notes it never does.
|
|
1206
|
+
const ruled = rowHasOnlyRule(
|
|
1207
|
+
painter,
|
|
1208
|
+
x,
|
|
1209
|
+
label.length,
|
|
1210
|
+
row,
|
|
1211
|
+
at.glyphs,
|
|
1212
|
+
);
|
|
1213
|
+
if (x > 3)
|
|
1214
|
+
for (let index = 0; index < label.length; index += 1)
|
|
1215
|
+
if (painter.isBlank(x + index, row) || ruled)
|
|
1216
|
+
painter.put(x + index, row, label[index]!, at.roles.muted);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
944
1223
|
/**
|
|
945
1224
|
* A background track: a quiet head and sustain line in the track's accent,
|
|
946
1225
|
* no hit flash, sparks or ghosts, so the focused track stays readable.
|
package/tui/hints.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeded hints: the empty-highway line and the prompt placeholder.
|
|
3
|
+
*
|
|
4
|
+
* Each list is chosen by what the session can do (an agent or commands
|
|
5
|
+
* only) and what the song holds (empty, pitched, drums), and one entry is
|
|
6
|
+
* picked by hashing the session id, so a session keeps the same words
|
|
7
|
+
* while it runs and the next session reads a different, equally short one.
|
|
8
|
+
* Every example is original and every command in it runs as typed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type HintState = Readonly<{
|
|
12
|
+
/** The session id (or any stable string): the seed. */
|
|
13
|
+
seed: string;
|
|
14
|
+
/** An agent provider is ready, so prose requests work. */
|
|
15
|
+
agent: boolean;
|
|
16
|
+
/** The song has notes, clips or layers. */
|
|
17
|
+
filled: boolean;
|
|
18
|
+
/** The focused track is a drum kit. */
|
|
19
|
+
drums?: boolean;
|
|
20
|
+
/** The focused track is a vocal track. */
|
|
21
|
+
vocal?: boolean;
|
|
22
|
+
/** The transport is running. */
|
|
23
|
+
playing?: boolean;
|
|
24
|
+
/** The agent is busy and Enter queues for after it. */
|
|
25
|
+
queue?: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
/** FNV-1a: a small, stable string hash. */
|
|
29
|
+
export function seedHash(seed: string): number {
|
|
30
|
+
let hash = 0x811c9dc5;
|
|
31
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
32
|
+
hash ^= seed.charCodeAt(index);
|
|
33
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
34
|
+
}
|
|
35
|
+
return hash >>> 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The entry of `list` the seed picks; `salt` decorrelates two lists. */
|
|
39
|
+
export function pick<T>(list: readonly T[], seed: string, salt = ""): T {
|
|
40
|
+
return list[seedHash(`${salt}:${seed}`) % list.length]!;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Prose requests an agent understands, for the placeholder. */
|
|
44
|
+
const ASK_EMPTY = [
|
|
45
|
+
"describe a song — “a slow lofi loop in F with soft drums”",
|
|
46
|
+
"describe a song — “a four-bar house groove at 124 BPM”",
|
|
47
|
+
"describe a song — “a sad waltz for piano and cello”",
|
|
48
|
+
"describe a song — “a bright synth arpeggio in D major”",
|
|
49
|
+
"describe a song — “a dusty boom bap beat with a walking bass”",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
const ASK_FILLED = [
|
|
53
|
+
"describe a change — “add a walking bass in A minor”",
|
|
54
|
+
"describe a change — “make the drums swing a little”",
|
|
55
|
+
"describe a change — “double the melody an octave up”",
|
|
56
|
+
"describe a change — “add reverb to the keys”",
|
|
57
|
+
"describe a change — “slow it to 90 BPM and add a fill”",
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
const ASK_DRUMS = [
|
|
61
|
+
"describe a change — “a ghost-note snare on the offbeats”",
|
|
62
|
+
"describe a change — “open hats on every and”",
|
|
63
|
+
"describe a change — “a crash on bar one, then a fill”",
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Commands that run without an agent, for the placeholder. `{style}` is the
|
|
68
|
+
* same seeded style the wide empty line suggests, so the two lines never
|
|
69
|
+
* offer competing first steps.
|
|
70
|
+
*/
|
|
71
|
+
const TYPE_EMPTY = [
|
|
72
|
+
"try: style {style} · space play · /help",
|
|
73
|
+
"try: style {style} 8 · then space to hear it",
|
|
74
|
+
"try: add C4 at 0 · add E4 at 1 · space play",
|
|
75
|
+
"try: style {style} · ctrl-z undo",
|
|
76
|
+
] as const;
|
|
77
|
+
|
|
78
|
+
const TYPE_FILLED = [
|
|
79
|
+
"try: tempo 96 · reverb 0.3 · space play",
|
|
80
|
+
"try: style again · ctrl-z undo",
|
|
81
|
+
"try: add G4 at 2 · bars 8 · space play",
|
|
82
|
+
"try: volume 0.6 · pan -0.2 · /help",
|
|
83
|
+
"try: ctrl-t tape · loop 1-2 · space play",
|
|
84
|
+
"try: copy all 1-4 to 5 · loop 5-8",
|
|
85
|
+
"try: knobs · mix · /help mix",
|
|
86
|
+
"try: panes · pane tape · /help panes",
|
|
87
|
+
"try: loop 1-2 · keys record · ctrl-z takes back a pass",
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
const TYPE_DRUMS = [
|
|
91
|
+
"try: hit kick at 0 · hit snare at 1 · space play",
|
|
92
|
+
"try: euclid hat 7 16 · tempo 100",
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The keys on the empty line (design §8.5): transport, play mode and the
|
|
97
|
+
* menu while paused; while playing, how to stop and, with an agent, that a
|
|
98
|
+
* request still works.
|
|
99
|
+
*/
|
|
100
|
+
const KEYS_PAUSED = "space play · ctrl-p play mode · ctrl-k menu";
|
|
101
|
+
const KEYS_PLAYING_AGENT = "space stop · type a request";
|
|
102
|
+
const KEYS_PLAYING = "space stop · ctrl-p play mode";
|
|
103
|
+
|
|
104
|
+
/** How to start a drum track or a vocal track, before the keys. */
|
|
105
|
+
const LEAD_DRUMS = "hit kick at 0";
|
|
106
|
+
const LEAD_VOCAL = 'lyrics "la la" · sing ooh · ctrl-k › Voice';
|
|
107
|
+
|
|
108
|
+
/** Styles a wide empty line suggests, one per session (all have cards). */
|
|
109
|
+
export const SUGGESTED_STYLES = [
|
|
110
|
+
"deep-house",
|
|
111
|
+
"lofi-hip-hop",
|
|
112
|
+
"bossa-nova",
|
|
113
|
+
"nu-disco",
|
|
114
|
+
"synthwave",
|
|
115
|
+
"golden-age",
|
|
116
|
+
] as const;
|
|
117
|
+
|
|
118
|
+
/** From this width the empty line also suggests a seeded style. */
|
|
119
|
+
export const STYLE_HINT_WIDTH = 100;
|
|
120
|
+
|
|
121
|
+
/** The prompt placeholder for this session and state. */
|
|
122
|
+
export function placeholderHint(state: HintState): string {
|
|
123
|
+
if (state.agent && state.queue) return "queue a request for after this one…";
|
|
124
|
+
const list = state.agent
|
|
125
|
+
? state.drums && state.filled
|
|
126
|
+
? ASK_DRUMS
|
|
127
|
+
: state.filled
|
|
128
|
+
? ASK_FILLED
|
|
129
|
+
: ASK_EMPTY
|
|
130
|
+
: state.drums
|
|
131
|
+
? TYPE_DRUMS
|
|
132
|
+
: state.filled
|
|
133
|
+
? TYPE_FILLED
|
|
134
|
+
: TYPE_EMPTY;
|
|
135
|
+
return pick(list, state.seed, "placeholder").replace(
|
|
136
|
+
"{style}",
|
|
137
|
+
sessionStyle(state.seed),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The one style this session suggests, on the empty line and the prompt. */
|
|
142
|
+
export function sessionStyle(seed: string): string {
|
|
143
|
+
return pick(SUGGESTED_STYLES, seed, "style");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The empty-highway line, `<name> · empty · <lead> · <keys>`, shortened to
|
|
148
|
+
* `width`: the lead goes first, then the keys shrink to their first
|
|
149
|
+
* clause, then the name alone. A drum track leads with `hit kick at 0`, a
|
|
150
|
+
* vocal track with lyrics and sing, and from 100 columns a pitched track
|
|
151
|
+
* suggests a seeded style.
|
|
152
|
+
*/
|
|
153
|
+
export function emptyHint(
|
|
154
|
+
state: HintState,
|
|
155
|
+
name: string,
|
|
156
|
+
width: number,
|
|
157
|
+
): string {
|
|
158
|
+
const keys = state.playing
|
|
159
|
+
? state.agent
|
|
160
|
+
? KEYS_PLAYING_AGENT
|
|
161
|
+
: KEYS_PLAYING
|
|
162
|
+
: KEYS_PAUSED;
|
|
163
|
+
const lead = state.drums
|
|
164
|
+
? LEAD_DRUMS
|
|
165
|
+
: state.vocal
|
|
166
|
+
? LEAD_VOCAL
|
|
167
|
+
: width >= STYLE_HINT_WIDTH && !state.playing
|
|
168
|
+
? `try style ${sessionStyle(state.seed)}`
|
|
169
|
+
: undefined;
|
|
170
|
+
const head = `${name} · empty`;
|
|
171
|
+
const candidates = [
|
|
172
|
+
...(lead ? [`${head} · ${lead} · ${keys}`] : []),
|
|
173
|
+
`${head} · ${keys}`,
|
|
174
|
+
`${head} · ${keys.split(" · ")[0]}`,
|
|
175
|
+
];
|
|
176
|
+
for (const candidate of candidates)
|
|
177
|
+
if (candidate.length <= width) return candidate;
|
|
178
|
+
return head.slice(0, Math.max(0, width));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Every hint string, for the shipped-text and command checks. */
|
|
182
|
+
export const ALL_HINTS: readonly string[] = [
|
|
183
|
+
...ASK_EMPTY,
|
|
184
|
+
...ASK_FILLED,
|
|
185
|
+
...ASK_DRUMS,
|
|
186
|
+
...TYPE_EMPTY.flatMap((hint) =>
|
|
187
|
+
SUGGESTED_STYLES.map((id) => hint.replace("{style}", id)),
|
|
188
|
+
),
|
|
189
|
+
...TYPE_FILLED,
|
|
190
|
+
...TYPE_DRUMS,
|
|
191
|
+
KEYS_PAUSED,
|
|
192
|
+
KEYS_PLAYING_AGENT,
|
|
193
|
+
KEYS_PLAYING,
|
|
194
|
+
LEAD_DRUMS,
|
|
195
|
+
LEAD_VOCAL,
|
|
196
|
+
...SUGGESTED_STYLES.map((id) => `try style ${id}`),
|
|
197
|
+
];
|
package/tui/hits.ts
CHANGED
|
@@ -27,7 +27,13 @@ export type HitTarget =
|
|
|
27
27
|
/** The piano roll / highway area. */
|
|
28
28
|
| { kind: "highway" }
|
|
29
29
|
/** Scrollable text (`/help`, the transcript, the `?` panel). */
|
|
30
|
-
| { kind: "text" }
|
|
30
|
+
| { kind: "text" }
|
|
31
|
+
/** TAPE's ruler: column `x - left + firstCell` is a cell (jump, loop drag). */
|
|
32
|
+
| { kind: "tape-ruler"; left: number; firstCell: number }
|
|
33
|
+
/** A TAPE track row (focus; shift-drag sets the loop). */
|
|
34
|
+
| { kind: "tape-row"; row: number; left: number; firstCell: number }
|
|
35
|
+
/** TAPE's clipboard chip (click pastes). */
|
|
36
|
+
| { kind: "tape-clipboard" };
|
|
31
37
|
|
|
32
38
|
export interface HitRegion {
|
|
33
39
|
x: number;
|