@pepperhorn/chordl-core 0.3.4 → 0.4.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/dist/chord-sheet/codec.d.ts +1 -1
- package/dist/chord-sheet/codec.js +1 -1
- package/dist/chord-sheet/codec.js.map +1 -1
- package/dist/chord-sheet/defaults.d.ts +1 -1
- package/dist/chord-sheet/index.d.ts +3 -3
- package/dist/chord-sheet/index.js +3 -3
- package/dist/chord-sheet/index.js.map +1 -1
- package/dist/engine/auto-fingering.js +1 -1
- package/dist/engine/auto-fingering.js.map +1 -1
- package/dist/engine/highlight-mapper.d.ts +1 -1
- package/dist/engine/highlight-mapper.js +2 -2
- package/dist/engine/highlight-mapper.js.map +1 -1
- package/dist/engine/keyboard-layout.d.ts +1 -1
- package/dist/engine/keyboard-layout.js +1 -1
- package/dist/engine/keyboard-layout.js.map +1 -1
- package/dist/engine/mei-builder.d.ts +27 -0
- package/dist/engine/mei-builder.js +138 -0
- package/dist/engine/mei-builder.js.map +1 -0
- package/dist/engine/staff-glyphs.js +5 -10
- package/dist/engine/staff-glyphs.js.map +1 -1
- package/dist/engine/staff-layout.js +2 -2
- package/dist/engine/staff-layout.js.map +1 -1
- package/dist/index.d.ts +48 -42
- package/dist/index.js +32 -28
- package/dist/index.js.map +1 -1
- package/dist/parser/natural-language.d.ts +1 -1
- package/dist/parser/natural-language.js +241 -31
- package/dist/parser/natural-language.js.map +1 -1
- package/dist/parser/progression-parser.d.ts +1 -1
- package/dist/parser/progression-parser.js +1 -1
- package/dist/parser/progression-parser.js.map +1 -1
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +46 -25
- package/dist/pipeline.js.map +1 -1
- package/dist/progression/index.d.ts +5 -5
- package/dist/progression/index.js +3 -3
- package/dist/progression/index.js.map +1 -1
- package/dist/progression/progression-resolver.js +5 -5
- package/dist/progression/progression-resolver.js.map +1 -1
- package/dist/progression/roman-numeral.js +1 -1
- package/dist/progression/roman-numeral.js.map +1 -1
- package/dist/resolver/auto-layout.d.ts +1 -1
- package/dist/resolver/auto-layout.js +2 -2
- package/dist/resolver/auto-layout.js.map +1 -1
- package/dist/resolver/chord-resolver.d.ts +11 -1
- package/dist/resolver/chord-resolver.js +28 -2
- package/dist/resolver/chord-resolver.js.map +1 -1
- package/dist/resolver/scale-resolver.d.ts +5 -2
- package/dist/resolver/scale-resolver.js +28 -8
- package/dist/resolver/scale-resolver.js.map +1 -1
- package/dist/themes/boomwhacker.d.ts +1 -1
- package/dist/themes/boomwhacker.js +1 -1
- package/dist/themes/boomwhacker.js.map +1 -1
- package/dist/themes/crf.d.ts +1 -1
- package/dist/themes/crf.js +1 -1
- package/dist/themes/crf.js.map +1 -1
- package/dist/themes/index.d.ts +1 -1
- package/dist/themes/index.js +3 -3
- package/dist/themes/index.js.map +1 -1
- package/dist/themes/simple.d.ts +1 -1
- package/dist/themes/simple.js +1 -1
- package/dist/themes/simple.js.map +1 -1
- package/dist/theory/constrained-variants.d.ts +46 -0
- package/dist/theory/constrained-variants.js +117 -0
- package/dist/theory/constrained-variants.js.map +1 -0
- package/dist/theory/degree-labels.d.ts +39 -0
- package/dist/theory/degree-labels.js +102 -1
- package/dist/theory/degree-labels.js.map +1 -1
- package/dist/theory/hand-constraints.d.ts +104 -0
- package/dist/theory/hand-constraints.js +214 -0
- package/dist/theory/hand-constraints.js.map +1 -0
- package/dist/types.d.ts +9 -1
- package/package.json +4 -4
|
@@ -3,7 +3,10 @@ export interface ResolvedScale {
|
|
|
3
3
|
notes: string[];
|
|
4
4
|
root: string;
|
|
5
5
|
type: string;
|
|
6
|
-
/** Single-octave Tonal intervals (e.g. ["1P","2M","3M","4P","5P","6M","7M"])
|
|
6
|
+
/** Single-octave Tonal intervals (e.g. ["1P","2M","3M","4P","5P","6M","7M"]),
|
|
7
|
+
* rotated to match `notes` when a custom starting note is used. */
|
|
7
8
|
intervals: string[];
|
|
9
|
+
/** Index into the un-rotated scale that `notes` starts on (0 = root). */
|
|
10
|
+
startIndex: number;
|
|
8
11
|
}
|
|
9
|
-
export declare function resolveScale(root: string, scaleType: string, direction?: "ascending" | "descending", octaves?: number): ResolvedScale;
|
|
12
|
+
export declare function resolveScale(root: string, scaleType: string, direction?: "ascending" | "descending", octaves?: number, startOn?: string): ResolvedScale;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scale resolver — resolves scale names to notes via Tonal.
|
|
3
|
-
* Handles melodic minor direction, multi-octave expansion,
|
|
4
|
-
* generates Tonal interval arrays for degree
|
|
3
|
+
* Handles melodic minor direction, multi-octave expansion, rotation to a
|
|
4
|
+
* custom starting note, and generates Tonal interval arrays for degree
|
|
5
|
+
* label computation.
|
|
5
6
|
*/
|
|
6
|
-
import { Scale } from "tonal";
|
|
7
|
+
import { Note, Scale } from "tonal";
|
|
7
8
|
/** Normalize common scale name variants for Tonal compatibility. */
|
|
8
9
|
const SCALE_NAME_MAP = {
|
|
9
10
|
"natural minor": "minor",
|
|
@@ -14,7 +15,7 @@ const SCALE_NAME_MAP = {
|
|
|
14
15
|
"whole tone": "whole tone",
|
|
15
16
|
"bebop": "bebop major",
|
|
16
17
|
};
|
|
17
|
-
export function resolveScale(root, scaleType, direction, octaves = 1) {
|
|
18
|
+
export function resolveScale(root, scaleType, direction, octaves = 1, startOn) {
|
|
18
19
|
// Melodic minor descending = natural minor
|
|
19
20
|
let effectiveType = scaleType;
|
|
20
21
|
if (scaleType === "melodic minor" && direction === "descending") {
|
|
@@ -26,20 +27,39 @@ export function resolveScale(root, scaleType, direction, octaves = 1) {
|
|
|
26
27
|
if (scale.empty) {
|
|
27
28
|
throw new Error(`Unknown scale: "${root} ${scaleType}"`);
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
let oneOctaveNotes = scale.notes;
|
|
31
|
+
let intervals = scale.intervals;
|
|
32
|
+
let startIndex = 0;
|
|
33
|
+
// "starting on X" — rotate the scale so it runs one octave (or N octaves)
|
|
34
|
+
// from that note instead of the root. Matched by pitch (chroma) so an
|
|
35
|
+
// enharmonic spelling like G# still finds Ab.
|
|
36
|
+
if (startOn) {
|
|
37
|
+
const targetChroma = Note.chroma(startOn);
|
|
38
|
+
startIndex = oneOctaveNotes.findIndex((n) => Note.chroma(n) === targetChroma);
|
|
39
|
+
if (startIndex < 0) {
|
|
40
|
+
throw new Error(`${startOn} isn't in ${root} ${scaleType} — the notes are: ${oneOctaveNotes.join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
if (startIndex > 0) {
|
|
43
|
+
oneOctaveNotes = [
|
|
44
|
+
...oneOctaveNotes.slice(startIndex),
|
|
45
|
+
...oneOctaveNotes.slice(0, startIndex),
|
|
46
|
+
];
|
|
47
|
+
intervals = [...intervals.slice(startIndex), ...intervals.slice(0, startIndex)];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Expand across octaves: repeat pitch classes + add final starting note
|
|
32
51
|
const notes = [];
|
|
33
52
|
for (let oct = 0; oct < octaves; oct++) {
|
|
34
53
|
notes.push(...oneOctaveNotes);
|
|
35
54
|
}
|
|
36
|
-
// Add the final
|
|
55
|
+
// Add the final starting note to complete the last octave
|
|
37
56
|
notes.push(oneOctaveNotes[0]);
|
|
38
57
|
return {
|
|
39
58
|
notes,
|
|
40
59
|
root,
|
|
41
60
|
type: scaleType,
|
|
42
61
|
intervals,
|
|
62
|
+
startIndex,
|
|
43
63
|
};
|
|
44
64
|
}
|
|
45
65
|
//# sourceMappingURL=scale-resolver.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scale-resolver.js","sourceRoot":"","sources":["../../src/resolver/scale-resolver.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"scale-resolver.js","sourceRoot":"","sources":["../../src/resolver/scale-resolver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAcpC,oEAAoE;AACpE,MAAM,cAAc,GAA2B;IAC7C,eAAe,EAAE,OAAO;IACxB,gBAAgB,EAAE,gBAAgB;IAClC,eAAe,EAAE,eAAe;IAChC,kBAAkB,EAAE,kBAAkB;IACtC,kBAAkB,EAAE,kBAAkB;IACtC,YAAY,EAAE,YAAY;IAC1B,OAAO,EAAE,aAAa;CACvB,CAAC;AAEF,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,SAAiB,EACjB,SAAsC,EACtC,UAAkB,CAAC,EACnB,OAAgB;IAEhB,2CAA2C;IAC3C,IAAI,aAAa,GAAG,SAAS,CAAC;IAC9B,IAAI,SAAS,KAAK,eAAe,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;QAChE,aAAa,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,2BAA2B;IAC3B,MAAM,SAAS,GAAG,cAAc,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC;IAEjE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,IAAI,SAAS,EAAE,CAAC,CAAC;IAChD,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,IAAI,SAAS,GAAG,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,cAAc,GAAG,KAAK,CAAC,KAAK,CAAC;IACjC,IAAI,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAChC,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,0EAA0E;IAC1E,sEAAsE;IACtE,8CAA8C;IAC9C,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC1C,UAAU,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC;QAC9E,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CACb,GAAG,OAAO,aAAa,IAAI,IAAI,SAAS,qBAAqB,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACzF,CAAC;QACJ,CAAC;QACD,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACnB,cAAc,GAAG;gBACf,GAAG,cAAc,CAAC,KAAK,CAAC,UAAU,CAAC;gBACnC,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC;aACvC,CAAC;YACF,SAAS,GAAG,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,CAAC;IAChC,CAAC;IACD,0DAA0D;IAC1D,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;IAE9B,OAAO;QACL,KAAK;QACL,IAAI;QACJ,IAAI,EAAE,SAAS;QACf,SAAS;QACT,UAAU;KACX,CAAC;AACJ,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { ColorTheme } from "../types";
|
|
1
|
+
import type { ColorTheme } from "../types.js";
|
|
2
2
|
export declare const boomwhackerTheme: ColorTheme;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boomwhacker.js","sourceRoot":"","sources":["../../src/themes/boomwhacker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"boomwhacker.js","sourceRoot":"","sources":["../../src/themes/boomwhacker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEpF,2CAA2C;AAC3C,MAAM,YAAY,GAA2B;IAC3C,CAAC,EAAE,SAAS;IACZ,IAAI,EAAE,SAAS;IACf,CAAC,EAAE,SAAS;IACZ,IAAI,EAAE,SAAS;IACf,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,IAAI,EAAE,SAAS;IACf,CAAC,EAAE,SAAS;IACZ,IAAI,EAAE,SAAS;IACf,CAAC,EAAE,SAAS;IACZ,IAAI,EAAE,SAAS;IACf,CAAC,EAAE,SAAS;CACb,CAAC;AAEF,0CAA0C;AAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,CAAC,sBAAsB;AAEhD,SAAS,IAAI,CAAC,GAAW;IACvB,OAAO,GAAG,GAAG,WAAW,CAAC;AAC3B,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAe;IAC1C,IAAI,EAAE,aAAa;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE;QAC9B,IAAI,CAAC,WAAW;YAAE,OAAO,kBAAkB,CAAC;QAC5C,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC;IAClD,CAAC;IACD,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE;QAC9B,IAAI,CAAC,WAAW;YAAE,OAAO,kBAAkB,CAAC;QAC5C,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC;IAClD,CAAC;CACF,CAAC"}
|
package/dist/themes/crf.d.ts
CHANGED
package/dist/themes/crf.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_WHITE_FILL, DEFAULT_BLACK_FILL } from "../engine/svg-constants";
|
|
1
|
+
import { DEFAULT_WHITE_FILL, DEFAULT_BLACK_FILL } from "../engine/svg-constants.js";
|
|
2
2
|
export const CRF_PITCH_PALETTE = {
|
|
3
3
|
C: { hex: "#f86e6e", cmyk: [0, 56, 56, 3] },
|
|
4
4
|
"C#": { hex: "#f58841", cmyk: [0, 44, 73, 4] },
|
package/dist/themes/crf.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crf.js","sourceRoot":"","sources":["../../src/themes/crf.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"crf.js","sourceRoot":"","sources":["../../src/themes/crf.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAiBpF,MAAM,CAAC,MAAM,iBAAiB,GAAkC;IAC9D,CAAC,EAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE;IAC9C,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE;IAC9C,CAAC,EAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE;IAC9C,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE;IAC/C,CAAC,EAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE;IAC7C,CAAC,EAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE;IAC9C,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE;IAC/C,CAAC,EAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE;IAC/C,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE;IAC7C,CAAC,EAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE;IAC9C,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE;IAC9C,CAAC,EAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE;CAChD,CAAC;AAEF,2FAA2F;AAC3F,MAAM,UAAU,UAAU,CAAC,IAAsC;IAC/D,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IAC1B,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAe;IAClC,IAAI,EAAE,KAAK;IACX,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE;QAC9B,IAAI,CAAC,WAAW;YAAE,OAAO,kBAAkB,CAAC;QAC5C,OAAO,iBAAiB,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,kBAAkB,CAAC;IAC5D,CAAC;IACD,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE;QAC9B,IAAI,CAAC,WAAW;YAAE,OAAO,kBAAkB,CAAC;QAC5C,OAAO,iBAAiB,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,kBAAkB,CAAC;IAC5D,CAAC;CACF,CAAC"}
|
package/dist/themes/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ColorTheme } from "../types";
|
|
1
|
+
import type { ColorTheme } from "../types.js";
|
|
2
2
|
export declare function getTheme(name: string): ColorTheme | undefined;
|
|
3
3
|
export declare function resolveTheme(theme: ColorTheme | string | undefined, highlightColor?: string): ColorTheme | undefined;
|
package/dist/themes/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { boomwhackerTheme } from "./boomwhacker";
|
|
2
|
-
import { crfTheme } from "./crf";
|
|
3
|
-
import { createSimpleTheme } from "./simple";
|
|
1
|
+
import { boomwhackerTheme } from "./boomwhacker.js";
|
|
2
|
+
import { crfTheme } from "./crf.js";
|
|
3
|
+
import { createSimpleTheme } from "./simple.js";
|
|
4
4
|
const themeRegistry = {
|
|
5
5
|
boomwhacker: boomwhackerTheme,
|
|
6
6
|
crf: crfTheme,
|
package/dist/themes/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/themes/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/themes/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,aAAa,GAA+B;IAChD,WAAW,EAAE,gBAAgB;IAC7B,GAAG,EAAE,QAAQ;IACb,MAAM,EAAE,iBAAiB,EAAE;CAC5B,CAAC;AAEF,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,KAAsC,EACtC,cAAuB;IAEvB,IAAI,CAAC,KAAK,IAAI,CAAC,cAAc;QAAE,OAAO,SAAS,CAAC;IAChD,IAAI,CAAC,KAAK,IAAI,cAAc;QAAE,OAAO,iBAAiB,CAAC,cAAc,CAAC,CAAC;IACvE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,KAAK,KAAK,QAAQ,IAAI,cAAc,EAAE,CAAC;YACzC,OAAO,iBAAiB,CAAC,cAAc,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/themes/simple.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { ColorTheme } from "../types";
|
|
1
|
+
import type { ColorTheme } from "../types.js";
|
|
2
2
|
export declare function createSimpleTheme(highlightColor?: string): ColorTheme;
|
package/dist/themes/simple.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_WHITE_FILL, DEFAULT_BLACK_FILL } from "../engine/svg-constants";
|
|
1
|
+
import { DEFAULT_WHITE_FILL, DEFAULT_BLACK_FILL } from "../engine/svg-constants.js";
|
|
2
2
|
const DEFAULT_HIGHLIGHT = "#a0c6e8";
|
|
3
3
|
const DEFAULT_BLACK_HIGHLIGHT = "#5a8ab5";
|
|
4
4
|
export function createSimpleTheme(highlightColor) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simple.js","sourceRoot":"","sources":["../../src/themes/simple.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"simple.js","sourceRoot":"","sources":["../../src/themes/simple.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEpF,MAAM,iBAAiB,GAAG,SAAS,CAAC;AACpC,MAAM,uBAAuB,GAAG,SAAS,CAAC;AAE1C,MAAM,UAAU,iBAAiB,CAAC,cAAuB;IACvD,MAAM,cAAc,GAAG,cAAc,IAAI,iBAAiB,CAAC;IAC3D,MAAM,cAAc,GAAG,cAAc,IAAI,uBAAuB,CAAC;IAEjE,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,CAC/B,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,kBAAkB;QACnD,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,CAC/B,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,kBAAkB;KACpD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { VoicingQuality, VoicingVariant } from "@pepperhorn/chordl-voicings";
|
|
2
|
+
import type { ConstrainedVoicing, HandConstraints } from "./hand-constraints.js";
|
|
3
|
+
/**
|
|
4
|
+
* A source voicing refitted to a hand.
|
|
5
|
+
*
|
|
6
|
+
* `id` and `label` name the character of the *source* voicing — "Drop 2+4",
|
|
7
|
+
* "Rootless A" — not the shape of `voicing.notes`. `VoicingVariant.notes` is
|
|
8
|
+
* pitch classes only, with no octave information, so those pitch classes are
|
|
9
|
+
* re-stacked close-position from `baseOctave` and then folded and reduced to
|
|
10
|
+
* fit the hand. A voicing whose real intervals span fifteen semitones comes
|
|
11
|
+
* back inside the hand's reach, and the label still says "Drop 2+4".
|
|
12
|
+
*
|
|
13
|
+
* So: read the label as provenance, not as geometry. A consumer that needs the
|
|
14
|
+
* true open-position shape should realize the voicing through chordl-voicings
|
|
15
|
+
* directly rather than infer it from this label.
|
|
16
|
+
*/
|
|
17
|
+
export interface ConstrainedVariant {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
source: VoicingVariant["source"];
|
|
21
|
+
voicing: ConstrainedVoicing;
|
|
22
|
+
}
|
|
23
|
+
export interface GenerateConstrainedOptions {
|
|
24
|
+
/** Tonal interval names, e.g. ["1P","3M","5P","7M"]. */
|
|
25
|
+
intervals?: string[];
|
|
26
|
+
/** Tonal chord type, e.g. "major seventh". */
|
|
27
|
+
chordType?: string;
|
|
28
|
+
styleHint?: string;
|
|
29
|
+
excludeIds?: string[];
|
|
30
|
+
baseOctave?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate voicing variants and fit each to a hand.
|
|
34
|
+
*
|
|
35
|
+
* Variants that satisfy the constraints are returned first, then those that
|
|
36
|
+
* could not be satisfied without losing a chord tone. Ordering matters because
|
|
37
|
+
* an inversion often fits where root position cannot: Cmaj7 spans 11 semitones
|
|
38
|
+
* in root position but 8 in first inversion.
|
|
39
|
+
*
|
|
40
|
+
* @param root - root note, e.g. "C"
|
|
41
|
+
* @param quality - voicing quality from mapToVoicingQuality, may be undefined
|
|
42
|
+
* @param resolvedNotes - pitch classes in root position
|
|
43
|
+
* @param count - how many variants to generate before constraining
|
|
44
|
+
* @param constraints - the hand's reach and note count
|
|
45
|
+
*/
|
|
46
|
+
export declare function generateConstrainedVariants(root: string, quality: VoicingQuality | undefined, resolvedNotes: string[], count: number, constraints: HandConstraints, options?: GenerateConstrainedOptions): ConstrainedVariant[];
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voicing variants that fit a given hand.
|
|
3
|
+
*
|
|
4
|
+
* This lives in core rather than chordl-voicings because it needs
|
|
5
|
+
* classifyTones/dropOrder, and voicings is a leaf package that core depends
|
|
6
|
+
* on — the dependency cannot run the other way.
|
|
7
|
+
*/
|
|
8
|
+
import { generateVariants } from "@pepperhorn/chordl-voicings";
|
|
9
|
+
import { classifyTones, dropOrder } from "./chord-tones.js";
|
|
10
|
+
import { constrainVoicing, pcSemitone } from "./hand-constraints.js";
|
|
11
|
+
/**
|
|
12
|
+
* Drop order when the chord's intervals are unknown.
|
|
13
|
+
*
|
|
14
|
+
* resolveChord returns undefined intervals for special-builder chords, so
|
|
15
|
+
* roles cannot be classified. But it returns notes in root-position order, so
|
|
16
|
+
* position stands in for role: 0 = root, 1 = 3rd, 2 = 5th, 3 = 7th, 4+ =
|
|
17
|
+
* extensions. That is enough to mirror what classifyTones would conclude on
|
|
18
|
+
* the primary path, rather than diverging from it.
|
|
19
|
+
*
|
|
20
|
+
* Triads keep the 5th — on piano a triad needs it to still be a triad, which
|
|
21
|
+
* is exactly what assignRole concludes for degree 5 in the "triad" family.
|
|
22
|
+
* Larger chords treat the 5th as omittable and protect the 3rd and 7th, so
|
|
23
|
+
* the root stays droppable and a shell voicing can form.
|
|
24
|
+
*/
|
|
25
|
+
function fallbackDropOrder(notes) {
|
|
26
|
+
if (notes.length <= 3) {
|
|
27
|
+
const keep = notes.slice(1, 3);
|
|
28
|
+
return { order: notes[0] === undefined ? [] : [notes[0]], keep };
|
|
29
|
+
}
|
|
30
|
+
const order = [];
|
|
31
|
+
for (let i = notes.length - 1; i >= 4; i--)
|
|
32
|
+
order.push(notes[i]); // extensions, top down
|
|
33
|
+
if (notes[2] !== undefined)
|
|
34
|
+
order.push(notes[2]); // 5th — conventionally omittable
|
|
35
|
+
if (notes[0] !== undefined)
|
|
36
|
+
order.push(notes[0]); // root — last, forming a shell
|
|
37
|
+
const keep = [notes[1], notes[3]].filter((n) => n !== undefined);
|
|
38
|
+
return { order, keep };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Put a variant's unclassified notes at the head of the drop order.
|
|
42
|
+
*
|
|
43
|
+
* `order` and `keep` come from the resolved chord tones, so a library
|
|
44
|
+
* voicing's added 9th or 13th — the whole point of a rootless voicing — is in
|
|
45
|
+
* neither list. `constrainVoicing` only ever drops what `dropOrder` names, so
|
|
46
|
+
* without this the reduction stalls with the voicing still over the cap.
|
|
47
|
+
* Extensions are colour tones and the safest thing to lose, so they go first,
|
|
48
|
+
* in the order the voicing lists them.
|
|
49
|
+
*
|
|
50
|
+
* Membership is tested by semitone because chordl-voicings spells library
|
|
51
|
+
* variants with sharps where core's resolver yields flats.
|
|
52
|
+
*/
|
|
53
|
+
function withUnclassifiedFirst(notes, order, keep) {
|
|
54
|
+
const classified = new Set([...order, ...keep].map(pcSemitone));
|
|
55
|
+
const extras = [];
|
|
56
|
+
for (const note of notes) {
|
|
57
|
+
const semitone = pcSemitone(note);
|
|
58
|
+
if (classified.has(semitone))
|
|
59
|
+
continue;
|
|
60
|
+
classified.add(semitone);
|
|
61
|
+
extras.push(note);
|
|
62
|
+
}
|
|
63
|
+
return extras.length === 0 ? order : [...extras, ...order];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generate voicing variants and fit each to a hand.
|
|
67
|
+
*
|
|
68
|
+
* Variants that satisfy the constraints are returned first, then those that
|
|
69
|
+
* could not be satisfied without losing a chord tone. Ordering matters because
|
|
70
|
+
* an inversion often fits where root position cannot: Cmaj7 spans 11 semitones
|
|
71
|
+
* in root position but 8 in first inversion.
|
|
72
|
+
*
|
|
73
|
+
* @param root - root note, e.g. "C"
|
|
74
|
+
* @param quality - voicing quality from mapToVoicingQuality, may be undefined
|
|
75
|
+
* @param resolvedNotes - pitch classes in root position
|
|
76
|
+
* @param count - how many variants to generate before constraining
|
|
77
|
+
* @param constraints - the hand's reach and note count
|
|
78
|
+
*/
|
|
79
|
+
export function generateConstrainedVariants(root, quality, resolvedNotes, count, constraints, options = {}) {
|
|
80
|
+
const variants = generateVariants(root, quality, resolvedNotes, count, {
|
|
81
|
+
styleHint: options.styleHint,
|
|
82
|
+
excludeIds: options.excludeIds,
|
|
83
|
+
});
|
|
84
|
+
if (variants.length === 0)
|
|
85
|
+
return [];
|
|
86
|
+
let order;
|
|
87
|
+
let keep;
|
|
88
|
+
if (options.intervals && options.intervals.length > 0) {
|
|
89
|
+
const analysis = classifyTones(root, options.chordType ?? "", options.intervals, resolvedNotes, "piano");
|
|
90
|
+
order = dropOrder(analysis);
|
|
91
|
+
// Identity tones only — the root is droppable as a last resort, which is
|
|
92
|
+
// exactly how a shell voicing is formed.
|
|
93
|
+
keep = analysis.identityTones;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
({ order, keep } = fallbackDropOrder(resolvedNotes));
|
|
97
|
+
}
|
|
98
|
+
const constrained = variants.map((v) => ({
|
|
99
|
+
id: v.id,
|
|
100
|
+
label: v.label,
|
|
101
|
+
source: v.source,
|
|
102
|
+
voicing: constrainVoicing({
|
|
103
|
+
notes: v.notes,
|
|
104
|
+
handHints: v.handHints,
|
|
105
|
+
dropOrder: withUnclassifiedFirst(v.notes, order, keep),
|
|
106
|
+
keepAtLeast: keep,
|
|
107
|
+
constraints,
|
|
108
|
+
baseOctave: options.baseOctave,
|
|
109
|
+
}),
|
|
110
|
+
}));
|
|
111
|
+
// Stable partition: satisfied first, original order preserved within groups.
|
|
112
|
+
return [
|
|
113
|
+
...constrained.filter((c) => c.voicing.satisfied),
|
|
114
|
+
...constrained.filter((c) => !c.voicing.satisfied),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=constrained-variants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constrained-variants.js","sourceRoot":"","sources":["../../src/theory/constrained-variants.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,3 +29,42 @@ export declare function intervalToDegreeLabel(interval: string): string;
|
|
|
29
29
|
* degreesForIntervals(["1P", "3M", "5A", "7m"]) → ["I", "III", "#V", "bVII"]
|
|
30
30
|
*/
|
|
31
31
|
export declare function degreesForIntervals(intervals: string[]): string[];
|
|
32
|
+
/**
|
|
33
|
+
* Label each rendered note with the degree it plays in the chord, keyed by
|
|
34
|
+
* pitch rather than by position.
|
|
35
|
+
*
|
|
36
|
+
* Position is not usable here: `notes` is rotated by inversions and by
|
|
37
|
+
* "starting on", and repeated across octaves for arpeggios, while
|
|
38
|
+
* `resolved.intervals` stays canonical (the resolver deliberately does not
|
|
39
|
+
* rotate it — the voicing lookup classifies by degree number). Zipping the two
|
|
40
|
+
* by index therefore called whatever note came first the root.
|
|
41
|
+
*
|
|
42
|
+
* Matching is spelling-first, then chroma. Spelling has to win: `C7#5b13`
|
|
43
|
+
* renders both `G#` (5A) and `Ab` (6m), which share chroma 8, and a chroma-first
|
|
44
|
+
* lookup would give them the same label. Chroma is the fallback for the cases
|
|
45
|
+
* spelling cannot cover — a `7d` off C is `Bbb` but renders as `A`, and a
|
|
46
|
+
* voicing may respell `Bb` as `A#`.
|
|
47
|
+
*
|
|
48
|
+
* A note in neither map — an added tone from a style voicing, e.g. the 9th in a
|
|
49
|
+
* rootless `Cmaj7` — is labelled from its distance to the root instead of
|
|
50
|
+
* borrowing a chord tone's label, and a 2nd, 4th or 6th is raised an octave so
|
|
51
|
+
* it reads as the extension it is: `IX`, `XI`, `XIII`. The quality rides along,
|
|
52
|
+
* so an altered extension still reads `bIX` / `#XI` / `bXIII`. Raising is safe
|
|
53
|
+
* precisely because this branch only fires for notes absent from the chord's
|
|
54
|
+
* own interval list: a chord that genuinely owns a 2nd, 4th or 6th (`Csus2`,
|
|
55
|
+
* `Csus4`, `C6`) matches by spelling or chroma first and keeps `II`, `IV`, `VI`.
|
|
56
|
+
* Anything that still cannot be named comes back `undefined`, and the keyboard
|
|
57
|
+
* draws no label for it.
|
|
58
|
+
*
|
|
59
|
+
* @param root The chord root, e.g. "C".
|
|
60
|
+
* @param intervals The chord's canonical intervals, e.g. ["1P","3M","5P"].
|
|
61
|
+
* @param notes The notes as rendered, in render order.
|
|
62
|
+
* @returns One label per note, index-parallel to `notes`.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* degreeLabelsForNotes("C", ["1P","3M","5P"], ["E","G","C"]) → ["III","V","I"]
|
|
66
|
+
* degreeLabelsForNotes("C", ["3M","5P","8P"], ["E","G","C"]) → ["III","V","8"]
|
|
67
|
+
* degreeLabelsForNotes("C", ["1P","3M","5P","7M"], ["E","G","B","D"])
|
|
68
|
+
* → ["III","V","VII","IX"] // the D is an added tone, so it reads as a 9th
|
|
69
|
+
*/
|
|
70
|
+
export declare function degreeLabelsForNotes(root: string, intervals: string[] | undefined, notes: string[]): (string | undefined)[];
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - Minor/Diminished = flat (b)
|
|
10
10
|
* - Augmented = sharp (#)
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { Note, Interval } from "tonal";
|
|
13
|
+
import { parseInterval } from "./chord-tones.js";
|
|
13
14
|
const ROMAN = ["", "I", "II", "III", "IV", "V", "VI", "VII"];
|
|
14
15
|
const ROMAN_EXT = {
|
|
15
16
|
9: "IX", 10: "X", 11: "XI", 12: "XII", 13: "XIII",
|
|
@@ -71,4 +72,104 @@ export function intervalToDegreeLabel(interval) {
|
|
|
71
72
|
export function degreesForIntervals(intervals) {
|
|
72
73
|
return intervals.map(intervalToDegreeLabel);
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Raise a simple 2nd, 4th or 6th by an octave so it reads as the extension it
|
|
77
|
+
* is: 2→9, 4→11, 6→13. The quality is carried over untouched, so `2m` becomes
|
|
78
|
+
* `9m` and `4A` becomes `11A` — a flattened or raised extension keeps its
|
|
79
|
+
* accidental. Degrees 1, 3, 5, 7 and 8 are returned unchanged.
|
|
80
|
+
*
|
|
81
|
+
* Only for the non-chord-tone fallback in `degreeLabelsForNotes`: pitch-class
|
|
82
|
+
* distances are always simple, so an added 9th arrives here as a bare `2M`.
|
|
83
|
+
*/
|
|
84
|
+
function raiseExtension(interval) {
|
|
85
|
+
const parsed = Interval.get(interval);
|
|
86
|
+
if (parsed.empty || parsed.num == null)
|
|
87
|
+
return interval;
|
|
88
|
+
if (parsed.num !== 2 && parsed.num !== 4 && parsed.num !== 6)
|
|
89
|
+
return interval;
|
|
90
|
+
return `${parsed.num + 7}${parsed.q}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Label each rendered note with the degree it plays in the chord, keyed by
|
|
94
|
+
* pitch rather than by position.
|
|
95
|
+
*
|
|
96
|
+
* Position is not usable here: `notes` is rotated by inversions and by
|
|
97
|
+
* "starting on", and repeated across octaves for arpeggios, while
|
|
98
|
+
* `resolved.intervals` stays canonical (the resolver deliberately does not
|
|
99
|
+
* rotate it — the voicing lookup classifies by degree number). Zipping the two
|
|
100
|
+
* by index therefore called whatever note came first the root.
|
|
101
|
+
*
|
|
102
|
+
* Matching is spelling-first, then chroma. Spelling has to win: `C7#5b13`
|
|
103
|
+
* renders both `G#` (5A) and `Ab` (6m), which share chroma 8, and a chroma-first
|
|
104
|
+
* lookup would give them the same label. Chroma is the fallback for the cases
|
|
105
|
+
* spelling cannot cover — a `7d` off C is `Bbb` but renders as `A`, and a
|
|
106
|
+
* voicing may respell `Bb` as `A#`.
|
|
107
|
+
*
|
|
108
|
+
* A note in neither map — an added tone from a style voicing, e.g. the 9th in a
|
|
109
|
+
* rootless `Cmaj7` — is labelled from its distance to the root instead of
|
|
110
|
+
* borrowing a chord tone's label, and a 2nd, 4th or 6th is raised an octave so
|
|
111
|
+
* it reads as the extension it is: `IX`, `XI`, `XIII`. The quality rides along,
|
|
112
|
+
* so an altered extension still reads `bIX` / `#XI` / `bXIII`. Raising is safe
|
|
113
|
+
* precisely because this branch only fires for notes absent from the chord's
|
|
114
|
+
* own interval list: a chord that genuinely owns a 2nd, 4th or 6th (`Csus2`,
|
|
115
|
+
* `Csus4`, `C6`) matches by spelling or chroma first and keeps `II`, `IV`, `VI`.
|
|
116
|
+
* Anything that still cannot be named comes back `undefined`, and the keyboard
|
|
117
|
+
* draws no label for it.
|
|
118
|
+
*
|
|
119
|
+
* @param root The chord root, e.g. "C".
|
|
120
|
+
* @param intervals The chord's canonical intervals, e.g. ["1P","3M","5P"].
|
|
121
|
+
* @param notes The notes as rendered, in render order.
|
|
122
|
+
* @returns One label per note, index-parallel to `notes`.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* degreeLabelsForNotes("C", ["1P","3M","5P"], ["E","G","C"]) → ["III","V","I"]
|
|
126
|
+
* degreeLabelsForNotes("C", ["3M","5P","8P"], ["E","G","C"]) → ["III","V","8"]
|
|
127
|
+
* degreeLabelsForNotes("C", ["1P","3M","5P","7M"], ["E","G","B","D"])
|
|
128
|
+
* → ["III","V","VII","IX"] // the D is an added tone, so it reads as a 9th
|
|
129
|
+
*/
|
|
130
|
+
export function degreeLabelsForNotes(root, intervals, notes) {
|
|
131
|
+
if (!intervals || intervals.length === 0)
|
|
132
|
+
return notes.map(() => undefined);
|
|
133
|
+
// First interval to claim a pitch keeps it: a chord that names the same
|
|
134
|
+
// sounding pitch twice should read as the lower of the two degrees.
|
|
135
|
+
const bySpelling = new Map();
|
|
136
|
+
const byChroma = new Map();
|
|
137
|
+
for (const iv of intervals) {
|
|
138
|
+
const pc = Note.pitchClass(Note.transpose(root, iv));
|
|
139
|
+
if (!pc)
|
|
140
|
+
continue;
|
|
141
|
+
const label = intervalToDegreeLabel(iv);
|
|
142
|
+
// A slash chord whose bass sits below the root arrives with a negative
|
|
143
|
+
// interval — C/F is ["-5P","1P","3M","5P"] — and parseInterval's `^(\d+)`
|
|
144
|
+
// does not match the leading minus, so the label is the "?" sentinel.
|
|
145
|
+
// Leaving it out of the maps hands the note to the distance fallback,
|
|
146
|
+
// which names it from the root and gets bVII for C/Bb. Storing it printed
|
|
147
|
+
// a literal "?" under the bass key.
|
|
148
|
+
if (label === "?")
|
|
149
|
+
continue;
|
|
150
|
+
if (!bySpelling.has(pc))
|
|
151
|
+
bySpelling.set(pc, label);
|
|
152
|
+
const chroma = Note.chroma(pc);
|
|
153
|
+
if (Number.isFinite(chroma) && !byChroma.has(chroma))
|
|
154
|
+
byChroma.set(chroma, label);
|
|
155
|
+
}
|
|
156
|
+
return notes.map((n) => {
|
|
157
|
+
const pc = Note.pitchClass(n) || n;
|
|
158
|
+
const spelled = bySpelling.get(pc);
|
|
159
|
+
if (spelled !== undefined)
|
|
160
|
+
return spelled;
|
|
161
|
+
const chroma = Note.chroma(pc);
|
|
162
|
+
if (Number.isFinite(chroma)) {
|
|
163
|
+
const enharmonic = byChroma.get(chroma);
|
|
164
|
+
if (enharmonic !== undefined)
|
|
165
|
+
return enharmonic;
|
|
166
|
+
}
|
|
167
|
+
// Not a chord tone — name it by its distance from the root anyway.
|
|
168
|
+
const distance = Interval.distance(root, pc);
|
|
169
|
+
if (!distance)
|
|
170
|
+
return undefined;
|
|
171
|
+
const label = intervalToDegreeLabel(raiseExtension(distance));
|
|
172
|
+
return label === "?" ? undefined : label;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
74
175
|
//# sourceMappingURL=degree-labels.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"degree-labels.js","sourceRoot":"","sources":["../../src/theory/degree-labels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"degree-labels.js","sourceRoot":"","sources":["../../src/theory/degree-labels.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Hand } from "@pepperhorn/chordl-voicings";
|
|
2
|
+
export interface HandConstraints {
|
|
3
|
+
/** Maximum semitones between lowest and highest note in one hand. */
|
|
4
|
+
maxSpanPerHand: number;
|
|
5
|
+
/** Maximum notes in one hand. Values below 2 are clamped to 2. */
|
|
6
|
+
maxNotesPerHand: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PlacedNote {
|
|
9
|
+
note: string;
|
|
10
|
+
octave: number;
|
|
11
|
+
midi: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* MIDI number for a pitch class in a given octave, C4 = 60.
|
|
15
|
+
*
|
|
16
|
+
* PC_SEMITONES handles both sharps and flats, so no spelling normalization is
|
|
17
|
+
* needed here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function midiOf(note: string, octave: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Stack pitch classes into an ascending voicing.
|
|
22
|
+
*
|
|
23
|
+
* @param notes - pitch classes in voicing order
|
|
24
|
+
* @param baseOctave - octave of the first note
|
|
25
|
+
* @param maxSpan - optional semitone limit; folds notes down where it can
|
|
26
|
+
* (0 = no folding). Folding is preferred over dropping because it keeps
|
|
27
|
+
* every chord tone.
|
|
28
|
+
*/
|
|
29
|
+
export declare function placeAscending(notes: string[], baseOctave: number, maxSpan?: number): PlacedNote[];
|
|
30
|
+
/** Semitones between the lowest and highest pitch. Zero for 0 or 1 notes. */
|
|
31
|
+
export declare function spanOf(pitches: number[]): number;
|
|
32
|
+
export interface ConstrainedNote extends PlacedNote {
|
|
33
|
+
hand: Hand;
|
|
34
|
+
}
|
|
35
|
+
export interface ConstrainVoicingInput {
|
|
36
|
+
/** Pitch classes in voicing order. */
|
|
37
|
+
notes: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Per-note hand assignment. When absent the whole voicing counts as one
|
|
40
|
+
* hand — variants from the `inversion` and `algorithmic` sources carry no
|
|
41
|
+
* hand hints, so both caps apply to the voicing as a whole.
|
|
42
|
+
*/
|
|
43
|
+
handHints?: Hand[];
|
|
44
|
+
/**
|
|
45
|
+
* Safest-to-drop first, from core's `dropOrder(analysis)`. Matched against
|
|
46
|
+
* `notes` by semitone, so either list may be spelled with sharps or flats.
|
|
47
|
+
*/
|
|
48
|
+
dropOrder: string[];
|
|
49
|
+
/**
|
|
50
|
+
* Notes that must survive — the chord's identity tones. Matched by semitone,
|
|
51
|
+
* like `dropOrder`.
|
|
52
|
+
*/
|
|
53
|
+
keepAtLeast: string[];
|
|
54
|
+
constraints: HandConstraints;
|
|
55
|
+
/** Octave of the lowest note. Default 3. */
|
|
56
|
+
baseOctave?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface ConstrainedVoicing {
|
|
59
|
+
/**
|
|
60
|
+
* Surviving notes in voicing order, each keeping its input spelling. Never
|
|
61
|
+
* two notes on the same MIDI number, and never fewer than two notes in a
|
|
62
|
+
* hand unless the input itself had fewer.
|
|
63
|
+
*/
|
|
64
|
+
notes: ConstrainedNote[];
|
|
65
|
+
/**
|
|
66
|
+
* Pitch classes removed, in the order they were dropped, spelled as the
|
|
67
|
+
* input spelled them rather than as `dropOrder` did.
|
|
68
|
+
*/
|
|
69
|
+
dropped: string[];
|
|
70
|
+
/** Largest per-hand span in the result, semitones. */
|
|
71
|
+
span: number;
|
|
72
|
+
/** True when every hand is within both caps. */
|
|
73
|
+
satisfied: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Semitone value for matching pitch classes across spellings.
|
|
77
|
+
*
|
|
78
|
+
* `PC_SEMITONES` maps sharps and flats onto the same number, so "Bb" and "A#"
|
|
79
|
+
* compare equal — necessary because chordl-voicings spells library variants
|
|
80
|
+
* with sharps while core's resolver yields flats. Unknown spellings map to -1
|
|
81
|
+
* rather than 0 so they never collide with C.
|
|
82
|
+
*/
|
|
83
|
+
export declare function pcSemitone(note: string): number;
|
|
84
|
+
/**
|
|
85
|
+
* Fit a voicing inside a hand's reach and note count.
|
|
86
|
+
*
|
|
87
|
+
* Strategy, in order:
|
|
88
|
+
* 1. Fold octaves — keeps every chord tone, so it is always preferred.
|
|
89
|
+
* 2. Drop notes in `dropOrder`, never below `keepAtLeast`, and never below
|
|
90
|
+
* two notes in any one hand.
|
|
91
|
+
*
|
|
92
|
+
* `dropOrder` and `keepAtLeast` are matched by semitone rather than by
|
|
93
|
+
* spelling, so a drop order written in flats still recognises a voicing
|
|
94
|
+
* spelled in sharps. Each returned note keeps its own original spelling —
|
|
95
|
+
* only the matching normalizes.
|
|
96
|
+
*
|
|
97
|
+
* When the constraints still cannot be met without dropping an identity tone
|
|
98
|
+
* or taking a hand below a dyad, returns the closest legal voicing with
|
|
99
|
+
* `satisfied: false` rather than returning nothing or a chord that is no
|
|
100
|
+
* longer the chord asked for. Callers wanting a voicing that genuinely fits
|
|
101
|
+
* should try other variants — an inversion often fits where root position
|
|
102
|
+
* cannot.
|
|
103
|
+
*/
|
|
104
|
+
export declare function constrainVoicing(input: ConstrainVoicingInput): ConstrainedVoicing;
|