@parent-tobias/chord-component 1.0.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,356 @@
1
+ /**
2
+ * IndexedDB service for storing and retrieving chord data
3
+ */
4
+
5
+ const DB_NAME = 'ChordComponentsDB';
6
+ const DB_VERSION = 2; // Incremented for new store
7
+ const STORE_NAME = 'chordData';
8
+ const USER_STORE_NAME = 'userChords';
9
+
10
+ export interface ChordData {
11
+ instrument: string;
12
+ chords: Record<string, any>;
13
+ timestamp: number;
14
+ }
15
+
16
+ export interface UserChordData {
17
+ key: string; // composite key: "instrument:chordName"
18
+ instrument: string;
19
+ chordName: string;
20
+ fingers: any[];
21
+ barres: any[];
22
+ position?: number; // Starting fret position (1 = first fret, etc.)
23
+ timestamp: number;
24
+ }
25
+
26
+ class IndexedDBService {
27
+ private dbPromise: Promise<IDBDatabase> | null = null;
28
+
29
+ /**
30
+ * Initialize and open the database
31
+ */
32
+ private async openDB(): Promise<IDBDatabase> {
33
+ if (this.dbPromise) {
34
+ return this.dbPromise;
35
+ }
36
+
37
+ this.dbPromise = new Promise((resolve, reject) => {
38
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
39
+
40
+ request.onerror = () => {
41
+ reject(new Error(`Failed to open IndexedDB: ${request.error}`));
42
+ };
43
+
44
+ request.onsuccess = () => {
45
+ resolve(request.result);
46
+ };
47
+
48
+ request.onupgradeneeded = (event) => {
49
+ const db = (event.target as IDBOpenDBRequest).result;
50
+
51
+ // Create chord data store if it doesn't exist
52
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
53
+ const objectStore = db.createObjectStore(STORE_NAME, { keyPath: 'instrument' });
54
+ objectStore.createIndex('timestamp', 'timestamp', { unique: false });
55
+ }
56
+
57
+ // Create user chords store if it doesn't exist
58
+ if (!db.objectStoreNames.contains(USER_STORE_NAME)) {
59
+ const userStore = db.createObjectStore(USER_STORE_NAME, { keyPath: 'key' });
60
+ userStore.createIndex('instrument', 'instrument', { unique: false });
61
+ userStore.createIndex('timestamp', 'timestamp', { unique: false });
62
+ }
63
+ };
64
+ });
65
+
66
+ return this.dbPromise;
67
+ }
68
+
69
+ /**
70
+ * Get chord data for a specific instrument from IndexedDB
71
+ */
72
+ async getChordData(instrument: string): Promise<ChordData | null> {
73
+ try {
74
+ const db = await this.openDB();
75
+
76
+ return new Promise((resolve, reject) => {
77
+ const transaction = db.transaction([STORE_NAME], 'readonly');
78
+ const objectStore = transaction.objectStore(STORE_NAME);
79
+ const request = objectStore.get(instrument);
80
+
81
+ request.onsuccess = () => {
82
+ resolve(request.result || null);
83
+ };
84
+
85
+ request.onerror = () => {
86
+ reject(new Error(`Failed to get chord data: ${request.error}`));
87
+ };
88
+ });
89
+ } catch (error) {
90
+ console.error('IndexedDB error:', error);
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Store chord data for a specific instrument in IndexedDB
97
+ */
98
+ async setChordData(instrument: string, chords: Record<string, any>): Promise<void> {
99
+ try {
100
+ const db = await this.openDB();
101
+
102
+ const data: ChordData = {
103
+ instrument,
104
+ chords,
105
+ timestamp: Date.now()
106
+ };
107
+
108
+ return new Promise((resolve, reject) => {
109
+ const transaction = db.transaction([STORE_NAME], 'readwrite');
110
+ const objectStore = transaction.objectStore(STORE_NAME);
111
+ const request = objectStore.put(data);
112
+
113
+ request.onsuccess = () => {
114
+ resolve();
115
+ };
116
+
117
+ request.onerror = () => {
118
+ reject(new Error(`Failed to store chord data: ${request.error}`));
119
+ };
120
+ });
121
+ } catch (error) {
122
+ console.error('IndexedDB error:', error);
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get all stored instruments
129
+ */
130
+ async getAllInstruments(): Promise<string[]> {
131
+ try {
132
+ const db = await this.openDB();
133
+
134
+ return new Promise((resolve, reject) => {
135
+ const transaction = db.transaction([STORE_NAME], 'readonly');
136
+ const objectStore = transaction.objectStore(STORE_NAME);
137
+ const request = objectStore.getAllKeys();
138
+
139
+ request.onsuccess = () => {
140
+ resolve(request.result as string[]);
141
+ };
142
+
143
+ request.onerror = () => {
144
+ reject(new Error(`Failed to get instruments: ${request.error}`));
145
+ };
146
+ });
147
+ } catch (error) {
148
+ console.error('IndexedDB error:', error);
149
+ return [];
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Clear all chord data from IndexedDB
155
+ */
156
+ async clearAll(): Promise<void> {
157
+ try {
158
+ const db = await this.openDB();
159
+
160
+ return new Promise((resolve, reject) => {
161
+ const transaction = db.transaction([STORE_NAME], 'readwrite');
162
+ const objectStore = transaction.objectStore(STORE_NAME);
163
+ const request = objectStore.clear();
164
+
165
+ request.onsuccess = () => {
166
+ resolve();
167
+ };
168
+
169
+ request.onerror = () => {
170
+ reject(new Error(`Failed to clear data: ${request.error}`));
171
+ };
172
+ });
173
+ } catch (error) {
174
+ console.error('IndexedDB error:', error);
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Check if IndexedDB is available
181
+ */
182
+ isAvailable(): boolean {
183
+ return typeof indexedDB !== 'undefined';
184
+ }
185
+
186
+ /**
187
+ * Save a user-defined chord
188
+ */
189
+ async saveUserChord(instrument: string, chordName: string, chordData: { fingers: any[], barres: any[], position?: number }): Promise<void> {
190
+ try {
191
+ const db = await this.openDB();
192
+
193
+ const data: UserChordData = {
194
+ key: `${instrument}:${chordName}`,
195
+ instrument,
196
+ chordName,
197
+ fingers: chordData.fingers,
198
+ barres: chordData.barres,
199
+ position: chordData.position,
200
+ timestamp: Date.now()
201
+ };
202
+
203
+ return new Promise((resolve, reject) => {
204
+ const transaction = db.transaction([USER_STORE_NAME], 'readwrite');
205
+ const objectStore = transaction.objectStore(USER_STORE_NAME);
206
+ const request = objectStore.put(data);
207
+
208
+ request.onsuccess = () => {
209
+ resolve();
210
+ };
211
+
212
+ request.onerror = () => {
213
+ reject(new Error(`Failed to save user chord: ${request.error}`));
214
+ };
215
+ });
216
+ } catch (error) {
217
+ console.error('IndexedDB error:', error);
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Get a user-defined chord
224
+ */
225
+ async getUserChord(instrument: string, chordName: string): Promise<UserChordData | null> {
226
+ try {
227
+ const db = await this.openDB();
228
+ const key = `${instrument}:${chordName}`;
229
+
230
+ return new Promise((resolve, reject) => {
231
+ const transaction = db.transaction([USER_STORE_NAME], 'readonly');
232
+ const objectStore = transaction.objectStore(USER_STORE_NAME);
233
+ const request = objectStore.get(key);
234
+
235
+ request.onsuccess = () => {
236
+ resolve(request.result || null);
237
+ };
238
+
239
+ request.onerror = () => {
240
+ reject(new Error(`Failed to get user chord: ${request.error}`));
241
+ };
242
+ });
243
+ } catch (error) {
244
+ console.error('IndexedDB error:', error);
245
+ return null;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Delete a user-defined chord
251
+ */
252
+ async deleteUserChord(instrument: string, chordName: string): Promise<void> {
253
+ try {
254
+ const db = await this.openDB();
255
+ const key = `${instrument}:${chordName}`;
256
+
257
+ return new Promise((resolve, reject) => {
258
+ const transaction = db.transaction([USER_STORE_NAME], 'readwrite');
259
+ const objectStore = transaction.objectStore(USER_STORE_NAME);
260
+ const request = objectStore.delete(key);
261
+
262
+ request.onsuccess = () => {
263
+ resolve();
264
+ };
265
+
266
+ request.onerror = () => {
267
+ reject(new Error(`Failed to delete user chord: ${request.error}`));
268
+ };
269
+ });
270
+ } catch (error) {
271
+ console.error('IndexedDB error:', error);
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Get all user-defined chords for an instrument
278
+ */
279
+ async getUserChordsByInstrument(instrument: string): Promise<UserChordData[]> {
280
+ try {
281
+ const db = await this.openDB();
282
+
283
+ return new Promise((resolve, reject) => {
284
+ const transaction = db.transaction([USER_STORE_NAME], 'readonly');
285
+ const objectStore = transaction.objectStore(USER_STORE_NAME);
286
+ const index = objectStore.index('instrument');
287
+ const request = index.getAll(instrument);
288
+
289
+ request.onsuccess = () => {
290
+ resolve(request.result || []);
291
+ };
292
+
293
+ request.onerror = () => {
294
+ reject(new Error(`Failed to get user chords: ${request.error}`));
295
+ };
296
+ });
297
+ } catch (error) {
298
+ console.error('IndexedDB error:', error);
299
+ return [];
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Get all user-defined chords
305
+ */
306
+ async getAllUserChords(): Promise<UserChordData[]> {
307
+ try {
308
+ const db = await this.openDB();
309
+
310
+ return new Promise((resolve, reject) => {
311
+ const transaction = db.transaction([USER_STORE_NAME], 'readonly');
312
+ const objectStore = transaction.objectStore(USER_STORE_NAME);
313
+ const request = objectStore.getAll();
314
+
315
+ request.onsuccess = () => {
316
+ resolve(request.result || []);
317
+ };
318
+
319
+ request.onerror = () => {
320
+ reject(new Error(`Failed to get all user chords: ${request.error}`));
321
+ };
322
+ });
323
+ } catch (error) {
324
+ console.error('IndexedDB error:', error);
325
+ return [];
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Clear all user-defined chords
331
+ */
332
+ async clearUserChords(): Promise<void> {
333
+ try {
334
+ const db = await this.openDB();
335
+
336
+ return new Promise((resolve, reject) => {
337
+ const transaction = db.transaction([USER_STORE_NAME], 'readwrite');
338
+ const objectStore = transaction.objectStore(USER_STORE_NAME);
339
+ const request = objectStore.clear();
340
+
341
+ request.onsuccess = () => {
342
+ resolve();
343
+ };
344
+
345
+ request.onerror = () => {
346
+ reject(new Error(`Failed to clear user chords: ${request.error}`));
347
+ };
348
+ });
349
+ } catch (error) {
350
+ console.error('IndexedDB error:', error);
351
+ throw error;
352
+ }
353
+ }
354
+ }
355
+
356
+ export const indexedDBService = new IndexedDBService();
@@ -0,0 +1,216 @@
1
+ import type { Finger } from 'svguitar';
2
+
3
+ export type MuKey = {
4
+ key: string
5
+ accidental: string
6
+ relativeMinor: string
7
+ }
8
+
9
+ export type MuChord = {
10
+ variant: string
11
+ tones: number[]
12
+ }
13
+
14
+ export type MuScale = {
15
+ variant: string
16
+ tones: number[]
17
+ }
18
+
19
+ export type MuChordsByScale = {
20
+ variant: string
21
+ chords: string[]
22
+ }
23
+
24
+ export type MuInstrument = {
25
+ name: string
26
+ strings: string[]
27
+ frets: number
28
+ }
29
+
30
+ export type MuChordDescriptor = {
31
+ key: string
32
+ chord: string
33
+ alt: string
34
+ }
35
+
36
+ export const keys: MuKey[] = [
37
+ {key: "A", accidental: "#", relativeMinor: 'F#'},
38
+ {key: "A#", accidental: "#", relativeMinor: 'G'},
39
+ {key: "Bb", accidental: 'b', relativeMinor: 'G'},
40
+ {key: "B", accidental: "#", relativeMinor: 'G#'},
41
+ {key: "C", accidental: "b", relativeMinor: 'A'},
42
+ {key: "C#", accidental: "#", relativeMinor: 'A#'},
43
+ {key: "Db", accidental: "b", relativeMinor: 'Bb'},
44
+ {key: "D", accidental: "#", relativeMinor: 'B'},
45
+ {key: "D#", accidental: "#", relativeMinor: 'C'},
46
+ {key: "Eb", accidental: "b", relativeMinor: 'C'},
47
+ {key: "E", accidental: "#", relativeMinor: 'C#'},
48
+ {key: "F", accidental: "b", relativeMinor: 'D'},
49
+ {key: "F#", accidental: "#", relativeMinor: 'D#'},
50
+ {key: "Gb", accidental: "b", relativeMinor: 'Eb'},
51
+ {key: "G", accidental: "#", relativeMinor: 'E'},
52
+ {key: "G#", accidental: "#", relativeMinor: 'F'},
53
+ {key: "Ab", accidental: "b", relativeMinor: 'F'}
54
+ ];
55
+
56
+ export const notes: string[][] = [
57
+ ["A"],
58
+ ["A#", "Bb"],
59
+ ["B"],
60
+ ["C"],
61
+ ["C#", "Db"],
62
+ ["D"],
63
+ ["D#", "Eb"],
64
+ ["E"],
65
+ ["F"],
66
+ ["F#", "Gb"],
67
+ ["G"],
68
+ ["G#", "Ab"]
69
+ ];
70
+
71
+ export const chords: MuChord[] = [
72
+ { variant: "maj", tones: [0, 4, 7] },
73
+ { variant: "m", tones: [0, 3, 7]},
74
+ { variant: "min", tones: [0, 3, 7] },
75
+ { variant: "dim", tones: [0, 3, 6] },
76
+ { variant: "aug", tones: [0, 4, 8] },
77
+ { variant: "7", tones: [0, 4, 7, 10]},
78
+ { variant: "m7", tones: [0, 3, 7, 10]},
79
+ { variant: "maj7", tones: [0, 4, 7, 11]},
80
+ { variant: "aug7", tones: [0, 4, 8, 10]},
81
+ { variant: "dim7", tones: [0, 3, 6, 9]},
82
+ { variant: "m7b5", tones: [0, 3, 6, 10]},
83
+ { variant: "mMaj7",tones: [0, 3, 7, 11]},
84
+ { variant: "sus2", tones: [0, 2, 7]},
85
+ { variant: "sus4", tones: [0, 5, 7]},
86
+ { variant: "7sus2",tones: [0, 2, 7, 10]},
87
+ { variant: "7sus4",tones: [0, 5, 7, 10]},
88
+ { variant: "9", tones: [0, 4, 7, 10, 14]},
89
+ { variant: "m9", tones: [0, 3, 7, 10, 14]},
90
+ { variant: "maj9", tones: [0, 4, 7, 11, 14]},
91
+ { variant: "11", tones: [0, 4, 7, 10, 14, 17]},
92
+ { variant: "m11", tones: [0, 3, 7, 10, 14, 17]},
93
+ { variant: "13", tones: [0, 4, 7, 10, 14, 17, 21]},
94
+ { variant: "m13", tones: [0, 3, 7, 10, 14, 17, 21]},
95
+ { variant: "5", tones: [0, 7]},
96
+ { variant: "6", tones: [0, 4, 7, 9]},
97
+ { variant: "m6", tones: [0, 3, 7, 9]},
98
+ { variant: "add9", tones: [0, 4, 7, 14]},
99
+ { variant: "mAdd9", tones: [0, 3, 7, 14]}
100
+ ];
101
+
102
+ export const scales: MuScale[] = [
103
+ { variant: "major", tones: [0, 2, 4, 5, 7, 9, 11] },
104
+ { variant: "minor", tones: [0, 2, 3, 5, 7, 8, 10] },
105
+ { variant: "major pentatonic", tones: [0, 2, 4, 7, 9] },
106
+ { variant: "minor pentatonic", tones: [0, 3, 5, 7, 10] },
107
+ { variant: "blues", tones: [0, 3, 5, 6, 7, 10] }
108
+ ];
109
+
110
+ export const chordsPerScale: MuChordsByScale[] = [
111
+ {variant: 'major', chords: ['maj','min','min','maj','maj','min','dim']},
112
+ {variant: 'minor', chords: ['min','dim','maj','min','min','maj','maj']}
113
+ ]
114
+
115
+ export const instruments: MuInstrument[] = [
116
+ { name: 'Standard Ukulele', strings: ["G","C","E","A"], frets: 19},
117
+ { name: 'Baritone Ukulele', strings: ["D","G","B","E"], frets: 19},
118
+ { name: '5ths tuned Ukulele', strings: ["C","G","D","A"], frets: 19},
119
+ { name: 'Standard Guitar', strings: ["E","A","D","G","B","E"], frets: 15},
120
+ { name: 'Drop-D Guitar', strings: ["D","A","D","G","B","E"], frets: 15},
121
+ { name: 'Standard Mandolin', strings: ["G","D","A","E"], frets: 20}
122
+ ];
123
+
124
+ const keyChordRegex = /\[([A-Ga-g](?:#|b)?)(m|min|maj|aug|dim|7|m7|maj7|aug7|dim7|m7b5|mMaj7|sus2|sus4|7sus2|7sus4|9|m9|maj9|11|m11|13|m13|5|6|m6|add9|mAdd9)?(-[a-zA-Z0-9]*)?\]/gm;
125
+
126
+ /**
127
+ * parseChords()
128
+ * - Given a chordpro song file, we will have any number of [chordName] symbols
129
+ * inline.This function will give us a Map containing each of the unique
130
+ * instances of those chords.
131
+ */
132
+ export const parseChords = (string:string):Map<string,MuChordDescriptor>=>{
133
+ const chordMap = new Map();
134
+ // turn the `matchAll` set into an actual array
135
+ [...string.matchAll(keyChordRegex)]
136
+ // the result of each match, I want the three capture groups:
137
+ // key = the A-G with b or # (or none for a natural)
138
+ // chord = the variant chord in that key
139
+ // alt = some notation, might be '-alt', might be '-v2'
140
+ .forEach(([, key, chord, alt])=>{
141
+ // add an entry to the Map - the key being the original "Am7-alt" or "Gbadd9"
142
+ chordMap.set(`${key}${chord ? chord : ''}${alt ? alt: ''}`, {key, chord, alt})
143
+ })
144
+ return chordMap;
145
+ }
146
+
147
+ /**
148
+ * chordOnInstrument()()
149
+ * - Given an instrument object (defined in the instruments array above), and a keychord
150
+ * (in this case, the actual notes in the chord as in `['A','C#','E']), we find the first
151
+ * instance of a chord note on each string.
152
+ * - to be done: At this point, there is no "weighting". We are getting the first note in
153
+ * in the chord on a given string, which may or may not define the complete chord. How to
154
+ * weight for completeness?
155
+ */
156
+ export const chordOnInstrument = (instrument:MuInstrument | undefined) =>
157
+ (chord: {notes:string[]|undefined }|undefined):Finger[]|undefined => {
158
+ if(!instrument || !chord ) return;
159
+
160
+ const {strings} = instrument;
161
+ return [...strings].reverse().map((note, index)=>{
162
+ let fret = 0;
163
+ let baseIndex = findBase(note);
164
+ let noteNames = notes[baseIndex];
165
+ while(noteNames.every(noteName=>!chord?.notes?.includes(noteName))){
166
+ ++fret;
167
+ noteNames = notes[(fret+baseIndex)%notes.length];
168
+ }
169
+ return [index+1, fret]
170
+ })
171
+ }
172
+
173
+ /**
174
+ * Quick way of indexing a given note to a scale index. Thus `C` returns 3, while
175
+ * both `C#` and `Db` return 4.
176
+ */
177
+ export const findBase = (note:string):number=>notes.findIndex((tone)=> tone.includes(note) )
178
+
179
+ export const chordToNotes = (chordName:string):{name: string, notes: string[]|undefined} => {
180
+ const chordData = Array.from(parseChords(`[${chordName}]`));
181
+
182
+ if(!chordData || !chordData.length) return {name:'', notes: []};
183
+
184
+ const [,{key, chord, alt}] = chordData[0];
185
+ const {accidental} = keys.find(
186
+ (keySignature)=>keySignature.key===key
187
+ ) ?? {accidental:''};
188
+ const baseIndex = findBase(key);
189
+ return ({
190
+ name: `${key}${chord?chord: ''}${alt?alt:''}`,
191
+ notes: chords.find(chordName=>chord ? chordName.variant===chord : chordName.variant==='maj')?.tones.map(tone =>
192
+ notes[(tone + baseIndex) % notes.length]
193
+ .find((note, _, arr) => arr.length > 1 && accidental ?
194
+ note.endsWith(accidental) :
195
+ arr[0])!
196
+ )
197
+ })
198
+ }
199
+
200
+ export const scaleTones = (base:string, variant:string) =>{
201
+ // given a base note, and a variant (major/minor/?), we can return the tones
202
+ // in a given scale.
203
+ const baseIndex = findBase(base);
204
+ const {accidental} = keys.find(
205
+ (keySignature)=>keySignature.key===base
206
+ ) ?? {accidental: ''};
207
+ const noteNames = scales.find(({variant: variantName}: {variant: string})=>variantName===variant)
208
+ ?.tones.map(
209
+ (interval)=>notes[(interval+baseIndex)%notes.length]
210
+ .find((note, _, arr) => arr.length > 1 && accidental ?
211
+ note.endsWith(accidental) :
212
+ arr[0])
213
+ );
214
+
215
+ return noteNames;
216
+ }