@hraness/dawg 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- 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 +30 -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 +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -13
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +12 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- 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 +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +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 +160 -28
- 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 +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -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 +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -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 +616 -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/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1397 -253
- 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/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +759 -177
- 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 +47 -7
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +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 +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ctrl-k rows for the vocoder (0.7, vocoder.md §8): Effects › Voice ›
|
|
3
|
+
* Vocoder (Source picker, Preset, one row per VOCODER_PARAMS key in table
|
|
4
|
+
* order) and Voice › voice presets › Vocoder. Every row runs a
|
|
5
|
+
* `vocoder …` command, so it stages for the audition loop like any other.
|
|
6
|
+
*/
|
|
7
|
+
import type { NumberParam } from "../../core/params.ts";
|
|
8
|
+
import type { Track } from "../../core/score.ts";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_VOCODER_PRESET,
|
|
11
|
+
VOCODER_INSTRUMENT,
|
|
12
|
+
VOCODER_PARAMS,
|
|
13
|
+
VOCODER_PRESETS,
|
|
14
|
+
VOCODER_PRESET_NAMES,
|
|
15
|
+
resolveVocoder,
|
|
16
|
+
vocoderPresetOf,
|
|
17
|
+
type TrackVocoder,
|
|
18
|
+
} from "../../core/vocoder.ts";
|
|
19
|
+
import {
|
|
20
|
+
vocoderEntryLines,
|
|
21
|
+
trackHasAudio,
|
|
22
|
+
vocoderCandidates,
|
|
23
|
+
} from "../commands/vocoder.ts";
|
|
24
|
+
import { num, specStep, type MenuContext, type MenuNode } from "./menu.ts";
|
|
25
|
+
|
|
26
|
+
const NONE = "none";
|
|
27
|
+
|
|
28
|
+
function trackOf(context: MenuContext): Track | undefined {
|
|
29
|
+
return context.score.tracks.find((track) => track.id === context.trackId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One row per VOCODER_PARAMS key; x and `preset` return it to the preset. */
|
|
33
|
+
function paramRow(vocoder: TrackVocoder, name: string): MenuNode {
|
|
34
|
+
const spec = VOCODER_PARAMS[name]!;
|
|
35
|
+
const own = (vocoder as Record<string, unknown>)[name];
|
|
36
|
+
const preset = vocoderPresetOf(vocoder);
|
|
37
|
+
const fallback = (VOCODER_PRESETS[preset].values as Record<string, unknown>)[
|
|
38
|
+
name
|
|
39
|
+
];
|
|
40
|
+
const resolved = (resolveVocoder(vocoder) as Record<string, unknown>)[name];
|
|
41
|
+
if (spec.kind === "enum")
|
|
42
|
+
return {
|
|
43
|
+
kind: "choice",
|
|
44
|
+
label: name,
|
|
45
|
+
value: own === undefined ? "preset" : String(own),
|
|
46
|
+
options: ["preset", ...spec.values],
|
|
47
|
+
command: (option) =>
|
|
48
|
+
option === "preset"
|
|
49
|
+
? `vocoder ${name} reset`
|
|
50
|
+
: `vocoder ${name} ${option}`,
|
|
51
|
+
help: `${spec.doc} · preset ${String(resolved)}`,
|
|
52
|
+
};
|
|
53
|
+
if (spec.kind === "boolean")
|
|
54
|
+
return {
|
|
55
|
+
kind: "choice",
|
|
56
|
+
label: name,
|
|
57
|
+
value: own === undefined ? "preset" : own ? "on" : "off",
|
|
58
|
+
options: ["preset", "on", "off"],
|
|
59
|
+
command: (option) =>
|
|
60
|
+
option === "preset"
|
|
61
|
+
? `vocoder ${name} reset`
|
|
62
|
+
: `vocoder ${name} ${option}`,
|
|
63
|
+
help: `${spec.doc} · preset ${fallback ? "on" : "off"}`,
|
|
64
|
+
};
|
|
65
|
+
const number = spec as NumberParam;
|
|
66
|
+
// gate's preset value is "auto" (from the source's assets): show it as off.
|
|
67
|
+
const value = typeof resolved === "number" ? resolved : undefined;
|
|
68
|
+
return {
|
|
69
|
+
kind: "number",
|
|
70
|
+
label: name,
|
|
71
|
+
value,
|
|
72
|
+
min: number.min,
|
|
73
|
+
max: number.max,
|
|
74
|
+
step: specStep(number),
|
|
75
|
+
format: (v) => (number.unit ? `${num(v)} ${number.unit}` : num(v)),
|
|
76
|
+
command: (v) => `vocoder ${name} ${num(v)}`,
|
|
77
|
+
reset: `vocoder ${name} reset`,
|
|
78
|
+
...(value === undefined ? { off: "auto", start: number.min } : {}),
|
|
79
|
+
help: `${number.doc} · preset ${typeof fallback === "number" ? num(fallback) : String(resolved ?? "auto")} · x resets`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A source as the picker shows it: its name, or its id when names clash. */
|
|
84
|
+
function sourceLabel(track: Track, all: readonly Track[]): string {
|
|
85
|
+
const clash = all.some((t) => t.id !== track.id && t.name === track.name);
|
|
86
|
+
return clash || !track.name.trim() ? track.id : track.name;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `vocoder src <name>`, quoted when the name has spaces. */
|
|
90
|
+
function srcCommand(label: string): string {
|
|
91
|
+
return /\s/.test(label) ? `vocoder src "${label}"` : `vocoder src ${label}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function entryRows(): MenuNode[] {
|
|
95
|
+
return vocoderEntryLines().map((line, index) => ({
|
|
96
|
+
kind: "info",
|
|
97
|
+
label: index === 0 ? "no voice yet" : "",
|
|
98
|
+
value: line.trim(),
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The rows inside Effects › Voice › Vocoder for the focused track. */
|
|
103
|
+
export function vocoderRows(context: MenuContext): MenuNode[] {
|
|
104
|
+
const track = trackOf(context);
|
|
105
|
+
if (!track || track.instrument === "kit") return [];
|
|
106
|
+
const vocoder = track.vocoder;
|
|
107
|
+
const candidates = vocoderCandidates(context.score, track.id);
|
|
108
|
+
const all = context.score.tracks;
|
|
109
|
+
if (!vocoder) {
|
|
110
|
+
if (trackHasAudio(track))
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
kind: "action",
|
|
114
|
+
label: "vocode",
|
|
115
|
+
command: "vocoder",
|
|
116
|
+
help: "adds a vocoder track driven by this voice (follows the song's chords, or drones on the key's root); mutes it",
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
if (candidates.length === 0) return entryRows();
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
kind: "choice",
|
|
123
|
+
label: "source",
|
|
124
|
+
value: NONE,
|
|
125
|
+
options: [NONE, ...candidates.map((t) => sourceLabel(t, all))],
|
|
126
|
+
command: (option) =>
|
|
127
|
+
option === NONE ? "vocoder off" : srcCommand(option),
|
|
128
|
+
help: "the voice that shapes this track's sound",
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
const others = context.score.tracks.filter(
|
|
133
|
+
(t) => t.id !== track.id && t.instrument !== "kit",
|
|
134
|
+
);
|
|
135
|
+
const options = [
|
|
136
|
+
...candidates.map((t) => sourceLabel(t, all)),
|
|
137
|
+
...others
|
|
138
|
+
.filter((t) => !candidates.some((c) => c.id === t.id))
|
|
139
|
+
.map((t) => sourceLabel(t, all)),
|
|
140
|
+
];
|
|
141
|
+
const current = all.find((t) => t.id === vocoder.src);
|
|
142
|
+
const nodes: MenuNode[] = [
|
|
143
|
+
{
|
|
144
|
+
kind: "choice",
|
|
145
|
+
label: "source",
|
|
146
|
+
value: current ? sourceLabel(current, all) : (vocoder.src ?? NONE),
|
|
147
|
+
options: vocoder.src === undefined ? [NONE, ...options] : options,
|
|
148
|
+
command: (option) => (option === NONE ? "vocoder" : srcCommand(option)),
|
|
149
|
+
help: "the modulator; it is heard through the vocoder even when muted",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
kind: "choice",
|
|
153
|
+
label: "preset",
|
|
154
|
+
value: vocoder.preset ?? DEFAULT_VOCODER_PRESET,
|
|
155
|
+
options: VOCODER_PRESET_NAMES,
|
|
156
|
+
command: (option) => `vocoder preset ${option}`,
|
|
157
|
+
help: "classic robot talkbox choir glass whisper smear lofi · keeps your overrides",
|
|
158
|
+
},
|
|
159
|
+
...Object.keys(VOCODER_PARAMS).map((name) => paramRow(vocoder, name)),
|
|
160
|
+
];
|
|
161
|
+
if (Object.keys(vocoder).some((key) => key !== "preset" && key !== "src"))
|
|
162
|
+
nodes.push({
|
|
163
|
+
kind: "action",
|
|
164
|
+
label: "reset to preset",
|
|
165
|
+
command: "vocoder reset",
|
|
166
|
+
help: "clear overrides, keep the source and preset",
|
|
167
|
+
});
|
|
168
|
+
nodes.push({
|
|
169
|
+
kind: "action",
|
|
170
|
+
label: "remove the vocoder",
|
|
171
|
+
command: "vocoder off",
|
|
172
|
+
help: "the track plays its own sound again",
|
|
173
|
+
});
|
|
174
|
+
return nodes;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Effects › Voice › Vocoder. */
|
|
178
|
+
export function vocoderEffectNode(context: MenuContext): MenuNode[] {
|
|
179
|
+
const track = trackOf(context);
|
|
180
|
+
if (!track || track.instrument === "kit") return [];
|
|
181
|
+
const vocoder = track.vocoder;
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
kind: "menu",
|
|
185
|
+
id: "voice:vocoder",
|
|
186
|
+
label: "vocoder",
|
|
187
|
+
detail: vocoder
|
|
188
|
+
? `${vocoder.preset ?? DEFAULT_VOCODER_PRESET}${vocoder.src ? ` · from ${vocoder.src}` : " · no source"}`
|
|
189
|
+
: trackHasAudio(track)
|
|
190
|
+
? "vocode"
|
|
191
|
+
: "off",
|
|
192
|
+
help: "a voice shapes this track's sound: robot, talkbox, choir",
|
|
193
|
+
build: vocoderRows,
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Voice › voice presets › Vocoder: the built-in carrier. */
|
|
199
|
+
export function vocoderBrowseNode(context: MenuContext): MenuNode[] {
|
|
200
|
+
const track = trackOf(context);
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
kind: "menu",
|
|
204
|
+
id: "voices:vocoder",
|
|
205
|
+
label: "vocoder",
|
|
206
|
+
detail: `${VOCODER_PRESET_NAMES.length} presets · robot, talkbox, choir …`,
|
|
207
|
+
help: "a built-in synth carrier spoken through a voice track",
|
|
208
|
+
build: (inner) => {
|
|
209
|
+
const here = trackOf(inner) ?? track;
|
|
210
|
+
if (here && trackHasAudio(here))
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
kind: "action",
|
|
214
|
+
label: "vocode",
|
|
215
|
+
command: "vocoder",
|
|
216
|
+
help: "adds a vocoder track driven by this voice",
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
if (here?.instrument !== VOCODER_INSTRUMENT)
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
kind: "action",
|
|
223
|
+
label: "make a vocoder",
|
|
224
|
+
command: `instrument ${VOCODER_INSTRUMENT}`,
|
|
225
|
+
help: "a supersaw carrier; then pick its presets and source here",
|
|
226
|
+
},
|
|
227
|
+
...(vocoderCandidates(inner.score, inner.trackId).length === 0
|
|
228
|
+
? entryRows()
|
|
229
|
+
: []),
|
|
230
|
+
];
|
|
231
|
+
return VOCODER_PRESET_NAMES.map((name): MenuNode => ({
|
|
232
|
+
kind: "action",
|
|
233
|
+
label: `${name.padEnd(8)} ${VOCODER_PRESETS[name].doc}`,
|
|
234
|
+
command: `vocoder ${name}`,
|
|
235
|
+
help: VOCODER_PRESETS[name].doc,
|
|
236
|
+
})).concat(
|
|
237
|
+
vocoderCandidates(inner.score, inner.trackId).length === 0
|
|
238
|
+
? entryRows()
|
|
239
|
+
: vocoderRows(inner).filter((row) => row.label === "source"),
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
}
|
package/src/tui/wind-menu.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ctrl-k menu rows for the wind engine (0.6.1): the "Winds and brass" group
|
|
3
|
-
* under Sound ›
|
|
3
|
+
* under Sound › instruments, and the WIND_PARAMS rows under Sound ›
|
|
4
4
|
* Parameters on a wind-engine track. Every row runs a `wind …` command.
|
|
5
5
|
*/
|
|
6
6
|
import type { EnumParam, NumberParam } from "../../core/params.ts";
|
|
@@ -27,12 +27,12 @@ function presetRow(name: WindPresetName): MenuNode {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** Sound ›
|
|
30
|
+
/** Sound › instruments › winds and brass: flutes, reeds, brass. */
|
|
31
31
|
export function windsMenu(): MenuNode {
|
|
32
32
|
return {
|
|
33
33
|
kind: "menu",
|
|
34
34
|
id: "winds",
|
|
35
|
-
label: "
|
|
35
|
+
label: "winds and brass",
|
|
36
36
|
detail: `${WIND_PRESET_NAMES.length} blown · flute, sax, trumpet …`,
|
|
37
37
|
help: "breath-driven waveguides: flutes, reeds and brass; wind players 4 for a section",
|
|
38
38
|
build: () =>
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package version, read from package.json by a static JSON import so
|
|
3
|
+
* `bun build --compile` embeds it: a runtime read relative to
|
|
4
|
+
* import.meta.url finds nothing inside a compiled binary.
|
|
5
|
+
*/
|
|
6
|
+
import pkg from "../package.json" with { type: "json" };
|
|
7
|
+
|
|
8
|
+
export const VERSION: string = pkg.version;
|
package/src/web/fetch.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
9
9
|
import { isIP } from "node:net";
|
|
10
|
+
import { checkServerIdentity, type PeerCertificate } from "node:tls";
|
|
10
11
|
import {
|
|
11
12
|
decodeEntities,
|
|
12
13
|
describeFetchError,
|
|
@@ -81,26 +82,64 @@ function isPrivateV4(address: string): boolean {
|
|
|
81
82
|
);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
/** Expand an IPv6 address (zone stripped, embedded dotted IPv4 allowed) to 8 groups. */
|
|
86
|
+
function expandV6(address: string): number[] | undefined {
|
|
87
|
+
let text = address.toLowerCase().split("%")[0]!;
|
|
88
|
+
const dotted = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text);
|
|
89
|
+
if (dotted) {
|
|
90
|
+
const [a, b, c, d] = dotted.slice(1).map(Number) as [
|
|
91
|
+
number,
|
|
92
|
+
number,
|
|
93
|
+
number,
|
|
94
|
+
number,
|
|
95
|
+
];
|
|
96
|
+
text = `${text.slice(0, dotted.index)}${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;
|
|
96
97
|
}
|
|
97
|
-
const
|
|
98
|
+
const halves = text.split("::");
|
|
99
|
+
if (halves.length > 2) return undefined;
|
|
100
|
+
const parse = (part: string) =>
|
|
101
|
+
part === ""
|
|
102
|
+
? []
|
|
103
|
+
: part.split(":").map((group) => Number.parseInt(group, 16));
|
|
104
|
+
const head = parse(halves[0]!);
|
|
105
|
+
const tail = halves.length === 2 ? parse(halves[1]!) : [];
|
|
106
|
+
const fill = 8 - head.length - tail.length;
|
|
107
|
+
if (halves.length === 1 ? fill !== 0 : fill < 1) return undefined;
|
|
108
|
+
const groups = [
|
|
109
|
+
...head,
|
|
110
|
+
...new Array<number>(halves.length === 2 ? fill : 0).fill(0),
|
|
111
|
+
...tail,
|
|
112
|
+
];
|
|
113
|
+
return groups.every((g) => Number.isInteger(g) && g >= 0 && g <= 0xffff)
|
|
114
|
+
? groups
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function v4From(high: number, low: number): string {
|
|
119
|
+
return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isPrivateV6(address: string): boolean {
|
|
123
|
+
const g = expandV6(address);
|
|
124
|
+
if (!g) return true;
|
|
125
|
+
const zero = (from: number, to: number) =>
|
|
126
|
+
g.slice(from, to).every((x) => x === 0);
|
|
127
|
+
// ::/96 covers :: and ::1 and the deprecated IPv4-compatible form (::a.b.c.d).
|
|
128
|
+
if (zero(0, 6)) return true;
|
|
129
|
+
// ::ffff:0:0/96 IPv4-mapped: as private as the address it carries.
|
|
130
|
+
if (zero(0, 5) && g[5] === 0xffff) return isPrivateV4(v4From(g[6]!, g[7]!));
|
|
131
|
+
// 2002::/16 6to4 embeds an IPv4 address in bits 16..47.
|
|
132
|
+
if (g[0] === 0x2002) return isPrivateV4(v4From(g[1]!, g[2]!));
|
|
133
|
+
const first = g[0]!;
|
|
98
134
|
return (
|
|
135
|
+
(first === 0x64 && g[1] === 0xff9b) || // 64:ff9b::/96 and 64:ff9b:1::/48 NAT64
|
|
136
|
+
(first === 0x100 && zero(1, 4)) || // 100::/64 discard
|
|
137
|
+
(first === 0x2001 && g[1] === 0) || // 2001::/32 Teredo (obfuscated IPv4)
|
|
138
|
+
(first === 0x2001 && g[1] === 0xdb8) || // 2001:db8::/32 documentation
|
|
99
139
|
(first & 0xfe00) === 0xfc00 || // fc00::/7 unique local
|
|
100
140
|
(first & 0xffc0) === 0xfe80 || // fe80::/10 link-local
|
|
101
|
-
(first &
|
|
102
|
-
|
|
103
|
-
lower.startsWith("2001:db8:") // documentation
|
|
141
|
+
(first & 0xffc0) === 0xfec0 || // fec0::/10 deprecated site-local
|
|
142
|
+
(first & 0xff00) === 0xff00 // ff00::/8 multicast
|
|
104
143
|
);
|
|
105
144
|
}
|
|
106
145
|
|
|
@@ -136,12 +175,19 @@ export function admitUrl(raw: unknown): URL {
|
|
|
136
175
|
return url;
|
|
137
176
|
}
|
|
138
177
|
|
|
139
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Check that a URL's host is public and return the address to connect to.
|
|
180
|
+
* Every resolved address must be public; the first one is returned so the
|
|
181
|
+
* request connects to exactly the address that was checked, never to a
|
|
182
|
+
* second, independent DNS answer (which a rebinding domain could point at
|
|
183
|
+
* loopback or a metadata service).
|
|
184
|
+
*/
|
|
185
|
+
async function assertPublicHost(url: URL, lookup: Lookup): Promise<string> {
|
|
140
186
|
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
141
187
|
if (isIP(host)) {
|
|
142
188
|
if (isPrivateAddress(host))
|
|
143
189
|
throw new WebError(`${host} is a private or local address`);
|
|
144
|
-
return;
|
|
190
|
+
return host;
|
|
145
191
|
}
|
|
146
192
|
let addresses: readonly string[];
|
|
147
193
|
try {
|
|
@@ -153,6 +199,42 @@ async function assertPublicHost(url: URL, lookup: Lookup): Promise<void> {
|
|
|
153
199
|
for (const address of addresses)
|
|
154
200
|
if (isPrivateAddress(address))
|
|
155
201
|
throw new WebError(`${host} resolves to a private or local address`);
|
|
202
|
+
return addresses[0]!;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Bun's fetch accepts TLS options beyond the standard RequestInit. */
|
|
206
|
+
type PinnedInit = RequestInit & {
|
|
207
|
+
tls?: {
|
|
208
|
+
serverName?: string;
|
|
209
|
+
checkServerIdentity?: (
|
|
210
|
+
hostname: string,
|
|
211
|
+
cert: PeerCertificate,
|
|
212
|
+
) => Error | undefined;
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Address a request to a checked IP while keeping the original name for the
|
|
218
|
+
* Host header and for TLS (SNI and certificate verification), so the
|
|
219
|
+
* connection cannot be redirected by a later DNS answer.
|
|
220
|
+
*/
|
|
221
|
+
export function pinRequest(
|
|
222
|
+
url: URL,
|
|
223
|
+
address: string,
|
|
224
|
+
): { target: string; host: string; tls?: PinnedInit["tls"] } {
|
|
225
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
226
|
+
const pinned = new URL(url.toString());
|
|
227
|
+
pinned.hostname = isIP(address) === 6 ? `[${address}]` : address;
|
|
228
|
+
const result: { target: string; host: string; tls?: PinnedInit["tls"] } = {
|
|
229
|
+
target: pinned.toString(),
|
|
230
|
+
host: url.host,
|
|
231
|
+
};
|
|
232
|
+
if (url.protocol === "https:" && !isIP(hostname))
|
|
233
|
+
result.tls = {
|
|
234
|
+
serverName: hostname,
|
|
235
|
+
checkServerIdentity: (_name, cert) => checkServerIdentity(hostname, cert),
|
|
236
|
+
};
|
|
237
|
+
return result;
|
|
156
238
|
}
|
|
157
239
|
|
|
158
240
|
export async function fetchUrl(
|
|
@@ -168,18 +250,22 @@ export async function fetchUrl(
|
|
|
168
250
|
let url = admitUrl(raw);
|
|
169
251
|
let response: Response | undefined;
|
|
170
252
|
for (let hop = 0; hop <= FETCH_LIMITS.maxRedirects; hop += 1) {
|
|
171
|
-
await assertPublicHost(url, lookup);
|
|
253
|
+
const address = await assertPublicHost(url, lookup);
|
|
254
|
+
const pin = pinRequest(url, address);
|
|
255
|
+
const init: PinnedInit = {
|
|
256
|
+
method: "GET",
|
|
257
|
+
redirect: "manual",
|
|
258
|
+
headers: {
|
|
259
|
+
host: pin.host,
|
|
260
|
+
accept:
|
|
261
|
+
"text/html,application/xhtml+xml,text/plain;q=0.9,application/json;q=0.8,*/*;q=0.5",
|
|
262
|
+
"user-agent": "Mozilla/5.0 (compatible; dawg-agent)",
|
|
263
|
+
},
|
|
264
|
+
signal: timer.signal,
|
|
265
|
+
};
|
|
266
|
+
if (pin.tls) init.tls = pin.tls;
|
|
172
267
|
try {
|
|
173
|
-
response = await fetcher(
|
|
174
|
-
method: "GET",
|
|
175
|
-
redirect: "manual",
|
|
176
|
-
headers: {
|
|
177
|
-
accept:
|
|
178
|
-
"text/html,application/xhtml+xml,text/plain;q=0.9,application/json;q=0.8,*/*;q=0.5",
|
|
179
|
-
"user-agent": "Mozilla/5.0 (compatible; dawg-agent)",
|
|
180
|
-
},
|
|
181
|
-
signal: timer.signal,
|
|
182
|
-
});
|
|
268
|
+
response = await fetcher(pin.target, init);
|
|
183
269
|
} catch (error) {
|
|
184
270
|
throw new WebError(
|
|
185
271
|
`fetch failed: ${timer.timedOut() ? `timed out after ${timeoutMs / 1000}s` : describeFetchError(error)}`,
|