@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,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which value each of the four knobs turns, per page (design §7.3): one
|
|
3
|
+
* table, read by the fader drawer's front page, the mixer page and euclid.
|
|
4
|
+
* A row is four menu paths from the Ctrl-K root (`sound/attack`,
|
|
5
|
+
* `effects/reverb/mix`, `mix/volume`): segments match a submenu's id or a
|
|
6
|
+
* row's label, and `more effects` is searched on the way. An undefined slot
|
|
7
|
+
* shows as `·` and ↑↓ skips it.
|
|
8
|
+
*
|
|
9
|
+
* The rule (tui/knobs.ts): blue moves, green sizes, white shapes, orange
|
|
10
|
+
* level. Pages not in the table (an effect without a row) derive one: their
|
|
11
|
+
* first three number rows on blue, green and white, and `mix` on orange.
|
|
12
|
+
*
|
|
13
|
+
* Data, not code, so the patcher can map a patch's macros onto the same
|
|
14
|
+
* four knobs by adding rows (`patch:<id>`), and a test checks every path
|
|
15
|
+
* resolves for every instrument family. A `*` at the end of a segment
|
|
16
|
+
* matches the first submenu whose id starts with it (`sample:*`).
|
|
17
|
+
*/
|
|
18
|
+
import { isWavetableInstrument, type Track } from "../../core/score.ts";
|
|
19
|
+
import { isDrumInstrument } from "../../core/drums.ts";
|
|
20
|
+
import {
|
|
21
|
+
isElectricFamily,
|
|
22
|
+
isKeysFamily,
|
|
23
|
+
isOrganFamily,
|
|
24
|
+
} from "../../core/keys.ts";
|
|
25
|
+
import { isStringTrack } from "../../core/strings.ts";
|
|
26
|
+
import { isGranularInstrument } from "../../core/granular.ts";
|
|
27
|
+
|
|
28
|
+
export type KnobRef = string | undefined;
|
|
29
|
+
export type KnobMap = readonly [KnobRef, KnobRef, KnobRef, KnobRef];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pages, by id. `sound:<family>` rows use each engine's own param names (the
|
|
33
|
+
* rows its Sound menu shows); orange is the track level on every sound page,
|
|
34
|
+
* so ◆ always means "how loud".
|
|
35
|
+
*/
|
|
36
|
+
export const KNOB_MAPS: Readonly<Record<string, KnobMap>> = Object.freeze({
|
|
37
|
+
"sound:synth": [
|
|
38
|
+
"sound/preset",
|
|
39
|
+
"sound/attack",
|
|
40
|
+
"sound/synth filter",
|
|
41
|
+
"mix/volume",
|
|
42
|
+
],
|
|
43
|
+
"sound:wavetable": [
|
|
44
|
+
"sound/position (wt)",
|
|
45
|
+
"sound/attack",
|
|
46
|
+
"sound/synth filter",
|
|
47
|
+
"mix/volume",
|
|
48
|
+
],
|
|
49
|
+
"sound:piano": [
|
|
50
|
+
"sound/preset",
|
|
51
|
+
"sound/decay",
|
|
52
|
+
"sound/hardness",
|
|
53
|
+
"mix/volume",
|
|
54
|
+
],
|
|
55
|
+
"sound:electric": ["sound/preset", "sound/bell", "sound/tone", "mix/volume"],
|
|
56
|
+
"sound:organ": ["sound/preset", "sound/perc", "sound/drive", "mix/volume"],
|
|
57
|
+
"sound:string": [
|
|
58
|
+
"sound/preset",
|
|
59
|
+
"sound/ring s",
|
|
60
|
+
"sound/bright",
|
|
61
|
+
"mix/volume",
|
|
62
|
+
],
|
|
63
|
+
"sound:wind": [
|
|
64
|
+
"sound/breath",
|
|
65
|
+
"sound/wind:advanced/vib",
|
|
66
|
+
"sound/bright",
|
|
67
|
+
"mix/volume",
|
|
68
|
+
],
|
|
69
|
+
"sound:modal": [
|
|
70
|
+
"sound/position",
|
|
71
|
+
"sound/ring",
|
|
72
|
+
"sound/hardness",
|
|
73
|
+
"mix/volume",
|
|
74
|
+
],
|
|
75
|
+
"sound:sing": ["sound/vowel", "sound/voices", "sound/bright", "mix/volume"],
|
|
76
|
+
"sound:granular": [
|
|
77
|
+
"sound/granular/position",
|
|
78
|
+
"sound/granular/grain (s)",
|
|
79
|
+
"sound/granular/spray (s)",
|
|
80
|
+
"mix/volume",
|
|
81
|
+
],
|
|
82
|
+
// `sample:*` is the track's first sample voice.
|
|
83
|
+
"sound:sampler": [
|
|
84
|
+
"sound/sample:*/begin",
|
|
85
|
+
"sound/sample:*/len",
|
|
86
|
+
"sound/sample:*/speed",
|
|
87
|
+
"mix/volume",
|
|
88
|
+
],
|
|
89
|
+
// A kit has no voice params: blue loosens the timing, green the room it
|
|
90
|
+
// plays in, white its tone.
|
|
91
|
+
"sound:kit": [
|
|
92
|
+
"sound/performance/humanize timing (ms)",
|
|
93
|
+
"effects/reverb/mix",
|
|
94
|
+
"effects/filter/cutoff",
|
|
95
|
+
"mix/volume",
|
|
96
|
+
],
|
|
97
|
+
// The mixer page: this track's pan, reverb send, tone and level.
|
|
98
|
+
mix: ["mix/pan", "effects/reverb/mix", "effects/filter/cutoff", "mix/volume"],
|
|
99
|
+
master: [
|
|
100
|
+
"mix/master/master:eq/low",
|
|
101
|
+
"mix/master/master:eq/bell1",
|
|
102
|
+
"mix/master/master:eq/high",
|
|
103
|
+
"mix/master/target LUFS",
|
|
104
|
+
],
|
|
105
|
+
tempo: [
|
|
106
|
+
"project/tempo",
|
|
107
|
+
"project/loop length",
|
|
108
|
+
"project/beats per bar",
|
|
109
|
+
undefined,
|
|
110
|
+
],
|
|
111
|
+
// Effects: time or rate, feedback / size / depth, tone, mix.
|
|
112
|
+
"fx:filter": [
|
|
113
|
+
"effects/filter/type",
|
|
114
|
+
"effects/filter/resonance",
|
|
115
|
+
"effects/filter/cutoff",
|
|
116
|
+
undefined,
|
|
117
|
+
],
|
|
118
|
+
"fx:delay": [
|
|
119
|
+
"effects/delay/beats",
|
|
120
|
+
"effects/delay/feedback",
|
|
121
|
+
undefined,
|
|
122
|
+
"effects/delay/mix",
|
|
123
|
+
],
|
|
124
|
+
"fx:reverb": [
|
|
125
|
+
"effects/reverb/impulse (ir)",
|
|
126
|
+
"effects/reverb/size",
|
|
127
|
+
undefined,
|
|
128
|
+
"effects/reverb/mix",
|
|
129
|
+
],
|
|
130
|
+
"fx:chorus": [
|
|
131
|
+
"effects/chorus/rate",
|
|
132
|
+
"effects/chorus/depth",
|
|
133
|
+
undefined,
|
|
134
|
+
"effects/chorus/mix",
|
|
135
|
+
],
|
|
136
|
+
/**
|
|
137
|
+
* The Euclid editor (design §8.7): its row's own fields, not menu paths.
|
|
138
|
+
* Blue picks the drum row; ←→ on it moves between rows.
|
|
139
|
+
*/
|
|
140
|
+
euclid: ["drum", "pulses", "rotate", "velocity"],
|
|
141
|
+
/**
|
|
142
|
+
* TAPE (design §7.3): its own values, not menu paths. Blue the playhead
|
|
143
|
+
* (a beat; ⇧ a bar), green the loop length, white the tempo of the
|
|
144
|
+
* segment under the playhead, orange the focused track's volume.
|
|
145
|
+
*/
|
|
146
|
+
tape: ["playhead", "loop", "tempo", "volume"],
|
|
147
|
+
"fx:distort": [
|
|
148
|
+
undefined,
|
|
149
|
+
"effects/distort/drive",
|
|
150
|
+
"effects/distort/tone",
|
|
151
|
+
"effects/distort/mix",
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/** The sound page's family for a track: which engine its Sound menu shows. */
|
|
156
|
+
export function soundFamily(track: Track): string {
|
|
157
|
+
if (isDrumInstrument(track.instrument)) return "kit";
|
|
158
|
+
if (track.sampler) return "sampler";
|
|
159
|
+
if (isGranularInstrument(track.instrument)) return "granular";
|
|
160
|
+
if (isOrganFamily(track.instrument) && track.keys) return "organ";
|
|
161
|
+
if (isKeysFamily(track.instrument) && track.keys)
|
|
162
|
+
return isElectricFamily(track.instrument) ? "electric" : "piano";
|
|
163
|
+
if (isStringTrack(track)) return "string";
|
|
164
|
+
if (track.instrument === "modal") return "modal";
|
|
165
|
+
if (track.instrument === "wind" && track.wind) return "wind";
|
|
166
|
+
if (track.instrument === "sing" && track.sing) return "sing";
|
|
167
|
+
if (isWavetableInstrument(track.instrument)) return "wavetable";
|
|
168
|
+
return "synth";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The page id for `knobs <page>`, or undefined when no page has that name. */
|
|
172
|
+
export function knobPageId(
|
|
173
|
+
word: string | undefined,
|
|
174
|
+
track: Track | undefined,
|
|
175
|
+
): string | undefined {
|
|
176
|
+
const name = (word ?? "sound").trim().toLowerCase().replace(/\s+/g, " ");
|
|
177
|
+
if (name === "sound" || name === "")
|
|
178
|
+
return `sound:${track ? soundFamily(track) : "synth"}`;
|
|
179
|
+
if (name === "mix" || name === "master" || name === "tempo") return name;
|
|
180
|
+
const fx = name.match(/^(?:fx|effect|effects)\s+(\S+)$/)?.[1];
|
|
181
|
+
if (fx) return `fx:${fx}`;
|
|
182
|
+
if (`sound:${name}` in KNOB_MAPS) return `sound:${name}`;
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The page's row; an unmapped effect gets none (the menu derives one). */
|
|
187
|
+
export function knobMap(page: string): KnobMap | undefined {
|
|
188
|
+
return KNOB_MAPS[page];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The pages `knobs` names, for help and completion. */
|
|
192
|
+
export const KNOB_PAGE_WORDS = Object.freeze([
|
|
193
|
+
"sound",
|
|
194
|
+
"mix",
|
|
195
|
+
"master",
|
|
196
|
+
"tempo",
|
|
197
|
+
"fx <effect>",
|
|
198
|
+
]);
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice › Clips and Lyrics, and Voice › voice presets >
|
|
3
|
+
* Vocal (0.7 clips lane). Every row runs a prompt command (`/clip`,
|
|
4
|
+
* `/lyrics`, `/vocal`), so the menu, the prompt and undo stay one path.
|
|
5
|
+
*/
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
CLIP_GAIN_MAX_DB,
|
|
9
|
+
CLIP_GAIN_MIN_DB,
|
|
10
|
+
clipGainDb,
|
|
11
|
+
lyricText,
|
|
12
|
+
} from "../../core/clips.ts";
|
|
13
|
+
import { SCORE_LIMITS, type AudioClip } from "../../core/score.ts";
|
|
14
|
+
import { userBarLabel, VOCAL_SETUPS } from "../commands/clips.ts";
|
|
15
|
+
import type { MenuContext, MenuNode } from "./menu.ts";
|
|
16
|
+
|
|
17
|
+
const round = (value: number, places = 2) => {
|
|
18
|
+
const scale = 10 ** places;
|
|
19
|
+
return Math.round(value * scale) / scale;
|
|
20
|
+
};
|
|
21
|
+
const dbText = (db: number) =>
|
|
22
|
+
Number.isFinite(db) ? `${db > 0 ? "+" : ""}${round(db, 1)} dB` : "-inf dB";
|
|
23
|
+
|
|
24
|
+
function clipDetail(context: MenuContext, clip: AudioClip): string {
|
|
25
|
+
return [
|
|
26
|
+
`bar ${userBarLabel(context.score, clip.startTick)}`,
|
|
27
|
+
clip.gain !== undefined ? dbText(clipGainDb(clip.gain)) : "",
|
|
28
|
+
clip.rev ? "rev" : "",
|
|
29
|
+
clip.mute ? "muted" : "",
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(" · ");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The rows for one clip: gain, fades, place, rev, mute, repeat, remove. */
|
|
36
|
+
function clipRows(context: MenuContext, id: string): MenuNode[] {
|
|
37
|
+
const track = context.score.tracks.find((t) => t.id === context.trackId);
|
|
38
|
+
const clip = track?.clips?.find((c) => c.id === id);
|
|
39
|
+
if (!clip) return [{ kind: "info", label: "clip", value: "removed" }];
|
|
40
|
+
const fadeIn = clip.fadeInTime ?? 0.005;
|
|
41
|
+
const fadeOut = clip.fadeTime ?? 0.005;
|
|
42
|
+
const fadeMax = SCORE_LIMITS.maxClipFadeSeconds;
|
|
43
|
+
const fadeStep = (value: number, direction: 1 | -1) =>
|
|
44
|
+
Math.max(
|
|
45
|
+
0,
|
|
46
|
+
Math.min(
|
|
47
|
+
fadeMax,
|
|
48
|
+
round(
|
|
49
|
+
value < 0.05 ? value + 0.005 * direction : value + 0.05 * direction,
|
|
50
|
+
3,
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
kind: "number",
|
|
57
|
+
label: "gain",
|
|
58
|
+
help: "clip gain in dB before the track's effects",
|
|
59
|
+
value: round(clipGainDb(clip.gain ?? 1), 1),
|
|
60
|
+
min: CLIP_GAIN_MIN_DB,
|
|
61
|
+
max: CLIP_GAIN_MAX_DB,
|
|
62
|
+
step: (value, direction) =>
|
|
63
|
+
Math.max(
|
|
64
|
+
CLIP_GAIN_MIN_DB,
|
|
65
|
+
Math.min(CLIP_GAIN_MAX_DB, round(value + direction, 1)),
|
|
66
|
+
),
|
|
67
|
+
format: dbText,
|
|
68
|
+
command: (value) => `/clip ${id} gain ${value}`,
|
|
69
|
+
reset: `/clip ${id} gain 0`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
kind: "number",
|
|
73
|
+
label: "fade in",
|
|
74
|
+
help: "equal-power fade-in, seconds",
|
|
75
|
+
value: fadeIn,
|
|
76
|
+
min: 0,
|
|
77
|
+
max: fadeMax,
|
|
78
|
+
step: fadeStep,
|
|
79
|
+
format: (value) => `${round(value, 3)} s`,
|
|
80
|
+
command: (value) => `/clip ${id} fade in ${value}`,
|
|
81
|
+
reset: `/clip ${id} fade in default`,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
kind: "number",
|
|
85
|
+
label: "fade out",
|
|
86
|
+
help: "equal-power fade-out, seconds",
|
|
87
|
+
value: fadeOut,
|
|
88
|
+
min: 0,
|
|
89
|
+
max: fadeMax,
|
|
90
|
+
step: fadeStep,
|
|
91
|
+
format: (value) => `${round(value, 3)} s`,
|
|
92
|
+
command: (value) => `/clip ${id} fade out ${value}`,
|
|
93
|
+
reset: `/clip ${id} fade out default`,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
kind: "number",
|
|
97
|
+
label: "start in file",
|
|
98
|
+
help: "seconds into the file where the clip starts playing",
|
|
99
|
+
value: clip.offset ?? 0,
|
|
100
|
+
min: 0,
|
|
101
|
+
max: SCORE_LIMITS.maxClipSeconds,
|
|
102
|
+
step: (value, direction) =>
|
|
103
|
+
Math.max(0, round(value + 0.1 * direction, 3)),
|
|
104
|
+
format: (value) => `${round(value, 3)} s`,
|
|
105
|
+
command: (value) => `/clip ${id} trim offset ${value}`,
|
|
106
|
+
reset: `/clip ${id} trim offset 0`,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
kind: "entry",
|
|
110
|
+
label: "length",
|
|
111
|
+
value: clip.dur === undefined ? "to file end" : `${round(clip.dur, 3)} s`,
|
|
112
|
+
help: "seconds of the file to play; `end` plays to the file's end",
|
|
113
|
+
placeholder: "seconds, e.g. 4.5, or end",
|
|
114
|
+
command: (text) => {
|
|
115
|
+
const value = text.trim().toLowerCase().replace(/s$/, "");
|
|
116
|
+
if (value === "end") return `/clip ${id} trim dur end`;
|
|
117
|
+
return Number(value) > 0 ? `/clip ${id} trim dur ${value}` : undefined;
|
|
118
|
+
},
|
|
119
|
+
example: `/clip ${id} trim dur 4.5`,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
kind: "toggle",
|
|
123
|
+
label: "reverse",
|
|
124
|
+
help: "play the clip backwards",
|
|
125
|
+
value: clip.rev === true,
|
|
126
|
+
command: () => `/clip ${id} rev`,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
kind: "toggle",
|
|
130
|
+
label: "mute",
|
|
131
|
+
help: "silence this clip only",
|
|
132
|
+
value: clip.mute === true,
|
|
133
|
+
command: () => `/clip ${id} mute`,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
kind: "entry",
|
|
137
|
+
label: "move to bar",
|
|
138
|
+
value: userBarLabel(context.score, clip.startTick),
|
|
139
|
+
help: "1-based bar, or bar.beat",
|
|
140
|
+
placeholder: "bar, e.g. 9 or 5.3",
|
|
141
|
+
command: (text) =>
|
|
142
|
+
text.trim() ? `/clip ${id} move ${text.trim()}` : undefined,
|
|
143
|
+
example: `/clip ${id} move 9`,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
kind: "entry",
|
|
147
|
+
label: "split at bar",
|
|
148
|
+
value: "",
|
|
149
|
+
help: "cut the clip in two at a bar (5 ms fades at the cut)",
|
|
150
|
+
placeholder: "bar, e.g. 7",
|
|
151
|
+
command: (text) =>
|
|
152
|
+
text.trim() ? `/clip ${id} split ${text.trim()}` : undefined,
|
|
153
|
+
example: `/clip ${id} split 7`,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
kind: "entry",
|
|
157
|
+
label: "repeat",
|
|
158
|
+
value: "",
|
|
159
|
+
help: "copies every N bars up to a bar: type `2 to 32`",
|
|
160
|
+
placeholder: "every N to bar",
|
|
161
|
+
command: (text) => {
|
|
162
|
+
const match = text
|
|
163
|
+
.trim()
|
|
164
|
+
.match(/^(?:every\s+)?(\d+(?:\.\d+)?)\s+(?:to\s+)?(\d+)$/i);
|
|
165
|
+
return match
|
|
166
|
+
? `/clip ${id} repeat every ${match[1]} to ${match[2]}`
|
|
167
|
+
: undefined;
|
|
168
|
+
},
|
|
169
|
+
example: `/clip ${id} repeat every 2 to 32`,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
kind: "info",
|
|
173
|
+
label: "file",
|
|
174
|
+
value: basename(clip.src),
|
|
175
|
+
help: clip.src,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
kind: "action",
|
|
179
|
+
label: "remove clip",
|
|
180
|
+
command: `/clip ${id} rm`,
|
|
181
|
+
help: "remove this clip (the file stays in the project; undo brings it back)",
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Voice rows from the clips lane: Clips and Lyrics. */
|
|
187
|
+
export function clipMenuRows(context: MenuContext): MenuNode[] {
|
|
188
|
+
const track = context.score.tracks.find((t) => t.id === context.trackId);
|
|
189
|
+
if (!track) return [];
|
|
190
|
+
const clips = track.clips ?? [];
|
|
191
|
+
const notes = context.score.notes.filter((n) => n.trackId === track.id);
|
|
192
|
+
const sung = lyricText(notes);
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
kind: "menu",
|
|
196
|
+
id: "voice:clips",
|
|
197
|
+
label: "clips",
|
|
198
|
+
detail: clips.length
|
|
199
|
+
? `${clips.length} clip${clips.length === 1 ? "" : "s"}`
|
|
200
|
+
: "import a vocal",
|
|
201
|
+
help: "audio clips on this track: import a file or the vocals stem, then edit",
|
|
202
|
+
build: (inner) => {
|
|
203
|
+
const now =
|
|
204
|
+
inner.score.tracks.find((t) => t.id === inner.trackId)?.clips ?? [];
|
|
205
|
+
return [
|
|
206
|
+
...now.map((clip): MenuNode => ({
|
|
207
|
+
kind: "menu",
|
|
208
|
+
id: `clip:${clip.id}`,
|
|
209
|
+
label: clip.id,
|
|
210
|
+
detail: clipDetail(inner, clip),
|
|
211
|
+
help: `${basename(clip.src)} · gain, fades, reverse, mute, move, split, repeat`,
|
|
212
|
+
build: (deeper) => clipRows(deeper, clip.id),
|
|
213
|
+
})),
|
|
214
|
+
{
|
|
215
|
+
kind: "entry",
|
|
216
|
+
label: "import a file",
|
|
217
|
+
value: "",
|
|
218
|
+
help: "copy an audio file into the track (48 kHz mono, peak -6 dBFS) at a bar",
|
|
219
|
+
placeholder: "<file> [bar]",
|
|
220
|
+
command: (text) =>
|
|
221
|
+
text.trim() ? `/vocal import ${text.trim()}` : undefined,
|
|
222
|
+
example: "/vocal import ~/take.wav 9",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
kind: "entry",
|
|
226
|
+
label: "vocals stem",
|
|
227
|
+
value: "",
|
|
228
|
+
help: "place the vocals stem from the last split_stems at a bar",
|
|
229
|
+
placeholder: "bar (default 1)",
|
|
230
|
+
command: (text) => `/vocal stem ${text.trim() || "1"}`,
|
|
231
|
+
example: "/vocal stem 1",
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
kind: "choice",
|
|
235
|
+
label: "setup",
|
|
236
|
+
value: "",
|
|
237
|
+
options: VOCAL_SETUPS.filter(
|
|
238
|
+
(setup) => !setup.needs?.includes("say"),
|
|
239
|
+
).map((setup) => setup.name),
|
|
240
|
+
help: "a vocal chain in one step (setups naming later tools say what is missing)",
|
|
241
|
+
command: (option) => `/vocal setups ${option}`,
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
kind: "menu",
|
|
248
|
+
id: "voice:lyrics",
|
|
249
|
+
label: "lyrics",
|
|
250
|
+
detail: sung ? sung.slice(0, 32) : "none",
|
|
251
|
+
help: "words on this track's notes: - splits syllables, _ holds, ~ skips",
|
|
252
|
+
build: (inner) => {
|
|
253
|
+
const text = lyricText(
|
|
254
|
+
inner.score.notes.filter((n) => n.trackId === inner.trackId),
|
|
255
|
+
);
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
kind: "entry",
|
|
259
|
+
label: "lyrics",
|
|
260
|
+
value: text,
|
|
261
|
+
help: "typed words are split into syllables onto the notes in order",
|
|
262
|
+
placeholder: "sun-lit morn-ing _ glow",
|
|
263
|
+
command: (typed) =>
|
|
264
|
+
typed.trim() ? `/lyrics ${typed.trim()}` : undefined,
|
|
265
|
+
example: "/lyrics sun-lit morn-ing",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
kind: "entry",
|
|
269
|
+
label: "from bar",
|
|
270
|
+
value: "",
|
|
271
|
+
help: "lyrics starting at a bar: type `9 la la la`",
|
|
272
|
+
placeholder: "<bar> <words>",
|
|
273
|
+
command: (typed) =>
|
|
274
|
+
/^\d+(?:\.\d+)?\s+\S/.test(typed.trim())
|
|
275
|
+
? `/lyrics ${typed.trim()}`
|
|
276
|
+
: undefined,
|
|
277
|
+
example: "/lyrics 9 la la la",
|
|
278
|
+
},
|
|
279
|
+
{ kind: "action", label: "clear lyrics", command: "/lyrics clear" },
|
|
280
|
+
];
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Voice › voice presets › vocal: the guide-note vocal track. */
|
|
287
|
+
export function vocalBrowseRows(_context: MenuContext): MenuNode[] {
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
290
|
+
kind: "action",
|
|
291
|
+
label:
|
|
292
|
+
"vocal clips sound, notes are a silent guide · hpf 90, 3:1, plate",
|
|
293
|
+
command: "instrument vocal",
|
|
294
|
+
help: "a vocal track: import a take or stem in Voice › clips",
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
}
|
package/src/tui/menu-time.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project › Tempo
|
|
3
|
-
* fermatas, and the focused track's rate, phase and
|
|
2
|
+
* Project › Tempo and meter: the song's tempo map, meter changes and
|
|
3
|
+
* fermatas, and the focused track's rate, phase and loop. Every row runs a
|
|
4
4
|
* prompt command from `src/commands/time.ts`, so the menu and the prompt
|
|
5
5
|
* stay one grammar.
|
|
6
6
|
*/
|
|
@@ -28,7 +28,7 @@ export function openingMeterCommand(score: TimeScore, value: number): string {
|
|
|
28
28
|
return unit === 4 ? `meter ${beats}` : `meter ${beats}/${unit}`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** One-line summary for the Project row and the Tempo
|
|
31
|
+
/** One-line summary for the Project row and the Tempo and meter row. */
|
|
32
32
|
export function tempoDetail(score: TimeScore): string {
|
|
33
33
|
const time = score.time;
|
|
34
34
|
const parts: string[] = [];
|
|
@@ -55,19 +55,24 @@ export function tempoDetail(score: TimeScore): string {
|
|
|
55
55
|
return parts.join(" · ");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/** The Tempo
|
|
58
|
+
/** The Tempo and meter submenu node for Project. */
|
|
59
59
|
export function tempoMenuNode(context: MenuContext): MenuNode {
|
|
60
60
|
return {
|
|
61
61
|
kind: "menu",
|
|
62
62
|
id: "tempo",
|
|
63
|
-
label: "tempo
|
|
63
|
+
label: "tempo and meter",
|
|
64
64
|
detail: tempoDetail(context.score),
|
|
65
65
|
help: "tempo changes, rit. and accel., fermatas, meter changes, track rate and phase",
|
|
66
66
|
build: tempoNodes,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
70
|
+
/** What x puts the song's tempo, meter and loop length back to. */
|
|
71
|
+
export const START_TEMPO_BPM = 120;
|
|
72
|
+
export const START_BEATS_PER_BAR = 4;
|
|
73
|
+
export const START_BARS = 8;
|
|
74
|
+
|
|
75
|
+
/** Rows of Project › Tempo and meter. */
|
|
71
76
|
export function tempoNodes(context: MenuContext): MenuNode[] {
|
|
72
77
|
const score = context.score;
|
|
73
78
|
const tpb = score.ticksPerBeat;
|
|
@@ -78,6 +83,7 @@ export function tempoNodes(context: MenuContext): MenuNode[] {
|
|
|
78
83
|
label: "start tempo",
|
|
79
84
|
help: "beats per minute at the top of the song",
|
|
80
85
|
value: score.tempoBpm,
|
|
86
|
+
start: START_TEMPO_BPM,
|
|
81
87
|
min: TIME_LIMITS.minBpm,
|
|
82
88
|
max: TIME_LIMITS.maxBpm,
|
|
83
89
|
step: stepBy(1, TIME_LIMITS.minBpm, TIME_LIMITS.maxBpm),
|
|
@@ -182,6 +188,7 @@ export function tempoNodes(context: MenuContext): MenuNode[] {
|
|
|
182
188
|
label: "beats per bar",
|
|
183
189
|
help: "the song's opening meter; meter changes below override it later",
|
|
184
190
|
value: score.beatsPerBar,
|
|
191
|
+
start: START_BEATS_PER_BAR,
|
|
185
192
|
min: 1,
|
|
186
193
|
max: TIME_LIMITS.maxBeatsPerBar,
|
|
187
194
|
step: stepBy(1, 1, TIME_LIMITS.maxBeatsPerBar),
|
|
@@ -223,7 +230,7 @@ export function tempoNodes(context: MenuContext): MenuNode[] {
|
|
|
223
230
|
id: "track-time",
|
|
224
231
|
label: `${track.name ?? track.id} time`,
|
|
225
232
|
detail: trackTimeDetail(context),
|
|
226
|
-
help: "the focused track's tempo ratio, phase and
|
|
233
|
+
help: "the focused track's tempo ratio, phase and loop (polytempo, polymeter, phasing)",
|
|
227
234
|
build: trackTimeNodes,
|
|
228
235
|
});
|
|
229
236
|
if (time?.tempo?.length)
|
|
@@ -278,7 +285,7 @@ function trackTimeDetail(context: MenuContext): string {
|
|
|
278
285
|
const parts: string[] = [];
|
|
279
286
|
if (time.rate !== undefined) parts.push(`rate ${fmt(time.rate, 4)}×`);
|
|
280
287
|
if (time.phase !== undefined) parts.push(`phase ${fmt(time.phase / tpb)}`);
|
|
281
|
-
if (time.cycle !== undefined) parts.push(`
|
|
288
|
+
if (time.cycle !== undefined) parts.push(`loop ${fmt(time.cycle / tpb)}`);
|
|
282
289
|
return parts.join(" · ");
|
|
283
290
|
}
|
|
284
291
|
|
|
@@ -320,8 +327,8 @@ export function trackTimeNodes(context: MenuContext): MenuNode[] {
|
|
|
320
327
|
},
|
|
321
328
|
{
|
|
322
329
|
kind: "number",
|
|
323
|
-
label: "
|
|
324
|
-
help: "beats before the track's
|
|
330
|
+
label: "loop",
|
|
331
|
+
help: "beats before the track's own loop repeats (polymeter) · x follows the song loop",
|
|
325
332
|
value: time?.cycle !== undefined ? time.cycle / tpb : undefined,
|
|
326
333
|
off: "song loop",
|
|
327
334
|
start: score.beatsPerBar,
|
|
@@ -329,8 +336,8 @@ export function trackTimeNodes(context: MenuContext): MenuNode[] {
|
|
|
329
336
|
max: TIME_LIMITS.maxTick / tpb,
|
|
330
337
|
step: stepBy(0.25, 0.25, TIME_LIMITS.maxTick / tpb),
|
|
331
338
|
format: (value) => `${fmt(value)} beat${value === 1 ? "" : "s"}`,
|
|
332
|
-
command: (value) => `track
|
|
333
|
-
reset: "track
|
|
339
|
+
command: (value) => `track loop ${fmt(value, 4)}`,
|
|
340
|
+
reset: "track loop off",
|
|
334
341
|
},
|
|
335
342
|
entry(
|
|
336
343
|
"phasing",
|
|
@@ -350,7 +357,7 @@ export function trackTimeNodes(context: MenuContext): MenuNode[] {
|
|
|
350
357
|
kind: "action",
|
|
351
358
|
label: "follow the song",
|
|
352
359
|
command: "track time off",
|
|
353
|
-
help: "clear rate, phase and
|
|
360
|
+
help: "clear rate, phase and loop",
|
|
354
361
|
},
|
|
355
362
|
];
|
|
356
363
|
}
|