@pepperhorn/chordl-core 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/audio/midi-export.d.ts +23 -0
- package/dist/audio/midi-export.js +155 -0
- package/dist/audio/midi-export.js.map +1 -0
- package/dist/chord-sheet/codec.d.ts +14 -0
- package/dist/chord-sheet/codec.js +119 -0
- package/dist/chord-sheet/codec.js.map +1 -0
- package/dist/chord-sheet/defaults.d.ts +13 -0
- package/dist/chord-sheet/defaults.js +48 -0
- package/dist/chord-sheet/defaults.js.map +1 -0
- package/dist/chord-sheet/index.d.ts +3 -0
- package/dist/chord-sheet/index.js +4 -0
- package/dist/chord-sheet/index.js.map +1 -0
- package/dist/chord-sheet/schema.d.ts +6 -0
- package/dist/chord-sheet/schema.js +17 -0
- package/dist/chord-sheet/schema.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -0
- package/dist/engine/auto-fingering.d.ts +36 -0
- package/dist/engine/auto-fingering.js +344 -0
- package/dist/engine/auto-fingering.js.map +1 -0
- package/dist/engine/highlight-mapper.d.ts +3 -0
- package/dist/engine/highlight-mapper.js +46 -0
- package/dist/engine/highlight-mapper.js.map +1 -0
- package/dist/engine/keyboard-layout.d.ts +6 -0
- package/dist/engine/keyboard-layout.js +65 -0
- package/dist/engine/keyboard-layout.js.map +1 -0
- package/dist/engine/note-spelling.d.ts +54 -0
- package/dist/engine/note-spelling.js +103 -0
- package/dist/engine/note-spelling.js.map +1 -0
- package/dist/engine/scale-fingering.d.ts +21 -0
- package/dist/engine/scale-fingering.js +130 -0
- package/dist/engine/scale-fingering.js.map +1 -0
- package/dist/engine/staff-constants.d.ts +16 -0
- package/dist/engine/staff-constants.js +20 -0
- package/dist/engine/staff-constants.js.map +1 -0
- package/dist/engine/staff-glyphs.d.ts +17 -0
- package/dist/engine/staff-glyphs.js +59 -0
- package/dist/engine/staff-glyphs.js.map +1 -0
- package/dist/engine/staff-layout.d.ts +28 -0
- package/dist/engine/staff-layout.js +238 -0
- package/dist/engine/staff-layout.js.map +1 -0
- package/dist/engine/svg-constants.d.ts +19 -0
- package/dist/engine/svg-constants.js +40 -0
- package/dist/engine/svg-constants.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +70 -0
- package/dist/logging.js +24 -0
- package/dist/logging.js.map +1 -0
- package/dist/parser/natural-language.d.ts +9 -0
- package/dist/parser/natural-language.js +406 -0
- package/dist/parser/natural-language.js.map +1 -0
- package/dist/parser/progression-parser.d.ts +16 -0
- package/dist/parser/progression-parser.js +97 -0
- package/dist/parser/progression-parser.js.map +1 -0
- package/dist/pipeline.d.ts +33 -0
- package/dist/pipeline.js +203 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/progression/form-templates.d.ts +11 -0
- package/dist/progression/form-templates.js +105 -0
- package/dist/progression/form-templates.js.map +1 -0
- package/dist/progression/index.d.ts +5 -0
- package/dist/progression/index.js +4 -0
- package/dist/progression/index.js.map +1 -0
- package/dist/progression/progression-resolver.d.ts +29 -0
- package/dist/progression/progression-resolver.js +135 -0
- package/dist/progression/progression-resolver.js.map +1 -0
- package/dist/progression/roman-numeral.d.ts +14 -0
- package/dist/progression/roman-numeral.js +78 -0
- package/dist/progression/roman-numeral.js.map +1 -0
- package/dist/resolver/auto-layout.d.ts +20 -0
- package/dist/resolver/auto-layout.js +195 -0
- package/dist/resolver/auto-layout.js.map +1 -0
- package/dist/resolver/chord-resolver.d.ts +8 -0
- package/dist/resolver/chord-resolver.js +172 -0
- package/dist/resolver/chord-resolver.js.map +1 -0
- package/dist/resolver/scale-resolver.d.ts +9 -0
- package/dist/resolver/scale-resolver.js +45 -0
- package/dist/resolver/scale-resolver.js.map +1 -0
- package/dist/themes/boomwhacker.d.ts +2 -0
- package/dist/themes/boomwhacker.js +35 -0
- package/dist/themes/boomwhacker.js.map +1 -0
- package/dist/themes/crf.d.ts +19 -0
- package/dist/themes/crf.js +34 -0
- package/dist/themes/crf.js.map +1 -0
- package/dist/themes/index.d.ts +3 -0
- package/dist/themes/index.js +25 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/themes/simple.d.ts +2 -0
- package/dist/themes/simple.js +13 -0
- package/dist/themes/simple.js.map +1 -0
- package/dist/theory/chord-tones.d.ts +56 -0
- package/dist/theory/chord-tones.js +215 -0
- package/dist/theory/chord-tones.js.map +1 -0
- package/dist/theory/degree-labels.d.ts +31 -0
- package/dist/theory/degree-labels.js +74 -0
- package/dist/theory/degree-labels.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 PepperHorn Music / Creative Ranges Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface MidiChordOptions {
|
|
2
|
+
notes: string[];
|
|
3
|
+
/** Left-hand notes (bass clef). Remaining notes go to right hand (treble clef). */
|
|
4
|
+
lhNotes?: string[];
|
|
5
|
+
octave?: number;
|
|
6
|
+
lhOctave?: number;
|
|
7
|
+
durationTicks?: number;
|
|
8
|
+
velocity?: number;
|
|
9
|
+
tempo?: number;
|
|
10
|
+
arpeggiated?: boolean;
|
|
11
|
+
arpDelayTicks?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Generate a MIDI file as a Uint8Array containing a single chord.
|
|
15
|
+
* When lhNotes is provided, produces a Format 1 file with LH on channel 0
|
|
16
|
+
* (bass clef, octave 3) and RH on channel 1 (treble clef, octave 4).
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateMidiFile(options: MidiChordOptions): Uint8Array;
|
|
19
|
+
/**
|
|
20
|
+
* Trigger a browser download of a MIDI file.
|
|
21
|
+
*/
|
|
22
|
+
export declare function downloadMidi(notes: string[], chordName: string, octave?: number, lhNotes?: string[], lhOctave?: number): void;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Note } from "tonal";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal MIDI file generator for a single chord.
|
|
4
|
+
* Produces a Format 0 (single track) Standard MIDI File.
|
|
5
|
+
*/
|
|
6
|
+
function toVarLen(value) {
|
|
7
|
+
if (value < 0x80)
|
|
8
|
+
return [value];
|
|
9
|
+
const bytes = [];
|
|
10
|
+
bytes.unshift(value & 0x7f);
|
|
11
|
+
value >>= 7;
|
|
12
|
+
while (value > 0) {
|
|
13
|
+
bytes.unshift((value & 0x7f) | 0x80);
|
|
14
|
+
value >>= 7;
|
|
15
|
+
}
|
|
16
|
+
return bytes;
|
|
17
|
+
}
|
|
18
|
+
function writeUint16(value) {
|
|
19
|
+
return [(value >> 8) & 0xff, value & 0xff];
|
|
20
|
+
}
|
|
21
|
+
function writeUint32(value) {
|
|
22
|
+
return [
|
|
23
|
+
(value >> 24) & 0xff,
|
|
24
|
+
(value >> 16) & 0xff,
|
|
25
|
+
(value >> 8) & 0xff,
|
|
26
|
+
value & 0xff,
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
function noteToMidi(note, defaultOctave = 4) {
|
|
30
|
+
if (/[0-9]$/.test(note))
|
|
31
|
+
return Note.midi(note);
|
|
32
|
+
return Note.midi(`${note}${defaultOctave}`);
|
|
33
|
+
}
|
|
34
|
+
/** Build raw track bytes for a set of MIDI note numbers on a given channel. */
|
|
35
|
+
function buildTrackData(midiNotes, channel, velocity, durationTicks, trackName, tempoEvent, arpeggiated, arpDelayTicks) {
|
|
36
|
+
const data = [];
|
|
37
|
+
const noteOn = 0x90 | channel;
|
|
38
|
+
const noteOff = 0x80 | channel;
|
|
39
|
+
// Tempo meta event (only on first track)
|
|
40
|
+
if (tempoEvent) {
|
|
41
|
+
const { microsecondsPerBeat } = tempoEvent;
|
|
42
|
+
data.push(0x00, 0xff, 0x51, 0x03, (microsecondsPerBeat >> 16) & 0xff, (microsecondsPerBeat >> 8) & 0xff, microsecondsPerBeat & 0xff);
|
|
43
|
+
}
|
|
44
|
+
// Track name (ASCII only, VarLen-encoded length)
|
|
45
|
+
const nameBytes = Array.from(trackName, (ch) => ch.charCodeAt(0) & 0x7f);
|
|
46
|
+
data.push(0x00, 0xff, 0x03, ...toVarLen(nameBytes.length), ...nameBytes);
|
|
47
|
+
// Note-on events
|
|
48
|
+
if (arpeggiated && arpDelayTicks) {
|
|
49
|
+
for (let i = 0; i < midiNotes.length; i++) {
|
|
50
|
+
const delta = i === 0 ? 0 : arpDelayTicks;
|
|
51
|
+
data.push(...toVarLen(delta), noteOn, midiNotes[i], velocity);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
for (let i = 0; i < midiNotes.length; i++) {
|
|
56
|
+
data.push(...toVarLen(0), noteOn, midiNotes[i], velocity);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Note-off events
|
|
60
|
+
const totalArpDelay = arpeggiated && arpDelayTicks ? (midiNotes.length - 1) * arpDelayTicks : 0;
|
|
61
|
+
for (let i = 0; i < midiNotes.length; i++) {
|
|
62
|
+
const delta = i === 0 ? durationTicks + totalArpDelay : 0;
|
|
63
|
+
data.push(...toVarLen(delta), noteOff, midiNotes[i], 0);
|
|
64
|
+
}
|
|
65
|
+
// End of track
|
|
66
|
+
data.push(0x00, 0xff, 0x2f, 0x00);
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Generate a MIDI file as a Uint8Array containing a single chord.
|
|
71
|
+
* When lhNotes is provided, produces a Format 1 file with LH on channel 0
|
|
72
|
+
* (bass clef, octave 3) and RH on channel 1 (treble clef, octave 4).
|
|
73
|
+
*/
|
|
74
|
+
export function generateMidiFile(options) {
|
|
75
|
+
const { notes, lhNotes, octave = 4, lhOctave: lhOctaveOpt, durationTicks = 960, velocity = 90, tempo = 120, arpeggiated = false, arpDelayTicks = 120, } = options;
|
|
76
|
+
const ticksPerQuarter = 480;
|
|
77
|
+
const microsecondsPerBeat = Math.round(60000000 / tempo);
|
|
78
|
+
if (lhNotes && lhNotes.length > 0) {
|
|
79
|
+
// Format 1: separate LH (bass clef) and RH (treble clef) tracks
|
|
80
|
+
const lhOctave = lhOctaveOpt ?? 3;
|
|
81
|
+
const rhOctave = octave;
|
|
82
|
+
const lhSet = new Set(lhNotes);
|
|
83
|
+
const rhNotes = notes.filter((n) => !lhSet.has(n));
|
|
84
|
+
const lhMidi = lhNotes.map((n) => noteToMidi(n, lhOctave)).filter((m) => m != null);
|
|
85
|
+
const rhMidi = rhNotes.map((n) => noteToMidi(n, rhOctave)).filter((m) => m != null);
|
|
86
|
+
if (lhMidi.length === 0 && rhMidi.length === 0)
|
|
87
|
+
return new Uint8Array(0);
|
|
88
|
+
const tracks = [];
|
|
89
|
+
if (lhMidi.length > 0) {
|
|
90
|
+
tracks.push(buildTrackData(lhMidi, 0, velocity, durationTicks, "L.H.", { microsecondsPerBeat }, arpeggiated, arpDelayTicks));
|
|
91
|
+
}
|
|
92
|
+
if (rhMidi.length > 0) {
|
|
93
|
+
tracks.push(buildTrackData(rhMidi, 1, velocity, durationTicks, "R.H.", tracks.length === 0 ? { microsecondsPerBeat } : undefined, arpeggiated, arpDelayTicks));
|
|
94
|
+
}
|
|
95
|
+
const header = [
|
|
96
|
+
0x4d, 0x54, 0x68, 0x64,
|
|
97
|
+
...writeUint32(6),
|
|
98
|
+
...writeUint16(1), // format 1
|
|
99
|
+
...writeUint16(tracks.length),
|
|
100
|
+
...writeUint16(ticksPerQuarter),
|
|
101
|
+
];
|
|
102
|
+
let totalSize = header.length;
|
|
103
|
+
const trackChunks = [];
|
|
104
|
+
for (const track of tracks) {
|
|
105
|
+
const chunk = [0x4d, 0x54, 0x72, 0x6b, ...writeUint32(track.length), ...track];
|
|
106
|
+
trackChunks.push(chunk);
|
|
107
|
+
totalSize += chunk.length;
|
|
108
|
+
}
|
|
109
|
+
const file = new Uint8Array(totalSize);
|
|
110
|
+
let offset = 0;
|
|
111
|
+
file.set(header, offset);
|
|
112
|
+
offset += header.length;
|
|
113
|
+
for (const chunk of trackChunks) {
|
|
114
|
+
file.set(chunk, offset);
|
|
115
|
+
offset += chunk.length;
|
|
116
|
+
}
|
|
117
|
+
return file;
|
|
118
|
+
}
|
|
119
|
+
// Single track (Format 0) — original behavior
|
|
120
|
+
const midiNotes = notes
|
|
121
|
+
.map((n) => noteToMidi(n, octave))
|
|
122
|
+
.filter((m) => m != null);
|
|
123
|
+
if (midiNotes.length === 0)
|
|
124
|
+
return new Uint8Array(0);
|
|
125
|
+
const trackData = buildTrackData(midiNotes, 0, velocity, durationTicks, "Chord", { microsecondsPerBeat }, arpeggiated, arpDelayTicks);
|
|
126
|
+
const header = [
|
|
127
|
+
0x4d, 0x54, 0x68, 0x64,
|
|
128
|
+
...writeUint32(6),
|
|
129
|
+
...writeUint16(0), // format 0
|
|
130
|
+
...writeUint16(1),
|
|
131
|
+
...writeUint16(ticksPerQuarter),
|
|
132
|
+
];
|
|
133
|
+
const trackHeader = [0x4d, 0x54, 0x72, 0x6b, ...writeUint32(trackData.length)];
|
|
134
|
+
const file = new Uint8Array(header.length + trackHeader.length + trackData.length);
|
|
135
|
+
file.set(header, 0);
|
|
136
|
+
file.set(trackHeader, header.length);
|
|
137
|
+
file.set(trackData, header.length + trackHeader.length);
|
|
138
|
+
return file;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Trigger a browser download of a MIDI file.
|
|
142
|
+
*/
|
|
143
|
+
export function downloadMidi(notes, chordName, octave = 4, lhNotes, lhOctave) {
|
|
144
|
+
const midi = generateMidiFile({ notes, octave, lhNotes, lhOctave });
|
|
145
|
+
if (midi.length === 0)
|
|
146
|
+
return;
|
|
147
|
+
const blob = new Blob([midi.buffer], { type: "audio/midi" });
|
|
148
|
+
const url = URL.createObjectURL(blob);
|
|
149
|
+
const a = document.createElement("a");
|
|
150
|
+
a.href = url;
|
|
151
|
+
a.download = `${chordName.replace(/[^a-zA-Z0-9\-_ ]/g, "_").slice(0, 100)}.mid`;
|
|
152
|
+
a.click();
|
|
153
|
+
URL.revokeObjectURL(url);
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=midi-export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"midi-export.js","sourceRoot":"","sources":["../../src/audio/midi-export.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChordSheetData } from "../types";
|
|
2
|
+
export interface CodecOptions {
|
|
3
|
+
/** Skip compression (for environments without CompressionStream). */
|
|
4
|
+
skipCompression?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Encode a ChordSheetData object into a portable string token.
|
|
8
|
+
* Format: bcs1.<base64url(deflate(JSON))>
|
|
9
|
+
*/
|
|
10
|
+
export declare function encodeChordSheet(data: ChordSheetData, options?: CodecOptions): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Decode a token string back into ChordSheetData.
|
|
13
|
+
*/
|
|
14
|
+
export declare function decodeChordSheet(token: string, _options?: CodecOptions): Promise<ChordSheetData>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { CHORD_SHEET_SCHEMA_VERSION, validateVersion } from "./schema";
|
|
2
|
+
const TOKEN_PREFIX = "bcs1.";
|
|
3
|
+
/**
|
|
4
|
+
* Encode a ChordSheetData object into a portable string token.
|
|
5
|
+
* Format: bcs1.<base64url(deflate(JSON))>
|
|
6
|
+
*/
|
|
7
|
+
export async function encodeChordSheet(data, options) {
|
|
8
|
+
const json = JSON.stringify({ ...data, v: data.v ?? CHORD_SHEET_SCHEMA_VERSION });
|
|
9
|
+
const bytes = new TextEncoder().encode(json);
|
|
10
|
+
if (!options?.skipCompression && typeof CompressionStream !== "undefined") {
|
|
11
|
+
try {
|
|
12
|
+
const compressed = await compress(bytes);
|
|
13
|
+
return TOKEN_PREFIX + base64urlEncode(compressed);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Fallback to uncompressed
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return TOKEN_PREFIX + base64urlEncode(bytes);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decode a token string back into ChordSheetData.
|
|
23
|
+
*/
|
|
24
|
+
export async function decodeChordSheet(token, _options) {
|
|
25
|
+
if (!token.startsWith(TOKEN_PREFIX)) {
|
|
26
|
+
throw new Error(`Invalid token: must start with "${TOKEN_PREFIX}"`);
|
|
27
|
+
}
|
|
28
|
+
const payload = token.slice(TOKEN_PREFIX.length);
|
|
29
|
+
const bytes = base64urlDecode(payload);
|
|
30
|
+
// Try decompressing first, fall back to raw JSON
|
|
31
|
+
let json;
|
|
32
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
33
|
+
try {
|
|
34
|
+
const decompressed = await decompress(bytes);
|
|
35
|
+
json = new TextDecoder().decode(decompressed);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
json = new TextDecoder().decode(bytes);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
json = new TextDecoder().decode(bytes);
|
|
43
|
+
}
|
|
44
|
+
let data;
|
|
45
|
+
try {
|
|
46
|
+
data = JSON.parse(json);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error("Invalid token: could not parse payload as JSON");
|
|
50
|
+
}
|
|
51
|
+
validateVersion(data.v);
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
55
|
+
function base64urlEncode(bytes) {
|
|
56
|
+
let binary = "";
|
|
57
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
58
|
+
binary += String.fromCharCode(bytes[i]);
|
|
59
|
+
}
|
|
60
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
61
|
+
}
|
|
62
|
+
function base64urlDecode(str) {
|
|
63
|
+
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
64
|
+
const binary = atob(padded);
|
|
65
|
+
const bytes = new Uint8Array(binary.length);
|
|
66
|
+
for (let i = 0; i < binary.length; i++) {
|
|
67
|
+
bytes[i] = binary.charCodeAt(i);
|
|
68
|
+
}
|
|
69
|
+
return bytes;
|
|
70
|
+
}
|
|
71
|
+
async function compress(input) {
|
|
72
|
+
const cs = new CompressionStream("deflate");
|
|
73
|
+
const writer = cs.writable.getWriter();
|
|
74
|
+
writer.write(input);
|
|
75
|
+
writer.close();
|
|
76
|
+
const reader = cs.readable.getReader();
|
|
77
|
+
const chunks = [];
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done)
|
|
81
|
+
break;
|
|
82
|
+
chunks.push(value);
|
|
83
|
+
}
|
|
84
|
+
return concatBytes(chunks);
|
|
85
|
+
}
|
|
86
|
+
async function decompress(input) {
|
|
87
|
+
const ds = new DecompressionStream("deflate");
|
|
88
|
+
const writer = ds.writable.getWriter();
|
|
89
|
+
// Write and close, but don't await — let errors surface on the readable side
|
|
90
|
+
const writePromise = writer.write(input).then(() => writer.close()).catch(() => { });
|
|
91
|
+
const reader = ds.readable.getReader();
|
|
92
|
+
const chunks = [];
|
|
93
|
+
try {
|
|
94
|
+
while (true) {
|
|
95
|
+
const { done, value } = await reader.read();
|
|
96
|
+
if (done)
|
|
97
|
+
break;
|
|
98
|
+
chunks.push(value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
// Ensure writer is cleaned up
|
|
103
|
+
await writePromise;
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
await writePromise;
|
|
107
|
+
return concatBytes(chunks);
|
|
108
|
+
}
|
|
109
|
+
function concatBytes(chunks) {
|
|
110
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
111
|
+
const out = new Uint8Array(total);
|
|
112
|
+
let offset = 0;
|
|
113
|
+
for (const c of chunks) {
|
|
114
|
+
out.set(c, offset);
|
|
115
|
+
offset += c.length;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.js","sourceRoot":"","sources":["../../src/chord-sheet/codec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DisplayDefaults } from "../types";
|
|
2
|
+
/** System-level defaults when nothing is specified. */
|
|
3
|
+
export declare const SYSTEM_DEFAULTS: Required<DisplayDefaults>;
|
|
4
|
+
/**
|
|
5
|
+
* Shallow-merge display defaults with undefined-stripping.
|
|
6
|
+
* Priority: chord > section > sheet > system.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveDefaults(sheet?: DisplayDefaults, section?: DisplayDefaults, chord?: Partial<DisplayDefaults>): Required<DisplayDefaults>;
|
|
9
|
+
/**
|
|
10
|
+
* Compute a chord reference ID from its position.
|
|
11
|
+
* Single section → "1","2","3"... Multiple sections → "A1","A2","B1"...
|
|
12
|
+
*/
|
|
13
|
+
export declare function chordRef(sectionIndex: number, chordIndex: number, sectionCount: number, sectionId?: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** System-level defaults when nothing is specified. */
|
|
2
|
+
export const SYSTEM_DEFAULTS = {
|
|
3
|
+
display: "keyboard",
|
|
4
|
+
format: "compact",
|
|
5
|
+
theme: "simple",
|
|
6
|
+
highlightColor: "#a0c6e8",
|
|
7
|
+
padding: 1,
|
|
8
|
+
scale: 0.5,
|
|
9
|
+
glyphs: "bravura",
|
|
10
|
+
showNoteNames: false,
|
|
11
|
+
noteNameSize: "base",
|
|
12
|
+
noteNameMode: "pitch-class",
|
|
13
|
+
showFingering: false,
|
|
14
|
+
fingeringSize: "base",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Shallow-merge display defaults with undefined-stripping.
|
|
18
|
+
* Priority: chord > section > sheet > system.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveDefaults(sheet, section, chord) {
|
|
21
|
+
return {
|
|
22
|
+
...SYSTEM_DEFAULTS,
|
|
23
|
+
...(sheet ? stripUndefined(sheet) : {}),
|
|
24
|
+
...(section ? stripUndefined(section) : {}),
|
|
25
|
+
...(chord ? stripUndefined(chord) : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Remove keys whose value is undefined so they don't overwrite lower layers. */
|
|
29
|
+
function stripUndefined(obj) {
|
|
30
|
+
const out = { ...obj };
|
|
31
|
+
for (const key of Object.keys(out)) {
|
|
32
|
+
if (out[key] === undefined)
|
|
33
|
+
delete out[key];
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compute a chord reference ID from its position.
|
|
39
|
+
* Single section → "1","2","3"... Multiple sections → "A1","A2","B1"...
|
|
40
|
+
*/
|
|
41
|
+
export function chordRef(sectionIndex, chordIndex, sectionCount, sectionId) {
|
|
42
|
+
const num = chordIndex + 1;
|
|
43
|
+
if (sectionCount <= 1)
|
|
44
|
+
return String(num);
|
|
45
|
+
const letter = sectionId ?? String.fromCharCode(65 + sectionIndex); // A, B, C...
|
|
46
|
+
return `${letter}${num}`;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/chord-sheet/defaults.ts"],"names":[],"mappings":"AAEA,uDAAuD;AACvD,MAAM,CAAC,MAAM,eAAe,GAA8B;IACxD,OAAO,EAAE,UAAU;IACnB,MAAM,EAAE,SAAS;IACjB,KAAK,EAAE,QAAQ;IACf,cAAc,EAAE,SAAS;IACzB,OAAO,EAAE,CAAC;IACV,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,SAAS;IACjB,aAAa,EAAE,KAAK;IACpB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,aAAa;IAC3B,aAAa,EAAE,KAAK;IACpB,aAAa,EAAE,MAAM;CACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAuB,EACvB,OAAyB,EACzB,KAAgC;IAEhC,OAAO;QACL,GAAG,eAAe;QAClB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACX,CAAC;AACjC,CAAC;AAED,iFAAiF;AACjF,SAAS,cAAc,CAAmB,GAAM;IAC9C,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,EAA6B,CAAC;IAClD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,GAAiB,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,YAAoB,EACpB,UAAkB,EAClB,YAAoB,EACpB,SAAkB;IAElB,MAAM,GAAG,GAAG,UAAU,GAAG,CAAC,CAAC;IAC3B,IAAI,YAAY,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,SAAS,IAAI,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,aAAa;IACjF,OAAO,GAAG,MAAM,GAAG,GAAG,EAAE,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/chord-sheet/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const CHORD_SHEET_SCHEMA_VERSION = "1.0";
|
|
2
|
+
/**
|
|
3
|
+
* Validate that a token's version is compatible with the current schema.
|
|
4
|
+
* Rejects incompatible major versions; minor differences are OK.
|
|
5
|
+
*/
|
|
6
|
+
export function validateVersion(v) {
|
|
7
|
+
if (!v || typeof v !== "string") {
|
|
8
|
+
throw new Error("Missing or invalid schema version");
|
|
9
|
+
}
|
|
10
|
+
const [major] = v.split(".");
|
|
11
|
+
const [currentMajor] = CHORD_SHEET_SCHEMA_VERSION.split(".");
|
|
12
|
+
if (major !== currentMajor) {
|
|
13
|
+
throw new Error(`Incompatible schema version "${v}" (current: "${CHORD_SHEET_SCHEMA_VERSION}"). ` +
|
|
14
|
+
`Major version mismatch — this sheet was created with a newer or older format.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/chord-sheet/schema.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,CAAS;IACvC,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,YAAY,CAAC,GAAG,0BAA0B,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7D,IAAI,KAAK,KAAK,YAAY,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,gCAAgC,CAAC,gBAAgB,0BAA0B,MAAM;YACjF,+EAA+E,CAChF,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const MAX_EXAMPLES = 3;
|
|
2
|
+
// --- Chord Logging ---
|
|
3
|
+
// Opt-in telemetry for shared learning.
|
|
4
|
+
// When enabled, chord requests and their results are sent to a write-only
|
|
5
|
+
// public ledger. No personal data is collected — just chord inputs, outputs,
|
|
6
|
+
// and an anonymized session hash derived server-side from the request IP.
|
|
7
|
+
//
|
|
8
|
+
// By pointing to the shared public ledger, we can all learn from the results
|
|
9
|
+
// and keep improving the voicing logic together.
|
|
10
|
+
//
|
|
11
|
+
// To use the shared ledger, uncomment CHORD_LOG_ENDPOINT below.
|
|
12
|
+
// To self-host, point it at your own compatible endpoint.
|
|
13
|
+
export const ENABLE_CHORD_LOGGING = false;
|
|
14
|
+
// export const CHORD_LOG_ENDPOINT = "https://ledger.betterchord.com/api/chord-log";
|
|
15
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAE9B,wBAAwB;AACxB,wCAAwC;AACxC,0EAA0E;AAC1E,6EAA6E;AAC7E,0EAA0E;AAC1E,EAAE;AACF,6EAA6E;AAC7E,iDAAiD;AACjD,EAAE;AACF,gEAAgE;AAChE,0DAA0D;AAE1D,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC;AAC1C,oFAAoF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-fingering engine for piano chord voicings.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Mark Levine's "The Jazz Piano Book" approach:
|
|
5
|
+
* - Chords are always playable — split across hands when needed
|
|
6
|
+
* - Max span per hand: a 12th (19 semitones)
|
|
7
|
+
* - Max notes per hand: 5
|
|
8
|
+
* - LH typically takes root + guide tones (3rd/7th)
|
|
9
|
+
* - RH takes upper extensions (9th, #11th, 13th)
|
|
10
|
+
*
|
|
11
|
+
* Finger assignment rules:
|
|
12
|
+
* - RH: 1 (thumb) = lowest, 5 (pinky) = highest
|
|
13
|
+
* - LH: 5 (pinky) = lowest, 1 (thumb) = highest
|
|
14
|
+
* - Avoid thumb on black keys except at the bottom of RH / top of LH
|
|
15
|
+
* - Wider intervals skip middle fingers (1-2-5 rather than 1-2-3)
|
|
16
|
+
*/
|
|
17
|
+
export type Hand = "rh" | "lh";
|
|
18
|
+
export interface HandAssignment {
|
|
19
|
+
/** Finger number per note (1-5), matching the input notes array order */
|
|
20
|
+
fingering: number[];
|
|
21
|
+
/** Hand assignment per note: "lh" or "rh" */
|
|
22
|
+
hands: Hand[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Assign fingering to chord notes, splitting across hands when needed.
|
|
26
|
+
*
|
|
27
|
+
* @param notes - Pitch classes in voicing order (e.g. ["B", "D#", "F#", ...])
|
|
28
|
+
* @param handHints - Optional per-note hand assignments from voicing library ("LH"/"RH")
|
|
29
|
+
* @param midiValues - Optional MIDI note numbers for accurate pitch ordering and span calculation.
|
|
30
|
+
* When provided, notes are sorted by actual pitch before splitting.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assignFingering(notes: string[], handHints?: ("LH" | "RH")[], midiValues?: number[]): HandAssignment;
|
|
33
|
+
/**
|
|
34
|
+
* Compute auto-fingering for a chord voicing (legacy API).
|
|
35
|
+
*/
|
|
36
|
+
export declare function autoFingering(notes: string[], hand?: Hand): number[];
|