@hymnbook/abc 0.0.1 → 0.2.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/dist/{index.mjs → index.cjs} +206 -44
- package/dist/{index.d.mts → index.d.cts} +67 -9
- package/dist/index.d.ts +67 -9
- package/dist/index.js +157 -92
- package/package.json +18 -2
- package/src/based.ts +11 -3
- package/src/deparser.ts +186 -0
- package/src/index.ts +4 -3
- package/src/{abc.ts → parser.ts} +54 -41
- package/src/{abcTypes.ts → types.ts} +6 -3
package/dist/index.js
CHANGED
|
@@ -1,51 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
AbcSong: () => AbcSong,
|
|
34
|
-
ValidationError: () => ValidationError,
|
|
35
|
-
addInfoFieldsToMelody: () => addInfoFieldsToMelody,
|
|
36
|
-
combineMelodyAndLyrics: () => combineMelodyAndLyrics,
|
|
37
|
-
convertStringToAbcTune: () => convertStringToAbcTune,
|
|
38
|
-
extractInfoFields: () => extractInfoFields,
|
|
39
|
-
extractNotesAndLyrics: () => extractNotesAndLyrics,
|
|
40
|
-
generateAbcForVerse: () => generateAbcForVerse,
|
|
41
|
-
getField: () => getField,
|
|
42
|
-
getForVerse: () => getForVerse,
|
|
43
|
-
parse: () => parse,
|
|
44
|
-
validate: () => validate
|
|
45
|
-
});
|
|
46
|
-
module.exports = __toCommonJS(index_exports);
|
|
47
|
-
|
|
48
|
-
// src/abcTypes.ts
|
|
1
|
+
// src/types.ts
|
|
49
2
|
var AbcSong = class {
|
|
50
3
|
// See also: https://abcnotation.com/wiki/abc:standard:v2.1#information_fields
|
|
51
4
|
area;
|
|
@@ -87,8 +40,8 @@ var AbcSong = class {
|
|
|
87
40
|
};
|
|
88
41
|
};
|
|
89
42
|
|
|
90
|
-
// src/
|
|
91
|
-
|
|
43
|
+
// src/parser.ts
|
|
44
|
+
import * as ABCJS from "abcjs";
|
|
92
45
|
|
|
93
46
|
// src/validation.ts
|
|
94
47
|
var ValidationError = class extends Error {
|
|
@@ -100,15 +53,155 @@ function validate(result, message) {
|
|
|
100
53
|
throw new ValidationError(message || "Value is not true");
|
|
101
54
|
}
|
|
102
55
|
|
|
103
|
-
// src/
|
|
56
|
+
// src/deparser.ts
|
|
57
|
+
var addInfoFieldsToMelody = (song, abc) => {
|
|
58
|
+
let result = "";
|
|
59
|
+
result += song.referenceNumber === void 0 ? "" : "X:" + song.referenceNumber + "\n";
|
|
60
|
+
result += song.title === void 0 ? "" : "T:" + song.title + "\n";
|
|
61
|
+
result += song.area === void 0 ? "" : "A:" + song.area + "\n";
|
|
62
|
+
result += song.book === void 0 ? "" : "B:" + song.book + "\n";
|
|
63
|
+
result += song.composer === void 0 ? "" : "C:" + song.composer + "\n";
|
|
64
|
+
result += song.discography === void 0 ? "" : "D:" + song.discography + "\n";
|
|
65
|
+
result += song.fileUrl === void 0 ? "" : "F:" + song.fileUrl + "\n";
|
|
66
|
+
result += song.group === void 0 ? "" : "G:" + song.group + "\n";
|
|
67
|
+
result += song.history === void 0 ? "" : "H:" + song.history + "\n";
|
|
68
|
+
result += song.instruction === void 0 ? "" : "I:" + song.instruction + "\n";
|
|
69
|
+
result += song.key === void 0 ? "" : "K:" + song.key + "\n";
|
|
70
|
+
result += song.unitNoteLength === void 0 ? "" : "L:" + song.unitNoteLength + "\n";
|
|
71
|
+
result += song.meter === void 0 ? "" : "M:" + song.meter + "\n";
|
|
72
|
+
result += song.macro === void 0 ? "" : "m:" + song.macro + "\n";
|
|
73
|
+
result += song.notes === void 0 ? "" : "N:" + song.notes + "\n";
|
|
74
|
+
result += song.origin === void 0 ? "" : "O:" + song.origin + "\n";
|
|
75
|
+
result += song.parts === void 0 ? "" : "P:" + song.parts + "\n";
|
|
76
|
+
result += song.tempo === void 0 ? "" : "Q:" + song.tempo + "\n";
|
|
77
|
+
result += song.rhythm === void 0 ? "" : "R:" + song.rhythm + "\n";
|
|
78
|
+
result += song.remark === void 0 ? "" : "r:" + song.remark + "\n";
|
|
79
|
+
result += song.source === void 0 ? "" : "S:" + song.source + "\n";
|
|
80
|
+
result += song.symbolLine === void 0 ? "" : "s:" + song.symbolLine + "\n";
|
|
81
|
+
result += song.userDefined === void 0 ? "" : "U:" + song.userDefined + "\n";
|
|
82
|
+
result += song.voice === void 0 ? "" : "V:" + song.voice + "\n";
|
|
83
|
+
result += song.transcription === void 0 ? "" : "Z:" + song.transcription + "\n";
|
|
84
|
+
return result + abc;
|
|
85
|
+
};
|
|
86
|
+
var durationToString = (duration) => {
|
|
87
|
+
duration /= 4;
|
|
88
|
+
if (duration > 3 / 8) return (duration * 4).toString();
|
|
89
|
+
if (duration == 1 / 4) return "";
|
|
90
|
+
if (duration == 3 / 8) return "3/2";
|
|
91
|
+
if (duration == 1 / 8) return "/2";
|
|
92
|
+
if (duration == 3 / 16) return "3/4";
|
|
93
|
+
if (duration == 1 / 16) return "/4";
|
|
94
|
+
if (duration == 3 / 32) return "3/8";
|
|
95
|
+
if (duration == 1 / 32) return "/8";
|
|
96
|
+
if (duration == 3 / 64) return "3/16";
|
|
97
|
+
return "";
|
|
98
|
+
};
|
|
99
|
+
var voiceBarToString = (voice) => {
|
|
100
|
+
let result = "";
|
|
101
|
+
switch (voice.type) {
|
|
102
|
+
case "bar_thin":
|
|
103
|
+
result += "|";
|
|
104
|
+
break;
|
|
105
|
+
case "bar_thin_thick":
|
|
106
|
+
result += "|]";
|
|
107
|
+
break;
|
|
108
|
+
case "bar_thin_thin":
|
|
109
|
+
result += "||";
|
|
110
|
+
break;
|
|
111
|
+
case "bar_thick_thin":
|
|
112
|
+
result += "[|";
|
|
113
|
+
break;
|
|
114
|
+
case "bar_right_repeat":
|
|
115
|
+
result += ":|";
|
|
116
|
+
break;
|
|
117
|
+
case "bar_left_repeat":
|
|
118
|
+
result += "|:";
|
|
119
|
+
break;
|
|
120
|
+
case "bar_dbl_repeat":
|
|
121
|
+
result += "::";
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (voice.startEnding) result += voice.startEnding;
|
|
125
|
+
return result + " ";
|
|
126
|
+
};
|
|
127
|
+
var voiceNoteToString = (voice, durationMultiplier, isBeam) => {
|
|
128
|
+
let notes = "";
|
|
129
|
+
let lyrics = "";
|
|
130
|
+
const duration = durationToString(voice.duration / durationMultiplier);
|
|
131
|
+
if (voice.startBeam) isBeam = true;
|
|
132
|
+
if (voice.endBeam) isBeam = false;
|
|
133
|
+
voice.chord?.forEach((chord) => {
|
|
134
|
+
const parsedChordName = chord.name.replaceAll(/♭/g, "b").replaceAll(/♯/g, "#");
|
|
135
|
+
notes += `"${parsedChordName}"`;
|
|
136
|
+
});
|
|
137
|
+
if (voice.pitches && voice.pitches?.length > 1) notes += "[";
|
|
138
|
+
voice.pitches?.forEach((pitch) => {
|
|
139
|
+
let note = pitch.name + duration;
|
|
140
|
+
if (pitch.startSlur) note = "(".repeat(pitch.startSlur.length) + note;
|
|
141
|
+
if (pitch.endSlur) note += ")".repeat(pitch.endSlur.length);
|
|
142
|
+
if (pitch.startTie) note += "-";
|
|
143
|
+
notes += note;
|
|
144
|
+
});
|
|
145
|
+
if (voice.pitches && voice.pitches?.length > 1) notes += "]";
|
|
146
|
+
if (voice.rest) {
|
|
147
|
+
if (voice.rest.type == "spacer") {
|
|
148
|
+
notes += "y";
|
|
149
|
+
} else if (voice.rest.type == "multimeasure") {
|
|
150
|
+
notes += "Z" + durationToString(voice.duration / 4);
|
|
151
|
+
} else {
|
|
152
|
+
notes += "z" + duration;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (voice.pitches) {
|
|
156
|
+
voice.lyric?.forEach((lyric) => {
|
|
157
|
+
let text = lyric.syllable.replace(/ /g, "~");
|
|
158
|
+
if (text == "") text = "*";
|
|
159
|
+
lyrics += text + lyric.divider;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!isBeam) notes += " ";
|
|
163
|
+
return {
|
|
164
|
+
notes,
|
|
165
|
+
lyrics,
|
|
166
|
+
isBeam
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
var stringify = (song) => {
|
|
170
|
+
const durationParts = (song.unitNoteLength || "1/8").split("/");
|
|
171
|
+
const durationMultiplier = durationParts.length > 1 ? +durationParts[0] / +durationParts[1] : +durationParts[0];
|
|
172
|
+
const melody = song.melody.flatMap((line) => {
|
|
173
|
+
const notes = [];
|
|
174
|
+
const lyrics = [];
|
|
175
|
+
let isBeam = false;
|
|
176
|
+
line.forEach((voice) => {
|
|
177
|
+
switch (voice.el_type) {
|
|
178
|
+
case "bar":
|
|
179
|
+
notes.push(voiceBarToString(voice));
|
|
180
|
+
break;
|
|
181
|
+
case "note":
|
|
182
|
+
const result2 = voiceNoteToString(voice, durationMultiplier, isBeam);
|
|
183
|
+
notes.push(result2.notes);
|
|
184
|
+
lyrics.push(result2.lyrics);
|
|
185
|
+
isBeam = result2.isBeam;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const result = [notes.join("").trim()];
|
|
190
|
+
const mergedLyrics = lyrics.join("").trim();
|
|
191
|
+
if (mergedLyrics.length > 0) {
|
|
192
|
+
result.push("w: " + mergedLyrics);
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}).join("\n");
|
|
196
|
+
return addInfoFieldsToMelody(song, melody);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// src/parser.ts
|
|
104
200
|
var parse = (abc) => {
|
|
105
201
|
abc = abc.replace(/%.*/g, "").replaceAll(/\n+/g, "\n");
|
|
106
202
|
const song = new AbcSong();
|
|
107
203
|
extractInfoFields(abc, song);
|
|
108
204
|
const tuneObject = convertStringToAbcTune(abc);
|
|
109
|
-
if (tuneObject === void 0) {
|
|
110
|
-
return void 0;
|
|
111
|
-
}
|
|
112
205
|
song.clef = tuneObject.lines[0].staff[0].clef || song.clef;
|
|
113
206
|
song.keySignature = tuneObject.lines[0].staff[0].key || song.keySignature;
|
|
114
207
|
song.melody = tuneObject.lines.map((line) => line.staff[0].voices[0]);
|
|
@@ -196,7 +289,7 @@ var extractInfoFields = (abc, song) => {
|
|
|
196
289
|
song.transcription = getField(abc, "Z");
|
|
197
290
|
return abc.replace(/%.*\n/g, "").replace(/(^|\n) *\t*[ABCDFGHIKLMmNOPQRrSsTUVXZ]:.*/g, "").replace(/\n+/g, "\n").replace(/^\n*/g, "").replace(/\n*$/g, "");
|
|
198
291
|
};
|
|
199
|
-
var
|
|
292
|
+
var squashNotesAndLyrics = (abc) => {
|
|
200
293
|
const notes = [];
|
|
201
294
|
const lyrics = [];
|
|
202
295
|
abc.split("\n").map((it) => it.trim()).forEach((it) => {
|
|
@@ -211,41 +304,13 @@ var extractNotesAndLyrics = (abc) => {
|
|
|
211
304
|
lyrics: lyrics.join(" ")
|
|
212
305
|
};
|
|
213
306
|
};
|
|
214
|
-
var addInfoFieldsToMelody = (song, abc) => {
|
|
215
|
-
let result = "";
|
|
216
|
-
result += song.referenceNumber === void 0 ? "" : "X:" + song.referenceNumber + "\n";
|
|
217
|
-
result += song.title === void 0 ? "" : "T:" + song.title + "\n";
|
|
218
|
-
result += song.area === void 0 ? "" : "A:" + song.area + "\n";
|
|
219
|
-
result += song.book === void 0 ? "" : "B:" + song.book + "\n";
|
|
220
|
-
result += song.composer === void 0 ? "" : "C:" + song.composer + "\n";
|
|
221
|
-
result += song.discography === void 0 ? "" : "D:" + song.discography + "\n";
|
|
222
|
-
result += song.fileUrl === void 0 ? "" : "F:" + song.fileUrl + "\n";
|
|
223
|
-
result += song.group === void 0 ? "" : "G:" + song.group + "\n";
|
|
224
|
-
result += song.history === void 0 ? "" : "H:" + song.history + "\n";
|
|
225
|
-
result += song.instruction === void 0 ? "" : "I:" + song.instruction + "\n";
|
|
226
|
-
result += song.key === void 0 ? "" : "K:" + song.key + "\n";
|
|
227
|
-
result += song.unitNoteLength === void 0 ? "" : "L:" + song.unitNoteLength + "\n";
|
|
228
|
-
result += song.meter === void 0 ? "" : "M:" + song.meter + "\n";
|
|
229
|
-
result += song.macro === void 0 ? "" : "m:" + song.macro + "\n";
|
|
230
|
-
result += song.notes === void 0 ? "" : "N:" + song.notes + "\n";
|
|
231
|
-
result += song.origin === void 0 ? "" : "O:" + song.origin + "\n";
|
|
232
|
-
result += song.parts === void 0 ? "" : "P:" + song.parts + "\n";
|
|
233
|
-
result += song.tempo === void 0 ? "" : "Q:" + song.tempo + "\n";
|
|
234
|
-
result += song.rhythm === void 0 ? "" : "R:" + song.rhythm + "\n";
|
|
235
|
-
result += song.remark === void 0 ? "" : "r:" + song.remark + "\n";
|
|
236
|
-
result += song.source === void 0 ? "" : "S:" + song.source + "\n";
|
|
237
|
-
result += song.symbolLine === void 0 ? "" : "s:" + song.symbolLine + "\n";
|
|
238
|
-
result += song.userDefined === void 0 ? "" : "U:" + song.userDefined + "\n";
|
|
239
|
-
result += song.voice === void 0 ? "" : "V:" + song.voice + "\n";
|
|
240
|
-
result += song.transcription === void 0 ? "" : "Z:" + song.transcription + "\n";
|
|
241
|
-
return result + abc;
|
|
242
|
-
};
|
|
243
307
|
var convertStringToAbcTune = (abc) => {
|
|
244
308
|
const objectArray = ABCJS.parseOnly(abc);
|
|
245
309
|
const object = objectArray;
|
|
246
310
|
validate(object != null, "Tune object may not be null");
|
|
247
311
|
validate(object.length > 0, "Tune object may not be empty");
|
|
248
312
|
validate(object[0].lines != null, "Tune object lines may not be null");
|
|
313
|
+
object[0].lines = object[0].lines.filter((it) => it.staff);
|
|
249
314
|
validate(object[0].lines.length > 0, "Tune object lines are empty");
|
|
250
315
|
validate(object[0].lines[0].staff != null, "Staffs may not be null");
|
|
251
316
|
validate(object[0].lines[0].staff.length > 0, "Staffs are empty");
|
|
@@ -268,10 +333,10 @@ var processAbcLyrics = (object) => {
|
|
|
268
333
|
)
|
|
269
334
|
);
|
|
270
335
|
};
|
|
271
|
-
var combineMelodyAndLyrics = (melody, lyrics) => {
|
|
336
|
+
var combineMelodyAndLyrics = (melody, lyrics, options = { trimLines: false }) => {
|
|
272
337
|
const song = new AbcSong();
|
|
273
338
|
const rawMelody = extractInfoFields(melody, song);
|
|
274
|
-
const melodyLines = rawMelody.replaceAll(/\n+/g, "\n").trim().split("\n");
|
|
339
|
+
const melodyLines = rawMelody.replaceAll(/\n+/g, "\n").trim().split("\n").map((it) => it.trim()).map((it) => options.trimLines ? it.replaceAll(/(^y+|y+$)*/gi, "").replaceAll(/ *y* *(\|+]*) *y* */gi, " $1 ").trim() : it);
|
|
275
340
|
const lyricLines = lyrics.replaceAll(/\n+/g, "\n").trim().split("\n");
|
|
276
341
|
const mixedMelody = [];
|
|
277
342
|
for (let i = 0; i < Math.max(melodyLines.length, lyricLines.length); i++) {
|
|
@@ -286,25 +351,25 @@ var getForVerse = (melody, verse) => melody.subMelodies.find((it) => (
|
|
|
286
351
|
// `.includes()` won't work due to the Realm data type of `verseUuids`.
|
|
287
352
|
it.verseUuids.some((it2) => it2 == verse.uuid)
|
|
288
353
|
));
|
|
289
|
-
var generateAbcForVerse = (verse, activeMelody) => {
|
|
354
|
+
var generateAbcForVerse = (verse, activeMelody, options = { trimLines: false }) => {
|
|
290
355
|
if (activeMelody === void 0) {
|
|
291
356
|
return "";
|
|
292
357
|
}
|
|
293
358
|
const melody = getForVerse(activeMelody, verse)?.melody || activeMelody.melody;
|
|
294
|
-
return combineMelodyAndLyrics(melody, verse.abcLyrics || "");
|
|
359
|
+
return combineMelodyAndLyrics(melody, verse.abcLyrics || "", options);
|
|
295
360
|
};
|
|
296
|
-
|
|
297
|
-
0 && (module.exports = {
|
|
361
|
+
export {
|
|
298
362
|
AbcSong,
|
|
299
363
|
ValidationError,
|
|
300
364
|
addInfoFieldsToMelody,
|
|
301
365
|
combineMelodyAndLyrics,
|
|
302
366
|
convertStringToAbcTune,
|
|
303
367
|
extractInfoFields,
|
|
304
|
-
extractNotesAndLyrics,
|
|
305
368
|
generateAbcForVerse,
|
|
306
369
|
getField,
|
|
307
370
|
getForVerse,
|
|
308
371
|
parse,
|
|
372
|
+
squashNotesAndLyrics,
|
|
373
|
+
stringify,
|
|
309
374
|
validate
|
|
310
|
-
}
|
|
375
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hymnbook/abc",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"module": "./dist/index.mjs",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
8
9
|
"scripts": {
|
|
9
|
-
"
|
|
10
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"link": "npm run build && rm -f /Users/samuel/workspace/js/hymnbook2/node_modules/@hymnbook/abc/dist/*; ln /Users/samuel/workspace/js/hymnbook-abc/dist/* /Users/samuel/workspace/js/hymnbook2/node_modules/@hymnbook/abc/dist/",
|
|
13
|
+
"build-publish": "npm run build && npm publish --access public",
|
|
14
|
+
"publish-patch": "npm run test && npm version patch && npm run build-publish",
|
|
15
|
+
"publish-minor": "npm run test && npm version minor && npm run build-publish",
|
|
16
|
+
"publish-major": "npm run test && npm version major && npm run build-publish"
|
|
10
17
|
},
|
|
11
18
|
"repository": {
|
|
12
19
|
"type": "git",
|
|
@@ -23,6 +30,9 @@
|
|
|
23
30
|
},
|
|
24
31
|
"homepage": "https://github.com/sampie777/hymnbook-abc",
|
|
25
32
|
"devDependencies": {
|
|
33
|
+
"@jest/globals": "^30.2.0",
|
|
34
|
+
"jest": "^30.2.0",
|
|
35
|
+
"ts-jest": "^29.4.6",
|
|
26
36
|
"tsup": "^8.5.1",
|
|
27
37
|
"typescript": "^5.9.3"
|
|
28
38
|
},
|
|
@@ -35,5 +45,11 @@
|
|
|
35
45
|
],
|
|
36
46
|
"dependencies": {
|
|
37
47
|
"abcjs": "^6.5.2"
|
|
48
|
+
},
|
|
49
|
+
"exports": {
|
|
50
|
+
".": {
|
|
51
|
+
"import": "./dist/index.js",
|
|
52
|
+
"require": "./dist/index.cjs"
|
|
53
|
+
}
|
|
38
54
|
}
|
|
39
55
|
}
|
package/src/based.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { combineMelodyAndLyrics } from "./
|
|
1
|
+
import { combineMelodyAndLyrics } from "./parser";
|
|
2
2
|
import { AbcMelody, AbcSubMelody, Verse } from "./basedTypes";
|
|
3
3
|
|
|
4
4
|
|
|
@@ -7,13 +7,21 @@ export const getForVerse = (melody: AbcMelody, verse: Verse): AbcSubMelody | und
|
|
|
7
7
|
// `.includes()` won't work due to the Realm data type of `verseUuids`.
|
|
8
8
|
it.verseUuids.some(it => it == verse.uuid));
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Combine an ABC melody with the lyrics of a verse into a single ABC notation string.
|
|
12
|
+
*
|
|
13
|
+
* @param verse
|
|
14
|
+
* @param activeMelody - The active melody to use. The method returns an empty string if this is undefined.
|
|
15
|
+
* @param options { trimLines?: boolean } - Whether to trim `y` spacers from start/end of lines.
|
|
16
|
+
*/
|
|
10
17
|
export const generateAbcForVerse = (
|
|
11
18
|
verse: Verse,
|
|
12
|
-
activeMelody?: AbcMelody
|
|
19
|
+
activeMelody?: AbcMelody,
|
|
20
|
+
options: { trimLines?: boolean } = { trimLines: false },
|
|
13
21
|
): string => {
|
|
14
22
|
if (activeMelody === undefined) {
|
|
15
23
|
return "";
|
|
16
24
|
}
|
|
17
25
|
const melody = getForVerse(activeMelody, verse)?.melody || activeMelody.melody;
|
|
18
|
-
return combineMelodyAndLyrics(melody, verse.abcLyrics || "")
|
|
26
|
+
return combineMelodyAndLyrics(melody, verse.abcLyrics || "", options)
|
|
19
27
|
};
|
package/src/deparser.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { AbcSong, VoiceItemBar, VoiceItemNote } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts an AbcSong object's info fields to ABC notation and merges them with the given melody string.
|
|
5
|
+
* @param song
|
|
6
|
+
* @param abc
|
|
7
|
+
*/
|
|
8
|
+
export const addInfoFieldsToMelody = (song: AbcSong, abc: string): string => {
|
|
9
|
+
let result = "";
|
|
10
|
+
// See for following order of fields: https://abcnotation.com/wiki/abc:standard:v2.1#description_of_information_fields
|
|
11
|
+
result += song.referenceNumber === undefined ? "" : "X:" + song.referenceNumber + "\n";
|
|
12
|
+
result += song.title === undefined ? "" : "T:" + song.title + "\n";
|
|
13
|
+
result += song.area === undefined ? "" : "A:" + song.area + "\n";
|
|
14
|
+
result += song.book === undefined ? "" : "B:" + song.book + "\n";
|
|
15
|
+
result += song.composer === undefined ? "" : "C:" + song.composer + "\n";
|
|
16
|
+
result += song.discography === undefined ? "" : "D:" + song.discography + "\n";
|
|
17
|
+
result += song.fileUrl === undefined ? "" : "F:" + song.fileUrl + "\n";
|
|
18
|
+
result += song.group === undefined ? "" : "G:" + song.group + "\n";
|
|
19
|
+
result += song.history === undefined ? "" : "H:" + song.history + "\n";
|
|
20
|
+
result += song.instruction === undefined ? "" : "I:" + song.instruction + "\n";
|
|
21
|
+
result += song.key === undefined ? "" : "K:" + song.key + "\n";
|
|
22
|
+
result += song.unitNoteLength === undefined ? "" : "L:" + song.unitNoteLength + "\n";
|
|
23
|
+
result += song.meter === undefined ? "" : "M:" + song.meter + "\n";
|
|
24
|
+
result += song.macro === undefined ? "" : "m:" + song.macro + "\n";
|
|
25
|
+
result += song.notes === undefined ? "" : "N:" + song.notes + "\n";
|
|
26
|
+
result += song.origin === undefined ? "" : "O:" + song.origin + "\n";
|
|
27
|
+
result += song.parts === undefined ? "" : "P:" + song.parts + "\n";
|
|
28
|
+
result += song.tempo === undefined ? "" : "Q:" + song.tempo + "\n";
|
|
29
|
+
result += song.rhythm === undefined ? "" : "R:" + song.rhythm + "\n";
|
|
30
|
+
result += song.remark === undefined ? "" : "r:" + song.remark + "\n";
|
|
31
|
+
result += song.source === undefined ? "" : "S:" + song.source + "\n";
|
|
32
|
+
result += song.symbolLine === undefined ? "" : "s:" + song.symbolLine + "\n";
|
|
33
|
+
result += song.userDefined === undefined ? "" : "U:" + song.userDefined + "\n";
|
|
34
|
+
result += song.voice === undefined ? "" : "V:" + song.voice + "\n";
|
|
35
|
+
result += song.transcription === undefined ? "" : "Z:" + song.transcription + "\n";
|
|
36
|
+
return result + abc;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const durationToString = (duration: number): string => {
|
|
40
|
+
duration /= 4;
|
|
41
|
+
if (duration > 3 / 8) return (duration * 4).toString();
|
|
42
|
+
if (duration == 1 / 4) return "";
|
|
43
|
+
if (duration == 3 / 8) return "3/2";
|
|
44
|
+
if (duration == 1 / 8) return "/2";
|
|
45
|
+
if (duration == 3 / 16) return "3/4";
|
|
46
|
+
if (duration == 1 / 16) return "/4";
|
|
47
|
+
if (duration == 3 / 32) return "3/8";
|
|
48
|
+
if (duration == 1 / 32) return "/8";
|
|
49
|
+
if (duration == 3 / 64) return "3/16";
|
|
50
|
+
|
|
51
|
+
return ""
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const voiceBarToString = (voice: VoiceItemBar): string => {
|
|
55
|
+
let result = "";
|
|
56
|
+
|
|
57
|
+
switch (voice.type) {
|
|
58
|
+
case "bar_thin":
|
|
59
|
+
result += "|"
|
|
60
|
+
break;
|
|
61
|
+
case "bar_thin_thick":
|
|
62
|
+
result += "|]"
|
|
63
|
+
break;
|
|
64
|
+
case "bar_thin_thin":
|
|
65
|
+
result += "||"
|
|
66
|
+
break;
|
|
67
|
+
case "bar_thick_thin":
|
|
68
|
+
result += "[|"
|
|
69
|
+
break;
|
|
70
|
+
case "bar_right_repeat":
|
|
71
|
+
result += ":|"
|
|
72
|
+
break;
|
|
73
|
+
case "bar_left_repeat":
|
|
74
|
+
result += "|:"
|
|
75
|
+
break;
|
|
76
|
+
case "bar_dbl_repeat":
|
|
77
|
+
result += "::"
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (voice.startEnding) result += voice.startEnding
|
|
82
|
+
|
|
83
|
+
return result + " ";
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const voiceNoteToString = (voice: VoiceItemNote, durationMultiplier: number, isBeam: boolean) => {
|
|
87
|
+
let notes = "";
|
|
88
|
+
let lyrics = "";
|
|
89
|
+
|
|
90
|
+
const duration = durationToString(voice.duration / durationMultiplier);
|
|
91
|
+
|
|
92
|
+
if (voice.startBeam) isBeam = true;
|
|
93
|
+
if (voice.endBeam) isBeam = false;
|
|
94
|
+
|
|
95
|
+
/* Process chords */
|
|
96
|
+
voice.chord?.forEach(chord => {
|
|
97
|
+
const parsedChordName = chord.name
|
|
98
|
+
.replaceAll(/♭/g, "b")
|
|
99
|
+
.replaceAll(/♯/g, "#")
|
|
100
|
+
notes += `"${parsedChordName}"`;
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/* Process notes */
|
|
104
|
+
if (voice.pitches && voice.pitches?.length > 1) notes += "["
|
|
105
|
+
voice.pitches?.forEach(pitch => {
|
|
106
|
+
let note = pitch.name + duration;
|
|
107
|
+
|
|
108
|
+
if (pitch.startSlur) note = "(".repeat(pitch.startSlur.length) + note;
|
|
109
|
+
if (pitch.endSlur) note += ")".repeat(pitch.endSlur.length)
|
|
110
|
+
|
|
111
|
+
if (pitch.startTie) note += "-";
|
|
112
|
+
|
|
113
|
+
notes += note;
|
|
114
|
+
})
|
|
115
|
+
if (voice.pitches && voice.pitches?.length > 1) notes += "]"
|
|
116
|
+
|
|
117
|
+
/* Process rests */
|
|
118
|
+
if (voice.rest) {
|
|
119
|
+
if (voice.rest.type == "spacer") {
|
|
120
|
+
notes += "y";
|
|
121
|
+
} else if (voice.rest.type == "multimeasure") {
|
|
122
|
+
notes += "Z" + durationToString(voice.duration / 4)
|
|
123
|
+
} else {
|
|
124
|
+
notes += "z" + duration;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Process lyrics */
|
|
129
|
+
if (voice.pitches) {
|
|
130
|
+
voice.lyric?.forEach(lyric => {
|
|
131
|
+
let text = lyric.syllable.replace(/ /g, "~");
|
|
132
|
+
if (text == "") text = "*";
|
|
133
|
+
lyrics += text + lyric.divider
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!isBeam) notes += " ";
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
notes: notes,
|
|
141
|
+
lyrics: lyrics,
|
|
142
|
+
isBeam: isBeam,
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convert an AbcSong object to an ABC notation string.
|
|
148
|
+
* @param song
|
|
149
|
+
*/
|
|
150
|
+
export const stringify = (song: AbcSong) => {
|
|
151
|
+
const durationParts = (song.unitNoteLength || "1/8").split("/");
|
|
152
|
+
const durationMultiplier = durationParts.length > 1
|
|
153
|
+
? +durationParts[0] / +durationParts[1]
|
|
154
|
+
: +durationParts[0];
|
|
155
|
+
|
|
156
|
+
const melody = song.melody.flatMap(line => {
|
|
157
|
+
const notes: string[] = [];
|
|
158
|
+
const lyrics: string[] = [];
|
|
159
|
+
let isBeam = false;
|
|
160
|
+
|
|
161
|
+
line.forEach(voice => {
|
|
162
|
+
switch (voice.el_type) {
|
|
163
|
+
case "bar":
|
|
164
|
+
notes.push(voiceBarToString(voice))
|
|
165
|
+
break;
|
|
166
|
+
case "note":
|
|
167
|
+
const result = voiceNoteToString(voice, durationMultiplier, isBeam)
|
|
168
|
+
notes.push(result.notes)
|
|
169
|
+
lyrics.push(result.lyrics)
|
|
170
|
+
isBeam = result.isBeam
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const result: string[] = [notes.join("").trim()]
|
|
176
|
+
const mergedLyrics = lyrics.join("").trim()
|
|
177
|
+
if (mergedLyrics.length > 0) {
|
|
178
|
+
result.push("w: " + mergedLyrics)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return result;
|
|
182
|
+
})
|
|
183
|
+
.join("\n")
|
|
184
|
+
|
|
185
|
+
return addInfoFieldsToMelody(song, melody);
|
|
186
|
+
};
|
package/src/index.ts
CHANGED