@hymnbook/abc 0.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/src/abc.ts ADDED
@@ -0,0 +1,271 @@
1
+ import * as ABCJS from "abcjs";
2
+ import { TuneObjectArray } from "abcjs";
3
+ import {
4
+ AbcLyric,
5
+ AbcPitchStartSlur,
6
+ AbcSong,
7
+ NoteGroupInterface,
8
+ TuneObject,
9
+ VoiceItem,
10
+ VoiceItemNote
11
+ } from "./abcTypes";
12
+ import { validate } from "./validation";
13
+
14
+ // See also https://abcnotation.com/examples
15
+
16
+
17
+ export const parse = (abc: string): AbcSong | undefined => {
18
+ // Remove comments
19
+ abc = abc
20
+ .replace(/%.*/g, "")
21
+ .replaceAll(/\n+/g, "\n")
22
+
23
+ const song = new AbcSong();
24
+ extractInfoFields(abc, song);
25
+
26
+ const tuneObject = convertStringToAbcTune(abc);
27
+ if (tuneObject === undefined) {
28
+ return undefined;
29
+ }
30
+
31
+ // Get the first staff only (thus in case of multiple instrument play, only take the first instrument)
32
+ song.clef = tuneObject.lines!![0].staff!![0].clef || song.clef;
33
+ song.keySignature = tuneObject.lines!![0].staff!![0].key || song.keySignature;
34
+ song.melody = tuneObject.lines!!.map(line => line.staff!![0].voices!![0])
35
+
36
+ processSlurs(song);
37
+
38
+ return song;
39
+ };
40
+
41
+ const processSlurs = (song: AbcSong) => {
42
+ song.melody.forEach(processSlursForLine)
43
+ };
44
+
45
+ const processSlursForLine = (line: VoiceItem[]) => {
46
+ const emptyLyric = (): AbcLyric[] => [{ divider: " ", syllable: "" }];
47
+
48
+ const shiftLyrics = (notes: VoiceItemNote[], fromIndex: number) => {
49
+ let shiftedLyric: AbcLyric[] | undefined = undefined;
50
+ notes.forEach((note, index) => {
51
+ if (index < fromIndex) return;
52
+ if (shiftedLyric === undefined) {
53
+ shiftedLyric = note.lyric;
54
+ note.lyric = emptyLyric();
55
+ return;
56
+ }
57
+
58
+ const nextShiftedLyric = note.lyric;
59
+ note.lyric = shiftedLyric;
60
+ shiftedLyric = nextShiftedLyric;
61
+ });
62
+ };
63
+
64
+ const notes = line
65
+ .filter(it => it.el_type == "note")
66
+ .map(it => it as VoiceItemNote)
67
+ .filter(it => it.pitches);
68
+
69
+ let slurLyric: AbcLyric[] | undefined = undefined;
70
+ notes.forEach((note, index) => {
71
+ const endSlurPitches = note.pitches!!.filter(it => it.endSlur);
72
+ const startSlurPitches = note.pitches!!.filter(it => it.startSlur);
73
+
74
+ // Search for an increased startSlur identifier (101 -> 102, not 101 -> 201)
75
+ // to identify ABC melodies as '(D)' which ends and starts a slur with itself.
76
+ // Due to buggy ABC parser behavior, the slur doesn't first start and end with
77
+ // itself (having the same identifier), but ends first and start a new slur (with increased identifier).
78
+ // Of course, it is possible that the note is in an actual slur (like '(a (b) c)'),
79
+ // but then the `slurLyric` would be set.
80
+ if (slurLyric === undefined && endSlurPitches.length && startSlurPitches.length) {
81
+ const endSlurIds = endSlurPitches
82
+ .filter(it => it.endSlur)
83
+ .flatMap(it => it.endSlur!!);
84
+
85
+ const startSlurIds = startSlurPitches
86
+ .filter(it => it.startSlur)
87
+ .flatMap(it => it.startSlur as AbcPitchStartSlur[])
88
+ .map(it => it.label);
89
+
90
+ const hasUpFollowingId = startSlurIds.some(it => endSlurIds.includes(it - 1));
91
+
92
+ if (hasUpFollowingId) {
93
+ return;
94
+ }
95
+ }
96
+
97
+ // When inside a slur
98
+ if (slurLyric) {
99
+ // Check if lyric is a spacer ('*'). These are shown as lyrics with zero length syllables.
100
+ // If it's a spacer, ignore the shift, as the spacer already does this job for us.
101
+ const lyricIsSpacer = note.lyric
102
+ && note.lyric.every(it => it.syllable == "" && it.divider == " ");
103
+
104
+ if (!lyricIsSpacer) {
105
+ shiftLyrics(notes, index);
106
+ }
107
+ }
108
+
109
+ // When slur ends
110
+ if (endSlurPitches.length && !startSlurPitches.length) {
111
+ slurLyric = undefined;
112
+ }
113
+
114
+ // When slur starts
115
+ if (startSlurPitches.length && !endSlurPitches.length) {
116
+ slurLyric = note.lyric;
117
+ }
118
+ });
119
+ };
120
+
121
+ export const getField = (abc: string, field: string, _default?: string): (string | undefined) => {
122
+ const result = abc.match(new RegExp("(^|\n) *\t*" + field + ":(.*)?"));
123
+ if (result == null || result.length !== 3 || result[2] == null) {
124
+ return _default;
125
+ }
126
+ return result[2].trim();
127
+ };
128
+
129
+ export const extractInfoFields = (abc: string, song: AbcSong): string => {
130
+ song.area = getField(abc, "A");
131
+ song.book = getField(abc, "B");
132
+ song.composer = getField(abc, "C");
133
+ song.discography = getField(abc, "D");
134
+ song.fileUrl = getField(abc, "F");
135
+ song.group = getField(abc, "G");
136
+ song.history = getField(abc, "H");
137
+ song.instruction = getField(abc, "I");
138
+ song.key = getField(abc, "K");
139
+ song.unitNoteLength = getField(abc, "L");
140
+ song.meter = getField(abc, "M");
141
+ song.macro = getField(abc, "m");
142
+ song.notes = getField(abc, "N");
143
+ song.origin = getField(abc, "O");
144
+ song.parts = getField(abc, "P");
145
+ song.tempo = getField(abc, "Q");
146
+ song.rhythm = getField(abc, "R");
147
+ song.remark = getField(abc, "r");
148
+ song.source = getField(abc, "S");
149
+ song.symbolLine = getField(abc, "s");
150
+ song.title = getField(abc, "T");
151
+ song.userDefined = getField(abc, "U");
152
+ song.voice = getField(abc, "V");
153
+ song.referenceNumber = getField(abc, "X", "1");
154
+ song.transcription = getField(abc, "Z");
155
+ return abc
156
+ .replace(/%.*\n/g, "")
157
+ .replace(/(^|\n) *\t*[ABCDFGHIKLMmNOPQRrSsTUVXZ]:.*/g, "")
158
+ .replace(/\n+/g, "\n")
159
+ .replace(/^\n*/g, "")
160
+ .replace(/\n*$/g, "");
161
+ };
162
+
163
+ export const extractNotesAndLyrics = (abc: string): NoteGroupInterface => {
164
+ const notes: string[] = [];
165
+ const lyrics: string[] = [];
166
+ abc.split("\n")
167
+ .map(it => it.trim())
168
+ .forEach(it => {
169
+ if (it.startsWith("w:") || it.startsWith("W:")) {
170
+ lyrics.push(it.substring(2).trim());
171
+ } else {
172
+ notes.push(it);
173
+ }
174
+ });
175
+
176
+ return {
177
+ notes: notes.join(" "),
178
+ lyrics: lyrics.join(" ")
179
+ } as NoteGroupInterface;
180
+ };
181
+
182
+ export const addInfoFieldsToMelody = (song: AbcSong, abc: string): string => {
183
+ let result = "";
184
+ // See for following order of fields: https://abcnotation.com/wiki/abc:standard:v2.1#description_of_information_fields
185
+ result += song.referenceNumber === undefined ? "" : "X:" + song.referenceNumber + "\n";
186
+ result += song.title === undefined ? "" : "T:" + song.title + "\n";
187
+ result += song.area === undefined ? "" : "A:" + song.area + "\n";
188
+ result += song.book === undefined ? "" : "B:" + song.book + "\n";
189
+ result += song.composer === undefined ? "" : "C:" + song.composer + "\n";
190
+ result += song.discography === undefined ? "" : "D:" + song.discography + "\n";
191
+ result += song.fileUrl === undefined ? "" : "F:" + song.fileUrl + "\n";
192
+ result += song.group === undefined ? "" : "G:" + song.group + "\n";
193
+ result += song.history === undefined ? "" : "H:" + song.history + "\n";
194
+ result += song.instruction === undefined ? "" : "I:" + song.instruction + "\n";
195
+ result += song.key === undefined ? "" : "K:" + song.key + "\n";
196
+ result += song.unitNoteLength === undefined ? "" : "L:" + song.unitNoteLength + "\n";
197
+ result += song.meter === undefined ? "" : "M:" + song.meter + "\n";
198
+ result += song.macro === undefined ? "" : "m:" + song.macro + "\n";
199
+ result += song.notes === undefined ? "" : "N:" + song.notes + "\n";
200
+ result += song.origin === undefined ? "" : "O:" + song.origin + "\n";
201
+ result += song.parts === undefined ? "" : "P:" + song.parts + "\n";
202
+ result += song.tempo === undefined ? "" : "Q:" + song.tempo + "\n";
203
+ result += song.rhythm === undefined ? "" : "R:" + song.rhythm + "\n";
204
+ result += song.remark === undefined ? "" : "r:" + song.remark + "\n";
205
+ result += song.source === undefined ? "" : "S:" + song.source + "\n";
206
+ result += song.symbolLine === undefined ? "" : "s:" + song.symbolLine + "\n";
207
+ result += song.userDefined === undefined ? "" : "U:" + song.userDefined + "\n";
208
+ result += song.voice === undefined ? "" : "V:" + song.voice + "\n";
209
+ result += song.transcription === undefined ? "" : "Z:" + song.transcription + "\n";
210
+ return result + abc;
211
+ };
212
+
213
+ export const convertStringToAbcTune = (abc: string): TuneObject => {
214
+ const objectArray: TuneObjectArray = ABCJS.parseOnly(abc);
215
+ // Convert types
216
+ const object: TuneObject[] = objectArray as unknown as TuneObject[];
217
+
218
+ validate(object != null, "Tune object may not be null");
219
+ validate(object.length > 0, "Tune object may not be empty");
220
+ validate(object[0].lines != null, "Tune object lines may not be null");
221
+ validate(object[0].lines.length > 0, "Tune object lines are empty");
222
+ validate(object[0].lines[0].staff != null, "Staffs may not be null");
223
+ validate(object[0].lines[0].staff!!.length > 0, "Staffs are empty");
224
+ validate(object[0].lines[0].staff!![0].voices != null, "Voices may not be null");
225
+ validate(object[0].lines[0].staff!![0].voices!!.length > 0, "Voices may not be empty");
226
+ validate(object[0].lines[0].staff!![0].voices!!.some(it => it.length > 0), "Voices are all empty");
227
+
228
+ processAbcLyrics(object);
229
+
230
+ return object[0];
231
+ };
232
+
233
+ const processAbcLyrics = (object: Array<TuneObject>) => {
234
+ object[0].lines!!.forEach(line =>
235
+ line.staff!!.forEach(staff =>
236
+ staff.voices!!.forEach(element =>
237
+ element.filter(voice => voice.el_type === "note")
238
+ .map(voice => voice as VoiceItemNote)
239
+ .forEach(voice => {
240
+ voice.lyric?.forEach(lyric =>
241
+ lyric.syllable = lyric.syllable.replace(" ", " ")
242
+ );
243
+ })
244
+ )
245
+ )
246
+ );
247
+ };
248
+
249
+ // Match each lyric line with each melody line
250
+ export const combineMelodyAndLyrics = (melody: string, lyrics: string): string => {
251
+ const song = new AbcSong();
252
+ const rawMelody = extractInfoFields(melody, song);
253
+
254
+ const melodyLines = rawMelody
255
+ .replaceAll(/\n+/g, "\n")
256
+ .trim()
257
+ .split("\n")
258
+ const lyricLines = lyrics
259
+ .replaceAll(/\n+/g, "\n")
260
+ .trim()
261
+ .split("\n")
262
+
263
+ const mixedMelody: string[] = [];
264
+ for (let i = 0; i < Math.max(melodyLines.length, lyricLines.length); i++) {
265
+ // When the lines are not of equal length, we allow to show empty lines for missing lines.
266
+ mixedMelody.push(melodyLines[i] ?? "C ".repeat(10));
267
+ mixedMelody.push(lyricLines[i] ? "w: " + lyricLines[i] : "");
268
+ }
269
+
270
+ return addInfoFieldsToMelody(song, mixedMelody.join("\n")).trim();
271
+ }
@@ -0,0 +1,314 @@
1
+ // See also: https://github.com/paulrosen/abcjs/blob/2616d88ddf0222e255c508f944df3089960c13dc/types/index.d.ts
2
+
3
+ import { SynthOptions } from "abcjs";
4
+
5
+ export type AccidentalName = "flat" | "natural" | "sharp" | "dblsharp" | "dblflat" | "quarterflat" | "quartersharp";
6
+ export type ChordPlacement = "above" | "below" | "left" | "right" | "default";
7
+ export type StemDirection = "up" | "down" | "auto" | "none";
8
+ export type AbcType =
9
+ "bar_thin"
10
+ | "bar_thin_thick"
11
+ | "bar_thin_thin"
12
+ | "bar_thick_thin"
13
+ | "bar_right_repeat"
14
+ | "bar_left_repeat"
15
+ | "bar_double_repeat";
16
+ export type AbcElementType = "note" | "bar";
17
+ export type Clef =
18
+ "treble"
19
+ | "tenor"
20
+ | "bass"
21
+ | "alto"
22
+ | "treble+8"
23
+ | "tenor+8"
24
+ | "bass+8"
25
+ | "alto+8"
26
+ | "treble-8"
27
+ | "tenor-8"
28
+ | "bass-8"
29
+ | "alto-8"
30
+ | "none"
31
+ | "perc";
32
+ export type NoteHeadType = "normal" | "harmonic" | "rhythm" | "x" | "triangle";
33
+ export type NoteLetter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "a" | "b" | "c" | "d" | "e" | "f" | "g";
34
+ export type KeyRoot = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "HP" | "Hp" | "none";
35
+ export type KeyAccidentalName = "" | "#" | "b";
36
+ export type Mode = "" | "m" | "Dor" | "Mix" | "Loc" | "Phr" | "Lyd";
37
+ export type ChordType =
38
+ ""
39
+ | "m"
40
+ | "7"
41
+ | "m7"
42
+ | "maj7"
43
+ | "M7"
44
+ | "6"
45
+ | "m6"
46
+ | "aug"
47
+ | "+"
48
+ | "aug7"
49
+ | "dim"
50
+ | "dim7"
51
+ | "9"
52
+ |
53
+ "m9"
54
+ | "maj9"
55
+ | "M9"
56
+ | "11"
57
+ | "dim9"
58
+ | "sus"
59
+ | "sus9"
60
+ | "7sus4"
61
+ | "7sus9"
62
+ | "5";
63
+ export type BracePosition = "start" | "continue" | "end";
64
+
65
+ type NumberFunction = () => number;
66
+
67
+ export interface AbcChord {
68
+ name: string;
69
+ position: ChordPlacement;
70
+ }
71
+
72
+ export interface AbcPitchStartSlur {
73
+ label: number; // An identifier for this slur in the current slur sequence (if there are more than one slurs for this note)
74
+ }
75
+
76
+ export interface AbcPitch {
77
+ pitch: number;
78
+ verticalPos: number;
79
+ name?: string;
80
+ accidental?: AccidentalName;
81
+ startTie?: {};
82
+ endTie?: boolean;
83
+ startSlur?: AbcPitchStartSlur[];
84
+ endSlur?: number[]; // An array of slur identifiers which should end here
85
+ }
86
+
87
+ export interface AbcRest {
88
+ type: "rest" | "whole" | "multimeasure" | "spacer";
89
+ text?: number;
90
+ }
91
+
92
+ export interface AbcLyric {
93
+ syllable: string;
94
+ divider: " " | "-" | "_";
95
+ }
96
+
97
+ export interface VoiceItemBar {
98
+ el_type: "bar";
99
+ type: AbcType;
100
+ startChar: number;
101
+ endChar: number;
102
+ }
103
+
104
+ export interface NoteProperties {
105
+ duration: number;
106
+ pitches?: AbcPitch[];
107
+ lyric?: AbcLyric[];
108
+ chord?: AbcChord[];
109
+ rest?: AbcRest;
110
+ }
111
+
112
+ export interface VoiceItemNote extends NoteProperties {
113
+ el_type: "note";
114
+ startChar: number;
115
+ endChar: number;
116
+ }
117
+
118
+ export interface VoiceItemStem {
119
+ el_type: "stem";
120
+ direction: StemDirection;
121
+ }
122
+
123
+ export interface VoiceItemClef {
124
+ el_type: "clef";
125
+ stafflines?: number;
126
+ staffscale?: number;
127
+ transpose?: number;
128
+ type: Clef;
129
+ verticalPos: number;
130
+ clefPos?: number;
131
+ startChar: number;
132
+ endChar: number;
133
+ }
134
+
135
+ export interface VoiceItemGap {
136
+ el_type: "gap";
137
+ gap: number;
138
+ }
139
+
140
+ export interface VoiceItemKey extends KeySignature {
141
+ el_type: "key";
142
+ startChar: number;
143
+ endChar: number;
144
+ }
145
+
146
+ export type VoiceItem = VoiceItemClef | VoiceItemBar | VoiceItemGap | VoiceItemKey | VoiceItemStem | VoiceItemNote;
147
+
148
+ export interface Accidental {
149
+ acc: AccidentalName;
150
+ note: NoteLetter;
151
+ verticalPos: number;
152
+ }
153
+
154
+ export interface AbcClef {
155
+ clefPos: number;
156
+ type: Clef;
157
+ verticalPos: number;
158
+ }
159
+
160
+ export interface KeySignature {
161
+ accidentals?: Array<Accidental>;
162
+ root: KeyRoot;
163
+ acc: KeyAccidentalName;
164
+ mode: Mode;
165
+ }
166
+
167
+ export interface AbcStaff {
168
+ barNumber?: number;
169
+ brace: BracePosition;
170
+ bracket: BracePosition;
171
+ connectBarLines: BracePosition;
172
+ stafflines?: number;
173
+ clef?: AbcClef;
174
+ key?: KeySignature;
175
+ voices?: Array<Array<VoiceItem>>;
176
+ }
177
+
178
+ export interface AbcLine {
179
+ staff?: AbcStaff[];
180
+ }
181
+
182
+ export interface MetaText {
183
+ "abc-copyright"?: string;
184
+ "abc-creator"?: string;
185
+ "abc-version"?: string;
186
+ "abc-charset"?: string;
187
+ "abc-edited-by"?: string;
188
+ author?: string;
189
+ book?: string;
190
+ composer?: string;
191
+ discography?: string;
192
+ footer?: {
193
+ left: string;
194
+ center: string;
195
+ right: string;
196
+ };
197
+ group?: string;
198
+ header?: {
199
+ left: string;
200
+ center: string;
201
+ right: string;
202
+ };
203
+ history?: string;
204
+ instruction?: string;
205
+ measurebox?: boolean;
206
+ notes?: string;
207
+ origin?: string;
208
+ partOrder?: string;
209
+ rhythm?: string;
210
+ source?: string;
211
+ textBlock?: string;
212
+ title?: string;
213
+ transcription?: string;
214
+ url?: string;
215
+ }
216
+
217
+ export interface TuneObject {
218
+ formatting: object;
219
+ media: string;
220
+ version: string;
221
+ metaText: MetaText;
222
+ lines: AbcLine[];
223
+
224
+ getTotalTime: NumberFunction;
225
+ getTotalBeats: NumberFunction;
226
+ getBarLength: NumberFunction;
227
+ getBeatLength: NumberFunction;
228
+ getBeatsPerMeasure: NumberFunction;
229
+ getBpm: NumberFunction;
230
+ getPickupLength: NumberFunction;
231
+ getKeySignature: () => KeySignature;
232
+ getElementFromChar: (charPos: number) => VoiceItem | null;
233
+ millisecondsPerMeasure: NumberFunction;
234
+ lineBreaks?: Array<number>;
235
+ visualTranspose?: number;
236
+ setUpAudio: (options?: SynthOptions) => AudioTracks;
237
+ }
238
+
239
+ export interface AudioTracks {
240
+ tempo: number;
241
+ instrument: number;
242
+ tracks: AudioTrack[][];
243
+ totalDuration: number;
244
+ }
245
+
246
+ export type AudioTrack = AudioTrackProgram | AudioTrackText | AudioTrackNote;
247
+
248
+ export interface AudioTrackProgram {
249
+ cmd: "program";
250
+ channel: number;
251
+ }
252
+
253
+ export interface AudioTrackText {
254
+ cmd: "text";
255
+ // ...?
256
+ }
257
+
258
+ export interface AudioTrackNote {
259
+ cmd: "note";
260
+ duration: number;
261
+ volume: number;
262
+ pitch: number;
263
+ gap: number;
264
+ instrument: number;
265
+ start: number;
266
+ startChar: number;
267
+ endChar: number;
268
+ }
269
+
270
+ export class AbcSong {
271
+ // See also: https://abcnotation.com/wiki/abc:standard:v2.1#information_fields
272
+ area?: string;
273
+ book?: string;
274
+ composer?: string;
275
+ discography?: string;
276
+ fileUrl?: string;
277
+ group?: string;
278
+ history?: string;
279
+ instruction?: string;
280
+ key?: string;
281
+ unitNoteLength?: string;
282
+ meter?: string;
283
+ macro?: string;
284
+ notes?: string;
285
+ origin?: string;
286
+ parts?: string;
287
+ tempo?: string;
288
+ rhythm?: string;
289
+ remark?: string;
290
+ source?: string;
291
+ symbolLine?: string;
292
+ title?: string;
293
+ userDefined?: string;
294
+ voice?: string;
295
+ referenceNumber?: string;
296
+ transcription?: string;
297
+ melody: VoiceItem[][] = [];
298
+ clef: AbcClef = {
299
+ clefPos: 4,
300
+ type: "treble",
301
+ verticalPos: 0
302
+ };
303
+ keySignature: KeySignature = {
304
+ accidentals: [],
305
+ root: "C",
306
+ acc: "",
307
+ mode: ""
308
+ };
309
+ }
310
+
311
+ export interface NoteGroupInterface {
312
+ notes: string,
313
+ lyrics: string
314
+ }
package/src/based.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { combineMelodyAndLyrics } from "./abc";
2
+ import { AbcMelody, AbcSubMelody, Verse } from "./basedTypes";
3
+
4
+
5
+ export const getForVerse = (melody: AbcMelody, verse: Verse): AbcSubMelody | undefined =>
6
+ melody.subMelodies.find(it =>
7
+ // `.includes()` won't work due to the Realm data type of `verseUuids`.
8
+ it.verseUuids.some(it => it == verse.uuid));
9
+
10
+ export const generateAbcForVerse = (
11
+ verse: Verse,
12
+ activeMelody?: AbcMelody
13
+ ): string => {
14
+ if (activeMelody === undefined) {
15
+ return "";
16
+ }
17
+ const melody = getForVerse(activeMelody, verse)?.melody || activeMelody.melody;
18
+ return combineMelodyAndLyrics(melody, verse.abcLyrics || "")
19
+ };
@@ -0,0 +1,14 @@
1
+ export type Verse = {
2
+ uuid: string;
3
+ abcLyrics?: string;
4
+ }
5
+
6
+ export type AbcSubMelody = {
7
+ melody: string;
8
+ verseUuids: string[];
9
+ }
10
+
11
+ export type AbcMelody = {
12
+ melody: string;
13
+ subMelodies: AbcSubMelody[];
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './abcTypes';
2
+ export * from './abc';
3
+ export * from './basedTypes';
4
+ export * from './based';
5
+ export * from './validation';
@@ -0,0 +1,9 @@
1
+ export class ValidationError extends Error {
2
+ }
3
+
4
+ export function validate(result: boolean, message?: string) {
5
+ if (result) {
6
+ return;
7
+ }
8
+ throw new ValidationError(message || "Value is not true");
9
+ }