@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.
- package/LICENSE +21 -0
- package/README.md +359 -0
- package/dist/chord-data-service.js +190 -0
- package/dist/chord-diagram.js +181 -0
- package/dist/chord-editor.js +691 -0
- package/dist/chord-list.js +119 -0
- package/dist/default-chords.js +322 -0
- package/dist/index.js +26 -0
- package/dist/indexed-db-service.js +228 -0
- package/dist/music-utils.js +128 -0
- package/dist/node_modules/@lit/reactive-element/css-tag.js +42 -0
- package/dist/node_modules/@lit/reactive-element/decorators/base.js +9 -0
- package/dist/node_modules/@lit/reactive-element/decorators/custom-element.js +13 -0
- package/dist/node_modules/@lit/reactive-element/decorators/property.js +37 -0
- package/dist/node_modules/@lit/reactive-element/decorators/query.js +20 -0
- package/dist/node_modules/@lit/reactive-element/decorators/state.js +12 -0
- package/dist/node_modules/@lit/reactive-element/reactive-element.js +251 -0
- package/package.json +83 -0
- package/src/chord-data-service.ts +275 -0
- package/src/chord-diagram.ts +255 -0
- package/src/chord-editor.ts +919 -0
- package/src/chord-list.ts +145 -0
- package/src/default-chords.ts +333 -0
- package/src/index.ts +7 -0
- package/src/indexed-db-service.ts +356 -0
- package/src/music-utils.ts +216 -0
|
@@ -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
|
+
}
|