@real-music-packages/web-core 0.30.0 → 0.31.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.
@@ -0,0 +1,127 @@
1
+ // src/intervals.ts
2
+ var INTERVALS = [
3
+ { semitones: 1, label: "m2", mnemonic: "Jaws theme" },
4
+ { semitones: 2, label: "M2", mnemonic: "Happy Birthday opening" },
5
+ { semitones: 3, label: "m3", mnemonic: "Brahms' Lullaby" },
6
+ { semitones: 4, label: "M3", mnemonic: "When the Saints" },
7
+ { semitones: 5, label: "P4", mnemonic: "Here Comes the Bride" },
8
+ { semitones: 6, label: "TT", mnemonic: "The Simpsons theme" },
9
+ { semitones: 7, label: "P5", mnemonic: "Twinkle Twinkle" },
10
+ { semitones: 8, label: "m6", mnemonic: "Love Story theme" },
11
+ { semitones: 9, label: "M6", mnemonic: "NBC chimes" },
12
+ { semitones: 10, label: "m7", mnemonic: "Somewhere (West Side Story)" },
13
+ { semitones: 11, label: "M7", mnemonic: "Take On Me chorus" },
14
+ { semitones: 12, label: "P8", mnemonic: "Somewhere Over the Rainbow" }
15
+ ];
16
+ function intervalBySemitones(semitones) {
17
+ return INTERVALS.find((i) => i.semitones === semitones);
18
+ }
19
+
20
+ // src/music/fretboard.ts
21
+ var STRINGS = [40, 45, 50, 55, 59, 64];
22
+ var STRING_COUNT = STRINGS.length;
23
+ var MAX_FRET = 15;
24
+ function noteToFret(midi, stringIndex) {
25
+ const fret = midi - STRINGS[stringIndex];
26
+ if (fret < 0 || fret > MAX_FRET) return null;
27
+ return fret;
28
+ }
29
+ function midiToPositions(midi) {
30
+ const positions = [];
31
+ for (let s = 0; s < STRING_COUNT; s++) {
32
+ const fret = noteToFret(midi, s);
33
+ if (fret !== null) {
34
+ positions.push({ string: s, fret });
35
+ }
36
+ }
37
+ return positions;
38
+ }
39
+ function chosenPlacement(midi) {
40
+ for (let s = 0; s < STRING_COUNT; s++) {
41
+ const fret = noteToFret(midi, s);
42
+ if (fret !== null) {
43
+ return { string: s, fret };
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function buildPlacementMap(midiNotes) {
49
+ const map = /* @__PURE__ */ new Map();
50
+ for (const midi of midiNotes) {
51
+ const pos = chosenPlacement(midi);
52
+ if (pos !== null) {
53
+ map.set(midi, pos);
54
+ }
55
+ }
56
+ return map;
57
+ }
58
+ function enumerateBoardCells(visibleFrets = MAX_FRET) {
59
+ const cells = [];
60
+ for (let s = 0; s < STRING_COUNT; s++) {
61
+ for (let f = 0; f <= visibleFrets; f++) {
62
+ const midi = STRINGS[s] + f;
63
+ cells.push({ string: s, fret: f, midi });
64
+ }
65
+ }
66
+ return cells;
67
+ }
68
+ function bestPosition(midi, opts = {}) {
69
+ const max = opts.maxFret ?? MAX_FRET;
70
+ if (opts.preferHighString) {
71
+ for (let s = STRING_COUNT - 1; s >= 0; s--) {
72
+ const fret = midi - STRINGS[s];
73
+ if (fret >= 0 && fret <= max) return { string: s, fret };
74
+ }
75
+ return null;
76
+ }
77
+ for (let s = 0; s < STRING_COUNT; s++) {
78
+ const fret = midi - STRINGS[s];
79
+ if (fret >= 0 && fret <= max) return { string: s, fret };
80
+ }
81
+ return null;
82
+ }
83
+ function distributeChord(midiNotes) {
84
+ const sorted = [...midiNotes].sort((a, b) => a - b);
85
+ const usedStrings = /* @__PURE__ */ new Set();
86
+ const result = [];
87
+ for (const midi of sorted) {
88
+ let assigned = null;
89
+ for (let s = 0; s < STRING_COUNT; s++) {
90
+ if (usedStrings.has(s)) continue;
91
+ const fret = midi - STRINGS[s];
92
+ if (fret >= 0 && fret <= MAX_FRET) {
93
+ assigned = { string: s, fret };
94
+ break;
95
+ }
96
+ }
97
+ const idx = result.length;
98
+ if (assigned !== null) {
99
+ usedStrings.add(assigned.string);
100
+ result.push({
101
+ string: assigned.string,
102
+ fret: assigned.fret,
103
+ midi,
104
+ strumOffsetMs: idx * 12
105
+ });
106
+ } else {
107
+ result.push({ string: -1, fret: -1, midi, strumOffsetMs: idx * 12 });
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+
113
+ export {
114
+ INTERVALS,
115
+ intervalBySemitones,
116
+ STRINGS,
117
+ STRING_COUNT,
118
+ MAX_FRET,
119
+ noteToFret,
120
+ midiToPositions,
121
+ chosenPlacement,
122
+ buildPlacementMap,
123
+ enumerateBoardCells,
124
+ bestPosition,
125
+ distributeChord
126
+ };
127
+ //# sourceMappingURL=chunk-ILIXNGPE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/intervals.ts","../src/music/fretboard.ts"],"sourcesContent":["export interface IntervalDef { semitones: number; label: string; mnemonic: string }\n\nexport const INTERVALS: IntervalDef[] = [\n { semitones: 1, label: 'm2', mnemonic: 'Jaws theme' },\n { semitones: 2, label: 'M2', mnemonic: 'Happy Birthday opening' },\n { semitones: 3, label: 'm3', mnemonic: \"Brahms' Lullaby\" },\n { semitones: 4, label: 'M3', mnemonic: 'When the Saints' },\n { semitones: 5, label: 'P4', mnemonic: 'Here Comes the Bride' },\n { semitones: 6, label: 'TT', mnemonic: 'The Simpsons theme' },\n { semitones: 7, label: 'P5', mnemonic: 'Twinkle Twinkle' },\n { semitones: 8, label: 'm6', mnemonic: 'Love Story theme' },\n { semitones: 9, label: 'M6', mnemonic: 'NBC chimes' },\n { semitones: 10, label: 'm7', mnemonic: 'Somewhere (West Side Story)' },\n { semitones: 11, label: 'M7', mnemonic: 'Take On Me chorus' },\n { semitones: 12, label: 'P8', mnemonic: 'Somewhere Over the Rainbow' },\n];\n\nexport function intervalBySemitones(semitones: number): IntervalDef | undefined {\n return INTERVALS.find(i => i.semitones === semitones);\n}\n","/**\n * fretboard.ts — Pure MIDI <-> guitar fretboard mapping utilities.\n *\n * Standard tuning EADGBE, index 0 = low E (6th string), index 5 = high E (1st string).\n * Open-string MIDI values: E2=40, A2=45, D3=50, G3=55, B3=59, E4=64.\n *\n * No Svelte, no audio, no side effects. Unit-testable in isolation.\n */\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Open-string MIDI pitches, low E to high E (index 0 = 6th string). */\nexport const STRINGS: readonly number[] = [40, 45, 50, 55, 59, 64];\n\n/** Number of strings on a standard guitar. */\nexport const STRING_COUNT = STRINGS.length; // 6\n\n/**\n * Maximum fret number rendered. Covers MIDI range 40–79 while staying\n * readable on mobile. Frets 0–15 give string 5 (high E, open=64) a top note\n * of 79 (G5), well above the piano's practical teaching range.\n */\nexport const MAX_FRET = 15;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A playable position on the fretboard. string is 0-based (0 = low E). */\nexport interface FretPosition {\n\t/** String index, 0 = low E (6th string), 5 = high E (1st string). */\n\tstring: number;\n\t/** Fret number, 0 = open. */\n\tfret: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Compute the fret number for a MIDI note on a specific string, or null if\n * outside the playable range [0, MAX_FRET].\n */\nexport function noteToFret(midi: number, stringIndex: number): number | null {\n\tconst fret = midi - STRINGS[stringIndex];\n\tif (fret < 0 || fret > MAX_FRET) return null;\n\treturn fret;\n}\n\n/**\n * Return ALL playable positions for a MIDI note across all strings.\n * Results are ordered from lowest string (index 0) to highest (index 5).\n * Returns an empty array if the note is unreachable on any string.\n */\nexport function midiToPositions(midi: number): FretPosition[] {\n\tconst positions: FretPosition[] = [];\n\tfor (let s = 0; s < STRING_COUNT; s++) {\n\t\tconst fret = noteToFret(midi, s);\n\t\tif (fret !== null) {\n\t\t\tpositions.push({ string: s, fret });\n\t\t}\n\t}\n\treturn positions;\n}\n\n/**\n * Single canonical placement for a MIDI note: the lowest string that can\n * reach it. This mirrors the piano's \"lowest sensible voicing\" — a beginner\n * sees the note in the lowest fret cluster available.\n *\n * Returns null if the note is off-board (below MIDI 40 or above 64+15=79).\n * Callers should silently skip off-board notes, matching Piano.svelte's\n * behavior for notes outside the visible white-key range.\n */\nexport function chosenPlacement(midi: number): FretPosition | null {\n\tfor (let s = 0; s < STRING_COUNT; s++) {\n\t\tconst fret = noteToFret(midi, s);\n\t\tif (fret !== null) {\n\t\t\treturn { string: s, fret };\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Build a Map from MIDI number to its chosen (canonical) FretPosition for a\n * collection of MIDI values. Off-board values are omitted.\n */\nexport function buildPlacementMap(midiNotes: Iterable<number>): Map<number, FretPosition> {\n\tconst map = new Map<number, FretPosition>();\n\tfor (const midi of midiNotes) {\n\t\tconst pos = chosenPlacement(midi);\n\t\tif (pos !== null) {\n\t\t\tmap.set(midi, pos);\n\t\t}\n\t}\n\treturn map;\n}\n\n// ---------------------------------------------------------------------------\n// Board enumeration\n// ---------------------------------------------------------------------------\n\n/**\n * Enumerate every (string, fret, midi) cell on the board for a given visible\n * fret range [0, visibleFrets]. Useful for rendering the fretboard grid.\n */\nexport function enumerateBoardCells(\n\tvisibleFrets: number = MAX_FRET\n): Array<FretPosition & { midi: number }> {\n\tconst cells: Array<FretPosition & { midi: number }> = [];\n\tfor (let s = 0; s < STRING_COUNT; s++) {\n\t\tfor (let f = 0; f <= visibleFrets; f++) {\n\t\t\tconst midi = STRINGS[s] + f;\n\t\t\tcells.push({ string: s, fret: f, midi });\n\t\t}\n\t}\n\treturn cells;\n}\n\n// ---------------------------------------------------------------------------\n// Best single position (public alias with options)\n// ---------------------------------------------------------------------------\n\nexport interface BestPositionOpts {\n\t/** Maximum fret to consider. Defaults to MAX_FRET. */\n\tmaxFret?: number;\n\t/** Prefer higher strings (index closer to 5) over lower ones. Default false. */\n\tpreferHighString?: boolean;\n}\n\n/**\n * A single playable, low-fret choice for a MIDI note, respecting opts.\n * Returns null if no string can reach the note within maxFret.\n */\nexport function bestPosition(midi: number, opts: BestPositionOpts = {}): FretPosition | null {\n\tconst max = opts.maxFret ?? MAX_FRET;\n\tif (opts.preferHighString) {\n\t\t// Iterate high string first\n\t\tfor (let s = STRING_COUNT - 1; s >= 0; s--) {\n\t\t\tconst fret = midi - STRINGS[s];\n\t\t\tif (fret >= 0 && fret <= max) return { string: s, fret };\n\t\t}\n\t\treturn null;\n\t}\n\t// Default: lowest string first (same as chosenPlacement)\n\tfor (let s = 0; s < STRING_COUNT; s++) {\n\t\tconst fret = midi - STRINGS[s];\n\t\tif (fret >= 0 && fret <= max) return { string: s, fret };\n\t}\n\treturn null;\n}\n\n// ---------------------------------------------------------------------------\n// Chord voicing helper\n// ---------------------------------------------------------------------------\n\nexport interface ChordVoicing {\n\t/** Assigned string index. */\n\tstring: number;\n\t/** Assigned fret. */\n\tfret: number;\n\t/** The MIDI note placed here. */\n\tmidi: number;\n\t/**\n\t * Strum offset in milliseconds from the first note. Notes are sorted\n\t * ascending (low string first) with 12 ms between each successive pick.\n\t */\n\tstrumOffsetMs: number;\n}\n\n/**\n * Distribute a set of MIDI notes across distinct strings for a strummed chord.\n *\n * Algorithm:\n * 1. Sort incoming MIDI notes ascending (lowest pitch first).\n * 2. For each note, greedily assign to the lowest available string whose fret\n * is in [0, MAX_FRET] and has not yet been used.\n * 3. If no free string is available for a note, it is still included in the\n * result (midi/strumOffsetMs set) but string/fret will be -1 to signal\n * \"play at frequency only\" — audio is still correct, just unpositioned.\n * 4. Add 12 ms strum offset per successive note.\n *\n * This keeps audio faithful to the lesson's intended harmony (unchanged lesson\n * data) while sounding guitar-like.\n */\nexport function distributeChord(midiNotes: number[]): ChordVoicing[] {\n\tconst sorted = [...midiNotes].sort((a, b) => a - b);\n\tconst usedStrings = new Set<number>();\n\tconst result: ChordVoicing[] = [];\n\n\tfor (const midi of sorted) {\n\t\tlet assigned: FretPosition | null = null;\n\t\t// Try each string from low to high, skip already-used ones\n\t\tfor (let s = 0; s < STRING_COUNT; s++) {\n\t\t\tif (usedStrings.has(s)) continue;\n\t\t\tconst fret = midi - STRINGS[s];\n\t\t\tif (fret >= 0 && fret <= MAX_FRET) {\n\t\t\t\tassigned = { string: s, fret };\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tconst idx = result.length;\n\t\tif (assigned !== null) {\n\t\t\tusedStrings.add(assigned.string);\n\t\t\tresult.push({\n\t\t\t\tstring: assigned.string,\n\t\t\t\tfret: assigned.fret,\n\t\t\t\tmidi,\n\t\t\t\tstrumOffsetMs: idx * 12\n\t\t\t});\n\t\t} else {\n\t\t\t// Off-board or all strings taken: include but mark unpositioned\n\t\t\tresult.push({ string: -1, fret: -1, midi, strumOffsetMs: idx * 12 });\n\t\t}\n\t}\n\n\treturn result;\n}\n"],"mappings":";AAEO,IAAM,YAA2B;AAAA,EACtC,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,aAAa;AAAA,EACrD,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,yBAAyB;AAAA,EACjE,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,kBAAkB;AAAA,EAC1D,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,kBAAkB;AAAA,EAC1D,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,uBAAuB;AAAA,EAC/D,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,qBAAqB;AAAA,EAC7D,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,kBAAkB;AAAA,EAC1D,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,mBAAmB;AAAA,EAC3D,EAAE,WAAW,GAAI,OAAO,MAAM,UAAU,aAAa;AAAA,EACrD,EAAE,WAAW,IAAI,OAAO,MAAM,UAAU,8BAA8B;AAAA,EACtE,EAAE,WAAW,IAAI,OAAO,MAAM,UAAU,oBAAoB;AAAA,EAC5D,EAAE,WAAW,IAAI,OAAO,MAAM,UAAU,6BAA6B;AACvE;AAEO,SAAS,oBAAoB,WAA4C;AAC9E,SAAO,UAAU,KAAK,OAAK,EAAE,cAAc,SAAS;AACtD;;;ACLO,IAAM,UAA6B,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,EAAE;AAG1D,IAAM,eAAe,QAAQ;AAO7B,IAAM,WAAW;AAsBjB,SAAS,WAAW,MAAc,aAAoC;AAC5E,QAAM,OAAO,OAAO,QAAQ,WAAW;AACvC,MAAI,OAAO,KAAK,OAAO,SAAU,QAAO;AACxC,SAAO;AACR;AAOO,SAAS,gBAAgB,MAA8B;AAC7D,QAAM,YAA4B,CAAC;AACnC,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACtC,UAAM,OAAO,WAAW,MAAM,CAAC;AAC/B,QAAI,SAAS,MAAM;AAClB,gBAAU,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAC;AAAA,IACnC;AAAA,EACD;AACA,SAAO;AACR;AAWO,SAAS,gBAAgB,MAAmC;AAClE,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACtC,UAAM,OAAO,WAAW,MAAM,CAAC;AAC/B,QAAI,SAAS,MAAM;AAClB,aAAO,EAAE,QAAQ,GAAG,KAAK;AAAA,IAC1B;AAAA,EACD;AACA,SAAO;AACR;AAMO,SAAS,kBAAkB,WAAwD;AACzF,QAAM,MAAM,oBAAI,IAA0B;AAC1C,aAAW,QAAQ,WAAW;AAC7B,UAAM,MAAM,gBAAgB,IAAI;AAChC,QAAI,QAAQ,MAAM;AACjB,UAAI,IAAI,MAAM,GAAG;AAAA,IAClB;AAAA,EACD;AACA,SAAO;AACR;AAUO,SAAS,oBACf,eAAuB,UACkB;AACzC,QAAM,QAAgD,CAAC;AACvD,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACtC,aAAS,IAAI,GAAG,KAAK,cAAc,KAAK;AACvC,YAAM,OAAO,QAAQ,CAAC,IAAI;AAC1B,YAAM,KAAK,EAAE,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AAAA,IACxC;AAAA,EACD;AACA,SAAO;AACR;AAiBO,SAAS,aAAa,MAAc,OAAyB,CAAC,GAAwB;AAC5F,QAAM,MAAM,KAAK,WAAW;AAC5B,MAAI,KAAK,kBAAkB;AAE1B,aAAS,IAAI,eAAe,GAAG,KAAK,GAAG,KAAK;AAC3C,YAAM,OAAO,OAAO,QAAQ,CAAC;AAC7B,UAAI,QAAQ,KAAK,QAAQ,IAAK,QAAO,EAAE,QAAQ,GAAG,KAAK;AAAA,IACxD;AACA,WAAO;AAAA,EACR;AAEA,WAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACtC,UAAM,OAAO,OAAO,QAAQ,CAAC;AAC7B,QAAI,QAAQ,KAAK,QAAQ,IAAK,QAAO,EAAE,QAAQ,GAAG,KAAK;AAAA,EACxD;AACA,SAAO;AACR;AAmCO,SAAS,gBAAgB,WAAqC;AACpE,QAAM,SAAS,CAAC,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAClD,QAAM,cAAc,oBAAI,IAAY;AACpC,QAAM,SAAyB,CAAC;AAEhC,aAAW,QAAQ,QAAQ;AAC1B,QAAI,WAAgC;AAEpC,aAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACtC,UAAI,YAAY,IAAI,CAAC,EAAG;AACxB,YAAM,OAAO,OAAO,QAAQ,CAAC;AAC7B,UAAI,QAAQ,KAAK,QAAQ,UAAU;AAClC,mBAAW,EAAE,QAAQ,GAAG,KAAK;AAC7B;AAAA,MACD;AAAA,IACD;AAEA,UAAM,MAAM,OAAO;AACnB,QAAI,aAAa,MAAM;AACtB,kBAAY,IAAI,SAAS,MAAM;AAC/B,aAAO,KAAK;AAAA,QACX,QAAQ,SAAS;AAAA,QACjB,MAAM,SAAS;AAAA,QACf;AAAA,QACA,eAAe,MAAM;AAAA,MACtB,CAAC;AAAA,IACF,OAAO;AAEN,aAAO,KAAK,EAAE,QAAQ,IAAI,MAAM,IAAI,MAAM,eAAe,MAAM,GAAG,CAAC;AAAA,IACpE;AAAA,EACD;AAEA,SAAO;AACR;","names":[]}
package/dist/index.d.ts CHANGED
@@ -61,4 +61,103 @@ interface ChordTemplate {
61
61
  * most specific quality (e.g. maj7 before plain major). */
62
62
  declare const CHORD_TEMPLATES: ChordTemplate[];
63
63
 
64
- export { ALL_KEYS, CHORD_TEMPLATES, type ChordTemplate, INTERVALS, type IntervalDef, KEYS_PREFER_FLATS, MAJOR_SCALE_INTERVALS, NOTE_NAMES, NOTE_NAMES_FLAT, getMidiNote, getScaleDegree, getStability, intervalBySemitones, isInScale, midiToFrequency, midiToNoteName, noteNameToIndex, pitchClass, useFlatsForKeyFifths, useFlatsForKeyName };
64
+ /**
65
+ * fretboard.ts — Pure MIDI <-> guitar fretboard mapping utilities.
66
+ *
67
+ * Standard tuning EADGBE, index 0 = low E (6th string), index 5 = high E (1st string).
68
+ * Open-string MIDI values: E2=40, A2=45, D3=50, G3=55, B3=59, E4=64.
69
+ *
70
+ * No Svelte, no audio, no side effects. Unit-testable in isolation.
71
+ */
72
+ /** Open-string MIDI pitches, low E to high E (index 0 = 6th string). */
73
+ declare const STRINGS: readonly number[];
74
+ /** Number of strings on a standard guitar. */
75
+ declare const STRING_COUNT: number;
76
+ /**
77
+ * Maximum fret number rendered. Covers MIDI range 40–79 while staying
78
+ * readable on mobile. Frets 0–15 give string 5 (high E, open=64) a top note
79
+ * of 79 (G5), well above the piano's practical teaching range.
80
+ */
81
+ declare const MAX_FRET = 15;
82
+ /** A playable position on the fretboard. string is 0-based (0 = low E). */
83
+ interface FretPosition {
84
+ /** String index, 0 = low E (6th string), 5 = high E (1st string). */
85
+ string: number;
86
+ /** Fret number, 0 = open. */
87
+ fret: number;
88
+ }
89
+ /**
90
+ * Compute the fret number for a MIDI note on a specific string, or null if
91
+ * outside the playable range [0, MAX_FRET].
92
+ */
93
+ declare function noteToFret(midi: number, stringIndex: number): number | null;
94
+ /**
95
+ * Return ALL playable positions for a MIDI note across all strings.
96
+ * Results are ordered from lowest string (index 0) to highest (index 5).
97
+ * Returns an empty array if the note is unreachable on any string.
98
+ */
99
+ declare function midiToPositions(midi: number): FretPosition[];
100
+ /**
101
+ * Single canonical placement for a MIDI note: the lowest string that can
102
+ * reach it. This mirrors the piano's "lowest sensible voicing" — a beginner
103
+ * sees the note in the lowest fret cluster available.
104
+ *
105
+ * Returns null if the note is off-board (below MIDI 40 or above 64+15=79).
106
+ * Callers should silently skip off-board notes, matching Piano.svelte's
107
+ * behavior for notes outside the visible white-key range.
108
+ */
109
+ declare function chosenPlacement(midi: number): FretPosition | null;
110
+ /**
111
+ * Build a Map from MIDI number to its chosen (canonical) FretPosition for a
112
+ * collection of MIDI values. Off-board values are omitted.
113
+ */
114
+ declare function buildPlacementMap(midiNotes: Iterable<number>): Map<number, FretPosition>;
115
+ /**
116
+ * Enumerate every (string, fret, midi) cell on the board for a given visible
117
+ * fret range [0, visibleFrets]. Useful for rendering the fretboard grid.
118
+ */
119
+ declare function enumerateBoardCells(visibleFrets?: number): Array<FretPosition & {
120
+ midi: number;
121
+ }>;
122
+ interface BestPositionOpts {
123
+ /** Maximum fret to consider. Defaults to MAX_FRET. */
124
+ maxFret?: number;
125
+ /** Prefer higher strings (index closer to 5) over lower ones. Default false. */
126
+ preferHighString?: boolean;
127
+ }
128
+ /**
129
+ * A single playable, low-fret choice for a MIDI note, respecting opts.
130
+ * Returns null if no string can reach the note within maxFret.
131
+ */
132
+ declare function bestPosition(midi: number, opts?: BestPositionOpts): FretPosition | null;
133
+ interface ChordVoicing {
134
+ /** Assigned string index. */
135
+ string: number;
136
+ /** Assigned fret. */
137
+ fret: number;
138
+ /** The MIDI note placed here. */
139
+ midi: number;
140
+ /**
141
+ * Strum offset in milliseconds from the first note. Notes are sorted
142
+ * ascending (low string first) with 12 ms between each successive pick.
143
+ */
144
+ strumOffsetMs: number;
145
+ }
146
+ /**
147
+ * Distribute a set of MIDI notes across distinct strings for a strummed chord.
148
+ *
149
+ * Algorithm:
150
+ * 1. Sort incoming MIDI notes ascending (lowest pitch first).
151
+ * 2. For each note, greedily assign to the lowest available string whose fret
152
+ * is in [0, MAX_FRET] and has not yet been used.
153
+ * 3. If no free string is available for a note, it is still included in the
154
+ * result (midi/strumOffsetMs set) but string/fret will be -1 to signal
155
+ * "play at frequency only" — audio is still correct, just unpositioned.
156
+ * 4. Add 12 ms strum offset per successive note.
157
+ *
158
+ * This keeps audio faithful to the lesson's intended harmony (unchanged lesson
159
+ * data) while sounding guitar-like.
160
+ */
161
+ declare function distributeChord(midiNotes: number[]): ChordVoicing[];
162
+
163
+ export { ALL_KEYS, type BestPositionOpts, CHORD_TEMPLATES, type ChordTemplate, type ChordVoicing, type FretPosition, INTERVALS, type IntervalDef, KEYS_PREFER_FLATS, MAJOR_SCALE_INTERVALS, MAX_FRET, NOTE_NAMES, NOTE_NAMES_FLAT, STRINGS, STRING_COUNT, bestPosition, buildPlacementMap, chosenPlacement, distributeChord, enumerateBoardCells, getMidiNote, getScaleDegree, getStability, intervalBySemitones, isInScale, midiToFrequency, midiToNoteName, midiToPositions, noteNameToIndex, noteToFret, pitchClass, useFlatsForKeyFifths, useFlatsForKeyName };
package/dist/index.js CHANGED
@@ -5,8 +5,18 @@ import {
5
5
  } from "./chunk-BYZBLH25.js";
6
6
  import {
7
7
  INTERVALS,
8
- intervalBySemitones
9
- } from "./chunk-N56UTMWA.js";
8
+ MAX_FRET,
9
+ STRINGS,
10
+ STRING_COUNT,
11
+ bestPosition,
12
+ buildPlacementMap,
13
+ chosenPlacement,
14
+ distributeChord,
15
+ enumerateBoardCells,
16
+ intervalBySemitones,
17
+ midiToPositions,
18
+ noteToFret
19
+ } from "./chunk-ILIXNGPE.js";
10
20
  import {
11
21
  ALL_KEYS,
12
22
  MAJOR_SCALE_INTERVALS,
@@ -46,8 +56,16 @@ export {
46
56
  INTERVALS,
47
57
  KEYS_PREFER_FLATS,
48
58
  MAJOR_SCALE_INTERVALS,
59
+ MAX_FRET,
49
60
  NOTE_NAMES,
50
61
  NOTE_NAMES_FLAT,
62
+ STRINGS,
63
+ STRING_COUNT,
64
+ bestPosition,
65
+ buildPlacementMap,
66
+ chosenPlacement,
67
+ distributeChord,
68
+ enumerateBoardCells,
51
69
  getMidiNote,
52
70
  getScaleDegree,
53
71
  getStability,
@@ -55,7 +73,9 @@ export {
55
73
  isInScale,
56
74
  midiToFrequency,
57
75
  midiToNoteName,
76
+ midiToPositions,
58
77
  noteNameToIndex,
78
+ noteToFret,
59
79
  pitchClass,
60
80
  useFlatsForKeyFifths,
61
81
  useFlatsForKeyName
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/frequency.ts","../src/chords.ts"],"sourcesContent":["/** Equal-temperament MIDI → frequency (A4 = MIDI 69 = 440 Hz). */\nexport function midiToFrequency(midi: number): number {\n return 440 * Math.pow(2, (midi - 69) / 12);\n}\n","export interface ChordTemplate { name: string; intervals: number[] }\n\n/** Chord-quality templates, richer qualities first so naming matches the\n * most specific quality (e.g. maj7 before plain major). */\nexport const CHORD_TEMPLATES: ChordTemplate[] = [\n { name: 'maj7', intervals: [0, 4, 7, 11] },\n { name: '7', intervals: [0, 4, 7, 10] },\n { name: 'm7', intervals: [0, 3, 7, 10] },\n { name: 'dim7', intervals: [0, 3, 6, 9] },\n { name: 'm7b5', intervals: [0, 3, 6, 10] },\n { name: '', intervals: [0, 4, 7] },\n { name: 'm', intervals: [0, 3, 7] },\n { name: 'dim', intervals: [0, 3, 6] },\n { name: 'aug', intervals: [0, 4, 8] },\n { name: 'sus4', intervals: [0, 5, 7] },\n { name: 'sus2', intervals: [0, 2, 7] },\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AACO,SAAS,gBAAgB,MAAsB;AACpD,SAAO,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE;AAC3C;;;ACCO,IAAM,kBAAmC;AAAA,EAC9C,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,EAAE,EAAE;AAAA,EACzC,EAAE,MAAM,KAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,EAAE,EAAE;AAAA,EACzC,EAAE,MAAM,MAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,EAAE,EAAE;AAAA,EACzC,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE;AAAA,EACxC,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,EAAE,EAAE;AAAA,EACzC,EAAE,MAAM,IAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,KAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,OAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,OAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AACvC;","names":[]}
1
+ {"version":3,"sources":["../src/frequency.ts","../src/chords.ts"],"sourcesContent":["/** Equal-temperament MIDI → frequency (A4 = MIDI 69 = 440 Hz). */\nexport function midiToFrequency(midi: number): number {\n return 440 * Math.pow(2, (midi - 69) / 12);\n}\n","export interface ChordTemplate { name: string; intervals: number[] }\n\n/** Chord-quality templates, richer qualities first so naming matches the\n * most specific quality (e.g. maj7 before plain major). */\nexport const CHORD_TEMPLATES: ChordTemplate[] = [\n { name: 'maj7', intervals: [0, 4, 7, 11] },\n { name: '7', intervals: [0, 4, 7, 10] },\n { name: 'm7', intervals: [0, 3, 7, 10] },\n { name: 'dim7', intervals: [0, 3, 6, 9] },\n { name: 'm7b5', intervals: [0, 3, 6, 10] },\n { name: '', intervals: [0, 4, 7] },\n { name: 'm', intervals: [0, 3, 7] },\n { name: 'dim', intervals: [0, 3, 6] },\n { name: 'aug', intervals: [0, 4, 8] },\n { name: 'sus4', intervals: [0, 5, 7] },\n { name: 'sus2', intervals: [0, 2, 7] },\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACO,SAAS,gBAAgB,MAAsB;AACpD,SAAO,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE;AAC3C;;;ACCO,IAAM,kBAAmC;AAAA,EAC9C,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,EAAE,EAAE;AAAA,EACzC,EAAE,MAAM,KAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,EAAE,EAAE;AAAA,EACzC,EAAE,MAAM,MAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,EAAE,EAAE;AAAA,EACzC,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE;AAAA,EACxC,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,GAAG,EAAE,EAAE;AAAA,EACzC,EAAE,MAAM,IAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,KAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,OAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,OAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AAAA,EACrC,EAAE,MAAM,QAAQ,WAAW,CAAC,GAAG,GAAG,CAAC,EAAE;AACvC;","names":[]}
@@ -1017,6 +1017,23 @@ interface KeyboardProps {
1017
1017
  }
1018
1018
  declare const keyboardFactory: LayerFactory<KeyboardProps>;
1019
1019
 
1020
+ interface FretboardProps {
1021
+ /** Highest fret drawn. Default: auto-fit the excerpt (min 5, max 15), padded. */
1022
+ visibleFrets?: number;
1023
+ /** Board height in world px. Default 220. */
1024
+ height?: number;
1025
+ /** Board bottom edge in world px. Default safeBox.bottom. */
1026
+ bottomY?: number;
1027
+ /** Right/left hand dot colours. Defaults R=theme.accent, L=theme.gold. */
1028
+ handColors?: {
1029
+ R: string;
1030
+ L: string;
1031
+ };
1032
+ /** Draw note-name labels inside lit dots. Default false. */
1033
+ showLabels?: boolean;
1034
+ }
1035
+ declare const fretboardFactory: LayerFactory<FretboardProps>;
1036
+
1020
1037
  interface FallingNotesProps {
1021
1038
  /** Pair with a `keyboard` layer (read its layout + hit-line). When false the
1022
1039
  * layer is standalone and builds its own layout from the props below.
@@ -1971,4 +1988,4 @@ interface EndCardProps {
1971
1988
  }
1972
1989
  declare const endCardFactory: LayerFactory<EndCardProps>;
1973
1990
 
1974
- export { type Affine, type AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatPulseProps, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ChordRibbonProps, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type ContourPlot, type ContourPoint, type CountInOpts, type CountdownProps, type CountingTrackProps, type CtaProps, DEFAULT_FUNCTION_COLORS, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type EndCardProps, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR, FOLLOW_BARS, FOLLOW_PAD, type FallingKeyboardDemoOpts, type FallingNotesProps, type FunctionColors, type FunctionalHarmonyProps, type GateError, type GateInput, type HandColors, type HarmonicFunction, type HarmonyMode, type HighlightRegion, type HookProps, type IntervalArcsProps, type KaraokeCaptionProps, type KaraokeWord, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type KineticMode, type KineticTextProps, type LabelMode, type Layer, type LayerFactory, type McqCardProps, type NotationEngraving, type NotationLayout, type NotationLayoutOpts, type NotationProps, type NotationRect, type OutputProbe, PIANO_HIGH, PIANO_LOW, type ParticleBurstProps, type PitchContourProps, type Placement, type PlayheadLine, type PortraitProps, type ProgressRingProps, type PromoCardsDemoOpts, type PulseStyle, type Quiz, type QuizOption, type QuizPhase, type RadialSpectrumProps, type RayEndpoints, type RecordSceneSpecOpts, type Rect, type RenderCtx, type ResolvedSegment, type RevealProps, SCALE_INTERVALS, type SafeGuidesProps, type ScaleHighlightProps, type SceneSpec, type ScheduleTarget, type Score, type ScoreFromMusicXMLOpts, type ScoreNote, type ScrollCursorProps, type Section, type SectionMinimapProps, type SegmentAudio, type SegmentAudioCtx, type SegmentTransition, type SpecLayer, type SpectrumInput, type SpectrumProps, type StaffKeyboardRayProps, type StatCounterProps, type TempoMap, type TensionGraphProps, type TensionPoint, type TextureProps, type TimeAnchor, type TimelineSegment, type WaveformInput, type WaveformProps, activeChord, activeCue, activeSection, animatedSlot, applySchedule, applyToContext, assertGate, audioPlayheadLine, ballArc, ballX, beatGrid, beatPhase, beatPulseFactory, blackKeys, bpmOf, brandingFactory, buildScene, cameraForFollow, cameraTransform, chordRibbonFactory, circleOfFifthsDemoSpec, circleOfFifthsFactory, clamp, clickTrackSchedule, contourMinimapDemoSpec, contourPoints, contourPolyline, countInLeadSec, countInSchedule, countdownFactory, countdownRemaining, countdownSeconds, countingDegreeDemoSpec, countingTrackFactory, cropAroundBox, ctaFactory, cubicEaseInOut, cueOpacity, degreeLabel, degreeLabelsFactory, distinctMeasureIndices, distinctOnsets, dotAt, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, endCardFactory, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, firstMeasureBox, followBoxAt, followSrcBox, followWindowStart, fracSlotPoint, frameRect, functionColor, functionalHarmonyFactory, getKeyboardLayout, getLayerFactory, getNotationEngraving, harmonyDemoSpec, highlightIntensity, hookFactory, identityCamera, inRange, intervalArcsFactory, invLerp, isBlackKey, karaokeCaptionFactory, kenBurns, keyCenterX, keyColumnWidth, keyRect, keySlot, keyboardFactory, keyboardLayout, kineticTextFactory, lerp, lerpBox, lerpCamera, linear, mapBoxThroughLayout, mcqCardFactory, mcqDemoSpec, measureColumnsFromLayout, measureCount, measureSpanBox, measureSpans, measureSystemMap, timeToX as minimapTimeToX, msPerBeat, notationFactory, notationLayout, noteColor, noteSetXRange, parseKey, parseTimeSig, particleBurstFactory, pcToSlot, pitchAt, pitchContourFactory, pitchRange, playheadLine, portraitFactory, progress01, progressRingFactory, projectPoint, promoCardsDemoSpec, pulseAt, quizPhase, radialSpectrumFactory, rayEndpoints, rayPointAt, recordSceneSpec, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, revealProgress, runGate, safeGuidesFactory, scaleHighlightFactory, scoreFromMusicXML, scorePitchSpan, scrollCursorFactory, sectionMinimapFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, slotAngle, slotPc, slotPoint, spectrumFactory, staffAnchor, staffKeyboardRayFactory, staffRayDemoSpec, statCounterFactory, systemBox, tensionGraphFactory, textureFactory, transitionProgress, validateChordTrack, validateQuiz, validateSections, visualTimelineMs, vstackAudioPlayheadLine, vstackFollowBox, waveformFactory, whiteKeys, worldToViewport };
1991
+ export { type Affine, type AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatPulseProps, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ChordRibbonProps, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type ContourPlot, type ContourPoint, type CountInOpts, type CountdownProps, type CountingTrackProps, type CtaProps, DEFAULT_FUNCTION_COLORS, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type EndCardProps, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR, FOLLOW_BARS, FOLLOW_PAD, type FallingKeyboardDemoOpts, type FallingNotesProps, type FretboardProps, type FunctionColors, type FunctionalHarmonyProps, type GateError, type GateInput, type HandColors, type HarmonicFunction, type HarmonyMode, type HighlightRegion, type HookProps, type IntervalArcsProps, type KaraokeCaptionProps, type KaraokeWord, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type KineticMode, type KineticTextProps, type LabelMode, type Layer, type LayerFactory, type McqCardProps, type NotationEngraving, type NotationLayout, type NotationLayoutOpts, type NotationProps, type NotationRect, type OutputProbe, PIANO_HIGH, PIANO_LOW, type ParticleBurstProps, type PitchContourProps, type Placement, type PlayheadLine, type PortraitProps, type ProgressRingProps, type PromoCardsDemoOpts, type PulseStyle, type Quiz, type QuizOption, type QuizPhase, type RadialSpectrumProps, type RayEndpoints, type RecordSceneSpecOpts, type Rect, type RenderCtx, type ResolvedSegment, type RevealProps, SCALE_INTERVALS, type SafeGuidesProps, type ScaleHighlightProps, type SceneSpec, type ScheduleTarget, type Score, type ScoreFromMusicXMLOpts, type ScoreNote, type ScrollCursorProps, type Section, type SectionMinimapProps, type SegmentAudio, type SegmentAudioCtx, type SegmentTransition, type SpecLayer, type SpectrumInput, type SpectrumProps, type StaffKeyboardRayProps, type StatCounterProps, type TempoMap, type TensionGraphProps, type TensionPoint, type TextureProps, type TimeAnchor, type TimelineSegment, type WaveformInput, type WaveformProps, activeChord, activeCue, activeSection, animatedSlot, applySchedule, applyToContext, assertGate, audioPlayheadLine, ballArc, ballX, beatGrid, beatPhase, beatPulseFactory, blackKeys, bpmOf, brandingFactory, buildScene, cameraForFollow, cameraTransform, chordRibbonFactory, circleOfFifthsDemoSpec, circleOfFifthsFactory, clamp, clickTrackSchedule, contourMinimapDemoSpec, contourPoints, contourPolyline, countInLeadSec, countInSchedule, countdownFactory, countdownRemaining, countdownSeconds, countingDegreeDemoSpec, countingTrackFactory, cropAroundBox, ctaFactory, cubicEaseInOut, cueOpacity, degreeLabel, degreeLabelsFactory, distinctMeasureIndices, distinctOnsets, dotAt, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, endCardFactory, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, firstMeasureBox, followBoxAt, followSrcBox, followWindowStart, fracSlotPoint, frameRect, fretboardFactory, functionColor, functionalHarmonyFactory, getKeyboardLayout, getLayerFactory, getNotationEngraving, harmonyDemoSpec, highlightIntensity, hookFactory, identityCamera, inRange, intervalArcsFactory, invLerp, isBlackKey, karaokeCaptionFactory, kenBurns, keyCenterX, keyColumnWidth, keyRect, keySlot, keyboardFactory, keyboardLayout, kineticTextFactory, lerp, lerpBox, lerpCamera, linear, mapBoxThroughLayout, mcqCardFactory, mcqDemoSpec, measureColumnsFromLayout, measureCount, measureSpanBox, measureSpans, measureSystemMap, timeToX as minimapTimeToX, msPerBeat, notationFactory, notationLayout, noteColor, noteSetXRange, parseKey, parseTimeSig, particleBurstFactory, pcToSlot, pitchAt, pitchContourFactory, pitchRange, playheadLine, portraitFactory, progress01, progressRingFactory, projectPoint, promoCardsDemoSpec, pulseAt, quizPhase, radialSpectrumFactory, rayEndpoints, rayPointAt, recordSceneSpec, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, revealProgress, runGate, safeGuidesFactory, scaleHighlightFactory, scoreFromMusicXML, scorePitchSpan, scrollCursorFactory, sectionMinimapFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, slotAngle, slotPc, slotPoint, spectrumFactory, staffAnchor, staffKeyboardRayFactory, staffRayDemoSpec, statCounterFactory, systemBox, tensionGraphFactory, textureFactory, transitionProgress, validateChordTrack, validateQuiz, validateSections, visualTimelineMs, vstackAudioPlayheadLine, vstackFollowBox, waveformFactory, whiteKeys, worldToViewport };
@@ -1,6 +1,8 @@
1
1
  import {
2
+ STRING_COUNT,
3
+ buildPlacementMap,
2
4
  intervalBySemitones
3
- } from "../chunk-N56UTMWA.js";
5
+ } from "../chunk-ILIXNGPE.js";
4
6
  import {
5
7
  getScaleDegree,
6
8
  getStability,
@@ -1296,6 +1298,123 @@ var keyboardFactory = {
1296
1298
  }
1297
1299
  };
1298
1300
 
1301
+ // src/scene/layers/fretboard.ts
1302
+ var INLAY_FRETS = [3, 5, 7, 9, 12];
1303
+ var NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
1304
+ function fretboardLayer() {
1305
+ let placement = /* @__PURE__ */ new Map();
1306
+ let visibleFrets = 12;
1307
+ let hands = { R: "#7b2436", L: "#c8a55b" };
1308
+ let showLabels = false;
1309
+ let rect = { x: 0, y: 0, w: 0, h: 0 };
1310
+ return {
1311
+ key: "fretboard",
1312
+ init(ctx, props) {
1313
+ hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
1314
+ showLabels = props.showLabels ?? false;
1315
+ const midis = (ctx.score?.notes ?? []).map((n) => n.pitchMidi);
1316
+ placement = buildPlacementMap(midis);
1317
+ let maxFret = 0;
1318
+ for (const p of placement.values()) maxFret = Math.max(maxFret, p.fret);
1319
+ visibleFrets = props.visibleFrets ?? Math.min(15, Math.max(5, maxFret + 1));
1320
+ const sb = ctx.safeBox;
1321
+ const height = props.height ?? 220;
1322
+ const bottomY = props.bottomY ?? sb.bottom;
1323
+ rect = { x: sb.left, y: bottomY - height, w: sb.w, h: height };
1324
+ },
1325
+ draw(ctx, tMs) {
1326
+ const c = ctx.ctx2d;
1327
+ const { x, y, w, h } = rect;
1328
+ const nutW = Math.max(6, w * 0.012);
1329
+ const boardX = x + nutW;
1330
+ const boardW = w - nutW;
1331
+ const fretW = boardW / visibleFrets;
1332
+ const stringGap = h / (STRING_COUNT - 1);
1333
+ const stringY = (s) => y + h - s * stringGap;
1334
+ const fretX = (f) => boardX + f * fretW;
1335
+ const dotX = (f) => f === 0 ? x + nutW / 2 : fretX(f) - fretW / 2;
1336
+ c.save();
1337
+ c.fillStyle = "#f1e7d6";
1338
+ c.fillRect(x, y, w, h);
1339
+ c.fillStyle = "#2a2420";
1340
+ c.fillRect(x, y, nutW, h);
1341
+ c.strokeStyle = "#b8a98c";
1342
+ c.lineWidth = 2;
1343
+ for (let f = 1; f <= visibleFrets; f++) {
1344
+ c.beginPath();
1345
+ c.moveTo(fretX(f), y);
1346
+ c.lineTo(fretX(f), y + h);
1347
+ c.stroke();
1348
+ }
1349
+ c.fillStyle = "rgba(60,50,40,0.28)";
1350
+ for (const f of INLAY_FRETS) {
1351
+ if (f > visibleFrets) continue;
1352
+ const cx = dotX(f);
1353
+ const r = Math.min(stringGap, fretW) * 0.18;
1354
+ if (f === 12) {
1355
+ c.beginPath();
1356
+ c.arc(cx, stringY(STRING_COUNT - 1) + stringGap * 0.5, r, 0, Math.PI * 2);
1357
+ c.fill();
1358
+ c.beginPath();
1359
+ c.arc(cx, stringY(0) - stringGap * 0.5, r, 0, Math.PI * 2);
1360
+ c.fill();
1361
+ } else {
1362
+ c.beginPath();
1363
+ c.arc(cx, y + h / 2, r, 0, Math.PI * 2);
1364
+ c.fill();
1365
+ }
1366
+ }
1367
+ c.strokeStyle = "#5c5345";
1368
+ for (let s = 0; s < STRING_COUNT; s++) {
1369
+ c.lineWidth = 1 + (STRING_COUNT - 1 - s) * 0.5;
1370
+ c.beginPath();
1371
+ c.moveTo(x, stringY(s));
1372
+ c.lineTo(x + w, stringY(s));
1373
+ c.stroke();
1374
+ }
1375
+ const dotR = Math.min(stringGap, fretW) * 0.42;
1376
+ for (const n of ctx.score?.notes ?? []) {
1377
+ if (!(tMs >= n.onsetMs && tMs < n.onsetMs + n.durMs)) continue;
1378
+ const pos = placement.get(n.pitchMidi);
1379
+ if (!pos || pos.fret > visibleFrets) continue;
1380
+ const cx = dotX(pos.fret);
1381
+ const cy = stringY(pos.string);
1382
+ c.fillStyle = n.hand === "L" ? hands.L : hands.R;
1383
+ c.beginPath();
1384
+ c.arc(cx, cy, dotR, 0, Math.PI * 2);
1385
+ c.fill();
1386
+ if (showLabels) {
1387
+ c.fillStyle = "#fff";
1388
+ c.font = `${Math.round(dotR * 1.1)}px sans-serif`;
1389
+ c.textAlign = "center";
1390
+ c.textBaseline = "middle";
1391
+ c.fillText(NOTE_NAMES[n.pitchMidi % 12], cx, cy);
1392
+ }
1393
+ }
1394
+ c.restore();
1395
+ },
1396
+ dispose() {
1397
+ placement = /* @__PURE__ */ new Map();
1398
+ }
1399
+ };
1400
+ }
1401
+ var fretboardFactory = {
1402
+ key: "fretboard",
1403
+ create: fretboardLayer,
1404
+ validateProps(props) {
1405
+ const errs = [];
1406
+ if (props == null || typeof props !== "object") return ["fretboard: props must be an object"];
1407
+ const p = props;
1408
+ if (p.visibleFrets != null && (typeof p.visibleFrets !== "number" || p.visibleFrets < 1 || p.visibleFrets > 24))
1409
+ errs.push("fretboard.visibleFrets must be a number in 1..24");
1410
+ if (p.height != null && (typeof p.height !== "number" || p.height <= 0))
1411
+ errs.push("fretboard.height must be a positive number");
1412
+ if (p.bottomY != null && typeof p.bottomY !== "number") errs.push("fretboard.bottomY must be a number");
1413
+ if (p.showLabels != null && typeof p.showLabels !== "boolean") errs.push("fretboard.showLabels must be a boolean");
1414
+ return errs;
1415
+ }
1416
+ };
1417
+
1299
1418
  // src/scene/layers/fallingNotes.ts
1300
1419
  var DEFAULT_LEAD_MS = 2e3;
1301
1420
  function rgba(color, a) {
@@ -4662,6 +4781,7 @@ registerLayer(captionFactory);
4662
4781
  registerLayer(notationFactory);
4663
4782
  registerLayer(scrollCursorFactory);
4664
4783
  registerLayer(keyboardFactory);
4784
+ registerLayer(fretboardFactory);
4665
4785
  registerLayer(fallingNotesFactory);
4666
4786
  registerLayer(hookFactory);
4667
4787
  registerLayer(revealFactory);
@@ -5503,6 +5623,7 @@ export {
5503
5623
  followWindowStart,
5504
5624
  fracSlotPoint,
5505
5625
  frameRect,
5626
+ fretboardFactory,
5506
5627
  functionColor,
5507
5628
  functionalHarmonyFactory,
5508
5629
  getKeyboardLayout,