Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added detection mechanism for beat texts as lyrics. #506

Merged
merged 2 commits into from
Jan 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/ImporterSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ export class ImporterSettings {
* If part-groups should be merged into a single track.
*/
public mergePartGroupsInMusicXml: boolean = false;

/**
* If set to true, text annotations on beats are attempted to be parsed as
* lyrics considering spaces as separators and removing underscores.
* If a track/staff has explicit lyrics the beat texts will not be detected as lyrics.
*/
public beatTextAsLyrics: boolean = false;
}
4 changes: 4 additions & 0 deletions src/generated/ImporterSettingsSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class ImporterSettingsSerializer {
const o = new Map<string, unknown>();
o.set("encoding", obj.encoding);
o.set("mergePartGroupsInMusicXml", obj.mergePartGroupsInMusicXml);
o.set("beatTextAsLyrics", obj.beatTextAsLyrics);
return o;
}
public static setProperty(obj: ImporterSettings, property: string, v: unknown): boolean {
Expand All @@ -29,6 +30,9 @@ export class ImporterSettingsSerializer {
case "mergepartgroupsinmusicxml":
obj.mergePartGroupsInMusicXml = (v as boolean);
return true;
case "beattextaslyrics":
obj.beatTextAsLyrics = (v as boolean);
return true;
}
return false;
}
Expand Down
38 changes: 34 additions & 4 deletions src/importer/Gp3To5Importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export class Gp3To5Importer extends ScoreImporter {
private _trackCount: number = 0;
private _playbackInfos: PlaybackInformation[] = [];

private _beatTextChunksByTrack: Map<number, string[]> = new Map<number, string[]>();

public get name(): string {
return 'Guitar Pro 3-5';
}
Expand Down Expand Up @@ -508,10 +510,32 @@ export class Gp3To5Importer extends ScoreImporter {
if ((flags & 0x02) !== 0) {
this.readChord(newBeat);
}

let beatTextAsLyrics = this.settings.importer.beatTextAsLyrics
&& track.index !== this._lyricsTrack; // detect if not lyrics track

if ((flags & 0x04) !== 0) {
newBeat.text = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding);
const text = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding);
if (beatTextAsLyrics) {

const lyrics = new Lyrics();
lyrics.text = text.trim();
lyrics.finish(true);

// push them in reverse order to the store for applying them
// to the next beats being read
const beatLyrics:string[] = [];
for (let i = lyrics.chunks.length - 1; i >= 0; i--) {
beatLyrics.push(lyrics.chunks[i]);
}
this._beatTextChunksByTrack.set(track.index, beatLyrics);

} else {
newBeat.text = text;
}
}


let allNoteHarmonicType = HarmonicType.None;
if ((flags & 0x08) !== 0) {
allNoteHarmonicType = this.readBeatEffects(newBeat);
Expand All @@ -538,6 +562,12 @@ export class Gp3To5Importer extends ScoreImporter {
this.data.readByte();
}
}

if (beatTextAsLyrics && !newBeat.isRest &&
this._beatTextChunksByTrack.has(track.index) &&
this._beatTextChunksByTrack.get(track.index)!.length > 0) {
newBeat.lyrics = [this._beatTextChunksByTrack.get(track.index)!.pop()!];
}
}

public readChord(beat: Beat): void {
Expand Down Expand Up @@ -896,11 +926,11 @@ export class Gp3To5Importer extends ScoreImporter {
newNote.string = -1;
newNote.fret = -1;
}
if(swapAccidentals) {
if (swapAccidentals) {
const accidental = Tuning.defaultAccidentals[newNote.realValueWithoutHarmonic % 12];
if(accidental === '#') {
if (accidental === '#') {
newNote.accidentalMode = NoteAccidentalMode.ForceFlat;
} else if(accidental === 'b') {
} else if (accidental === 'b') {
newNote.accidentalMode = NoteAccidentalMode.ForceSharp;
}
// Note: forcing no sign to sharp not supported
Expand Down
28 changes: 21 additions & 7 deletions src/model/Lyrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ export class Lyrics {
*/
public chunks!: string[];

public finish(): void {
public finish(skipEmptyEntries: boolean = false): void {
this.chunks = [];
this.parse(this.text, 0, this.chunks);
this.parse(this.text, 0, this.chunks, skipEmptyEntries);
}

private parse(str: string, p: number, chunks: string[]): void {
private parse(str: string, p: number, chunks: string[], skipEmptyEntries: boolean): void {
if (!str) {
return;
}
Expand Down Expand Up @@ -97,7 +97,7 @@ export class Lyrics {
case Lyrics.CharCodeLF:
case Lyrics.CharCodeSpace:
let txt: string = str.substr(start, p - start);
chunks.push(this.prepareChunk(txt));
this.addChunk(txt, skipEmptyEntries);
state = LyricsState.IgnoreSpaces;
next = LyricsState.Begin;
break;
Expand All @@ -109,7 +109,7 @@ export class Lyrics {
break;
default:
let txt: string = str.substr(start, p - start);
chunks.push(this.prepareChunk(txt));
this.addChunk(txt, skipEmptyEntries);
skipSpace = true;
state = LyricsState.IgnoreSpaces;
next = LyricsState.Begin;
Expand All @@ -122,12 +122,26 @@ export class Lyrics {

if (state === LyricsState.Text) {
if (p !== start) {
chunks.push(str.substr(start, p - start));
this.addChunk(str.substr(start, p - start), skipEmptyEntries);
}
}
}
private addChunk(txt: string, skipEmptyEntries: boolean) {
txt = this.prepareChunk(txt);
if (!skipEmptyEntries || (txt.length > 0 && txt !== '-')) {
this.chunks.push(txt);
}
}

private prepareChunk(txt: string): string {
return txt.split('+').join(' ');
let chunk = txt.split('+').join(' ');

// trim off trailing _ like "You____" becomes "You"
let endLength = chunk.length;
while (endLength > 0 && chunk.charAt(endLength - 1) === '_') {
endLength--;
}

return endLength !== chunk.length ? chunk.substr(0, endLength) : chunk;
}
}
Binary file added test-data/guitarpro5/beat-text-lyrics.gp5
Binary file not shown.
34 changes: 34 additions & 0 deletions test/importer/Gp5Importer.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Settings } from '@src/alphatab';
import { Beat } from '@src/model/Beat';
import { Score } from '@src/model/Score';
import { GpImporterTestHelper } from '@test/importer/GpImporterTestHelper';

Expand Down Expand Up @@ -184,4 +186,36 @@ describe('Gp5ImporterTest', () => {
expect(score.tracks[7].name).toEqual('Track 8');
expect(score.tracks[8].name).toEqual('Percussion');
});
it('beat-text-lyrics', async () => {
const settings = new Settings();
settings.importer.beatTextAsLyrics = true;
const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/beat-text-lyrics.gp5', settings);
let score: Score = reader.readScore();

const expectedChunks: string[] = [
"",
"So", "close,",
"no", "mat", "ter", "how", "", "far.",
"", "",
"Could-", "n't", "be", "much", "more", "from", "the", "", "heart.",
"", "", "", "",
"For-", "ev-", "er", "trust-", "ing", "who", "we", "are.",
"", "", "", "", "", "",
"And", "noth-", "ing", "else", "",
"mat-", "ters.", "", ""
];

let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
const actualChunks: string[] = [];
while (beat != null) {
if (beat.lyrics) {
actualChunks.push(beat.lyrics[0]);
} else {
actualChunks.push('');
}
beat = beat.nextBeat;
}

expect(actualChunks.join(';')).toEqual(expectedChunks.join(';'));
});
});
8 changes: 4 additions & 4 deletions test/importer/GpImporterTestHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import { Settings } from '@src/Settings';
import { TestPlatform } from '@test/TestPlatform';

export class GpImporterTestHelper {
public static async prepareImporterWithFile(name: string): Promise<Gp3To5Importer> {
public static async prepareImporterWithFile(name: string, settings: Settings | null = null): Promise<Gp3To5Importer> {
let path: string = 'test-data/';
const buffer = await TestPlatform.loadFile(path + name);
return GpImporterTestHelper.prepareImporterWithBytes(buffer);
return GpImporterTestHelper.prepareImporterWithBytes(buffer, settings);
}

public static prepareImporterWithBytes(buffer: Uint8Array): Gp3To5Importer {
public static prepareImporterWithBytes(buffer: Uint8Array, settings: Settings | null = null): Gp3To5Importer {
let readerBase: Gp3To5Importer = new Gp3To5Importer();
readerBase.init(ByteBuffer.fromBuffer(buffer), new Settings());
readerBase.init(ByteBuffer.fromBuffer(buffer), settings ?? new Settings());
return readerBase;
}

Expand Down