@hraness/dawg 0.6.0 → 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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- 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/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- 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 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- 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 -12
- 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 +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- 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 +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -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 +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -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 +171 -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/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- 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 +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- 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 +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- 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/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- 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 +244 -0
- 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 +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- 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 +36 -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 +50 -7
- 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 +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- 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 +144 -0
- 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/chords.ts
CHANGED
|
@@ -49,6 +49,7 @@ export const QUALITIES = [
|
|
|
49
49
|
"mb6",
|
|
50
50
|
"b6",
|
|
51
51
|
"7#9",
|
|
52
|
+
"b5",
|
|
52
53
|
] as const;
|
|
53
54
|
export type Quality = (typeof QUALITIES)[number];
|
|
54
55
|
|
|
@@ -65,6 +66,7 @@ const QUALITY_INTERVALS: Readonly<Record<Quality, readonly number[]>> =
|
|
|
65
66
|
mb6: [0, 3, 7, 8],
|
|
66
67
|
b6: [0, 4, 7, 8],
|
|
67
68
|
"7#9": [0, 4, 7, 10, 15],
|
|
69
|
+
b5: [0, 4, 6],
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -111,6 +113,17 @@ export type Chord = Readonly<{
|
|
|
111
113
|
extensions: readonly Extension[];
|
|
112
114
|
/** Slash bass pitch class, when not the root. */
|
|
113
115
|
bass?: number | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Upper tensions beyond the four extension buttons, as semitones above
|
|
118
|
+
* the root (13 b9, 15 #9, 17 11, 18 #11, 21 13): only typed symbols
|
|
119
|
+
* such as `C11`, `G13` or `A7b9` carry them (0.6.1).
|
|
120
|
+
*/
|
|
121
|
+
tensions?: readonly number[] | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* The root's letter, 0..6 for C..B, when a roman numeral spelled it:
|
|
124
|
+
* `bVII` in C names Bb (not A#) and `vii` in F# names E# (not F).
|
|
125
|
+
*/
|
|
126
|
+
letter?: number | undefined;
|
|
114
127
|
}>;
|
|
115
128
|
|
|
116
129
|
export function makeChord(
|
|
@@ -118,6 +131,7 @@ export function makeChord(
|
|
|
118
131
|
quality: Quality,
|
|
119
132
|
extensions: Iterable<Extension> = [],
|
|
120
133
|
bass?: number,
|
|
134
|
+
tensions: readonly number[] = [],
|
|
121
135
|
): Chord {
|
|
122
136
|
const held = new Set(extensions);
|
|
123
137
|
const own = SECRET_EXTENSION[quality];
|
|
@@ -129,6 +143,11 @@ export function makeChord(
|
|
|
129
143
|
quality,
|
|
130
144
|
extensions: Object.freeze(ext),
|
|
131
145
|
...(slash !== undefined && slash !== pc ? { bass: slash } : {}),
|
|
146
|
+
...(tensions.length > 0
|
|
147
|
+
? {
|
|
148
|
+
tensions: Object.freeze([...new Set(tensions)].sort((a, b) => a - b)),
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
132
151
|
});
|
|
133
152
|
}
|
|
134
153
|
|
|
@@ -136,6 +155,7 @@ export function makeChord(
|
|
|
136
155
|
export function chordIntervals(chord: Chord): number[] {
|
|
137
156
|
const set = new Set(QUALITY_INTERVALS[chord.quality]);
|
|
138
157
|
for (const ext of chord.extensions) set.add(EXTENSION_INTERVAL[ext]);
|
|
158
|
+
for (const step of chord.tensions ?? []) set.add(step);
|
|
139
159
|
// m7 and M7 together keep both; 6 with m7 on a dim triad is the dim7's bb7.
|
|
140
160
|
return [...set].sort((a, b) => a - b);
|
|
141
161
|
}
|
|
@@ -182,12 +202,43 @@ export function noteName(pc: number, flats = false): string {
|
|
|
182
202
|
return (flats ? FLAT_NAMES : SHARP_NAMES)[mod12(pc)]!;
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
const LETTERS = "CDEFGAB";
|
|
206
|
+
const LETTER_PCS = [0, 2, 4, 5, 7, 9, 11] as const;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `pc` spelled on letter `letter` (0..6, C..B) with one accidental at
|
|
210
|
+
* most (E#, Cb, Bb); undefined when that would need a double accidental.
|
|
211
|
+
*/
|
|
212
|
+
export function spellOnLetter(pc: number, letter: number): string | undefined {
|
|
213
|
+
const l = ((Math.trunc(letter) % 7) + 7) % 7;
|
|
214
|
+
const diff = ((mod12(pc) - LETTER_PCS[l]! + 18) % 12) - 6;
|
|
215
|
+
if (Math.abs(diff) > 1) return undefined;
|
|
216
|
+
return `${LETTERS[l]}${diff === 1 ? "#" : diff === -1 ? "b" : ""}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The letter (0..6, C..B) a key's tonic is spelled on. */
|
|
220
|
+
function tonicLetter(key: Key): number {
|
|
221
|
+
return LETTERS.indexOf(noteName(key.tonic, keyUsesFlats(key))[0]!);
|
|
222
|
+
}
|
|
223
|
+
|
|
185
224
|
const SECRET_SUFFIX: Readonly<Partial<Record<Quality, string>>> = Object.freeze(
|
|
186
225
|
{ madd4: "m(add4)", mb6: "m(b6)", b6: "(b6)", "7#9": "7#9" },
|
|
187
226
|
);
|
|
188
227
|
|
|
189
228
|
/** Chord symbol suffix: `m7`, `maj9`, `7sus4`, `dim7`, `m7b5`, `6/9`. */
|
|
190
229
|
export function chordSuffix(chord: Chord): string {
|
|
230
|
+
if (chord.tensions?.length) {
|
|
231
|
+
// A typed extended chord keeps the symbol it was typed as.
|
|
232
|
+
const typed = TENSION_SUFFIXES.find(
|
|
233
|
+
([, quality, ext, tensions]) =>
|
|
234
|
+
quality === chord.quality &&
|
|
235
|
+
ext.join() === chord.extensions.join() &&
|
|
236
|
+
tensions.join() === chord.tensions!.join(),
|
|
237
|
+
);
|
|
238
|
+
if (typed) return typed[0];
|
|
239
|
+
const names = chord.tensions.map((step) => TENSION_NAMES[step] ?? step);
|
|
240
|
+
return `${chordSuffix({ ...chord, tensions: undefined })}(${names.join(",")})`;
|
|
241
|
+
}
|
|
191
242
|
const ext = new Set(chord.extensions);
|
|
192
243
|
const b7 = ext.has("m7");
|
|
193
244
|
const M7 = ext.has("M7");
|
|
@@ -248,6 +299,9 @@ export function chordSuffix(chord: Chord): string {
|
|
|
248
299
|
base = `${seventh}(no3)`;
|
|
249
300
|
if (nine) extras.push("9");
|
|
250
301
|
break;
|
|
302
|
+
case "b5":
|
|
303
|
+
base = `${ninth}b5`;
|
|
304
|
+
break;
|
|
251
305
|
default:
|
|
252
306
|
base = seventh; // secret qualities returned above
|
|
253
307
|
}
|
|
@@ -266,6 +320,7 @@ export function chordSuffix(chord: Chord): string {
|
|
|
266
320
|
mb6: "m(b6)",
|
|
267
321
|
b6: "(b6)",
|
|
268
322
|
"7#9": "7#9",
|
|
323
|
+
b5: "(b5)",
|
|
269
324
|
};
|
|
270
325
|
base = triad[q];
|
|
271
326
|
if (six && nine && (q === "maj" || q === "min")) return `${base}6/9`;
|
|
@@ -285,7 +340,11 @@ export function chordSuffix(chord: Chord): string {
|
|
|
285
340
|
export function chordName(chord: Chord, flats = false): string {
|
|
286
341
|
const slash =
|
|
287
342
|
chord.bass === undefined ? "" : `/${noteName(chord.bass, flats)}`;
|
|
288
|
-
|
|
343
|
+
const root =
|
|
344
|
+
(chord.letter === undefined
|
|
345
|
+
? undefined
|
|
346
|
+
: spellOnLetter(chord.root, chord.letter)) ?? noteName(chord.root, flats);
|
|
347
|
+
return `${root}${chordSuffix(chord)}${slash}`;
|
|
289
348
|
}
|
|
290
349
|
|
|
291
350
|
/** Suffix → quality and extensions, longest first when parsing. */
|
|
@@ -350,10 +409,74 @@ const SUFFIXES: readonly (readonly [string, Quality, readonly Extension[]])[] =
|
|
|
350
409
|
["(b6)", "b6", []],
|
|
351
410
|
["addb6", "b6", []],
|
|
352
411
|
["7#9", "7#9", []],
|
|
412
|
+
// 0.7: more spellings of the same chords.
|
|
413
|
+
["-maj7", "min", ["M7"]],
|
|
414
|
+
["mmaj7", "min", ["M7"]],
|
|
415
|
+
["mMaj7", "min", ["M7"]],
|
|
416
|
+
["m(maj7)", "min", ["M7"]],
|
|
417
|
+
["-Δ7", "min", ["M7"]],
|
|
418
|
+
["mΔ7", "min", ["M7"]],
|
|
419
|
+
["add2", "maj", ["9"]],
|
|
420
|
+
["2", "maj", ["9"]],
|
|
421
|
+
["madd2", "min", ["9"]],
|
|
422
|
+
["6add9", "maj", ["6", "9"]],
|
|
423
|
+
["m6add9", "min", ["6", "9"]],
|
|
424
|
+
["-6", "min", ["6"]],
|
|
425
|
+
["-9", "min", ["m7", "9"]],
|
|
426
|
+
["dom9", "maj", ["m7", "9"]],
|
|
427
|
+
["aug(maj7)", "aug", ["M7"]],
|
|
428
|
+
["augmaj7", "aug", ["M7"]],
|
|
429
|
+
["+maj7", "aug", ["M7"]],
|
|
430
|
+
["aug9", "aug", ["m7", "9"]],
|
|
431
|
+
["+9", "aug", ["m7", "9"]],
|
|
432
|
+
["m(maj9)", "min", ["M7", "9"]],
|
|
433
|
+
["mmaj9", "min", ["M7", "9"]],
|
|
434
|
+
["dim(maj7)", "dim", ["M7"]],
|
|
435
|
+
["7(no3)", "5", ["m7"]],
|
|
436
|
+
["maj7(no3)", "5", ["M7"]],
|
|
353
437
|
];
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
438
|
+
|
|
439
|
+
/** Typed upper-tension chords (0.6.1): suffix, quality, buttons, tensions. */
|
|
440
|
+
const TENSION_SUFFIXES: readonly (readonly [
|
|
441
|
+
string,
|
|
442
|
+
Quality,
|
|
443
|
+
readonly Extension[],
|
|
444
|
+
readonly number[],
|
|
445
|
+
])[] = [
|
|
446
|
+
["11", "maj", ["m7", "9"], [17]],
|
|
447
|
+
["m11", "min", ["m7", "9"], [17]],
|
|
448
|
+
["maj11", "maj", ["M7", "9"], [17]],
|
|
449
|
+
["add11", "maj", [], [17]],
|
|
450
|
+
["madd11", "min", [], [17]],
|
|
451
|
+
["13", "maj", ["m7", "9"], [21]],
|
|
452
|
+
["m13", "min", ["m7", "9"], [21]],
|
|
453
|
+
["maj13", "maj", ["M7", "9"], [21]],
|
|
454
|
+
["7b9", "maj", ["m7"], [13]],
|
|
455
|
+
["7#11", "maj", ["m7"], [18]],
|
|
456
|
+
["maj7#11", "maj", ["M7"], [18]],
|
|
457
|
+
["M7#11", "maj", ["M7"], [18]],
|
|
458
|
+
["7b13", "maj", ["m7"], [20]],
|
|
459
|
+
["13b9", "maj", ["m7"], [13, 21]],
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
const TENSION_NAMES: Readonly<Record<number, string>> = Object.freeze({
|
|
463
|
+
13: "b9",
|
|
464
|
+
15: "#9",
|
|
465
|
+
17: "11",
|
|
466
|
+
18: "#11",
|
|
467
|
+
20: "b13",
|
|
468
|
+
21: "13",
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const SUFFIX_TABLE = new Map<string, SuffixEntry>([
|
|
472
|
+
...SUFFIXES.map(
|
|
473
|
+
([suffix, quality, ext]) => [suffix, { quality, ext }] as const,
|
|
474
|
+
),
|
|
475
|
+
...TENSION_SUFFIXES.map(
|
|
476
|
+
([suffix, quality, ext, tensions]) =>
|
|
477
|
+
[suffix, { quality, ext, tensions }] as const,
|
|
478
|
+
),
|
|
479
|
+
]);
|
|
357
480
|
|
|
358
481
|
const LETTER: Readonly<Record<string, number>> = Object.freeze({
|
|
359
482
|
c: 0,
|
|
@@ -378,6 +501,81 @@ export function parsePitchClass(text: string): number | undefined {
|
|
|
378
501
|
return mod12(LETTER[match[1]!.toLowerCase()]! + accidental);
|
|
379
502
|
}
|
|
380
503
|
|
|
504
|
+
/** Alteration → upper tension (semitones above the root). */
|
|
505
|
+
const ALTERATION_TENSION: Readonly<Record<string, number>> = Object.freeze({
|
|
506
|
+
b9: 13,
|
|
507
|
+
"#9": 15,
|
|
508
|
+
"11": 17,
|
|
509
|
+
"#11": 18,
|
|
510
|
+
"+11": 18,
|
|
511
|
+
b13: 20,
|
|
512
|
+
"13": 21,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const TOKEN = "b5|#5|\\+5|b9|#9|#11|\\+11|b13|alt|add9|add11|add13|11|13|9|6";
|
|
516
|
+
/** Bare alterations or parenthesized comma lists of them, in any order. */
|
|
517
|
+
const ALTERATIONS = new RegExp(
|
|
518
|
+
`^(?:(?:${TOKEN})|\\((?:${TOKEN})(?:,(?:${TOKEN}))*\\))+$`,
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
type SuffixEntry = {
|
|
522
|
+
quality: Quality;
|
|
523
|
+
ext: readonly Extension[];
|
|
524
|
+
tensions?: readonly number[];
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* A suffix the table does not list, read as a listed base followed by
|
|
529
|
+
* alterations, bare or in parentheses: `7#5`, `9#11`, `13#11`, `7b9b13`,
|
|
530
|
+
* `9b5`, `maj7+5`, `7alt`, `7(b9,#9)`. A raised fifth makes the triad
|
|
531
|
+
* augmented, a lowered one makes a major triad `b5` (a minor one
|
|
532
|
+
* diminished); `alt` is b9, #9, #11 and b13 over a dominant seventh.
|
|
533
|
+
*/
|
|
534
|
+
function parseAlteredSuffix(suffix: string): SuffixEntry | undefined {
|
|
535
|
+
for (let cut = suffix.length - 1; cut >= 0; cut -= 1) {
|
|
536
|
+
const base = SUFFIX_TABLE.get(suffix.slice(0, cut));
|
|
537
|
+
if (!base) continue;
|
|
538
|
+
const raw = suffix.slice(cut);
|
|
539
|
+
if (!ALTERATIONS.test(raw)) continue;
|
|
540
|
+
const rest = raw.replace(/[(),]/g, "");
|
|
541
|
+
const tokens = rest.match(
|
|
542
|
+
/b5|#5|\+5|b9|#9|#11|\+11|b13|alt|add9|add11|add13|11|13|9|6/g,
|
|
543
|
+
);
|
|
544
|
+
if (!tokens || tokens.join("") !== rest) continue;
|
|
545
|
+
let quality: Quality = base.quality;
|
|
546
|
+
const ext = new Set<Extension>(base.ext);
|
|
547
|
+
const tensions = new Set<number>(base.tensions ?? []);
|
|
548
|
+
let ok = true;
|
|
549
|
+
for (const token of tokens) {
|
|
550
|
+
if (token === "#5" || token === "+5") {
|
|
551
|
+
if (quality === "maj" || quality === "aug") quality = "aug";
|
|
552
|
+
else ok = false;
|
|
553
|
+
} else if (token === "b5") {
|
|
554
|
+
if (quality === "maj" || quality === "b5") quality = "b5";
|
|
555
|
+
else if (quality === "min" || quality === "dim") quality = "dim";
|
|
556
|
+
else ok = false;
|
|
557
|
+
} else if (token === "alt") {
|
|
558
|
+
if (quality !== "maj") ok = false;
|
|
559
|
+
ext.add("m7");
|
|
560
|
+
for (const step of [13, 15, 18, 20]) tensions.add(step);
|
|
561
|
+
} else if (token === "9" || token === "add9") ext.add("9");
|
|
562
|
+
else if (token === "6") ext.add("6");
|
|
563
|
+
else {
|
|
564
|
+
const step = ALTERATION_TENSION[token.replace(/^add/, "")];
|
|
565
|
+
if (step === undefined) ok = false;
|
|
566
|
+
else tensions.add(step);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (!ok) continue;
|
|
570
|
+
return {
|
|
571
|
+
quality,
|
|
572
|
+
ext: [...ext],
|
|
573
|
+
tensions: [...tensions].sort((a, b) => a - b),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
381
579
|
/** Parse a chord symbol (`Cm7`, `F#dim`, `Bbmaj9`, `G7sus4`, `C/E`). */
|
|
382
580
|
export function parseChord(symbol: string): Chord | undefined {
|
|
383
581
|
if (typeof symbol !== "string" || symbol.length > 24) return undefined;
|
|
@@ -388,14 +586,15 @@ export function parseChord(symbol: string): Chord | undefined {
|
|
|
388
586
|
const match = trimmed.match(/^([A-Ga-g])(#|b|♯|♭)?([^/]*)(?:\/(.+))?$/);
|
|
389
587
|
if (!match) return undefined;
|
|
390
588
|
const root = parsePitchClass(`${match[1]}${match[2] ?? ""}`);
|
|
391
|
-
const entry =
|
|
589
|
+
const entry =
|
|
590
|
+
SUFFIX_TABLE.get(match[3] ?? "") ?? parseAlteredSuffix(match[3] ?? "");
|
|
392
591
|
if (root === undefined || !entry) return undefined;
|
|
393
592
|
let bass: number | undefined;
|
|
394
593
|
if (match[4] !== undefined) {
|
|
395
594
|
bass = parsePitchClass(match[4]);
|
|
396
595
|
if (bass === undefined) return undefined;
|
|
397
596
|
}
|
|
398
|
-
return makeChord(root, entry.quality, entry.ext, bass);
|
|
597
|
+
return makeChord(root, entry.quality, entry.ext, bass, entry.tensions);
|
|
399
598
|
}
|
|
400
599
|
|
|
401
600
|
// ---------------------------------------------------------------------------
|
|
@@ -448,7 +647,15 @@ const MODE_ALIASES: Readonly<Record<string, ModeName>> = Object.freeze({
|
|
|
448
647
|
});
|
|
449
648
|
|
|
450
649
|
export type ScaleFamily =
|
|
451
|
-
|
|
650
|
+
| "pentatonic"
|
|
651
|
+
| "blues"
|
|
652
|
+
| "maqam"
|
|
653
|
+
| "dastgah"
|
|
654
|
+
| "raga"
|
|
655
|
+
| "messiaen"
|
|
656
|
+
| "chromatic"
|
|
657
|
+
| "overtone"
|
|
658
|
+
| "quarter-tone";
|
|
452
659
|
|
|
453
660
|
/**
|
|
454
661
|
* Scales beyond the chord modes, for keys such as `D bayati`, `C yaman` or
|
|
@@ -495,6 +702,12 @@ export const SCALES = Object.freeze({
|
|
|
495
702
|
family: "blues",
|
|
496
703
|
aliases: ["major blues"],
|
|
497
704
|
},
|
|
705
|
+
"yonanuki-minor": {
|
|
706
|
+
steps: [0, 2, 3, 7, 8],
|
|
707
|
+
mode: "minor",
|
|
708
|
+
family: "pentatonic",
|
|
709
|
+
aliases: ["yonanuki", "yonanuki minor", "enka minor"],
|
|
710
|
+
},
|
|
498
711
|
hijaz: {
|
|
499
712
|
steps: [0, 1, 4, 5, 7, 8, 10],
|
|
500
713
|
mode: "phrygian-dominant",
|
|
@@ -671,6 +884,32 @@ export const SCALES = Object.freeze({
|
|
|
671
884
|
mode: "harmonic-minor",
|
|
672
885
|
family: "messiaen",
|
|
673
886
|
},
|
|
887
|
+
// All twelve pitch classes: the field of free atonality and the row.
|
|
888
|
+
chromatic: {
|
|
889
|
+
steps: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
|
890
|
+
mode: "minor",
|
|
891
|
+
family: "chromatic",
|
|
892
|
+
aliases: ["twelve-tone", "aggregate"],
|
|
893
|
+
},
|
|
894
|
+
// Partials 8 to 15 of the harmonic series over the tonic (Grisey,
|
|
895
|
+
// Murail): the acoustic scale with the 11th and 13th partials' and the
|
|
896
|
+
// 7th's just pitches, which its named tuning applies.
|
|
897
|
+
"harmonic-series": {
|
|
898
|
+
steps: [0, 2, 4, 6, 7, 9, 10, 11],
|
|
899
|
+
mode: "mixolydian",
|
|
900
|
+
family: "overtone",
|
|
901
|
+
intonation: [0, 204, 386, 551, 702, 841, 969, 1088],
|
|
902
|
+
aliases: ["overtone", "overtone-scale", "partials"],
|
|
903
|
+
},
|
|
904
|
+
// Each tempered degree of the major scale beside its quarter-tone
|
|
905
|
+
// shadow (Haba, Wyschnegradsky): neutral 2nd, 3rd, 6th and 7th and a
|
|
906
|
+
// quarter-sharp 4th, sounded as note cents over twelve-tone keys.
|
|
907
|
+
"quarter-tone": {
|
|
908
|
+
steps: [0, 1.5, 2, 3.5, 4, 5, 5.5, 7, 8.5, 9, 10.5],
|
|
909
|
+
mode: "major",
|
|
910
|
+
family: "quarter-tone",
|
|
911
|
+
aliases: ["quartertone", "24-tone"],
|
|
912
|
+
},
|
|
674
913
|
} as const satisfies Record<string, ScaleInfo>);
|
|
675
914
|
export type ScaleName = keyof typeof SCALES;
|
|
676
915
|
export const SCALE_NAMES = Object.keys(SCALES) as ScaleName[];
|
|
@@ -877,9 +1116,14 @@ export function romanOf(key: Key, chord: Chord): string {
|
|
|
877
1116
|
if (degree < 0) {
|
|
878
1117
|
// Name chromatic roots against the major scale: bIII, #iv°.
|
|
879
1118
|
const major = MODES.major.map((step) => mod12(key.tonic + step));
|
|
1119
|
+
const natural = major.indexOf(chord.root);
|
|
880
1120
|
const flat = major.indexOf(mod12(chord.root + 1));
|
|
881
1121
|
const sharp = major.indexOf(mod12(chord.root - 1));
|
|
882
|
-
if (
|
|
1122
|
+
if (natural >= 0) {
|
|
1123
|
+
// A major-scale note the mode alters: ♮II in Phrygian.
|
|
1124
|
+
degree = natural;
|
|
1125
|
+
accidental = "♮";
|
|
1126
|
+
} else if (flat >= 0) {
|
|
883
1127
|
degree = flat;
|
|
884
1128
|
accidental = "b";
|
|
885
1129
|
} else {
|
|
@@ -890,7 +1134,6 @@ export function romanOf(key: Key, chord: Chord): string {
|
|
|
890
1134
|
const lower =
|
|
891
1135
|
chord.quality === "min" ||
|
|
892
1136
|
chord.quality === "dim" ||
|
|
893
|
-
chord.quality === "5" ||
|
|
894
1137
|
chord.quality === "madd4" ||
|
|
895
1138
|
chord.quality === "mb6";
|
|
896
1139
|
const numeral = NUMERALS[degree]!;
|
|
@@ -907,7 +1150,33 @@ export function romanOf(key: Key, chord: Chord): string {
|
|
|
907
1150
|
: ext.has("M7")
|
|
908
1151
|
? "maj7"
|
|
909
1152
|
: "";
|
|
910
|
-
|
|
1153
|
+
// The short numeral when it reads back as this chord; else the chord's
|
|
1154
|
+
// own suffix in brackets (`I[7]`, `i[m6]`, `V[7#9]`), which is exact.
|
|
1155
|
+
const plain = `${accidental}${body}${mark}${seventh}`;
|
|
1156
|
+
const bare = makeChord(
|
|
1157
|
+
chord.root,
|
|
1158
|
+
chord.quality,
|
|
1159
|
+
chord.extensions,
|
|
1160
|
+
undefined,
|
|
1161
|
+
chord.tensions,
|
|
1162
|
+
);
|
|
1163
|
+
for (const candidate of [
|
|
1164
|
+
plain,
|
|
1165
|
+
`${accidental}${body}${mark}${seventh === "7" ? "dom7" : seventh}`,
|
|
1166
|
+
])
|
|
1167
|
+
if (sameChord(parseRoman(key, candidate), bare)) return candidate;
|
|
1168
|
+
return `${accidental}${body}[${chordSuffix(bare)}]`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function sameChord(a: Chord | undefined, b: Chord): boolean {
|
|
1172
|
+
return (
|
|
1173
|
+
a !== undefined &&
|
|
1174
|
+
a.root === b.root &&
|
|
1175
|
+
a.quality === b.quality &&
|
|
1176
|
+
a.bass === b.bass &&
|
|
1177
|
+
a.extensions.join() === b.extensions.join() &&
|
|
1178
|
+
(a.tensions ?? []).join() === (b.tensions ?? []).join()
|
|
1179
|
+
);
|
|
911
1180
|
}
|
|
912
1181
|
|
|
913
1182
|
/**
|
|
@@ -920,31 +1189,64 @@ export function romanOf(key: Key, chord: Chord): string {
|
|
|
920
1189
|
* major). `7` adds the diatonic seventh; `maj7`/`M7` and `dom7` are exact.
|
|
921
1190
|
*/
|
|
922
1191
|
export function parseRoman(key: Key, text: string): Chord | undefined {
|
|
923
|
-
|
|
1192
|
+
return romanIn(key, text, tonicLetter(key));
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/** `parseRoman` with the tonic spelled on `tonic` (0..6, C..B). */
|
|
1196
|
+
function romanIn(key: Key, text: string, tonic: number): Chord | undefined {
|
|
1197
|
+
if (typeof text !== "string" || text.length > 32) return undefined;
|
|
924
1198
|
const trimmed = text.trim();
|
|
1199
|
+
const exact = trimmed.match(
|
|
1200
|
+
/^(b|#|♭|♯|♮)?(vii|vi|v|iv|iii|ii|i|VII|VI|V|IV|III|II|I)\[([^\]]*)\]$/,
|
|
1201
|
+
);
|
|
1202
|
+
if (exact) {
|
|
1203
|
+
// `I[7]`: the numeral names the root, the bracket is a chord suffix.
|
|
1204
|
+
const degree = NUMERALS.indexOf(exact[2]!.toLowerCase());
|
|
1205
|
+
const shift =
|
|
1206
|
+
exact[1] === "b" || exact[1] === "♭"
|
|
1207
|
+
? -1
|
|
1208
|
+
: exact[1] === "♮" || !exact[1]
|
|
1209
|
+
? 0
|
|
1210
|
+
: 1;
|
|
1211
|
+
const root = !exact[1]
|
|
1212
|
+
? scaleOf(key)[degree]
|
|
1213
|
+
: mod12(key.tonic + MODES.major[degree]! + shift);
|
|
1214
|
+
if (root === undefined) return undefined;
|
|
1215
|
+
const letter = (tonic + degree) % 7;
|
|
1216
|
+
const chord = parseChord(
|
|
1217
|
+
`${spellOnLetter(root, letter) ?? noteName(root)}${exact[3]!}`,
|
|
1218
|
+
);
|
|
1219
|
+
return chord && Object.freeze({ ...chord, letter });
|
|
1220
|
+
}
|
|
925
1221
|
const slash = trimmed.match(/^(.+)\/(.+)$/);
|
|
926
1222
|
if (slash) {
|
|
927
1223
|
// V/x: the chord built on the degree of x in the key (secondary function).
|
|
928
|
-
const target =
|
|
1224
|
+
const target = romanIn(key, slash[2]!, tonic);
|
|
929
1225
|
if (!target) return undefined;
|
|
930
|
-
|
|
931
|
-
|
|
1226
|
+
return romanIn(
|
|
1227
|
+
{ tonic: target.root, mode: "major" },
|
|
1228
|
+
slash[1]!,
|
|
1229
|
+
target.letter ?? tonic,
|
|
1230
|
+
);
|
|
932
1231
|
}
|
|
933
1232
|
const match = trimmed.match(
|
|
934
|
-
/^(b
|
|
1233
|
+
/^(b|#|♭|♯|♮)?(vii|vi|v|iv|iii|ii|i|VII|VI|V|IV|III|II|I)(°|o|ø|\+)?(maj7|M7|dom7|7|9|maj9|6|sus4|sus2|sus|add9)?$/,
|
|
935
1234
|
);
|
|
936
1235
|
if (!match) return undefined;
|
|
937
1236
|
const accidental =
|
|
938
|
-
match[1] === "b" || match[1] === "♭"
|
|
1237
|
+
match[1] === "b" || match[1] === "♭"
|
|
1238
|
+
? -1
|
|
1239
|
+
: match[1] === "#" || match[1] === "♯"
|
|
1240
|
+
? 1
|
|
1241
|
+
: 0;
|
|
939
1242
|
const numeral = match[2]!;
|
|
940
1243
|
const lower = numeral === numeral.toLowerCase();
|
|
941
1244
|
const degree = NUMERALS.indexOf(numeral.toLowerCase());
|
|
942
1245
|
const mark = match[3];
|
|
943
1246
|
const suffix = match[4] ?? "";
|
|
944
|
-
const root =
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
: mod12(key.tonic + MODES.major[degree]! + accidental);
|
|
1247
|
+
const root = !match[1]
|
|
1248
|
+
? scaleOf(key)[degree]!
|
|
1249
|
+
: mod12(key.tonic + MODES.major[degree]! + accidental);
|
|
948
1250
|
const inKey = degreeOf(key, root);
|
|
949
1251
|
const triad = inKey === undefined ? undefined : diatonicChord(key, inKey);
|
|
950
1252
|
const seventh =
|
|
@@ -998,7 +1300,10 @@ export function parseRoman(key: Key, text: string): Chord | undefined {
|
|
|
998
1300
|
quality = "sus2";
|
|
999
1301
|
break;
|
|
1000
1302
|
}
|
|
1001
|
-
return
|
|
1303
|
+
return Object.freeze({
|
|
1304
|
+
...makeChord(root, quality, ext),
|
|
1305
|
+
letter: (tonic + degree) % 7,
|
|
1306
|
+
});
|
|
1002
1307
|
}
|
|
1003
1308
|
|
|
1004
1309
|
// ---------------------------------------------------------------------------
|
|
@@ -1040,7 +1345,17 @@ export function rotate(pitches: readonly number[], steps: number): number[] {
|
|
|
1040
1345
|
|
|
1041
1346
|
export function rootPosition(chord: Chord, anchor = 60): number[] {
|
|
1042
1347
|
const rootPitch = anchor + mod12(chord.root - anchor);
|
|
1043
|
-
|
|
1348
|
+
const steps = chordIntervals(chord);
|
|
1349
|
+
// A natural 11 over a major 3rd and a 7th is the avoid-note clash (E
|
|
1350
|
+
// under F a minor 9th up in C11), so the voicing drops the 3rd: C11 is
|
|
1351
|
+
// C G Bb D F, the sus voicing; C13 already leaves the 11 out.
|
|
1352
|
+
const clash =
|
|
1353
|
+
steps.includes(4) &&
|
|
1354
|
+
steps.includes(17) &&
|
|
1355
|
+
(steps.includes(10) || steps.includes(11));
|
|
1356
|
+
return steps
|
|
1357
|
+
.filter((step) => !(clash && step === 4))
|
|
1358
|
+
.map((step) => rootPitch + step);
|
|
1044
1359
|
}
|
|
1045
1360
|
|
|
1046
1361
|
/** Open voicings: `open` drops the second voice from the top an octave
|
|
@@ -1209,6 +1524,8 @@ export const PERFORM_MODES = [
|
|
|
1209
1524
|
"harp",
|
|
1210
1525
|
"slop",
|
|
1211
1526
|
"pattern",
|
|
1527
|
+
// 0.6.1: fretboard-voiced strokes (see `voiceGuitar`, `strokeVoicing`).
|
|
1528
|
+
"guitar",
|
|
1212
1529
|
] as const;
|
|
1213
1530
|
export type PerformMode = (typeof PERFORM_MODES)[number];
|
|
1214
1531
|
|
|
@@ -1228,6 +1545,23 @@ export type PerformOptions = Readonly<{
|
|
|
1228
1545
|
pattern?: string | number;
|
|
1229
1546
|
/** 0..1. */
|
|
1230
1547
|
velocity?: number;
|
|
1548
|
+
/** Guitar mode: the stroke grid (a STROKE_PATTERNS name or D U d u x - .). */
|
|
1549
|
+
strokes?: string;
|
|
1550
|
+
/** Guitar mode: grid step in beats, default 1/2. */
|
|
1551
|
+
step?: number;
|
|
1552
|
+
/** Guitar mode: seconds a full six-string down stroke takes, default 0.022. */
|
|
1553
|
+
speed?: number;
|
|
1554
|
+
/** Guitar mode: tempo in BPM that `speed` converts with, default 120. */
|
|
1555
|
+
tempo?: number;
|
|
1556
|
+
/** Guitar mode: the guitar's tuning, capo, hand, ring and position. */
|
|
1557
|
+
guitar?: GuitarSetup;
|
|
1558
|
+
/** Guitar mode: the chord's root pitch class (default: the lowest note). */
|
|
1559
|
+
root?: number;
|
|
1560
|
+
/**
|
|
1561
|
+
* Guitar mode: a slash bass pitch class that must sound lowest, even when
|
|
1562
|
+
* it is not a chord tone (B/E keeps its E pedal). Default: the lowest note.
|
|
1563
|
+
*/
|
|
1564
|
+
slash?: number;
|
|
1231
1565
|
}>;
|
|
1232
1566
|
|
|
1233
1567
|
export type PerformedNote = Readonly<{
|
|
@@ -1544,6 +1878,24 @@ export function perform(
|
|
|
1544
1878
|
}
|
|
1545
1879
|
return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
|
|
1546
1880
|
}
|
|
1881
|
+
case "guitar": {
|
|
1882
|
+
const bass = options.slash ?? notes[0]!;
|
|
1883
|
+
const voicing = voiceGuitar(
|
|
1884
|
+
notes.map(mod12),
|
|
1885
|
+
mod12(bass),
|
|
1886
|
+
options.root ?? mod12(bass),
|
|
1887
|
+
options.guitar,
|
|
1888
|
+
);
|
|
1889
|
+
if (!voicing) return notes.map((pitch) => at(pitch, start, end));
|
|
1890
|
+
return strokeVoicing(voicing, start, length, {
|
|
1891
|
+
...(options.strokes !== undefined ? { strokes: options.strokes } : {}),
|
|
1892
|
+
...(options.step !== undefined ? { step: options.step } : {}),
|
|
1893
|
+
...(options.speed !== undefined ? { speed: options.speed } : {}),
|
|
1894
|
+
...(options.tempo !== undefined ? { tempo: options.tempo } : {}),
|
|
1895
|
+
velocity,
|
|
1896
|
+
strings: guitarStrings(options.guitar?.tune).length,
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1547
1899
|
case "harp": {
|
|
1548
1900
|
const gap = Math.max(0, options.strum ?? DEFAULT_STRUM * 2);
|
|
1549
1901
|
return spanned.map((pitch, index) =>
|
|
@@ -1578,6 +1930,382 @@ export function perform(
|
|
|
1578
1930
|
}
|
|
1579
1931
|
}
|
|
1580
1932
|
|
|
1933
|
+
// ---------------------------------------------------------------------------
|
|
1934
|
+
// Fretboard (0.6.1): guitar voicings and strokes
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* Open-string MIDI pitches, low string to high, for `guitar tune <name>`.
|
|
1938
|
+
* Ported from the guitar design lane (proto/guitar/strum.ts TUNINGS).
|
|
1939
|
+
*/
|
|
1940
|
+
export const GUITAR_TUNINGS = Object.freeze({
|
|
1941
|
+
standard: [40, 45, 50, 55, 59, 64],
|
|
1942
|
+
dropd: [38, 45, 50, 55, 59, 64],
|
|
1943
|
+
doubledropd: [38, 45, 50, 55, 59, 62],
|
|
1944
|
+
dadgad: [38, 45, 50, 55, 57, 62],
|
|
1945
|
+
openg: [38, 43, 50, 55, 59, 62],
|
|
1946
|
+
opend: [38, 45, 50, 54, 57, 62],
|
|
1947
|
+
opene: [40, 47, 52, 56, 59, 64],
|
|
1948
|
+
halfdown: [39, 44, 49, 54, 58, 63],
|
|
1949
|
+
nashville: [52, 57, 62, 67, 59, 64],
|
|
1950
|
+
bass: [28, 33, 38, 43],
|
|
1951
|
+
ukulele: [67, 60, 64, 69],
|
|
1952
|
+
requinto: [45, 50, 55, 60, 64, 69],
|
|
1953
|
+
} as const satisfies Record<string, readonly number[]>);
|
|
1954
|
+
export type GuitarTuningName = keyof typeof GUITAR_TUNINGS;
|
|
1955
|
+
export const GUITAR_TUNING_NAMES = Object.keys(
|
|
1956
|
+
GUITAR_TUNINGS,
|
|
1957
|
+
) as GuitarTuningName[];
|
|
1958
|
+
|
|
1959
|
+
/** How a guitar is set up for voicing chords (`Track.guitar`). */
|
|
1960
|
+
export type GuitarSetup = Readonly<{
|
|
1961
|
+
/** A GUITAR_TUNINGS name or open-string pitches low to high. */
|
|
1962
|
+
tune?: string | readonly number[];
|
|
1963
|
+
/** Capo fret 0..12. */
|
|
1964
|
+
capo?: number;
|
|
1965
|
+
/** Hand stretch in frets 3..6 (default 4: one fret per finger). */
|
|
1966
|
+
hand?: number;
|
|
1967
|
+
/** 0 closed shapes (no open strings) .. 1 prefer ringing open strings. */
|
|
1968
|
+
ring?: number;
|
|
1969
|
+
/** Preferred fret position 0..12 (0 open position). */
|
|
1970
|
+
position?: number;
|
|
1971
|
+
}>;
|
|
1972
|
+
|
|
1973
|
+
/** One fretted chord: frets per string low to high (-1 muted). */
|
|
1974
|
+
export type GuitarVoicing = Readonly<{
|
|
1975
|
+
frets: readonly number[];
|
|
1976
|
+
/** Sounding pitches, low string to high. */
|
|
1977
|
+
pitches: readonly number[];
|
|
1978
|
+
/** The string each pitch sounds on. */
|
|
1979
|
+
strings: readonly number[];
|
|
1980
|
+
}>;
|
|
1981
|
+
|
|
1982
|
+
/** Open-string pitches for a tuning name or list (standard when unknown). */
|
|
1983
|
+
export function guitarStrings(tune: GuitarSetup["tune"]): readonly number[] {
|
|
1984
|
+
if (Array.isArray(tune)) return tune as readonly number[];
|
|
1985
|
+
const name = String(tune ?? "standard")
|
|
1986
|
+
.toLowerCase()
|
|
1987
|
+
.replace(/[\s_-]/g, "");
|
|
1988
|
+
return (
|
|
1989
|
+
(GUITAR_TUNINGS as Record<string, readonly number[]>)[name] ??
|
|
1990
|
+
GUITAR_TUNINGS.standard
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Fingers a fretting uses. Strings at the lowest fret count as one barre
|
|
1996
|
+
* only when no open string lies between them (a barre cannot skip an open
|
|
1997
|
+
* string); otherwise one finger each.
|
|
1998
|
+
*/
|
|
1999
|
+
export function guitarFingers(frets: readonly number[]): number {
|
|
2000
|
+
const fretted = frets.filter((f) => f > 0);
|
|
2001
|
+
if (fretted.length === 0) return 0;
|
|
2002
|
+
const min = Math.min(...fretted);
|
|
2003
|
+
const at = frets.flatMap((f, i) => (f === min ? [i] : []));
|
|
2004
|
+
const between = frets.slice(at[0]!, at[at.length - 1]! + 1);
|
|
2005
|
+
const barre = at.length > 1 && !between.includes(0);
|
|
2006
|
+
return (barre ? 1 : at.length) + fretted.filter((f) => f > min).length;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* The chord tones a voicing must keep, most required first, after dropping
|
|
2011
|
+
* `drops` tones in the guitarist's order: the 5th, then the 9th; the root,
|
|
2012
|
+
* the 3rd and the 7th always stay (after the design lane's review).
|
|
2013
|
+
*/
|
|
2014
|
+
function requiredTones(tones: readonly number[], root: number, drops: number) {
|
|
2015
|
+
// The 5th, then the 11th (only beside a 3rd: a sus4 keeps its 4th), then
|
|
2016
|
+
// the 9th. dawg's symbols reach an 11th through m(add4)/madd4 (+9).
|
|
2017
|
+
const third = tones.some((pc) => [3, 4].includes(mod12(pc - root)));
|
|
2018
|
+
const order = (third ? [7, 5, 2] : [7, 2]).map((step) => mod12(root + step));
|
|
2019
|
+
const dropped = order.filter((pc) => tones.includes(pc)).slice(0, drops);
|
|
2020
|
+
return tones.filter((pc) => !dropped.includes(pc));
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
/**
|
|
2024
|
+
* The most playable fretting of pitch classes `tones` over `bass` on a
|
|
2025
|
+
* guitar `setup`: the bass is the lowest sounding note, every required
|
|
2026
|
+
* tone sounds, at most four fingers within a `hand`-fret stretch, mutes
|
|
2027
|
+
* only under the bass or one inside, and no barre over an open string.
|
|
2028
|
+
* Cost prefers more strings, low positions, open strings (by `ring`), a
|
|
2029
|
+
* fifth present, no doubled third, and small moves from `previous`.
|
|
2030
|
+
* Undefined when nothing fits. Ported from proto/guitar/strum.ts.
|
|
2031
|
+
*/
|
|
2032
|
+
export function voiceGuitar(
|
|
2033
|
+
tones: readonly number[],
|
|
2034
|
+
bass: number,
|
|
2035
|
+
root: number,
|
|
2036
|
+
setup: GuitarSetup = {},
|
|
2037
|
+
previous?: readonly number[],
|
|
2038
|
+
): GuitarVoicing | undefined {
|
|
2039
|
+
// Play mode voices every pad press; dense chords at a wide hand take
|
|
2040
|
+
// ~15 ms to search, so repeats come from a small LRU (results are frozen).
|
|
2041
|
+
const key = JSON.stringify([
|
|
2042
|
+
[...new Set([...tones, bass].map(mod12))],
|
|
2043
|
+
mod12(bass),
|
|
2044
|
+
mod12(root),
|
|
2045
|
+
setup.tune ?? null,
|
|
2046
|
+
setup.capo ?? null,
|
|
2047
|
+
setup.hand ?? null,
|
|
2048
|
+
setup.ring ?? null,
|
|
2049
|
+
setup.position ?? null,
|
|
2050
|
+
previous ?? null,
|
|
2051
|
+
]);
|
|
2052
|
+
if (voicingCache.has(key)) {
|
|
2053
|
+
const hit = voicingCache.get(key);
|
|
2054
|
+
voicingCache.delete(key);
|
|
2055
|
+
voicingCache.set(key, hit);
|
|
2056
|
+
return hit;
|
|
2057
|
+
}
|
|
2058
|
+
const voiced = searchGuitar(tones, bass, root, setup, previous);
|
|
2059
|
+
voicingCache.set(key, voiced);
|
|
2060
|
+
if (voicingCache.size > VOICING_CACHE_SIZE)
|
|
2061
|
+
voicingCache.delete(voicingCache.keys().next().value!);
|
|
2062
|
+
return voiced;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
const VOICING_CACHE_SIZE = 256;
|
|
2066
|
+
const voicingCache = new Map<string, GuitarVoicing | undefined>();
|
|
2067
|
+
|
|
2068
|
+
function searchGuitar(
|
|
2069
|
+
tones: readonly number[],
|
|
2070
|
+
bass: number,
|
|
2071
|
+
root: number,
|
|
2072
|
+
setup: GuitarSetup,
|
|
2073
|
+
previous?: readonly number[],
|
|
2074
|
+
): GuitarVoicing | undefined {
|
|
2075
|
+
const pcs = [...new Set([...tones, bass].map(mod12))];
|
|
2076
|
+
const capo = clampInt(setup.capo ?? 0, 0, 12);
|
|
2077
|
+
const open = guitarStrings(setup.tune).map((pitch) => pitch + capo);
|
|
2078
|
+
const n = open.length;
|
|
2079
|
+
const ring = Math.min(1, Math.max(0, setup.ring ?? 0.5));
|
|
2080
|
+
const stretch = clampInt(setup.hand ?? 4, 3, 6);
|
|
2081
|
+
const fifth = mod12(root + 7);
|
|
2082
|
+
const third = pcs.find((pc) => [3, 4].includes(mod12(pc - root)));
|
|
2083
|
+
const prev = previous ? [...previous].sort((a, b) => a - b) : undefined;
|
|
2084
|
+
for (let drops = 0; drops <= 3; drops += 1) {
|
|
2085
|
+
const required = requiredTones(pcs, root, drops);
|
|
2086
|
+
let best: { frets: number[]; cost: number } | undefined;
|
|
2087
|
+
const choose = (frets: readonly number[]) => {
|
|
2088
|
+
const idx = frets.flatMap((f, i) => (f < 0 ? [] : [i]));
|
|
2089
|
+
if (idx.length < Math.min(n, n >= 6 ? 4 : 3)) return;
|
|
2090
|
+
const interior = frets.slice(idx[0]).filter((f) => f < 0).length;
|
|
2091
|
+
if (interior > (n >= 6 ? 1 : 0)) return;
|
|
2092
|
+
const pitches = idx.map((i) => open[i]! + frets[i]!);
|
|
2093
|
+
if (mod12(Math.min(...pitches)) !== mod12(bass)) return;
|
|
2094
|
+
const sounding = new Set(pitches.map(mod12));
|
|
2095
|
+
if (required.some((pc) => !sounding.has(pc))) return;
|
|
2096
|
+
const fingers = guitarFingers(frets);
|
|
2097
|
+
if (fingers > 4) return;
|
|
2098
|
+
const fretted = frets.filter((f) => f > 0);
|
|
2099
|
+
const span = fretted.length
|
|
2100
|
+
? Math.max(...fretted) - Math.min(...fretted)
|
|
2101
|
+
: 0;
|
|
2102
|
+
if (span > stretch - 1) return;
|
|
2103
|
+
const opens = frets.filter((f) => f === 0).length;
|
|
2104
|
+
if (ring === 0 && opens > 0) return;
|
|
2105
|
+
const pos = fretted.length ? Math.min(...fretted) : 0;
|
|
2106
|
+
let cost =
|
|
2107
|
+
span * 0.6 +
|
|
2108
|
+
pos * 0.25 +
|
|
2109
|
+
fingers * 0.4 -
|
|
2110
|
+
idx.length -
|
|
2111
|
+
opens * 0.7 * ring;
|
|
2112
|
+
cost += interior * 1.5;
|
|
2113
|
+
if (!sounding.has(fifth) && pcs.includes(fifth)) cost += 0.6;
|
|
2114
|
+
if (
|
|
2115
|
+
third !== undefined &&
|
|
2116
|
+
pitches.filter((p) => mod12(p) === third).length > 1
|
|
2117
|
+
)
|
|
2118
|
+
cost += 0.5;
|
|
2119
|
+
if (setup.position !== undefined)
|
|
2120
|
+
cost += Math.abs(pos - setup.position) * 0.5;
|
|
2121
|
+
if (prev && prev.length) {
|
|
2122
|
+
const cur = [...pitches].sort((a, b) => a - b);
|
|
2123
|
+
let move = 0;
|
|
2124
|
+
for (let i = 0; i < cur.length; i += 1)
|
|
2125
|
+
move += Math.abs(cur[i]! - prev[Math.min(i, prev.length - 1)]!);
|
|
2126
|
+
cost += move * 0.05;
|
|
2127
|
+
}
|
|
2128
|
+
if (
|
|
2129
|
+
!best ||
|
|
2130
|
+
cost < best.cost - 1e-9 ||
|
|
2131
|
+
(Math.abs(cost - best.cost) <= 1e-9 && frets.join() < best.frets.join())
|
|
2132
|
+
)
|
|
2133
|
+
best = { frets: [...frets], cost };
|
|
2134
|
+
};
|
|
2135
|
+
for (let p = 1; p <= 12; p += 1) {
|
|
2136
|
+
const options = open.map((o) => {
|
|
2137
|
+
const list = [-1];
|
|
2138
|
+
if (pcs.includes(mod12(o))) list.push(0);
|
|
2139
|
+
for (let f = p; f < p + stretch; f += 1)
|
|
2140
|
+
if (pcs.includes(mod12(o + f))) list.push(f);
|
|
2141
|
+
return list;
|
|
2142
|
+
});
|
|
2143
|
+
const frets = new Array<number>(n).fill(-1);
|
|
2144
|
+
const walk = (s: number): void => {
|
|
2145
|
+
if (s === n) return choose(frets);
|
|
2146
|
+
for (const f of options[s]!) {
|
|
2147
|
+
frets[s] = f;
|
|
2148
|
+
walk(s + 1);
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
walk(0);
|
|
2152
|
+
}
|
|
2153
|
+
if (best) {
|
|
2154
|
+
const { frets } = best as { frets: number[] };
|
|
2155
|
+
const strings = frets.flatMap((f, i) => (f < 0 ? [] : [i]));
|
|
2156
|
+
return Object.freeze({
|
|
2157
|
+
frets: Object.freeze(frets),
|
|
2158
|
+
pitches: Object.freeze(strings.map((i) => open[i]! + frets[i]!)),
|
|
2159
|
+
strings: Object.freeze(strings),
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return undefined;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
/** A voicing as tab, low string first: `x 3 2 0 1 0`. */
|
|
2167
|
+
export function guitarTab(frets: readonly number[]): string {
|
|
2168
|
+
return frets.map((f) => (f < 0 ? "x" : String(f))).join(" ");
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Stroke grids for perform mode `guitar`, one character per step: D down
|
|
2173
|
+
* (all strings), U up (top four), d light down (top four), u light up (top
|
|
2174
|
+
* three), x muted chuck, `-` or `.` rest (strings ring on). After the
|
|
2175
|
+
* design lane's STRUM_PATTERNS.
|
|
2176
|
+
*/
|
|
2177
|
+
export const STROKE_PATTERNS = Object.freeze({
|
|
2178
|
+
down: "D",
|
|
2179
|
+
folk: "D-DU-UDU",
|
|
2180
|
+
pop: "D-DU-UD-",
|
|
2181
|
+
punk: "DDDDDDDD",
|
|
2182
|
+
funk: "xUxUDUxUxUDUxUxU",
|
|
2183
|
+
reggae: "-D-D",
|
|
2184
|
+
waltz: "Ddd",
|
|
2185
|
+
jangle: "D-DUDUDU",
|
|
2186
|
+
island: "D-DU-UDU",
|
|
2187
|
+
} as const satisfies Record<string, string>);
|
|
2188
|
+
export const STROKE_PATTERN_NAMES = Object.keys(STROKE_PATTERNS);
|
|
2189
|
+
const STROKE_CHARS = /^[DUdux.\-]+$/;
|
|
2190
|
+
|
|
2191
|
+
/** Default seconds a full six-string down stroke takes (22 ms). */
|
|
2192
|
+
export const DEFAULT_STROKE_SPEED = 0.022;
|
|
2193
|
+
/** Default stroke grid step in beats (8ths). */
|
|
2194
|
+
export const DEFAULT_STROKE_STEP = 0.5;
|
|
2195
|
+
|
|
2196
|
+
/** A stroke grid by name, or a literal grid of D U d u x - .; undefined if bad. */
|
|
2197
|
+
export function strokeGrid(text: string | undefined): string | undefined {
|
|
2198
|
+
const raw = (text ?? "down").trim();
|
|
2199
|
+
const named = (STROKE_PATTERNS as Record<string, string>)[raw.toLowerCase()];
|
|
2200
|
+
if (named) return named;
|
|
2201
|
+
const grid = raw.replace(/[|\s]/g, "");
|
|
2202
|
+
return grid.length > 0 && grid.length <= 64 && STROKE_CHARS.test(grid)
|
|
2203
|
+
? grid
|
|
2204
|
+
: undefined;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/**
|
|
2208
|
+
* Strum fretted `voicing` from `start` for `length` beats with stroke grid
|
|
2209
|
+
* `strokes` (repeats every grid length, one step per `step` beats). A full
|
|
2210
|
+
* down stroke sweeps the six strings in `speed` seconds at `tempo` BPM;
|
|
2211
|
+
* each struck string rings until it is struck again or the chord ends; a
|
|
2212
|
+
* chuck (x) is a short muted hit that stops the strings. Velocity accents
|
|
2213
|
+
* downbeats and lightens upstrokes.
|
|
2214
|
+
*/
|
|
2215
|
+
export function strokeVoicing(
|
|
2216
|
+
voicing: GuitarVoicing,
|
|
2217
|
+
start: number,
|
|
2218
|
+
length: number,
|
|
2219
|
+
options: Readonly<{
|
|
2220
|
+
strokes?: string;
|
|
2221
|
+
step?: number;
|
|
2222
|
+
speed?: number;
|
|
2223
|
+
tempo?: number;
|
|
2224
|
+
velocity?: number;
|
|
2225
|
+
strings?: number;
|
|
2226
|
+
}> = {},
|
|
2227
|
+
): PerformedNote[] {
|
|
2228
|
+
const grid = strokeGrid(options.strokes) ?? "D";
|
|
2229
|
+
const step =
|
|
2230
|
+
options.step && options.step > 0 ? options.step : DEFAULT_STROKE_STEP;
|
|
2231
|
+
const tempo = options.tempo && options.tempo > 0 ? options.tempo : 120;
|
|
2232
|
+
const sweep =
|
|
2233
|
+
(Math.max(0, options.speed ?? DEFAULT_STROKE_SPEED) * tempo) / 60;
|
|
2234
|
+
const base = options.velocity ?? 0.8;
|
|
2235
|
+
const total = Math.max(2, options.strings ?? 6);
|
|
2236
|
+
const sounding = voicing.strings;
|
|
2237
|
+
const end = start + length;
|
|
2238
|
+
type Open = { note: PerformedNote; index: number };
|
|
2239
|
+
const ringing = new Map<number, Open>();
|
|
2240
|
+
const out: PerformedNote[] = [];
|
|
2241
|
+
const stop = (string: number, at: number) => {
|
|
2242
|
+
const held = ringing.get(string);
|
|
2243
|
+
if (!held) return;
|
|
2244
|
+
const cut = round6(
|
|
2245
|
+
Math.max(1e-3, Math.min(held.note.length, at - held.note.start)),
|
|
2246
|
+
);
|
|
2247
|
+
out[held.index] = { ...held.note, length: cut };
|
|
2248
|
+
ringing.delete(string);
|
|
2249
|
+
};
|
|
2250
|
+
const steps = Math.max(1, Math.round(length / step));
|
|
2251
|
+
for (let s = 0; s < steps; s += 1) {
|
|
2252
|
+
const ch = grid[s % grid.length]!;
|
|
2253
|
+
if (ch === "-" || ch === ".") continue;
|
|
2254
|
+
const t0 = start + s * step;
|
|
2255
|
+
if (t0 >= end - 1e-9) break;
|
|
2256
|
+
const beat = s * step;
|
|
2257
|
+
const accent =
|
|
2258
|
+
Math.abs(beat - Math.round(beat)) < 1e-9
|
|
2259
|
+
? Math.round(beat) % 2 === 0
|
|
2260
|
+
? 1
|
|
2261
|
+
: 0.92
|
|
2262
|
+
: 0.82;
|
|
2263
|
+
let order: number[];
|
|
2264
|
+
let velocity = base * accent;
|
|
2265
|
+
let ring = end - t0;
|
|
2266
|
+
const all = sounding.map((_, k) => k);
|
|
2267
|
+
switch (ch) {
|
|
2268
|
+
case "D":
|
|
2269
|
+
order = all;
|
|
2270
|
+
break;
|
|
2271
|
+
case "U":
|
|
2272
|
+
order = all.slice(-4).reverse();
|
|
2273
|
+
velocity *= 0.85;
|
|
2274
|
+
break;
|
|
2275
|
+
case "d":
|
|
2276
|
+
order = all.slice(-4);
|
|
2277
|
+
velocity *= 0.62;
|
|
2278
|
+
break;
|
|
2279
|
+
case "u":
|
|
2280
|
+
order = all.slice(-3).reverse();
|
|
2281
|
+
velocity *= 0.55;
|
|
2282
|
+
break;
|
|
2283
|
+
default:
|
|
2284
|
+
order = all;
|
|
2285
|
+
velocity *= 0.5;
|
|
2286
|
+
ring = Math.min(ring, 0.03 * (tempo / 60));
|
|
2287
|
+
}
|
|
2288
|
+
const first = sounding[order[0]!]!;
|
|
2289
|
+
order.forEach((k, position) => {
|
|
2290
|
+
const string = sounding[k]!;
|
|
2291
|
+
const at = t0 + (Math.abs(string - first) / (total - 1)) * sweep;
|
|
2292
|
+
if (at >= end - 1e-9) return;
|
|
2293
|
+
stop(string, at);
|
|
2294
|
+
const note: PerformedNote = {
|
|
2295
|
+
pitch: voicing.pitches[k]!,
|
|
2296
|
+
start: round6(at),
|
|
2297
|
+
length: round6(Math.max(1e-3, Math.min(ring, end - at))),
|
|
2298
|
+
velocity: round6(
|
|
2299
|
+
Math.max(0.05, Math.min(1, velocity * (1 - 0.04 * position))),
|
|
2300
|
+
),
|
|
2301
|
+
};
|
|
2302
|
+
out.push(note);
|
|
2303
|
+
if (ch !== "x") ringing.set(string, { note, index: out.length - 1 });
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
1581
2309
|
// ---------------------------------------------------------------------------
|
|
1582
2310
|
// Progressions
|
|
1583
2311
|
|
|
@@ -1969,6 +2697,14 @@ export function renderProgression(options: RenderOptions): RenderedProgression {
|
|
|
1969
2697
|
...perform(chord.pitches, at, span, {
|
|
1970
2698
|
...options.perform,
|
|
1971
2699
|
seed: (options.perform?.seed ?? 0) + index,
|
|
2700
|
+
...(options.perform?.mode === "guitar"
|
|
2701
|
+
? {
|
|
2702
|
+
root: options.chords[index]!.root,
|
|
2703
|
+
...(options.chords[index]!.bass !== undefined
|
|
2704
|
+
? { slash: options.chords[index]!.bass }
|
|
2705
|
+
: {}),
|
|
2706
|
+
}
|
|
2707
|
+
: {}),
|
|
1972
2708
|
}),
|
|
1973
2709
|
);
|
|
1974
2710
|
const source = options.chords[index]!;
|
|
@@ -2000,6 +2736,6 @@ export function renderProgression(options: RenderOptions): RenderedProgression {
|
|
|
2000
2736
|
export const CHORD_PROCESS = [
|
|
2001
2737
|
"Chords (Orchid-style): pick chords from the song key's diatonic set (in C major: C Dm Em F G Am Bdim; minor keys i ii° III iv v VI VII, V major for cadences).",
|
|
2002
2738
|
"Move tonic (I vi iii) → predominant (IV ii) → dominant (V vii°) → tonic; loops end on V or IV to lead home.",
|
|
2003
|
-
"Add sevenths/9ths as
|
|
2739
|
+
"Add sevenths/9ths as color (6, m7, M7, 9 extensions); voice-lead each chord to the inversion nearest the previous one in C3–G5 so common tones hold; put the root an octave or two below as bass.",
|
|
2004
2740
|
"Use suggest_progression to get voice-led chords and write_chords to write them (block, strum, arpeggio, harp, slop or a rhythm pattern; bassMode chords|unison|single|solo), instead of hand-placing chord notes with add_notes.",
|
|
2005
2741
|
].join(" ");
|