@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
package/core/keys.ts
CHANGED
|
@@ -57,15 +57,15 @@ export function isElectricFamily(
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Why una corda would not sound on this track (0.6.1), or undefined when it
|
|
60
|
-
* does: only the
|
|
60
|
+
* does: only the modeled pianos with a `keys` object read `softPedal`.
|
|
61
61
|
*/
|
|
62
62
|
export function softPedalNote(
|
|
63
63
|
track: Readonly<{ id: string; instrument: string; keys?: unknown }>,
|
|
64
64
|
): string | undefined {
|
|
65
65
|
if (!isPianoFamily(track.instrument))
|
|
66
|
-
return `${track.id} is ${track.instrument}; una corda acts on the
|
|
66
|
+
return `${track.id} is ${track.instrument}; una corda acts on the modeled pianos (${PIANO_FAMILIES.join(" ")})`;
|
|
67
67
|
if (track.keys === undefined)
|
|
68
|
-
return `${track.id} has no
|
|
68
|
+
return `${track.id} has no modeled piano yet; keys preset ${track.instrument} first`;
|
|
69
69
|
return undefined;
|
|
70
70
|
}
|
|
71
71
|
|
package/core/loop.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type Track,
|
|
11
11
|
} from "./score.ts";
|
|
12
12
|
import type { SongMaster } from "./master.ts";
|
|
13
|
+
import type { SongStyle } from "./style-provenance.ts";
|
|
13
14
|
import type { Tuning } from "./tuning.ts";
|
|
14
15
|
|
|
15
16
|
export const LOOP_FORMAT = "track.loop/v1" as const;
|
|
@@ -30,6 +31,8 @@ export type TrackLoopV1 = Readonly<{
|
|
|
30
31
|
notes: readonly Note[];
|
|
31
32
|
/** Song master (dawg 0.5); absent means none. */
|
|
32
33
|
master?: SongMaster;
|
|
34
|
+
/** Which style and seed made the song (quality-08); absent means none. */
|
|
35
|
+
style?: SongStyle;
|
|
33
36
|
/** Song sections (0.5); omitted when the song has none. */
|
|
34
37
|
sections?: readonly Section[];
|
|
35
38
|
/** Song form (0.5); omitted when empty. */
|
|
@@ -53,11 +56,13 @@ export function encodeLoopDocument(score: TrackScore): TrackLoopV1 {
|
|
|
53
56
|
tracks: score.tracks,
|
|
54
57
|
notes: score.notes,
|
|
55
58
|
...(score.master ? { master: score.master } : {}),
|
|
59
|
+
...(score.style ? { style: score.style } : {}),
|
|
56
60
|
...(score.sections.length > 0 ? { sections: score.sections } : {}),
|
|
57
61
|
...(score.form.length > 0 ? { form: score.form } : {}),
|
|
58
62
|
...(score.loopSection === undefined
|
|
59
63
|
? {}
|
|
60
64
|
: { loopSection: score.loopSection }),
|
|
65
|
+
...(score.calibration ? { calibration: score.calibration } : {}),
|
|
61
66
|
});
|
|
62
67
|
}
|
|
63
68
|
|
package/core/lyrics.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lyric grammar and syllable assignment (0.7) shared by `/lyrics`,
|
|
3
|
+
* `set_lyrics` and the SDK's `lyrics()`. No imports: the SDK carries a copy
|
|
4
|
+
* between marker lines (`bun core/sdk/sync-lyrics.ts`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ---- lyrics (copied into core/sdk/v1.ts)
|
|
8
|
+
|
|
9
|
+
/** Longest lyric on one note (SCORE_LIMITS.maxLyricLength). */
|
|
10
|
+
export const LYRIC_LIMIT = 32;
|
|
11
|
+
|
|
12
|
+
/** One lyric token: a syllable, a held note (`_`) or a skipped note (`~`). */
|
|
13
|
+
export type LyricToken = Readonly<{
|
|
14
|
+
syl: string;
|
|
15
|
+
/** Index of the word the token belongs to. */
|
|
16
|
+
word: number;
|
|
17
|
+
/** First syllable of its word. */
|
|
18
|
+
first: boolean;
|
|
19
|
+
kind: "syl" | "hold" | "rest";
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The lyric grammar: words split by spaces, syllables by `-`, `_` holds the
|
|
24
|
+
* previous syllable over the next note (melisma), `~` skips a note.
|
|
25
|
+
* "sun-lit morn-ing _ glow" gives sun lit morn ing _ glow.
|
|
26
|
+
*/
|
|
27
|
+
export function parseLyric(text: string): LyricToken[] {
|
|
28
|
+
const out: LyricToken[] = [];
|
|
29
|
+
let word = -1;
|
|
30
|
+
for (const raw of text.trim().split(/\s+/u)) {
|
|
31
|
+
if (raw === "") continue;
|
|
32
|
+
if (raw === "_") out.push({ syl: "_", word, first: false, kind: "hold" });
|
|
33
|
+
else if (raw === "~")
|
|
34
|
+
out.push({ syl: "~", word, first: false, kind: "rest" });
|
|
35
|
+
else {
|
|
36
|
+
word += 1;
|
|
37
|
+
raw
|
|
38
|
+
.split("-")
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.forEach((syl, index) =>
|
|
41
|
+
out.push({ syl, word, first: index === 0, kind: "syl" }),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const VOWEL = /[aeiouàáâäèéêëìíîïòóôöùúûü]/u;
|
|
49
|
+
/** Consonant pairs that sound as one consonant and are never split. */
|
|
50
|
+
const SYL_DIGRAPHS = new Set(["th", "sh", "ch", "ph", "wh", "ng", "ck", "gh"]);
|
|
51
|
+
/** Digraphs that end a syllable (no English word starts with them). */
|
|
52
|
+
const SYL_CODA_ONLY = new Set(["ng", "ck", "gh", "x"]);
|
|
53
|
+
/** Consonant clusters a syllable may start with (maximal onset). */
|
|
54
|
+
const SYL_ONSETS = new Set(
|
|
55
|
+
(
|
|
56
|
+
"bl br cl cr dr fl fr gl gr pl pr sc sk sl sm sn sp st sw tr tw dw " +
|
|
57
|
+
"thr shr chr phr phl spl spr str scr squ skr"
|
|
58
|
+
).split(" "),
|
|
59
|
+
);
|
|
60
|
+
/** Common words ending in a silent `e` that start compounds (some-thing). */
|
|
61
|
+
const SYL_SILENT_E_HEADS = (
|
|
62
|
+
"some home life time love fire side care where there here more one " +
|
|
63
|
+
"make lone like name game base wide grace face place space stone bone"
|
|
64
|
+
).split(" ");
|
|
65
|
+
/** Suffixes kept whole after a silent `e` (love-ly, care-ful). */
|
|
66
|
+
const SYL_SUFFIXES = ["ly", "ful", "less", "ness", "ment"];
|
|
67
|
+
/** Unstressed endings that close a short vowel before them (nev-er). */
|
|
68
|
+
const SYL_CLOSING_ENDINGS = new Set([
|
|
69
|
+
"er",
|
|
70
|
+
"en",
|
|
71
|
+
"el",
|
|
72
|
+
"et",
|
|
73
|
+
"ed",
|
|
74
|
+
"es",
|
|
75
|
+
"est",
|
|
76
|
+
"ing",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Syllables of a word typed without hyphens: a guess for English, which a
|
|
81
|
+
* hyphen always overrides (`nev-er`). Each run of vowels (and `y` after a
|
|
82
|
+
* consonant) is one syllable; a final silent `e` does not count, but a
|
|
83
|
+
* consonant plus `le` is its own syllable (lit-tle, ta-ble). Consonant
|
|
84
|
+
* pairs that sound as one (th sh ch ph wh ng ck gh) never split. Between
|
|
85
|
+
* vowels a cluster gives the next syllable the longest onset English
|
|
86
|
+
* allows (mon-ster, chil-dren); one consonant goes with the next vowel
|
|
87
|
+
* (ba-by, to-night) unless the previous vowel is short before an
|
|
88
|
+
* unstressed ending (nev-er, sing-ing). "something" gives some thing,
|
|
89
|
+
* "forever" for ev er.
|
|
90
|
+
*/
|
|
91
|
+
export function autoSyllabify(word: string): string[] {
|
|
92
|
+
const w = word.toLowerCase();
|
|
93
|
+
// Compounds and suffixes after a silent e: some-thing, love-ly.
|
|
94
|
+
if (w.length >= 6) {
|
|
95
|
+
for (const head of SYL_SILENT_E_HEADS)
|
|
96
|
+
if (w.startsWith(head) && VOWEL.test(w.slice(head.length)))
|
|
97
|
+
return [
|
|
98
|
+
word.slice(0, head.length),
|
|
99
|
+
...autoSyllabify(word.slice(head.length)),
|
|
100
|
+
];
|
|
101
|
+
for (const suffix of SYL_SUFFIXES) {
|
|
102
|
+
const stem = w.slice(0, -suffix.length);
|
|
103
|
+
if (
|
|
104
|
+
w.endsWith(suffix) &&
|
|
105
|
+
stem.length >= 3 &&
|
|
106
|
+
stem.endsWith("e") &&
|
|
107
|
+
!VOWEL.test(stem[stem.length - 2]!)
|
|
108
|
+
)
|
|
109
|
+
return [
|
|
110
|
+
...autoSyllabify(word.slice(0, stem.length)),
|
|
111
|
+
word.slice(stem.length),
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Letters into units: a vowel, a consonant, a digraph, or `qu`.
|
|
116
|
+
type Unit = { at: number; text: string; vowel: boolean };
|
|
117
|
+
const units: Unit[] = [];
|
|
118
|
+
for (let i = 0; i < w.length;) {
|
|
119
|
+
const pair = w.slice(i, i + 2);
|
|
120
|
+
if (pair === "qu" || SYL_DIGRAPHS.has(pair)) {
|
|
121
|
+
units.push({ at: i, text: pair, vowel: false });
|
|
122
|
+
i += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const ch = w[i]!;
|
|
126
|
+
const prev = units.at(-1);
|
|
127
|
+
const vowel =
|
|
128
|
+
VOWEL.test(ch) || (ch === "y" && prev !== undefined && !prev.vowel);
|
|
129
|
+
units.push({ at: i, text: ch, vowel });
|
|
130
|
+
i += 1;
|
|
131
|
+
}
|
|
132
|
+
// Vowel groups as [first unit, last unit].
|
|
133
|
+
const groups: [number, number][] = [];
|
|
134
|
+
for (let u = 0; u < units.length;) {
|
|
135
|
+
if (units[u]!.vowel) {
|
|
136
|
+
let v = u;
|
|
137
|
+
while (v + 1 < units.length && units[v + 1]!.vowel) v += 1;
|
|
138
|
+
groups.push([u, v]);
|
|
139
|
+
u = v + 1;
|
|
140
|
+
} else u += 1;
|
|
141
|
+
}
|
|
142
|
+
const lastUnit = units.length - 1;
|
|
143
|
+
const finalLe =
|
|
144
|
+
w.endsWith("le") &&
|
|
145
|
+
units.length >= 3 &&
|
|
146
|
+
units[lastUnit - 1]!.text === "l" &&
|
|
147
|
+
!units[lastUnit - 2]!.vowel;
|
|
148
|
+
const last = groups.at(-1);
|
|
149
|
+
if (
|
|
150
|
+
groups.length > 1 &&
|
|
151
|
+
last &&
|
|
152
|
+
last[0] === lastUnit &&
|
|
153
|
+
last[1] === lastUnit &&
|
|
154
|
+
units[lastUnit]!.text === "e" &&
|
|
155
|
+
!units[lastUnit - 1]!.vowel &&
|
|
156
|
+
!finalLe
|
|
157
|
+
)
|
|
158
|
+
groups.pop();
|
|
159
|
+
if (groups.length <= 1) return [word];
|
|
160
|
+
const cuts: number[] = [];
|
|
161
|
+
for (let g = 1; g < groups.length; g += 1) {
|
|
162
|
+
const prev = groups[g - 1]!;
|
|
163
|
+
const next = groups[g]!;
|
|
164
|
+
const cluster = units.slice(prev[1] + 1, next[0]);
|
|
165
|
+
const n = cluster.length;
|
|
166
|
+
const isLast = g === groups.length - 1;
|
|
167
|
+
let onset: number; // units of the cluster that start the next syllable
|
|
168
|
+
if (isLast && finalLe && n >= 2)
|
|
169
|
+
onset = cluster[n - 2]!.text === "ck" ? 1 : 2;
|
|
170
|
+
else if (n === 1) {
|
|
171
|
+
const unit = cluster[0]!.text;
|
|
172
|
+
const prevText = units
|
|
173
|
+
.slice(prev[0], prev[1] + 1)
|
|
174
|
+
.map((u) => u.text)
|
|
175
|
+
.join("");
|
|
176
|
+
const ending = w.slice(units[next[0]]!.at);
|
|
177
|
+
const short = prevText.length === 1 && "eiou".includes(prevText);
|
|
178
|
+
const closes =
|
|
179
|
+
SYL_CODA_ONLY.has(unit) ||
|
|
180
|
+
(short && isLast && SYL_CLOSING_ENDINGS.has(ending)) ||
|
|
181
|
+
(unit === "r" && short && units[next[0]]!.text === "e");
|
|
182
|
+
onset = closes ? 0 : 1;
|
|
183
|
+
} else {
|
|
184
|
+
onset = 1;
|
|
185
|
+
for (let k = n - 1; k >= 2; k -= 1)
|
|
186
|
+
if (
|
|
187
|
+
SYL_ONSETS.has(
|
|
188
|
+
cluster
|
|
189
|
+
.slice(n - k)
|
|
190
|
+
.map((u) => u.text)
|
|
191
|
+
.join(""),
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
onset = k;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (SYL_CODA_ONLY.has(cluster[n - 1]!.text)) onset = 0;
|
|
198
|
+
}
|
|
199
|
+
const first = units[next[0] - onset]!;
|
|
200
|
+
cuts.push(onset === 0 ? units[next[0]]!.at : first.at);
|
|
201
|
+
}
|
|
202
|
+
const out: string[] = [];
|
|
203
|
+
let at = 0;
|
|
204
|
+
for (const cut of cuts) {
|
|
205
|
+
out.push(word.slice(at, cut));
|
|
206
|
+
at = cut;
|
|
207
|
+
}
|
|
208
|
+
out.push(word.slice(at));
|
|
209
|
+
return out.filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** What `assignLyrics` put on each note, and what did not fit. */
|
|
213
|
+
export type LyricAssignment = Readonly<{
|
|
214
|
+
/** Note id to its lyric (`_` holds); notes `~` skipped are absent. */
|
|
215
|
+
lyrics: ReadonlyMap<string, string>;
|
|
216
|
+
/** Syllables left over after the last note. */
|
|
217
|
+
dropped: readonly string[];
|
|
218
|
+
/** Words split automatically. */
|
|
219
|
+
split: readonly string[];
|
|
220
|
+
}>;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Lyrics onto `notes` in time order. Hyphens split syllables as typed;
|
|
224
|
+
* when the text has fewer syllables than there are notes, words typed
|
|
225
|
+
* whole are split by `autoSyllabify`, and any notes still left hold the
|
|
226
|
+
* last syllable (melisma) instead of failing. Syllables past the last note
|
|
227
|
+
* are reported in `dropped`. Notes sharing an onset take one token, on
|
|
228
|
+
* the top note, with `_` on the others. Each lyric is cut to the 32-character limit.
|
|
229
|
+
*/
|
|
230
|
+
export function assignLyrics(
|
|
231
|
+
text: string,
|
|
232
|
+
notes: readonly Readonly<{ id: string; startTick: number; pitch: number }>[],
|
|
233
|
+
): LyricAssignment {
|
|
234
|
+
const sorted = [...notes].sort(
|
|
235
|
+
(a, b) => a.startTick - b.startTick || b.pitch - a.pitch,
|
|
236
|
+
);
|
|
237
|
+
// Notes sharing an onset (a chord or a doubled note) take one token: the
|
|
238
|
+
// top note carries it and the rest hold.
|
|
239
|
+
const ordered: (typeof sorted)[number][] = [];
|
|
240
|
+
const under = new Map<string, string[]>();
|
|
241
|
+
for (const note of sorted) {
|
|
242
|
+
const top = ordered.at(-1);
|
|
243
|
+
if (top && top.startTick === note.startTick)
|
|
244
|
+
under.get(top.id)!.push(note.id);
|
|
245
|
+
else {
|
|
246
|
+
ordered.push(note);
|
|
247
|
+
under.set(note.id, []);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
let tokens = parseLyric(text);
|
|
251
|
+
const split: string[] = [];
|
|
252
|
+
if (tokens.length < ordered.length) {
|
|
253
|
+
// Split every word typed whole; keep the split only if it still fits.
|
|
254
|
+
const out: LyricToken[] = [];
|
|
255
|
+
const words: string[] = [];
|
|
256
|
+
for (const token of tokens) {
|
|
257
|
+
const whole =
|
|
258
|
+
token.kind === "syl" &&
|
|
259
|
+
token.first &&
|
|
260
|
+
!tokens.some((t) => t.word === token.word && !t.first);
|
|
261
|
+
if (!whole) {
|
|
262
|
+
out.push(token);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const parts = autoSyllabify(token.syl);
|
|
266
|
+
if (parts.length > 1) words.push(token.syl);
|
|
267
|
+
parts.forEach((syl, index) =>
|
|
268
|
+
out.push({ syl, word: token.word, first: index === 0, kind: "syl" }),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (out.length <= ordered.length) {
|
|
272
|
+
tokens = out;
|
|
273
|
+
split.push(...words);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const lyrics = new Map<string, string>();
|
|
277
|
+
const limit = LYRIC_LIMIT;
|
|
278
|
+
ordered.forEach((note, index) => {
|
|
279
|
+
const token = tokens[index];
|
|
280
|
+
if (!token) {
|
|
281
|
+
if (tokens.length > 0)
|
|
282
|
+
for (const id of [note.id, ...under.get(note.id)!]) lyrics.set(id, "_");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (token.kind === "rest") return;
|
|
286
|
+
lyrics.set(
|
|
287
|
+
note.id,
|
|
288
|
+
token.kind === "hold" ? "_" : token.syl.slice(0, limit),
|
|
289
|
+
);
|
|
290
|
+
for (const id of under.get(note.id)!) lyrics.set(id, "_");
|
|
291
|
+
});
|
|
292
|
+
const dropped = tokens
|
|
293
|
+
.slice(ordered.length)
|
|
294
|
+
.filter((token) => token.kind === "syl")
|
|
295
|
+
.map((token) => token.syl);
|
|
296
|
+
return { lyrics, dropped, split };
|
|
297
|
+
}
|
package/core/master.ts
CHANGED
|
@@ -105,10 +105,10 @@ export const MASTER_SPECS: Readonly<Record<MasterUnit, MasterUnitSpec>> =
|
|
|
105
105
|
low: db(-12, 12, 0, "low shelf gain"),
|
|
106
106
|
lowfreq: hz(20, 1000, 100, "low shelf corner"),
|
|
107
107
|
bell1: db(-12, 12, 0, "first bell gain"),
|
|
108
|
-
bell1freq: hz(40, 16_000, 400, "first bell
|
|
108
|
+
bell1freq: hz(40, 16_000, 400, "first bell center"),
|
|
109
109
|
bell1q: q("first bell width: higher is narrower"),
|
|
110
110
|
bell2: db(-12, 12, 0, "second bell gain"),
|
|
111
|
-
bell2freq: hz(200, 18_000, 3_000, "second bell
|
|
111
|
+
bell2freq: hz(200, 18_000, 3_000, "second bell center"),
|
|
112
112
|
bell2q: q("second bell width: higher is narrower"),
|
|
113
113
|
high: db(-12, 12, 0, "high shelf gain"),
|
|
114
114
|
highfreq: hz(1_000, 20_000, 10_000, "high shelf corner"),
|
|
@@ -222,7 +222,7 @@ export const MASTER_SPECS: Readonly<Record<MasterUnit, MasterUnitSpec>> =
|
|
|
222
222
|
default: 120,
|
|
223
223
|
step: 10,
|
|
224
224
|
unit: "Hz",
|
|
225
|
-
doc: "below this the mix is mono (keeps bass
|
|
225
|
+
doc: "below this the mix is mono (keeps bass centered); 0 is off",
|
|
226
226
|
},
|
|
227
227
|
},
|
|
228
228
|
},
|
package/core/resonators.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { resolveInstrumentWord } from "./instruments.ts";
|
|
21
21
|
import { WIND_INSTRUMENT, windPresetFor, type TrackWind } from "./winds.ts";
|
|
22
|
+
import { SING_INSTRUMENT, isSingPreset, type TrackSing } from "./sing.ts";
|
|
22
23
|
import {
|
|
23
24
|
FxValidationError,
|
|
24
25
|
isRecord,
|
|
@@ -175,7 +176,7 @@ export const MODAL_PARAMS: Readonly<Record<string, ParamSpec>> = Object.freeze({
|
|
|
175
176
|
hardness: num(0, 1, 0.4, 0.05, "mallet hardness: 0 yarn/felt, 1 brass", {
|
|
176
177
|
automate: true,
|
|
177
178
|
}),
|
|
178
|
-
position: num(0, 1, 0.42, 0.05, "strike point: 0 end or edge, 0.5
|
|
179
|
+
position: num(0, 1, 0.42, 0.05, "strike point: 0 end or edge, 0.5 center", {
|
|
179
180
|
automate: true,
|
|
180
181
|
}),
|
|
181
182
|
ring: num(0.05, 30, 1.6, "log", "ring time (T60) at middle C", {
|
|
@@ -768,8 +769,20 @@ export function instrumentPatchForWord(word: string): {
|
|
|
768
769
|
instrument: string;
|
|
769
770
|
modal?: TrackModal;
|
|
770
771
|
wind?: TrackWind;
|
|
772
|
+
sing?: TrackSing;
|
|
771
773
|
} {
|
|
772
774
|
const meaning = resolveInstrumentWord(word);
|
|
775
|
+
// f07-sing: `choir`, `aah`, `khoomei` sing a preset.
|
|
776
|
+
if (meaning?.field === "sing" && meaning.instrument === SING_INSTRUMENT) {
|
|
777
|
+
const preset =
|
|
778
|
+
meaning.preset && isSingPreset(meaning.preset)
|
|
779
|
+
? meaning.preset
|
|
780
|
+
: undefined;
|
|
781
|
+
return {
|
|
782
|
+
instrument: SING_INSTRUMENT,
|
|
783
|
+
sing: Object.freeze(preset ? { preset } : {}) as TrackSing,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
773
786
|
// f061-gamelan-winds: `flute`, `sax`, `tuba` play a wind preset.
|
|
774
787
|
if (meaning?.field === "wind" && meaning.instrument === WIND_INSTRUMENT) {
|
|
775
788
|
const preset = meaning.preset ? windPresetFor(meaning.preset) : undefined;
|
|
@@ -793,7 +806,8 @@ export function isModalWord(word: string): boolean {
|
|
|
793
806
|
const meaning = resolveInstrumentWord(word);
|
|
794
807
|
return (
|
|
795
808
|
meaning?.instrument === MODAL_INSTRUMENT ||
|
|
796
|
-
(meaning?.field === "wind" && meaning.instrument === WIND_INSTRUMENT)
|
|
809
|
+
(meaning?.field === "wind" && meaning.instrument === WIND_INSTRUMENT) ||
|
|
810
|
+
(meaning?.field === "sing" && meaning.instrument === SING_INSTRUMENT)
|
|
797
811
|
);
|
|
798
812
|
}
|
|
799
813
|
|
package/core/routing.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-track references (0.7). A track field that names another track (the
|
|
3
|
+
* vocoder's `src`, autotune's `from`, 0.7.1 harmony's `guide`) registers
|
|
4
|
+
* here once, at module load, with the edges it reads and how it forgets a
|
|
5
|
+
* removed track. `removeTrack` then drops dangling references, the renderer
|
|
6
|
+
* orders stems with `routingOrder`, and stem digests include the notes a
|
|
7
|
+
* track follows through `notesDigestInputs`.
|
|
8
|
+
*
|
|
9
|
+
* Audio edges carry rendered sound (the target must render first, and a
|
|
10
|
+
* cycle is an error). Notes edges only read the other track's notes, so
|
|
11
|
+
* they never order rendering and never form a cycle.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
registerTrackRefDrop,
|
|
15
|
+
ScoreValidationError,
|
|
16
|
+
type Note,
|
|
17
|
+
type Track,
|
|
18
|
+
type TrackScore,
|
|
19
|
+
} from "./score.ts";
|
|
20
|
+
import { trackSlug } from "./slug.ts";
|
|
21
|
+
|
|
22
|
+
export type TrackRefKind = "audio" | "notes";
|
|
23
|
+
|
|
24
|
+
/** One reference from a track to another track. */
|
|
25
|
+
export type TrackRef = Readonly<{ trackId: string; kind: TrackRefKind }>;
|
|
26
|
+
|
|
27
|
+
export type TrackRefSpec = Readonly<{
|
|
28
|
+
/** The tracks `track` references through this field. */
|
|
29
|
+
refs(track: Track): readonly TrackRef[];
|
|
30
|
+
/** `track` without its references to `removedId`. */
|
|
31
|
+
drop(track: Track, removedId: string): Track;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
const specs = new Map<string, TrackRefSpec>();
|
|
35
|
+
|
|
36
|
+
/** Registers a referencing field once; registering it again replaces it. */
|
|
37
|
+
export function registerTrackRefs(field: string, spec: TrackRefSpec): void {
|
|
38
|
+
specs.set(field, spec);
|
|
39
|
+
registerTrackRefDrop(field, spec.drop);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Every reference `track` holds, in field registration order. */
|
|
43
|
+
export function trackRefs(track: Track): readonly TrackRef[] {
|
|
44
|
+
const refs: TrackRef[] = [];
|
|
45
|
+
for (const spec of specs.values()) refs.push(...spec.refs(track));
|
|
46
|
+
return refs;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Finds a track by id, then by name slug (`Lead Vox` and `lead-vox` both
|
|
51
|
+
* match). Returns undefined when nothing matches.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveTrackRef(
|
|
54
|
+
score: TrackScore,
|
|
55
|
+
idOrSlug: string,
|
|
56
|
+
): Track | undefined {
|
|
57
|
+
const byId = score.tracks.find((track) => track.id === idOrSlug);
|
|
58
|
+
if (byId) return byId;
|
|
59
|
+
const slug = trackSlug(idOrSlug);
|
|
60
|
+
return score.tracks.find((track) => trackSlug(track.name) === slug);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Track ids in render order: every audio source before the tracks that
|
|
65
|
+
* read it, otherwise score order. References to missing tracks are ignored.
|
|
66
|
+
* A cycle throws a ScoreValidationError that names it (`a -> b -> a`).
|
|
67
|
+
*/
|
|
68
|
+
export function routingOrder(score: TrackScore): readonly string[] {
|
|
69
|
+
const ids = new Set(score.tracks.map((track) => track.id));
|
|
70
|
+
const sources = new Map<string, string[]>();
|
|
71
|
+
for (const track of score.tracks)
|
|
72
|
+
sources.set(
|
|
73
|
+
track.id,
|
|
74
|
+
trackRefs(track)
|
|
75
|
+
.filter((ref) => ref.kind === "audio" && ids.has(ref.trackId))
|
|
76
|
+
.map((ref) => ref.trackId),
|
|
77
|
+
);
|
|
78
|
+
const order: string[] = [];
|
|
79
|
+
const state = new Map<string, "visiting" | "done">();
|
|
80
|
+
const stack: string[] = [];
|
|
81
|
+
const visit = (id: string): void => {
|
|
82
|
+
const seen = state.get(id);
|
|
83
|
+
if (seen === "done") return;
|
|
84
|
+
if (seen === "visiting") {
|
|
85
|
+
const cycle = [...stack.slice(stack.indexOf(id)), id];
|
|
86
|
+
throw new ScoreValidationError(
|
|
87
|
+
`track routing has a cycle: ${cycle.join(" -> ")}`,
|
|
88
|
+
"routing-cycle",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
state.set(id, "visiting");
|
|
92
|
+
stack.push(id);
|
|
93
|
+
for (const source of sources.get(id) ?? []) visit(source);
|
|
94
|
+
stack.pop();
|
|
95
|
+
state.set(id, "done");
|
|
96
|
+
order.push(id);
|
|
97
|
+
};
|
|
98
|
+
for (const track of score.tracks) visit(track.id);
|
|
99
|
+
return order;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The notes a track's render depends on through notes edges, by referenced
|
|
104
|
+
* track id (sorted, deduplicated), for its stem digest. Missing tracks and
|
|
105
|
+
* self references contribute nothing.
|
|
106
|
+
*/
|
|
107
|
+
export function notesDigestInputs(
|
|
108
|
+
score: TrackScore,
|
|
109
|
+
trackId: string,
|
|
110
|
+
): readonly Readonly<{ trackId: string; notes: readonly Note[] }>[] {
|
|
111
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
112
|
+
if (!track) return [];
|
|
113
|
+
const ids = new Set(score.tracks.map((candidate) => candidate.id));
|
|
114
|
+
const followed = [
|
|
115
|
+
...new Set(
|
|
116
|
+
trackRefs(track)
|
|
117
|
+
.filter(
|
|
118
|
+
(ref) =>
|
|
119
|
+
ref.kind === "notes" &&
|
|
120
|
+
ref.trackId !== trackId &&
|
|
121
|
+
ids.has(ref.trackId),
|
|
122
|
+
)
|
|
123
|
+
.map((ref) => ref.trackId),
|
|
124
|
+
),
|
|
125
|
+
].sort();
|
|
126
|
+
return followed.map((id) => ({
|
|
127
|
+
trackId: id,
|
|
128
|
+
notes: score.notes.filter((note) => note.trackId === id),
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The vocoder's modulator (0.7): `vocoder.src` is an audio edge, so the
|
|
134
|
+
* modulator renders first and removing it drops the `src` (the carrier then
|
|
135
|
+
* plays dry until a new source is set).
|
|
136
|
+
*/
|
|
137
|
+
registerTrackRefs("vocoder", {
|
|
138
|
+
refs: (track) =>
|
|
139
|
+
track.vocoder?.src !== undefined
|
|
140
|
+
? [{ trackId: track.vocoder.src, kind: "audio" }]
|
|
141
|
+
: [],
|
|
142
|
+
drop: (track, removedId) => {
|
|
143
|
+
if (track.vocoder?.src !== removedId) return track;
|
|
144
|
+
const { src: _src, ...rest } = track.vocoder;
|
|
145
|
+
return { ...track, vocoder: rest };
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Registered reference fields (one block per lane).
|
|
151
|
+
|
|
152
|
+
// autotune: `Track.autotune.from` follows another track's notes.
|
|
153
|
+
registerTrackRefs("autotune.from", {
|
|
154
|
+
refs: (track) =>
|
|
155
|
+
track.autotune?.from !== undefined
|
|
156
|
+
? [{ trackId: track.autotune.from, kind: "notes" }]
|
|
157
|
+
: [],
|
|
158
|
+
drop: (track, removedId) => {
|
|
159
|
+
if (track.autotune?.from !== removedId) return track;
|
|
160
|
+
// Without its melody the track follows its own notes; `to` and the
|
|
161
|
+
// preset stay as the user set them.
|
|
162
|
+
const { from: _from, ...autotune } = track.autotune;
|
|
163
|
+
return { ...track, autotune };
|
|
164
|
+
},
|
|
165
|
+
});
|