@hraness/dawg 0.6.0 → 0.6.1
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 +34 -0
- package/DAWG.md +338 -51
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +5 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +258 -6
- package/core/granular.ts +91 -0
- package/core/instruments.ts +161 -0
- package/core/keys.ts +573 -4
- package/core/resonators.ts +291 -3
- package/core/score.ts +349 -3
- package/core/sdk/print.ts +78 -3
- package/core/sdk/v1.ts +1320 -36
- package/core/sections.ts +12 -4
- package/core/strings.ts +237 -2
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +3 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -6
- package/package.json +1 -1
- package/src/agent/agent.ts +8 -2
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/ops.ts +4 -1
- package/src/agent/preview-tool.ts +2 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +200 -15
- package/src/agent/xcb-agent.ts +8 -2
- package/src/audio/arrange.ts +21 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +18 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +46 -5
- package/src/audio/fit.ts +45 -0
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +9 -5
- package/src/audio/instruments.ts +32 -3
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +143 -19
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +50 -6
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +136 -9
- package/src/audio/preview.ts +15 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +168 -21
- package/src/audio/samples.ts +26 -1
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +266 -20
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/wav.ts +119 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +103 -22
- package/src/commands/fx.ts +4 -1
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +91 -8
- package/src/commands/keys.ts +287 -11
- package/src/commands/modal.ts +67 -4
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +5 -1
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +473 -0
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +129 -7
- package/src/project/check.ts +5 -1
- package/src/render.ts +16 -0
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +1 -1
- package/src/tui/menu.ts +637 -35
- package/src/tui/modal-menu.ts +34 -7
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +34 -9
- package/src/tui/wind-menu.ts +144 -0
package/README.md
CHANGED
|
@@ -15,17 +15,17 @@ curl -fsSL https://dawg.sh/install | sh
|
|
|
15
15
|
or install the release tarball from GitHub directly:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
bun add -g https://github.com/hraness/dawg/releases/download/v0.6.
|
|
18
|
+
bun add -g https://github.com/hraness/dawg/releases/download/v0.6.1/hraness-dawg-0.6.1.tgz
|
|
19
19
|
dawg --help
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Each [release](https://github.com/hraness/dawg/releases) is immutable and ships the tarball, a `SHA256SUMS` file and a build provenance attestation. To check a download before installing it:
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
gh release download v0.6.
|
|
25
|
+
gh release download v0.6.1 --repo hraness/dawg
|
|
26
26
|
shasum -a 256 -c SHA256SUMS
|
|
27
|
-
gh attestation verify hraness-dawg-0.6.
|
|
28
|
-
bun add -g "$PWD/hraness-dawg-0.6.
|
|
27
|
+
gh attestation verify hraness-dawg-0.6.1.tgz --repo hraness/dawg
|
|
28
|
+
bun add -g "$PWD/hraness-dawg-0.6.1.tgz"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
dawg is also on npm as [`@hraness/dawg`](https://www.npmjs.com/package/@hraness/dawg):
|
package/core/chords.ts
CHANGED
|
@@ -111,6 +111,12 @@ export type Chord = Readonly<{
|
|
|
111
111
|
extensions: readonly Extension[];
|
|
112
112
|
/** Slash bass pitch class, when not the root. */
|
|
113
113
|
bass?: number | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Upper tensions beyond the four extension buttons, as semitones above
|
|
116
|
+
* the root (13 b9, 15 #9, 17 11, 18 #11, 21 13): only typed symbols
|
|
117
|
+
* such as `C11`, `G13` or `A7b9` carry them (0.6.1).
|
|
118
|
+
*/
|
|
119
|
+
tensions?: readonly number[] | undefined;
|
|
114
120
|
}>;
|
|
115
121
|
|
|
116
122
|
export function makeChord(
|
|
@@ -118,6 +124,7 @@ export function makeChord(
|
|
|
118
124
|
quality: Quality,
|
|
119
125
|
extensions: Iterable<Extension> = [],
|
|
120
126
|
bass?: number,
|
|
127
|
+
tensions: readonly number[] = [],
|
|
121
128
|
): Chord {
|
|
122
129
|
const held = new Set(extensions);
|
|
123
130
|
const own = SECRET_EXTENSION[quality];
|
|
@@ -129,6 +136,11 @@ export function makeChord(
|
|
|
129
136
|
quality,
|
|
130
137
|
extensions: Object.freeze(ext),
|
|
131
138
|
...(slash !== undefined && slash !== pc ? { bass: slash } : {}),
|
|
139
|
+
...(tensions.length > 0
|
|
140
|
+
? {
|
|
141
|
+
tensions: Object.freeze([...new Set(tensions)].sort((a, b) => a - b)),
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
132
144
|
});
|
|
133
145
|
}
|
|
134
146
|
|
|
@@ -136,6 +148,7 @@ export function makeChord(
|
|
|
136
148
|
export function chordIntervals(chord: Chord): number[] {
|
|
137
149
|
const set = new Set(QUALITY_INTERVALS[chord.quality]);
|
|
138
150
|
for (const ext of chord.extensions) set.add(EXTENSION_INTERVAL[ext]);
|
|
151
|
+
for (const step of chord.tensions ?? []) set.add(step);
|
|
139
152
|
// m7 and M7 together keep both; 6 with m7 on a dim triad is the dim7's bb7.
|
|
140
153
|
return [...set].sort((a, b) => a - b);
|
|
141
154
|
}
|
|
@@ -188,6 +201,18 @@ const SECRET_SUFFIX: Readonly<Partial<Record<Quality, string>>> = Object.freeze(
|
|
|
188
201
|
|
|
189
202
|
/** Chord symbol suffix: `m7`, `maj9`, `7sus4`, `dim7`, `m7b5`, `6/9`. */
|
|
190
203
|
export function chordSuffix(chord: Chord): string {
|
|
204
|
+
if (chord.tensions?.length) {
|
|
205
|
+
// A typed extended chord keeps the symbol it was typed as.
|
|
206
|
+
const typed = TENSION_SUFFIXES.find(
|
|
207
|
+
([, quality, ext, tensions]) =>
|
|
208
|
+
quality === chord.quality &&
|
|
209
|
+
ext.join() === chord.extensions.join() &&
|
|
210
|
+
tensions.join() === chord.tensions!.join(),
|
|
211
|
+
);
|
|
212
|
+
if (typed) return typed[0];
|
|
213
|
+
const names = chord.tensions.map((step) => TENSION_NAMES[step] ?? step);
|
|
214
|
+
return `${chordSuffix({ ...chord, tensions: undefined })}(${names.join(",")})`;
|
|
215
|
+
}
|
|
191
216
|
const ext = new Set(chord.extensions);
|
|
192
217
|
const b7 = ext.has("m7");
|
|
193
218
|
const M7 = ext.has("M7");
|
|
@@ -351,9 +376,51 @@ const SUFFIXES: readonly (readonly [string, Quality, readonly Extension[]])[] =
|
|
|
351
376
|
["addb6", "b6", []],
|
|
352
377
|
["7#9", "7#9", []],
|
|
353
378
|
];
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
379
|
+
|
|
380
|
+
/** Typed upper-tension chords (0.6.1): suffix, quality, buttons, tensions. */
|
|
381
|
+
const TENSION_SUFFIXES: readonly (readonly [
|
|
382
|
+
string,
|
|
383
|
+
Quality,
|
|
384
|
+
readonly Extension[],
|
|
385
|
+
readonly number[],
|
|
386
|
+
])[] = [
|
|
387
|
+
["11", "maj", ["m7", "9"], [17]],
|
|
388
|
+
["m11", "min", ["m7", "9"], [17]],
|
|
389
|
+
["maj11", "maj", ["M7", "9"], [17]],
|
|
390
|
+
["add11", "maj", [], [17]],
|
|
391
|
+
["madd11", "min", [], [17]],
|
|
392
|
+
["13", "maj", ["m7", "9"], [21]],
|
|
393
|
+
["m13", "min", ["m7", "9"], [21]],
|
|
394
|
+
["maj13", "maj", ["M7", "9"], [21]],
|
|
395
|
+
["7b9", "maj", ["m7"], [13]],
|
|
396
|
+
["7#11", "maj", ["m7"], [18]],
|
|
397
|
+
["maj7#11", "maj", ["M7"], [18]],
|
|
398
|
+
["M7#11", "maj", ["M7"], [18]],
|
|
399
|
+
["7b13", "maj", ["m7"], [20]],
|
|
400
|
+
["13b9", "maj", ["m7"], [13, 21]],
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
const TENSION_NAMES: Readonly<Record<number, string>> = Object.freeze({
|
|
404
|
+
13: "b9",
|
|
405
|
+
15: "#9",
|
|
406
|
+
17: "11",
|
|
407
|
+
18: "#11",
|
|
408
|
+
20: "b13",
|
|
409
|
+
21: "13",
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const SUFFIX_TABLE = new Map<
|
|
413
|
+
string,
|
|
414
|
+
{ quality: Quality; ext: readonly Extension[]; tensions?: readonly number[] }
|
|
415
|
+
>([
|
|
416
|
+
...SUFFIXES.map(
|
|
417
|
+
([suffix, quality, ext]) => [suffix, { quality, ext }] as const,
|
|
418
|
+
),
|
|
419
|
+
...TENSION_SUFFIXES.map(
|
|
420
|
+
([suffix, quality, ext, tensions]) =>
|
|
421
|
+
[suffix, { quality, ext, tensions }] as const,
|
|
422
|
+
),
|
|
423
|
+
]);
|
|
357
424
|
|
|
358
425
|
const LETTER: Readonly<Record<string, number>> = Object.freeze({
|
|
359
426
|
c: 0,
|
|
@@ -395,7 +462,7 @@ export function parseChord(symbol: string): Chord | undefined {
|
|
|
395
462
|
bass = parsePitchClass(match[4]);
|
|
396
463
|
if (bass === undefined) return undefined;
|
|
397
464
|
}
|
|
398
|
-
return makeChord(root, entry.quality, entry.ext, bass);
|
|
465
|
+
return makeChord(root, entry.quality, entry.ext, bass, entry.tensions);
|
|
399
466
|
}
|
|
400
467
|
|
|
401
468
|
// ---------------------------------------------------------------------------
|
|
@@ -1209,6 +1276,8 @@ export const PERFORM_MODES = [
|
|
|
1209
1276
|
"harp",
|
|
1210
1277
|
"slop",
|
|
1211
1278
|
"pattern",
|
|
1279
|
+
// 0.6.1: fretboard-voiced strokes (see `voiceGuitar`, `strokeVoicing`).
|
|
1280
|
+
"guitar",
|
|
1212
1281
|
] as const;
|
|
1213
1282
|
export type PerformMode = (typeof PERFORM_MODES)[number];
|
|
1214
1283
|
|
|
@@ -1228,6 +1297,23 @@ export type PerformOptions = Readonly<{
|
|
|
1228
1297
|
pattern?: string | number;
|
|
1229
1298
|
/** 0..1. */
|
|
1230
1299
|
velocity?: number;
|
|
1300
|
+
/** Guitar mode: the stroke grid (a STROKE_PATTERNS name or D U d u x - .). */
|
|
1301
|
+
strokes?: string;
|
|
1302
|
+
/** Guitar mode: grid step in beats, default 1/2. */
|
|
1303
|
+
step?: number;
|
|
1304
|
+
/** Guitar mode: seconds a full six-string down stroke takes, default 0.022. */
|
|
1305
|
+
speed?: number;
|
|
1306
|
+
/** Guitar mode: tempo in BPM that `speed` converts with, default 120. */
|
|
1307
|
+
tempo?: number;
|
|
1308
|
+
/** Guitar mode: the guitar's tuning, capo, hand, ring and position. */
|
|
1309
|
+
guitar?: GuitarSetup;
|
|
1310
|
+
/** Guitar mode: the chord's root pitch class (default: the lowest note). */
|
|
1311
|
+
root?: number;
|
|
1312
|
+
/**
|
|
1313
|
+
* Guitar mode: a slash bass pitch class that must sound lowest, even when
|
|
1314
|
+
* it is not a chord tone (B/E keeps its E pedal). Default: the lowest note.
|
|
1315
|
+
*/
|
|
1316
|
+
slash?: number;
|
|
1231
1317
|
}>;
|
|
1232
1318
|
|
|
1233
1319
|
export type PerformedNote = Readonly<{
|
|
@@ -1544,6 +1630,24 @@ export function perform(
|
|
|
1544
1630
|
}
|
|
1545
1631
|
return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
|
|
1546
1632
|
}
|
|
1633
|
+
case "guitar": {
|
|
1634
|
+
const bass = options.slash ?? notes[0]!;
|
|
1635
|
+
const voicing = voiceGuitar(
|
|
1636
|
+
notes.map(mod12),
|
|
1637
|
+
mod12(bass),
|
|
1638
|
+
options.root ?? mod12(bass),
|
|
1639
|
+
options.guitar,
|
|
1640
|
+
);
|
|
1641
|
+
if (!voicing) return notes.map((pitch) => at(pitch, start, end));
|
|
1642
|
+
return strokeVoicing(voicing, start, length, {
|
|
1643
|
+
...(options.strokes !== undefined ? { strokes: options.strokes } : {}),
|
|
1644
|
+
...(options.step !== undefined ? { step: options.step } : {}),
|
|
1645
|
+
...(options.speed !== undefined ? { speed: options.speed } : {}),
|
|
1646
|
+
...(options.tempo !== undefined ? { tempo: options.tempo } : {}),
|
|
1647
|
+
velocity,
|
|
1648
|
+
strings: guitarStrings(options.guitar?.tune).length,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1547
1651
|
case "harp": {
|
|
1548
1652
|
const gap = Math.max(0, options.strum ?? DEFAULT_STRUM * 2);
|
|
1549
1653
|
return spanned.map((pitch, index) =>
|
|
@@ -1578,6 +1682,382 @@ export function perform(
|
|
|
1578
1682
|
}
|
|
1579
1683
|
}
|
|
1580
1684
|
|
|
1685
|
+
// ---------------------------------------------------------------------------
|
|
1686
|
+
// Fretboard (0.6.1): guitar voicings and strokes
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Open-string MIDI pitches, low string to high, for `guitar tune <name>`.
|
|
1690
|
+
* Ported from the guitar design lane (proto/guitar/strum.ts TUNINGS).
|
|
1691
|
+
*/
|
|
1692
|
+
export const GUITAR_TUNINGS = Object.freeze({
|
|
1693
|
+
standard: [40, 45, 50, 55, 59, 64],
|
|
1694
|
+
dropd: [38, 45, 50, 55, 59, 64],
|
|
1695
|
+
doubledropd: [38, 45, 50, 55, 59, 62],
|
|
1696
|
+
dadgad: [38, 45, 50, 55, 57, 62],
|
|
1697
|
+
openg: [38, 43, 50, 55, 59, 62],
|
|
1698
|
+
opend: [38, 45, 50, 54, 57, 62],
|
|
1699
|
+
opene: [40, 47, 52, 56, 59, 64],
|
|
1700
|
+
halfdown: [39, 44, 49, 54, 58, 63],
|
|
1701
|
+
nashville: [52, 57, 62, 67, 59, 64],
|
|
1702
|
+
bass: [28, 33, 38, 43],
|
|
1703
|
+
ukulele: [67, 60, 64, 69],
|
|
1704
|
+
requinto: [45, 50, 55, 60, 64, 69],
|
|
1705
|
+
} as const satisfies Record<string, readonly number[]>);
|
|
1706
|
+
export type GuitarTuningName = keyof typeof GUITAR_TUNINGS;
|
|
1707
|
+
export const GUITAR_TUNING_NAMES = Object.keys(
|
|
1708
|
+
GUITAR_TUNINGS,
|
|
1709
|
+
) as GuitarTuningName[];
|
|
1710
|
+
|
|
1711
|
+
/** How a guitar is set up for voicing chords (`Track.guitar`). */
|
|
1712
|
+
export type GuitarSetup = Readonly<{
|
|
1713
|
+
/** A GUITAR_TUNINGS name or open-string pitches low to high. */
|
|
1714
|
+
tune?: string | readonly number[];
|
|
1715
|
+
/** Capo fret 0..12. */
|
|
1716
|
+
capo?: number;
|
|
1717
|
+
/** Hand stretch in frets 3..6 (default 4: one fret per finger). */
|
|
1718
|
+
hand?: number;
|
|
1719
|
+
/** 0 closed shapes (no open strings) .. 1 prefer ringing open strings. */
|
|
1720
|
+
ring?: number;
|
|
1721
|
+
/** Preferred fret position 0..12 (0 open position). */
|
|
1722
|
+
position?: number;
|
|
1723
|
+
}>;
|
|
1724
|
+
|
|
1725
|
+
/** One fretted chord: frets per string low to high (-1 muted). */
|
|
1726
|
+
export type GuitarVoicing = Readonly<{
|
|
1727
|
+
frets: readonly number[];
|
|
1728
|
+
/** Sounding pitches, low string to high. */
|
|
1729
|
+
pitches: readonly number[];
|
|
1730
|
+
/** The string each pitch sounds on. */
|
|
1731
|
+
strings: readonly number[];
|
|
1732
|
+
}>;
|
|
1733
|
+
|
|
1734
|
+
/** Open-string pitches for a tuning name or list (standard when unknown). */
|
|
1735
|
+
export function guitarStrings(tune: GuitarSetup["tune"]): readonly number[] {
|
|
1736
|
+
if (Array.isArray(tune)) return tune as readonly number[];
|
|
1737
|
+
const name = String(tune ?? "standard")
|
|
1738
|
+
.toLowerCase()
|
|
1739
|
+
.replace(/[\s_-]/g, "");
|
|
1740
|
+
return (
|
|
1741
|
+
(GUITAR_TUNINGS as Record<string, readonly number[]>)[name] ??
|
|
1742
|
+
GUITAR_TUNINGS.standard
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Fingers a fretting uses. Strings at the lowest fret count as one barre
|
|
1748
|
+
* only when no open string lies between them (a barre cannot skip an open
|
|
1749
|
+
* string); otherwise one finger each.
|
|
1750
|
+
*/
|
|
1751
|
+
export function guitarFingers(frets: readonly number[]): number {
|
|
1752
|
+
const fretted = frets.filter((f) => f > 0);
|
|
1753
|
+
if (fretted.length === 0) return 0;
|
|
1754
|
+
const min = Math.min(...fretted);
|
|
1755
|
+
const at = frets.flatMap((f, i) => (f === min ? [i] : []));
|
|
1756
|
+
const between = frets.slice(at[0]!, at[at.length - 1]! + 1);
|
|
1757
|
+
const barre = at.length > 1 && !between.includes(0);
|
|
1758
|
+
return (barre ? 1 : at.length) + fretted.filter((f) => f > min).length;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* The chord tones a voicing must keep, most required first, after dropping
|
|
1763
|
+
* `drops` tones in the guitarist's order: the 5th, then the 9th; the root,
|
|
1764
|
+
* the 3rd and the 7th always stay (after the design lane's review).
|
|
1765
|
+
*/
|
|
1766
|
+
function requiredTones(tones: readonly number[], root: number, drops: number) {
|
|
1767
|
+
// The 5th, then the 11th (only beside a 3rd: a sus4 keeps its 4th), then
|
|
1768
|
+
// the 9th. dawg's symbols reach an 11th through m(add4)/madd4 (+9).
|
|
1769
|
+
const third = tones.some((pc) => [3, 4].includes(mod12(pc - root)));
|
|
1770
|
+
const order = (third ? [7, 5, 2] : [7, 2]).map((step) => mod12(root + step));
|
|
1771
|
+
const dropped = order.filter((pc) => tones.includes(pc)).slice(0, drops);
|
|
1772
|
+
return tones.filter((pc) => !dropped.includes(pc));
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* The most playable fretting of pitch classes `tones` over `bass` on a
|
|
1777
|
+
* guitar `setup`: the bass is the lowest sounding note, every required
|
|
1778
|
+
* tone sounds, at most four fingers within a `hand`-fret stretch, mutes
|
|
1779
|
+
* only under the bass or one inside, and no barre over an open string.
|
|
1780
|
+
* Cost prefers more strings, low positions, open strings (by `ring`), a
|
|
1781
|
+
* fifth present, no doubled third, and small moves from `previous`.
|
|
1782
|
+
* Undefined when nothing fits. Ported from proto/guitar/strum.ts.
|
|
1783
|
+
*/
|
|
1784
|
+
export function voiceGuitar(
|
|
1785
|
+
tones: readonly number[],
|
|
1786
|
+
bass: number,
|
|
1787
|
+
root: number,
|
|
1788
|
+
setup: GuitarSetup = {},
|
|
1789
|
+
previous?: readonly number[],
|
|
1790
|
+
): GuitarVoicing | undefined {
|
|
1791
|
+
// Play mode voices every pad press; dense chords at a wide hand take
|
|
1792
|
+
// ~15 ms to search, so repeats come from a small LRU (results are frozen).
|
|
1793
|
+
const key = JSON.stringify([
|
|
1794
|
+
[...new Set([...tones, bass].map(mod12))],
|
|
1795
|
+
mod12(bass),
|
|
1796
|
+
mod12(root),
|
|
1797
|
+
setup.tune ?? null,
|
|
1798
|
+
setup.capo ?? null,
|
|
1799
|
+
setup.hand ?? null,
|
|
1800
|
+
setup.ring ?? null,
|
|
1801
|
+
setup.position ?? null,
|
|
1802
|
+
previous ?? null,
|
|
1803
|
+
]);
|
|
1804
|
+
if (voicingCache.has(key)) {
|
|
1805
|
+
const hit = voicingCache.get(key);
|
|
1806
|
+
voicingCache.delete(key);
|
|
1807
|
+
voicingCache.set(key, hit);
|
|
1808
|
+
return hit;
|
|
1809
|
+
}
|
|
1810
|
+
const voiced = searchGuitar(tones, bass, root, setup, previous);
|
|
1811
|
+
voicingCache.set(key, voiced);
|
|
1812
|
+
if (voicingCache.size > VOICING_CACHE_SIZE)
|
|
1813
|
+
voicingCache.delete(voicingCache.keys().next().value!);
|
|
1814
|
+
return voiced;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const VOICING_CACHE_SIZE = 256;
|
|
1818
|
+
const voicingCache = new Map<string, GuitarVoicing | undefined>();
|
|
1819
|
+
|
|
1820
|
+
function searchGuitar(
|
|
1821
|
+
tones: readonly number[],
|
|
1822
|
+
bass: number,
|
|
1823
|
+
root: number,
|
|
1824
|
+
setup: GuitarSetup,
|
|
1825
|
+
previous?: readonly number[],
|
|
1826
|
+
): GuitarVoicing | undefined {
|
|
1827
|
+
const pcs = [...new Set([...tones, bass].map(mod12))];
|
|
1828
|
+
const capo = clampInt(setup.capo ?? 0, 0, 12);
|
|
1829
|
+
const open = guitarStrings(setup.tune).map((pitch) => pitch + capo);
|
|
1830
|
+
const n = open.length;
|
|
1831
|
+
const ring = Math.min(1, Math.max(0, setup.ring ?? 0.5));
|
|
1832
|
+
const stretch = clampInt(setup.hand ?? 4, 3, 6);
|
|
1833
|
+
const fifth = mod12(root + 7);
|
|
1834
|
+
const third = pcs.find((pc) => [3, 4].includes(mod12(pc - root)));
|
|
1835
|
+
const prev = previous ? [...previous].sort((a, b) => a - b) : undefined;
|
|
1836
|
+
for (let drops = 0; drops <= 3; drops += 1) {
|
|
1837
|
+
const required = requiredTones(pcs, root, drops);
|
|
1838
|
+
let best: { frets: number[]; cost: number } | undefined;
|
|
1839
|
+
const choose = (frets: readonly number[]) => {
|
|
1840
|
+
const idx = frets.flatMap((f, i) => (f < 0 ? [] : [i]));
|
|
1841
|
+
if (idx.length < Math.min(n, n >= 6 ? 4 : 3)) return;
|
|
1842
|
+
const interior = frets.slice(idx[0]).filter((f) => f < 0).length;
|
|
1843
|
+
if (interior > (n >= 6 ? 1 : 0)) return;
|
|
1844
|
+
const pitches = idx.map((i) => open[i]! + frets[i]!);
|
|
1845
|
+
if (mod12(Math.min(...pitches)) !== mod12(bass)) return;
|
|
1846
|
+
const sounding = new Set(pitches.map(mod12));
|
|
1847
|
+
if (required.some((pc) => !sounding.has(pc))) return;
|
|
1848
|
+
const fingers = guitarFingers(frets);
|
|
1849
|
+
if (fingers > 4) return;
|
|
1850
|
+
const fretted = frets.filter((f) => f > 0);
|
|
1851
|
+
const span = fretted.length
|
|
1852
|
+
? Math.max(...fretted) - Math.min(...fretted)
|
|
1853
|
+
: 0;
|
|
1854
|
+
if (span > stretch - 1) return;
|
|
1855
|
+
const opens = frets.filter((f) => f === 0).length;
|
|
1856
|
+
if (ring === 0 && opens > 0) return;
|
|
1857
|
+
const pos = fretted.length ? Math.min(...fretted) : 0;
|
|
1858
|
+
let cost =
|
|
1859
|
+
span * 0.6 +
|
|
1860
|
+
pos * 0.25 +
|
|
1861
|
+
fingers * 0.4 -
|
|
1862
|
+
idx.length -
|
|
1863
|
+
opens * 0.7 * ring;
|
|
1864
|
+
cost += interior * 1.5;
|
|
1865
|
+
if (!sounding.has(fifth) && pcs.includes(fifth)) cost += 0.6;
|
|
1866
|
+
if (
|
|
1867
|
+
third !== undefined &&
|
|
1868
|
+
pitches.filter((p) => mod12(p) === third).length > 1
|
|
1869
|
+
)
|
|
1870
|
+
cost += 0.5;
|
|
1871
|
+
if (setup.position !== undefined)
|
|
1872
|
+
cost += Math.abs(pos - setup.position) * 0.5;
|
|
1873
|
+
if (prev && prev.length) {
|
|
1874
|
+
const cur = [...pitches].sort((a, b) => a - b);
|
|
1875
|
+
let move = 0;
|
|
1876
|
+
for (let i = 0; i < cur.length; i += 1)
|
|
1877
|
+
move += Math.abs(cur[i]! - prev[Math.min(i, prev.length - 1)]!);
|
|
1878
|
+
cost += move * 0.05;
|
|
1879
|
+
}
|
|
1880
|
+
if (
|
|
1881
|
+
!best ||
|
|
1882
|
+
cost < best.cost - 1e-9 ||
|
|
1883
|
+
(Math.abs(cost - best.cost) <= 1e-9 && frets.join() < best.frets.join())
|
|
1884
|
+
)
|
|
1885
|
+
best = { frets: [...frets], cost };
|
|
1886
|
+
};
|
|
1887
|
+
for (let p = 1; p <= 12; p += 1) {
|
|
1888
|
+
const options = open.map((o) => {
|
|
1889
|
+
const list = [-1];
|
|
1890
|
+
if (pcs.includes(mod12(o))) list.push(0);
|
|
1891
|
+
for (let f = p; f < p + stretch; f += 1)
|
|
1892
|
+
if (pcs.includes(mod12(o + f))) list.push(f);
|
|
1893
|
+
return list;
|
|
1894
|
+
});
|
|
1895
|
+
const frets = new Array<number>(n).fill(-1);
|
|
1896
|
+
const walk = (s: number): void => {
|
|
1897
|
+
if (s === n) return choose(frets);
|
|
1898
|
+
for (const f of options[s]!) {
|
|
1899
|
+
frets[s] = f;
|
|
1900
|
+
walk(s + 1);
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
walk(0);
|
|
1904
|
+
}
|
|
1905
|
+
if (best) {
|
|
1906
|
+
const { frets } = best as { frets: number[] };
|
|
1907
|
+
const strings = frets.flatMap((f, i) => (f < 0 ? [] : [i]));
|
|
1908
|
+
return Object.freeze({
|
|
1909
|
+
frets: Object.freeze(frets),
|
|
1910
|
+
pitches: Object.freeze(strings.map((i) => open[i]! + frets[i]!)),
|
|
1911
|
+
strings: Object.freeze(strings),
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return undefined;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/** A voicing as tab, low string first: `x 3 2 0 1 0`. */
|
|
1919
|
+
export function guitarTab(frets: readonly number[]): string {
|
|
1920
|
+
return frets.map((f) => (f < 0 ? "x" : String(f))).join(" ");
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Stroke grids for perform mode `guitar`, one character per step: D down
|
|
1925
|
+
* (all strings), U up (top four), d light down (top four), u light up (top
|
|
1926
|
+
* three), x muted chuck, `-` or `.` rest (strings ring on). After the
|
|
1927
|
+
* design lane's STRUM_PATTERNS.
|
|
1928
|
+
*/
|
|
1929
|
+
export const STROKE_PATTERNS = Object.freeze({
|
|
1930
|
+
down: "D",
|
|
1931
|
+
folk: "D-DU-UDU",
|
|
1932
|
+
pop: "D-DU-UD-",
|
|
1933
|
+
punk: "DDDDDDDD",
|
|
1934
|
+
funk: "xUxUDUxUxUDUxUxU",
|
|
1935
|
+
reggae: "-D-D",
|
|
1936
|
+
waltz: "Ddd",
|
|
1937
|
+
jangle: "D-DUDUDU",
|
|
1938
|
+
island: "D-DU-UDU",
|
|
1939
|
+
} as const satisfies Record<string, string>);
|
|
1940
|
+
export const STROKE_PATTERN_NAMES = Object.keys(STROKE_PATTERNS);
|
|
1941
|
+
const STROKE_CHARS = /^[DUdux.\-]+$/;
|
|
1942
|
+
|
|
1943
|
+
/** Default seconds a full six-string down stroke takes (22 ms). */
|
|
1944
|
+
export const DEFAULT_STROKE_SPEED = 0.022;
|
|
1945
|
+
/** Default stroke grid step in beats (8ths). */
|
|
1946
|
+
export const DEFAULT_STROKE_STEP = 0.5;
|
|
1947
|
+
|
|
1948
|
+
/** A stroke grid by name, or a literal grid of D U d u x - .; undefined if bad. */
|
|
1949
|
+
export function strokeGrid(text: string | undefined): string | undefined {
|
|
1950
|
+
const raw = (text ?? "down").trim();
|
|
1951
|
+
const named = (STROKE_PATTERNS as Record<string, string>)[raw.toLowerCase()];
|
|
1952
|
+
if (named) return named;
|
|
1953
|
+
const grid = raw.replace(/[|\s]/g, "");
|
|
1954
|
+
return grid.length > 0 && grid.length <= 64 && STROKE_CHARS.test(grid)
|
|
1955
|
+
? grid
|
|
1956
|
+
: undefined;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Strum fretted `voicing` from `start` for `length` beats with stroke grid
|
|
1961
|
+
* `strokes` (repeats every grid length, one step per `step` beats). A full
|
|
1962
|
+
* down stroke sweeps the six strings in `speed` seconds at `tempo` BPM;
|
|
1963
|
+
* each struck string rings until it is struck again or the chord ends; a
|
|
1964
|
+
* chuck (x) is a short muted hit that stops the strings. Velocity accents
|
|
1965
|
+
* downbeats and lightens upstrokes.
|
|
1966
|
+
*/
|
|
1967
|
+
export function strokeVoicing(
|
|
1968
|
+
voicing: GuitarVoicing,
|
|
1969
|
+
start: number,
|
|
1970
|
+
length: number,
|
|
1971
|
+
options: Readonly<{
|
|
1972
|
+
strokes?: string;
|
|
1973
|
+
step?: number;
|
|
1974
|
+
speed?: number;
|
|
1975
|
+
tempo?: number;
|
|
1976
|
+
velocity?: number;
|
|
1977
|
+
strings?: number;
|
|
1978
|
+
}> = {},
|
|
1979
|
+
): PerformedNote[] {
|
|
1980
|
+
const grid = strokeGrid(options.strokes) ?? "D";
|
|
1981
|
+
const step =
|
|
1982
|
+
options.step && options.step > 0 ? options.step : DEFAULT_STROKE_STEP;
|
|
1983
|
+
const tempo = options.tempo && options.tempo > 0 ? options.tempo : 120;
|
|
1984
|
+
const sweep =
|
|
1985
|
+
(Math.max(0, options.speed ?? DEFAULT_STROKE_SPEED) * tempo) / 60;
|
|
1986
|
+
const base = options.velocity ?? 0.8;
|
|
1987
|
+
const total = Math.max(2, options.strings ?? 6);
|
|
1988
|
+
const sounding = voicing.strings;
|
|
1989
|
+
const end = start + length;
|
|
1990
|
+
type Open = { note: PerformedNote; index: number };
|
|
1991
|
+
const ringing = new Map<number, Open>();
|
|
1992
|
+
const out: PerformedNote[] = [];
|
|
1993
|
+
const stop = (string: number, at: number) => {
|
|
1994
|
+
const held = ringing.get(string);
|
|
1995
|
+
if (!held) return;
|
|
1996
|
+
const cut = round6(
|
|
1997
|
+
Math.max(1e-3, Math.min(held.note.length, at - held.note.start)),
|
|
1998
|
+
);
|
|
1999
|
+
out[held.index] = { ...held.note, length: cut };
|
|
2000
|
+
ringing.delete(string);
|
|
2001
|
+
};
|
|
2002
|
+
const steps = Math.max(1, Math.round(length / step));
|
|
2003
|
+
for (let s = 0; s < steps; s += 1) {
|
|
2004
|
+
const ch = grid[s % grid.length]!;
|
|
2005
|
+
if (ch === "-" || ch === ".") continue;
|
|
2006
|
+
const t0 = start + s * step;
|
|
2007
|
+
if (t0 >= end - 1e-9) break;
|
|
2008
|
+
const beat = s * step;
|
|
2009
|
+
const accent =
|
|
2010
|
+
Math.abs(beat - Math.round(beat)) < 1e-9
|
|
2011
|
+
? Math.round(beat) % 2 === 0
|
|
2012
|
+
? 1
|
|
2013
|
+
: 0.92
|
|
2014
|
+
: 0.82;
|
|
2015
|
+
let order: number[];
|
|
2016
|
+
let velocity = base * accent;
|
|
2017
|
+
let ring = end - t0;
|
|
2018
|
+
const all = sounding.map((_, k) => k);
|
|
2019
|
+
switch (ch) {
|
|
2020
|
+
case "D":
|
|
2021
|
+
order = all;
|
|
2022
|
+
break;
|
|
2023
|
+
case "U":
|
|
2024
|
+
order = all.slice(-4).reverse();
|
|
2025
|
+
velocity *= 0.85;
|
|
2026
|
+
break;
|
|
2027
|
+
case "d":
|
|
2028
|
+
order = all.slice(-4);
|
|
2029
|
+
velocity *= 0.62;
|
|
2030
|
+
break;
|
|
2031
|
+
case "u":
|
|
2032
|
+
order = all.slice(-3).reverse();
|
|
2033
|
+
velocity *= 0.55;
|
|
2034
|
+
break;
|
|
2035
|
+
default:
|
|
2036
|
+
order = all;
|
|
2037
|
+
velocity *= 0.5;
|
|
2038
|
+
ring = Math.min(ring, 0.03 * (tempo / 60));
|
|
2039
|
+
}
|
|
2040
|
+
const first = sounding[order[0]!]!;
|
|
2041
|
+
order.forEach((k, position) => {
|
|
2042
|
+
const string = sounding[k]!;
|
|
2043
|
+
const at = t0 + (Math.abs(string - first) / (total - 1)) * sweep;
|
|
2044
|
+
if (at >= end - 1e-9) return;
|
|
2045
|
+
stop(string, at);
|
|
2046
|
+
const note: PerformedNote = {
|
|
2047
|
+
pitch: voicing.pitches[k]!,
|
|
2048
|
+
start: round6(at),
|
|
2049
|
+
length: round6(Math.max(1e-3, Math.min(ring, end - at))),
|
|
2050
|
+
velocity: round6(
|
|
2051
|
+
Math.max(0.05, Math.min(1, velocity * (1 - 0.04 * position))),
|
|
2052
|
+
),
|
|
2053
|
+
};
|
|
2054
|
+
out.push(note);
|
|
2055
|
+
if (ch !== "x") ringing.set(string, { note, index: out.length - 1 });
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1581
2061
|
// ---------------------------------------------------------------------------
|
|
1582
2062
|
// Progressions
|
|
1583
2063
|
|
|
@@ -1969,6 +2449,14 @@ export function renderProgression(options: RenderOptions): RenderedProgression {
|
|
|
1969
2449
|
...perform(chord.pitches, at, span, {
|
|
1970
2450
|
...options.perform,
|
|
1971
2451
|
seed: (options.perform?.seed ?? 0) + index,
|
|
2452
|
+
...(options.perform?.mode === "guitar"
|
|
2453
|
+
? {
|
|
2454
|
+
root: options.chords[index]!.root,
|
|
2455
|
+
...(options.chords[index]!.bass !== undefined
|
|
2456
|
+
? { slash: options.chords[index]!.bass }
|
|
2457
|
+
: {}),
|
|
2458
|
+
}
|
|
2459
|
+
: {}),
|
|
1972
2460
|
}),
|
|
1973
2461
|
);
|
|
1974
2462
|
const source = options.chords[index]!;
|