@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.
Files changed (73) hide show
  1. package/dist/chord-sheet/codec.d.ts +1 -1
  2. package/dist/chord-sheet/codec.js +1 -1
  3. package/dist/chord-sheet/codec.js.map +1 -1
  4. package/dist/chord-sheet/defaults.d.ts +1 -1
  5. package/dist/chord-sheet/index.d.ts +3 -3
  6. package/dist/chord-sheet/index.js +3 -3
  7. package/dist/chord-sheet/index.js.map +1 -1
  8. package/dist/engine/auto-fingering.js +1 -1
  9. package/dist/engine/auto-fingering.js.map +1 -1
  10. package/dist/engine/highlight-mapper.d.ts +1 -1
  11. package/dist/engine/highlight-mapper.js +2 -2
  12. package/dist/engine/highlight-mapper.js.map +1 -1
  13. package/dist/engine/keyboard-layout.d.ts +1 -1
  14. package/dist/engine/keyboard-layout.js +1 -1
  15. package/dist/engine/keyboard-layout.js.map +1 -1
  16. package/dist/engine/mei-builder.d.ts +27 -0
  17. package/dist/engine/mei-builder.js +138 -0
  18. package/dist/engine/mei-builder.js.map +1 -0
  19. package/dist/engine/staff-glyphs.js +5 -10
  20. package/dist/engine/staff-glyphs.js.map +1 -1
  21. package/dist/engine/staff-layout.js +2 -2
  22. package/dist/engine/staff-layout.js.map +1 -1
  23. package/dist/index.d.ts +48 -42
  24. package/dist/index.js +32 -28
  25. package/dist/index.js.map +1 -1
  26. package/dist/parser/natural-language.d.ts +1 -1
  27. package/dist/parser/natural-language.js +241 -31
  28. package/dist/parser/natural-language.js.map +1 -1
  29. package/dist/parser/progression-parser.d.ts +1 -1
  30. package/dist/parser/progression-parser.js +1 -1
  31. package/dist/parser/progression-parser.js.map +1 -1
  32. package/dist/pipeline.d.ts +1 -1
  33. package/dist/pipeline.js +46 -25
  34. package/dist/pipeline.js.map +1 -1
  35. package/dist/progression/index.d.ts +5 -5
  36. package/dist/progression/index.js +3 -3
  37. package/dist/progression/index.js.map +1 -1
  38. package/dist/progression/progression-resolver.js +5 -5
  39. package/dist/progression/progression-resolver.js.map +1 -1
  40. package/dist/progression/roman-numeral.js +1 -1
  41. package/dist/progression/roman-numeral.js.map +1 -1
  42. package/dist/resolver/auto-layout.d.ts +1 -1
  43. package/dist/resolver/auto-layout.js +2 -2
  44. package/dist/resolver/auto-layout.js.map +1 -1
  45. package/dist/resolver/chord-resolver.d.ts +11 -1
  46. package/dist/resolver/chord-resolver.js +28 -2
  47. package/dist/resolver/chord-resolver.js.map +1 -1
  48. package/dist/resolver/scale-resolver.d.ts +5 -2
  49. package/dist/resolver/scale-resolver.js +28 -8
  50. package/dist/resolver/scale-resolver.js.map +1 -1
  51. package/dist/themes/boomwhacker.d.ts +1 -1
  52. package/dist/themes/boomwhacker.js +1 -1
  53. package/dist/themes/boomwhacker.js.map +1 -1
  54. package/dist/themes/crf.d.ts +1 -1
  55. package/dist/themes/crf.js +1 -1
  56. package/dist/themes/crf.js.map +1 -1
  57. package/dist/themes/index.d.ts +1 -1
  58. package/dist/themes/index.js +3 -3
  59. package/dist/themes/index.js.map +1 -1
  60. package/dist/themes/simple.d.ts +1 -1
  61. package/dist/themes/simple.js +1 -1
  62. package/dist/themes/simple.js.map +1 -1
  63. package/dist/theory/constrained-variants.d.ts +46 -0
  64. package/dist/theory/constrained-variants.js +117 -0
  65. package/dist/theory/constrained-variants.js.map +1 -0
  66. package/dist/theory/degree-labels.d.ts +39 -0
  67. package/dist/theory/degree-labels.js +102 -1
  68. package/dist/theory/degree-labels.js.map +1 -1
  69. package/dist/theory/hand-constraints.d.ts +104 -0
  70. package/dist/theory/hand-constraints.js +214 -0
  71. package/dist/theory/hand-constraints.js.map +1 -0
  72. package/dist/types.d.ts +9 -1
  73. 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, and
4
- * generates Tonal interval arrays for degree label computation.
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
- const oneOctaveNotes = scale.notes;
30
- const intervals = scale.intervals;
31
- // Expand across octaves: repeat pitch classes + add final tonic
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 tonic to complete the last octave
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;;;;GAIG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAW9B,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;IAEnB,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,MAAM,cAAc,GAAG,KAAK,CAAC,KAAK,CAAC;IACnC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAElC,gEAAgE;IAChE,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,kDAAkD;IAClD,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;IAE9B,OAAO;QACL,KAAK;QACL,IAAI;QACJ,IAAI,EAAE,SAAS;QACf,SAAS;KACV,CAAC;AACJ,CAAC"}
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,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
  // Boomwhacker chromatic pitch-class colors
3
3
  const PITCH_COLORS = {
4
4
  C: "#e81c1c",
@@ -1 +1 @@
1
- {"version":3,"file":"boomwhacker.js","sourceRoot":"","sources":["../../src/themes/boomwhacker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAEjF,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"}
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"}
@@ -1,4 +1,4 @@
1
- import type { ColorTheme } from "../types";
1
+ import type { ColorTheme } from "../types.js";
2
2
  /**
3
3
  * Creative Ranges Foundation piano keyboard colors.
4
4
  *
@@ -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] },
@@ -1 +1 @@
1
- {"version":3,"file":"crf.js","sourceRoot":"","sources":["../../src/themes/crf.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAiBjF,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"}
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"}
@@ -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;
@@ -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,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/themes/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,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"}
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"}
@@ -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;
@@ -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,yBAAyB,CAAC;AAEjF,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"}
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":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAE/D,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAkCrE;;;;;;;;;;;;;GAaG;AACH,SAAS,iBAAiB,CAAC,KAAe;IACxC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/B,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IACnE,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,uBAAuB;IACzF,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iCAAiC;IACnF,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,+BAA+B;IACjF,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;IAC9E,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,qBAAqB,CAAC,KAAe,EAAE,KAAe,EAAE,IAAc;IAC7E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;IAChE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,SAAS;QACvC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,2BAA2B,CACzC,IAAY,EACZ,OAAmC,EACnC,aAAuB,EACvB,KAAa,EACb,WAA4B,EAC5B,UAAsC,EAAE;IAExC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE;QACrE,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,CAAC,CAAC;IACH,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErC,IAAI,KAAe,CAAC;IACpB,IAAI,IAAc,CAAC;IACnB,IAAI,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtD,MAAM,QAAQ,GAAG,aAAa,CAC5B,IAAI,EACJ,OAAO,CAAC,SAAS,IAAI,EAAE,EACvB,OAAO,CAAC,SAAS,EACjB,aAAa,EACb,OAAO,CACR,CAAC;QACF,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC5B,yEAAyE;QACzE,yCAAyC;QACzC,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC;IAChC,CAAC;SAAM,CAAC;QACN,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,iBAAiB,CAAC,aAAa,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACvC,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,OAAO,EAAE,gBAAgB,CAAC;YACxB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,SAAS,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC;YACtD,WAAW,EAAE,IAAI;YACjB,WAAW;YACX,UAAU,EAAE,OAAO,CAAC,UAAU;SAC/B,CAAC;KACH,CAAC,CAAC,CAAC;IAEJ,6EAA6E;IAC7E,OAAO;QACL,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;QACjD,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;KACnD,CAAC;AACJ,CAAC"}
@@ -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 { parseInterval } from "./chord-tones";
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,eAAe,CAAC;AAE9C,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAC7D,MAAM,SAAS,GAA2B;IACxC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM;CAClD,CAAC;AAEF;;;;GAIG;AACH,SAAS,cAAc,CAAC,MAAc;IACpC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAChD,IAAI,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAC7D,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACpD,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAE7B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC;QACzB,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAE1C,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IAEnC,uBAAuB;IACvB,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;QACpB,kDAAkD;QAClD,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;QACvC,OAAO,IAAI,OAAO,EAAE,CAAC;IACvB,CAAC;IACD,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;QACpB,OAAO,IAAI,OAAO,EAAE,CAAC;IACvB,CAAC;IACD,2EAA2E;IAC3E,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,KAAK,OAAO,EAAE,CAAC;IACxB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAAmB;IACrD,OAAO,SAAS,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAC9C,CAAC"}
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;