optimize chords
This commit is contained in:
@@ -4,7 +4,7 @@ import {TransposeMode} from './transpose-mode';
|
||||
import {SectionType} from './section-type';
|
||||
import {Section} from './section';
|
||||
import {LineType} from './line-type';
|
||||
import {Chord} from './chord';
|
||||
import {Chord, ChordAddDescriptor} from './chord';
|
||||
import {Line} from './line';
|
||||
|
||||
@Injectable({
|
||||
@@ -13,43 +13,94 @@ import {Line} from './line';
|
||||
export class TextRenderingService {
|
||||
private transposeService = inject(TransposeService);
|
||||
|
||||
private regexSection = /(Strophe|Refrain|Bridge)/;
|
||||
private readonly regexSection = /^\s*(Strophe|Refrain|Bridge)\b/i;
|
||||
private readonly chordRoots = [
|
||||
'C#',
|
||||
'Db',
|
||||
'D#',
|
||||
'Eb',
|
||||
'F#',
|
||||
'Gb',
|
||||
'G#',
|
||||
'Ab',
|
||||
'A#',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'A',
|
||||
'B',
|
||||
'H',
|
||||
'c#',
|
||||
'db',
|
||||
'd#',
|
||||
'eb',
|
||||
'f#',
|
||||
'gb',
|
||||
'g#',
|
||||
'ab',
|
||||
'a#',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
'a',
|
||||
'b',
|
||||
'h',
|
||||
] as const;
|
||||
private readonly suffixKeywords = ['moll', 'verm', 'maj', 'min', 'dur', 'dim', 'aug', 'sus', 'add', 'm'] as const;
|
||||
private readonly suffixChars = new Set(['#', 'b', '+', '-', '(', ')']);
|
||||
|
||||
public parse(text: string, transpose: TransposeMode | null, withComments = true): Section[] {
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
const arrayOfLines = text.split(/\r?\n/).filter(_ => _ && (!_.startsWith('#') || withComments));
|
||||
|
||||
const indices = {
|
||||
[SectionType.Bridge]: 0,
|
||||
[SectionType.Chorus]: 0,
|
||||
[SectionType.Verse]: 0,
|
||||
};
|
||||
return arrayOfLines.reduce((array, line) => {
|
||||
const sections: Section[] = [];
|
||||
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
if (!line || this.isCommentLine(line, withComments)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.getSectionTypeOfLine(line);
|
||||
if (this.regexSection.exec(line) && type !== null) {
|
||||
const section: Section = {
|
||||
if (type !== null) {
|
||||
sections.push({
|
||||
type,
|
||||
number: indices[type]++,
|
||||
lines: [],
|
||||
};
|
||||
return [...array, section];
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const lineOfLineText = this.getLineOfLineText(line, transpose);
|
||||
if (array.length === 0) return array;
|
||||
if (lineOfLineText) array[array.length - 1].lines.push(lineOfLineText);
|
||||
return array;
|
||||
}, [] as Section[]);
|
||||
|
||||
if (sections.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderedLine = this.getLineOfLineText(line, transpose);
|
||||
if (renderedLine) {
|
||||
sections[sections.length - 1].lines.push(renderedLine);
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
private getLineOfLineText(text: string, transpose: TransposeMode | null): Line | null {
|
||||
if (!text) return null;
|
||||
|
||||
const cords = this.readChords(text);
|
||||
const hasMatches = cords.length > 0;
|
||||
const chords = this.readChords(text);
|
||||
const hasMatches = chords.length > 0;
|
||||
const type = hasMatches ? LineType.chord : LineType.text;
|
||||
|
||||
const line: Line = {type, text, chords: hasMatches ? cords : null};
|
||||
const line: Line = {type, text, chords: hasMatches ? chords : null};
|
||||
return transpose !== null && transpose !== undefined ? this.transposeService.transpose(line, transpose.baseKey, transpose.targetKey) : this.transposeService.renderChords(line);
|
||||
}
|
||||
|
||||
@@ -61,13 +112,13 @@ export class TextRenderingService {
|
||||
if (!match || match.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const typeString = match[1];
|
||||
const typeString = match[1].toLowerCase();
|
||||
switch (typeString) {
|
||||
case 'Strophe':
|
||||
case 'strophe':
|
||||
return SectionType.Verse;
|
||||
case 'Refrain':
|
||||
case 'refrain':
|
||||
return SectionType.Chorus;
|
||||
case 'Bridge':
|
||||
case 'bridge':
|
||||
return SectionType.Bridge;
|
||||
}
|
||||
|
||||
@@ -75,34 +126,171 @@ export class TextRenderingService {
|
||||
}
|
||||
|
||||
private readChords(chordLine: string): Chord[] {
|
||||
let match: string[] | null;
|
||||
const chords: Chord[] = [];
|
||||
const tokens = chordLine.match(/\S+/g) ?? [];
|
||||
|
||||
// https://regex101.com/r/68jMB8/5
|
||||
const regex =
|
||||
/(C#|C|Db|D#|D|Eb|E|F#|F|Gb|G#|G|Ab|A#|A|B|H|c#|c|db|d#|d|eb|e|f#|f|gb|g#|g|ab|a#|a|b|h)(\/(C#|C|Db|D#|D|Eb|E|F#|F|Gb|G#|G|Ab|A#|A|B|H|c#|c|db|d#|d|eb|e|f#|f|gb|g#|g|ab|a#|a|b|h))?(\d+|maj7)?/gm;
|
||||
|
||||
while ((match = regex.exec(chordLine)) !== null) {
|
||||
const chord: Chord = {
|
||||
chord: match[1],
|
||||
length: match[0].length,
|
||||
position: regex.lastIndex - match[0].length,
|
||||
slashChord: null,
|
||||
add: null,
|
||||
};
|
||||
if (match[3]) {
|
||||
chord.slashChord = match[3];
|
||||
for (const token of tokens) {
|
||||
const position = chordLine.indexOf(token, chords.length > 0 ? chords[chords.length - 1].position + chords[chords.length - 1].length : 0);
|
||||
const chord = this.parseChordToken(token, position);
|
||||
if (chord) {
|
||||
chords.push(chord);
|
||||
}
|
||||
if (match[4]) {
|
||||
chord.add = match[4];
|
||||
}
|
||||
|
||||
chords.push(chord);
|
||||
}
|
||||
|
||||
const chordCount = chords.reduce((acc: number, cur: Chord) => acc + cur.length, 0);
|
||||
const lineCount = chordLine.replace(/\s/g, '').length;
|
||||
const isChrod = chordCount * 1.2 > lineCount;
|
||||
return isChrod ? chords : [];
|
||||
const isChordLine = chordCount * 1.2 > lineCount;
|
||||
return isChordLine ? chords : [];
|
||||
}
|
||||
|
||||
private isCommentLine(line: string, withComments: boolean): boolean {
|
||||
return !withComments && line.trimStart().startsWith('#');
|
||||
}
|
||||
|
||||
private parseChordToken(token: string, position: number): Chord | null {
|
||||
const root = this.readChordRoot(token, 0);
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let cursor = root.length;
|
||||
const suffix = this.readChordSuffix(token, cursor);
|
||||
cursor += suffix.length;
|
||||
|
||||
let slashChord: string | null = null;
|
||||
if (token[cursor] === '/') {
|
||||
const slash = this.readChordRoot(token, cursor + 1);
|
||||
if (!slash) {
|
||||
return null;
|
||||
}
|
||||
slashChord = slash;
|
||||
cursor += 1 + slash.length;
|
||||
}
|
||||
|
||||
if (cursor !== token.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
chord: root,
|
||||
length: token.length,
|
||||
position,
|
||||
slashChord,
|
||||
add: suffix || null,
|
||||
addDescriptor: this.parseChordAddDescriptor(suffix || null),
|
||||
};
|
||||
}
|
||||
|
||||
private readChordRoot(token: string, start: number): string | null {
|
||||
return this.chordRoots.find(root => token.startsWith(root, start)) ?? null;
|
||||
}
|
||||
|
||||
private readChordSuffix(token: string, start: number): string {
|
||||
let cursor = start;
|
||||
let suffix = '';
|
||||
|
||||
while (cursor < token.length) {
|
||||
const keyword = this.suffixKeywords.find(entry => token.startsWith(entry, cursor));
|
||||
if (keyword) {
|
||||
suffix += keyword;
|
||||
cursor += keyword.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
const char = token[cursor];
|
||||
if (this.isDigit(char) || this.suffixChars.has(char)) {
|
||||
suffix += char;
|
||||
cursor += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return suffix;
|
||||
}
|
||||
|
||||
private isDigit(char: string | undefined): boolean {
|
||||
return !!char && char >= '0' && char <= '9';
|
||||
}
|
||||
|
||||
private parseChordAddDescriptor(suffix: string | null): ChordAddDescriptor | null {
|
||||
if (!suffix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rest = suffix;
|
||||
let quality: ChordAddDescriptor['quality'] = null;
|
||||
|
||||
const qualityMatchers: Array<[string, NonNullable<ChordAddDescriptor['quality']>]> = [
|
||||
['moll', 'minor'],
|
||||
['min', 'minor'],
|
||||
['maj', 'major'],
|
||||
['dur', 'major'],
|
||||
['dim', 'diminished'],
|
||||
['verm', 'diminished'],
|
||||
['aug', 'augmented'],
|
||||
['m', 'minor'],
|
||||
];
|
||||
|
||||
for (const [prefix, normalized] of qualityMatchers) {
|
||||
if (rest.startsWith(prefix)) {
|
||||
quality = normalized;
|
||||
rest = rest.slice(prefix.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const descriptor: ChordAddDescriptor = {
|
||||
raw: suffix,
|
||||
quality,
|
||||
extensions: [],
|
||||
additions: [],
|
||||
suspensions: [],
|
||||
alterations: [],
|
||||
modifiers: [],
|
||||
};
|
||||
|
||||
while (rest.length > 0) {
|
||||
const additionMatch = rest.match(/^add([#b+\-]?\d+)/);
|
||||
if (additionMatch) {
|
||||
descriptor.additions.push(additionMatch[1]);
|
||||
rest = rest.slice(additionMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
const suspensionMatch = rest.match(/^sus(\d*)/);
|
||||
if (suspensionMatch) {
|
||||
descriptor.suspensions.push(suspensionMatch[1] || 'sus');
|
||||
rest = rest.slice(suspensionMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionMatch = rest.match(/^\d+/);
|
||||
if (extensionMatch) {
|
||||
descriptor.extensions.push(extensionMatch[0]);
|
||||
rest = rest.slice(extensionMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
const alterationMatch = rest.match(/^[#b+\-]\d+/);
|
||||
if (alterationMatch) {
|
||||
descriptor.alterations.push(alterationMatch[0]);
|
||||
rest = rest.slice(alterationMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
const modifierMatch = rest.match(/^\([^)]*\)/);
|
||||
if (modifierMatch) {
|
||||
descriptor.modifiers.push(modifierMatch[0]);
|
||||
rest = rest.slice(modifierMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptor.modifiers.push(rest);
|
||||
break;
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user