diff --git a/src/ImporterSettings.ts b/src/ImporterSettings.ts index 85d7b1d3f..0ac429bb9 100644 --- a/src/ImporterSettings.ts +++ b/src/ImporterSettings.ts @@ -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; } diff --git a/src/generated/ImporterSettingsSerializer.ts b/src/generated/ImporterSettingsSerializer.ts index 694f9991f..d7aebd3fa 100644 --- a/src/generated/ImporterSettingsSerializer.ts +++ b/src/generated/ImporterSettingsSerializer.ts @@ -19,6 +19,7 @@ export class ImporterSettingsSerializer { const o = new Map(); 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 { @@ -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; } diff --git a/src/importer/Gp3To5Importer.ts b/src/importer/Gp3To5Importer.ts index de6bfb978..1c04b129d 100644 --- a/src/importer/Gp3To5Importer.ts +++ b/src/importer/Gp3To5Importer.ts @@ -53,6 +53,8 @@ export class Gp3To5Importer extends ScoreImporter { private _trackCount: number = 0; private _playbackInfos: PlaybackInformation[] = []; + private _beatTextChunksByTrack: Map = new Map(); + public get name(): string { return 'Guitar Pro 3-5'; } @@ -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); @@ -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 { @@ -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 diff --git a/src/model/Lyrics.ts b/src/model/Lyrics.ts index ef534471c..1727ffca4 100644 --- a/src/model/Lyrics.ts +++ b/src/model/Lyrics.ts @@ -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; } @@ -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; @@ -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; @@ -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; } } diff --git a/test-data/guitarpro5/beat-text-lyrics.gp5 b/test-data/guitarpro5/beat-text-lyrics.gp5 new file mode 100644 index 000000000..7a711accd Binary files /dev/null and b/test-data/guitarpro5/beat-text-lyrics.gp5 differ diff --git a/test/importer/Gp5Importer.test.ts b/test/importer/Gp5Importer.test.ts index 52ae38e79..7a8e4f5fc 100644 --- a/test/importer/Gp5Importer.test.ts +++ b/test/importer/Gp5Importer.test.ts @@ -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'; @@ -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(';')); + }); }); diff --git a/test/importer/GpImporterTestHelper.ts b/test/importer/GpImporterTestHelper.ts index 7c95f94da..dc9aeed2c 100644 --- a/test/importer/GpImporterTestHelper.ts +++ b/test/importer/GpImporterTestHelper.ts @@ -21,15 +21,15 @@ import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; export class GpImporterTestHelper { - public static async prepareImporterWithFile(name: string): Promise { + public static async prepareImporterWithFile(name: string, settings: Settings | null = null): Promise { 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; }