@pepperhorn/chordl-core 0.3.4 → 0.5.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 (83) hide show
  1. package/dist/audio/midi-export.js +11 -2
  2. package/dist/audio/midi-export.js.map +1 -1
  3. package/dist/chord-sheet/codec.d.ts +1 -1
  4. package/dist/chord-sheet/codec.js +1 -1
  5. package/dist/chord-sheet/codec.js.map +1 -1
  6. package/dist/chord-sheet/defaults.d.ts +8 -1
  7. package/dist/chord-sheet/defaults.js +25 -1
  8. package/dist/chord-sheet/defaults.js.map +1 -1
  9. package/dist/chord-sheet/index.d.ts +3 -3
  10. package/dist/chord-sheet/index.js +3 -3
  11. package/dist/chord-sheet/index.js.map +1 -1
  12. package/dist/engine/auto-fingering.js +1 -1
  13. package/dist/engine/auto-fingering.js.map +1 -1
  14. package/dist/engine/highlight-mapper.d.ts +1 -1
  15. package/dist/engine/highlight-mapper.js +2 -2
  16. package/dist/engine/highlight-mapper.js.map +1 -1
  17. package/dist/engine/keyboard-layout.d.ts +1 -1
  18. package/dist/engine/keyboard-layout.js +1 -1
  19. package/dist/engine/keyboard-layout.js.map +1 -1
  20. package/dist/engine/mei-builder.d.ts +47 -0
  21. package/dist/engine/mei-builder.js +155 -0
  22. package/dist/engine/mei-builder.js.map +1 -0
  23. package/dist/engine/staff-glyphs.js +5 -10
  24. package/dist/engine/staff-glyphs.js.map +1 -1
  25. package/dist/engine/staff-layout.js +2 -2
  26. package/dist/engine/staff-layout.js.map +1 -1
  27. package/dist/index.d.ts +52 -42
  28. package/dist/index.js +34 -28
  29. package/dist/index.js.map +1 -1
  30. package/dist/parser/natural-language.d.ts +23 -1
  31. package/dist/parser/natural-language.js +282 -32
  32. package/dist/parser/natural-language.js.map +1 -1
  33. package/dist/parser/progression-parser.d.ts +1 -1
  34. package/dist/parser/progression-parser.js +1 -1
  35. package/dist/parser/progression-parser.js.map +1 -1
  36. package/dist/pipeline.d.ts +1 -1
  37. package/dist/pipeline.js +46 -25
  38. package/dist/pipeline.js.map +1 -1
  39. package/dist/progression/index.d.ts +5 -5
  40. package/dist/progression/index.js +3 -3
  41. package/dist/progression/index.js.map +1 -1
  42. package/dist/progression/progression-resolver.js +5 -5
  43. package/dist/progression/progression-resolver.js.map +1 -1
  44. package/dist/progression/roman-numeral.js +1 -1
  45. package/dist/progression/roman-numeral.js.map +1 -1
  46. package/dist/resolver/auto-layout.d.ts +12 -1
  47. package/dist/resolver/auto-layout.js +27 -11
  48. package/dist/resolver/auto-layout.js.map +1 -1
  49. package/dist/resolver/chord-resolver.d.ts +11 -1
  50. package/dist/resolver/chord-resolver.js +28 -2
  51. package/dist/resolver/chord-resolver.js.map +1 -1
  52. package/dist/resolver/scale-resolver.d.ts +5 -2
  53. package/dist/resolver/scale-resolver.js +28 -8
  54. package/dist/resolver/scale-resolver.js.map +1 -1
  55. package/dist/themes/boomwhacker.d.ts +1 -1
  56. package/dist/themes/boomwhacker.js +1 -1
  57. package/dist/themes/boomwhacker.js.map +1 -1
  58. package/dist/themes/crf.d.ts +1 -1
  59. package/dist/themes/crf.js +1 -1
  60. package/dist/themes/crf.js.map +1 -1
  61. package/dist/themes/index.d.ts +1 -1
  62. package/dist/themes/index.js +3 -3
  63. package/dist/themes/index.js.map +1 -1
  64. package/dist/themes/simple.d.ts +1 -1
  65. package/dist/themes/simple.js +1 -1
  66. package/dist/themes/simple.js.map +1 -1
  67. package/dist/theory/chord-identify.d.ts +72 -0
  68. package/dist/theory/chord-identify.js +334 -0
  69. package/dist/theory/chord-identify.js.map +1 -0
  70. package/dist/theory/constrained-variants.d.ts +46 -0
  71. package/dist/theory/constrained-variants.js +117 -0
  72. package/dist/theory/constrained-variants.js.map +1 -0
  73. package/dist/theory/degree-labels.d.ts +39 -0
  74. package/dist/theory/degree-labels.js +102 -1
  75. package/dist/theory/degree-labels.js.map +1 -1
  76. package/dist/theory/hand-constraints.d.ts +104 -0
  77. package/dist/theory/hand-constraints.js +214 -0
  78. package/dist/theory/hand-constraints.js.map +1 -0
  79. package/dist/theory/starting-note.d.ts +43 -0
  80. package/dist/theory/starting-note.js +97 -0
  81. package/dist/theory/starting-note.js.map +1 -0
  82. package/dist/types.d.ts +19 -1
  83. package/package.json +9 -4
@@ -0,0 +1,43 @@
1
+ /**
2
+ * "Starting on X" — the rule that decides which chord tone sits at the bottom.
3
+ *
4
+ * This lives in the theory layer rather than in a renderer because more than
5
+ * one view of a chord has to agree about it. `PianoChord` rotates the voicing
6
+ * it draws; the board's player rotates the voicing it *sounds*. A second copy
7
+ * of the rule in the second caller is the one kind of drift nothing on screen
8
+ * would reveal — the card would draw a first inversion and play root position,
9
+ * silently, so both call this.
10
+ *
11
+ * Degree resolution ("starting on the 3rd") lives here for the same reason: it
12
+ * is the first half of the same clause, and `bassDegree` ("over the 3rd") uses
13
+ * the identical lookup.
14
+ */
15
+ /** Ordinal names for the degrees a "starting on"/"over" clause can name. */
16
+ export declare const DEGREE_NAMES: Record<number, string>;
17
+ /**
18
+ * Find the chord tone matching a musical degree, regardless of quality.
19
+ * e.g. degree 3 finds Eb in Cm7 and E in Cmaj7.
20
+ */
21
+ export declare function degreeToNote(root: string, degree: number, notes: string[]): string | undefined;
22
+ /** Format the available degrees for error messages */
23
+ export declare function describeAvailableDegrees(root: string, notes: string[]): string;
24
+ export interface StartingNoteRotation {
25
+ /**
26
+ * Where the named note sits in the original list, or `-1` when the chord
27
+ * does not contain it. Callers distinguish the two "no rotation" cases from
28
+ * this: `0` is already the bottom note, `-1` is a note that isn't there.
29
+ */
30
+ index: number;
31
+ /** `notes` rotated so the named note is lowest — the input list when `index <= 0`. */
32
+ notes: string[];
33
+ }
34
+ /**
35
+ * Rotate a voicing so `startingNote` is its lowest note.
36
+ *
37
+ * Both sides are normalised before matching. `notes` keeps the chord's own
38
+ * spelling — Bbm is ["Bb", "Db", "F"] — so normalising only the input turned
39
+ * "Bbm starting on Db" into a search for "C#" and reported Db missing from a
40
+ * chord whose own error message listed it as the 3rd; matching literally is
41
+ * the mirror bug, and drops the rotation for "Bbm starting on C#".
42
+ */
43
+ export declare function rotateToStartingNote(notes: string[], startingNote: string): StartingNoteRotation;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * "Starting on X" — the rule that decides which chord tone sits at the bottom.
3
+ *
4
+ * This lives in the theory layer rather than in a renderer because more than
5
+ * one view of a chord has to agree about it. `PianoChord` rotates the voicing
6
+ * it draws; the board's player rotates the voicing it *sounds*. A second copy
7
+ * of the rule in the second caller is the one kind of drift nothing on screen
8
+ * would reveal — the card would draw a first inversion and play root position,
9
+ * silently, so both call this.
10
+ *
11
+ * Degree resolution ("starting on the 3rd") lives here for the same reason: it
12
+ * is the first half of the same clause, and `bassDegree` ("over the 3rd") uses
13
+ * the identical lookup.
14
+ */
15
+ import { Note } from "tonal";
16
+ import { FLAT_TO_SHARP } from "../engine/note-spelling.js";
17
+ /**
18
+ * Map semitones (mod 12) from root to a scale degree number.
19
+ * Handles both major and minor variants of each degree.
20
+ * Note: does not distinguish quality (e.g. b9 vs M9 both map to degree 2).
21
+ * This means degree-based lookups may match the wrong quality in altered chords.
22
+ */
23
+ function semitonesToDegree(semitones) {
24
+ const s = ((semitones % 12) + 12) % 12;
25
+ if (s === 0)
26
+ return 1;
27
+ if (s <= 2)
28
+ return 2; // m2=1, M2=2 (also 9th)
29
+ if (s <= 4)
30
+ return 3; // m3=3, M3=4
31
+ if (s === 5)
32
+ return 4; // P4=5 (also 11th)
33
+ if (s <= 7)
34
+ return 5; // d5=6, P5=7
35
+ if (s <= 9)
36
+ return 6; // m6=8, M6=9 (also 13th)
37
+ return 7; // m7=10, M7=11
38
+ }
39
+ /** Ordinal names for the degrees a "starting on"/"over" clause can name. */
40
+ export const DEGREE_NAMES = {
41
+ 1: "root", 2: "2nd", 3: "3rd", 4: "4th", 5: "5th", 6: "6th", 7: "7th",
42
+ 9: "9th", 11: "11th", 13: "13th",
43
+ };
44
+ /**
45
+ * Find the chord tone matching a musical degree, regardless of quality.
46
+ * e.g. degree 3 finds Eb in Cm7 and E in Cmaj7.
47
+ */
48
+ export function degreeToNote(root, degree, notes) {
49
+ const rootMidi = Note.midi(`${root}4`);
50
+ if (rootMidi == null)
51
+ return undefined;
52
+ // Normalize compound degrees: 9→2, 11→4, 13→6
53
+ const simpleDegree = degree > 7 ? degree - 7 : degree;
54
+ for (const note of notes) {
55
+ const noteMidi = Note.midi(`${note}4`);
56
+ if (noteMidi == null)
57
+ continue;
58
+ const semitones = ((noteMidi - rootMidi) % 12 + 12) % 12;
59
+ if (semitonesToDegree(semitones) === simpleDegree)
60
+ return note;
61
+ }
62
+ return undefined;
63
+ }
64
+ /** Format the available degrees for error messages */
65
+ export function describeAvailableDegrees(root, notes) {
66
+ const rootMidi = Note.midi(`${root}4`);
67
+ if (rootMidi == null)
68
+ return notes.join(", ");
69
+ const degrees = [];
70
+ for (const note of notes) {
71
+ const noteMidi = Note.midi(`${note}4`);
72
+ if (noteMidi == null)
73
+ continue;
74
+ const semitones = ((noteMidi - rootMidi) % 12 + 12) % 12;
75
+ const deg = semitonesToDegree(semitones);
76
+ const name = DEGREE_NAMES[deg] ?? `${deg}th`;
77
+ degrees.push(`${name} (${note})`);
78
+ }
79
+ return degrees.join(", ");
80
+ }
81
+ /**
82
+ * Rotate a voicing so `startingNote` is its lowest note.
83
+ *
84
+ * Both sides are normalised before matching. `notes` keeps the chord's own
85
+ * spelling — Bbm is ["Bb", "Db", "F"] — so normalising only the input turned
86
+ * "Bbm starting on Db" into a search for "C#" and reported Db missing from a
87
+ * chord whose own error message listed it as the 3rd; matching literally is
88
+ * the mirror bug, and drops the rotation for "Bbm starting on C#".
89
+ */
90
+ export function rotateToStartingNote(notes, startingNote) {
91
+ const norm = FLAT_TO_SHARP[startingNote] ?? startingNote;
92
+ const index = notes.findIndex((n) => (FLAT_TO_SHARP[n] ?? n) === norm);
93
+ if (index <= 0)
94
+ return { index, notes };
95
+ return { index, notes: [...notes.slice(index), ...notes.slice(0, index)] };
96
+ }
97
+ //# sourceMappingURL=starting-note.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"starting-note.js","sourceRoot":"","sources":["../../src/theory/starting-note.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -2,9 +2,10 @@ export type Format = "compact" | "exact";
2
2
  /** Tailwind-inspired text size scale for annotations. */
3
3
  export type TextSize = "base" | "lg" | "xl" | "2xl";
4
4
  /** Note name display mode. */
5
- export type NoteNameMode = "pitch-class" | "midi" | "degree" | "pitch-class+degree";
5
+ export type NoteNameMode = "pitch-class" | "midi" | "degree" | "pitch-class+degree" | "midi+degree";
6
6
  export type WhiteNote = "C" | "D" | "E" | "F" | "G" | "A" | "B";
7
7
  export type NoteName = string;
8
+ export type PlaybackInstrument = "acoustic_grand_piano" | "electric_guitar_clean" | "ukulele";
8
9
  export interface ColorTheme {
9
10
  name: string;
10
11
  whiteKey: (note: string, highlighted: boolean) => string;
@@ -34,6 +35,12 @@ export interface ParsedChordRequest {
34
35
  noteNameSize?: TextSize;
35
36
  /** Note name display mode: pitch-class (default) or midi (with octave numbers). */
36
37
  noteNameMode?: NoteNameMode;
38
+ /**
39
+ * Text size for degree labels. Independent of `noteNameSize` because the two
40
+ * are asked for separately — "note names in xl with degrees in lg" is one
41
+ * request with two sizes in it. Absent falls back to the note-name size.
42
+ */
43
+ degreeSize?: TextSize;
37
44
  /** Fingering values (aligned with chord notes). Numbers 1–5 plus extra symbols (0, -, x). Invalid entries become "?". */
38
45
  fingering?: (number | string)[];
39
46
  /** Auto-compute fingering when true (no explicit numbers given). */
@@ -75,6 +82,8 @@ export interface NotesGroup {
75
82
  hand?: "lh" | "rh";
76
83
  /** Clef context: affects inferred base octave and implies a hand. */
77
84
  clef?: "bass" | "treble";
85
+ /** Source chord symbol when the group came from one ("rh Cmaj7"). */
86
+ chord?: string;
78
87
  }
79
88
  export interface KeyDescriptor {
80
89
  note: string;
@@ -105,6 +114,10 @@ export interface DisplayDefaults {
105
114
  noteNameMode?: NoteNameMode;
106
115
  showFingering?: boolean;
107
116
  fingeringSize?: TextSize;
117
+ /** Arpeggio tempo on a sixteenth-note grid. */
118
+ arpeggioBpm?: number;
119
+ /** Transient colour used to identify the note currently being attacked. */
120
+ playbackHighlightColor?: string;
108
121
  }
109
122
  export interface ChordData {
110
123
  /** NL string — source of truth for chord resolution. */
@@ -119,6 +132,11 @@ export interface ChordData {
119
132
  theme?: ColorTheme | string;
120
133
  highlightColor?: string;
121
134
  padding?: number;
135
+ arpeggioBpm?: number;
136
+ playbackHighlightColor?: string;
137
+ /** Ordered MIDI pitches for the exact rendered voicing. */
138
+ playbackNotes?: number[];
139
+ playbackInstrument?: PlaybackInstrument;
122
140
  }
123
141
  export interface SectionData {
124
142
  /** Section letter ID (auto-assigned "A","B",... if omitted). */
package/package.json CHANGED
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "name": "@pepperhorn/chordl-core",
3
- "version": "0.3.4",
3
+ "version": "0.5.0",
4
4
  "description": "Pure TypeScript chord parsing, resolving, layout, and export engine",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/pepperhorn/chordl.git",
8
+ "directory": "packages/chordl-core"
9
+ },
5
10
  "type": "module",
6
- "main": "./dist/index.cjs",
11
+ "main": "./dist/index.js",
7
12
  "module": "./dist/index.js",
8
13
  "types": "./dist/index.d.ts",
9
14
  "exports": {
10
15
  ".": {
11
16
  "types": "./dist/index.d.ts",
12
17
  "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
18
+ "default": "./dist/index.js"
14
19
  }
15
20
  },
16
21
  "files": [
@@ -18,7 +23,7 @@
18
23
  ],
19
24
  "dependencies": {
20
25
  "tonal": "^6.3.0",
21
- "@pepperhorn/chordl-voicings": "0.3.0"
26
+ "@pepperhorn/chordl-voicings": "0.4.0"
22
27
  },
23
28
  "devDependencies": {
24
29
  "typescript": "^5.7.0",