From a2e74bd8530d580a090d0c72122fc7ce008f4112 Mon Sep 17 00:00:00 2001 From: chenwuji2000-cyber Date: Tue, 7 Jul 2026 20:16:46 +0800 Subject: [PATCH 1/8] Fix GP7 score metadata export --- packages/alphatab/src/exporter/GpifWriter.ts | 7 ++-- .../test/exporter/Gp7Exporter.test.ts | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index 39d33d6ef..003a8c29f 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -968,13 +968,12 @@ export class GpifWriter { scoreNode.addElement('PageHeader').setCData(''); scoreNode.addElement('PageFooter').setCData(''); - scoreNode.addElement('ScoreSystemsDefaultLayout').setCData(score.defaultSystemsLayout.toString()); - scoreNode.addElement('ScoreSystemsLayout').setCData(score.systemsLayout.join(' ')); + scoreNode.addElement('ScoreSystemsDefaultLayout').innerText = score.defaultSystemsLayout.toString(); + scoreNode.addElement('ScoreSystemsLayout').innerText = score.systemsLayout.join(' '); scoreNode.addElement('ScoreZoomPolicy').innerText = 'Value'; scoreNode.addElement('ScoreZoom').innerText = '1'; - // not fully clear at this point so we rather activate it - scoreNode.addElement('MultiVoice').innerText = '1>'; + scoreNode.addElement('MultiVoice').innerText = '0'; } private _writeMasterTrackNode(parent: XmlNode, score: Score) { diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index 271e705d3..2623735f2 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -40,6 +40,13 @@ describe('Gp7ExporterTest', () => { return new Gp7Exporter().export(score, null); } + function readExportedGpif(buffer: Uint8Array): string { + const settings = new Settings(); + const zip = new ZipReader(ByteBuffer.fromBuffer(buffer), settings.importer.maxDecodingBufferSize).read(); + const gpifData = zip.find(e => e.fileName === 'score.gpif')!.data; + return IOHelper.toString(gpifData, settings.importer.encoding); + } + async function testRoundTripEqual(name: string, ignoreKeys: string[] | null): Promise { const expected = await loadScore(name); if (!expected) { @@ -150,6 +157,31 @@ describe('Gp7ExporterTest', () => { ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', ['accidentalmode']); }); + it('alphatex-to-gp7-score-metadata', () => { + const tex = `\\title "Multitrack Metadata" + \\artist "alphaTab" + \\tempo 90 + . + \\track "Piano" + \\instrument acousticgrandpiano + C4.4 D4.4 E4.4 F4.4 + . + \\track "Guitar" + \\instrument acousticguitarsteel + 0.3.4 2.3.4 3.3.4 5.3.4 + `; + + const score = ScoreLoader.loadAlphaTex(tex); + const gpif = readExportedGpif(exportGp7(score)); + + expect(gpif).toContain('3'); + expect(gpif).toContain(''); + expect(gpif).not.toContain('0'); + expect(gpif).not.toContain('1>'); + }); + it('alphatex-drums-to-gp7', () => { const tex = `\\track "Drums" \\instrument percussion From e625b9fd532bd6afaa98170303646160bfee7fc5 Mon Sep 17 00:00:00 2001 From: chenwuji2000-cyber Date: Fri, 10 Jul 2026 18:48:39 +0800 Subject: [PATCH 2/8] Refine GPIF score layout fix Keep Guitar Pro's native MultiVoice value unchanged and strengthen the regression test with explicit score layout values.\n\nResearch and implementation generated by gpt5.6sol-max. --- packages/alphatab/src/exporter/GpifWriter.ts | 3 ++- packages/alphatab/test/exporter/Gp7Exporter.test.ts | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index 003a8c29f..62bd68657 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -973,7 +973,8 @@ export class GpifWriter { scoreNode.addElement('ScoreZoomPolicy').innerText = 'Value'; scoreNode.addElement('ScoreZoom').innerText = '1'; - scoreNode.addElement('MultiVoice').innerText = '0'; + // not fully clear at this point so we rather activate it + scoreNode.addElement('MultiVoice').innerText = '1>'; } private _writeMasterTrackNode(parent: XmlNode, score: Score) { diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index 2623735f2..bc9afe4a5 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -157,7 +157,7 @@ describe('Gp7ExporterTest', () => { ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', ['accidentalmode']); }); - it('alphatex-to-gp7-score-metadata', () => { + it('alphatex-to-gp7-score-system-layout-as-text', () => { const tex = `\\title "Multitrack Metadata" \\artist "alphaTab" \\tempo 90 @@ -172,14 +172,14 @@ describe('Gp7ExporterTest', () => { `; const score = ScoreLoader.loadAlphaTex(tex); + score.defaultSystemsLayout = 5; + score.systemsLayout = [3, 2, 3]; const gpif = readExportedGpif(exportGp7(score)); - expect(gpif).toContain('3'); - expect(gpif).toContain(''); + expect(gpif).toContain('5'); + expect(gpif).toContain('3 2 3'); expect(gpif).not.toContain('0'); - expect(gpif).not.toContain('1>'); }); it('alphatex-drums-to-gp7', () => { From 85545b86b17925fd09e8e5023bda1f5a22945626 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 10 Jul 2026 13:06:23 +0200 Subject: [PATCH 3/8] chore: revert to multivoice and add clarifications # Conflicts: # packages/alphatab/src/exporter/GpifWriter.ts # packages/alphatab/test/exporter/Gp7Exporter.test.ts --- packages/alphatab/src/exporter/GpifWriter.ts | 5 ++++- packages/alphatab/test/exporter/Gp7Exporter.test.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index 62bd68657..047c44128 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -973,7 +973,9 @@ export class GpifWriter { scoreNode.addElement('ScoreZoomPolicy').innerText = 'Value'; scoreNode.addElement('ScoreZoom').innerText = '1'; - // not fully clear at this point so we rather activate it + // Activate MultiVoice mode. the content looks strange, but + // 0> is 'Multi Voice' off (secondary voices are half-transparent/light-ray) + // 1> is 'Multi Voice' on (secondary voices are fully-visible/black) scoreNode.addElement('MultiVoice').innerText = '1>'; } @@ -1073,6 +1075,7 @@ export class GpifWriter { trackNode.addElement('ShortName').setCData(track.shortName); trackNode.addElement('Color').innerText = `${track.color.r} ${track.color.g} ${track.color.b}`; + // this typo is on purpose, guitar pro bug to have SystemsDefautLayout instead of SystemsDefaultLayout trackNode.addElement('SystemsDefautLayout').innerText = track.defaultSystemsLayout.toString(); trackNode.addElement('SystemsLayout').innerText = track.systemsLayout.join(' '); diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index bc9afe4a5..a83f6b0ee 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -180,6 +180,7 @@ describe('Gp7ExporterTest', () => { expect(gpif).toContain('3 2 3'); expect(gpif).not.toContain('1>'); }); it('alphatex-drums-to-gp7', () => { From 10bcd0f547507e8ef8f852f9bca51ef85c8c158b Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 10 Jul 2026 14:21:50 +0200 Subject: [PATCH 4/8] fix: use ANSI encoding for GP3-5 files unless overridden by user --- packages/alphatab/src/ImporterSettings.ts | 13 ++- .../src/generated/ImporterSettingsJson.ts | 12 ++- .../generated/ImporterSettingsSerializer.ts | 4 + .../alphatab/src/importer/Gp3To5Importer.ts | 80 ++++++++++--------- 4 files changed, 71 insertions(+), 38 deletions(-) diff --git a/packages/alphatab/src/ImporterSettings.ts b/packages/alphatab/src/ImporterSettings.ts index a6553f4b7..86ac15d9e 100644 --- a/packages/alphatab/src/ImporterSettings.ts +++ b/packages/alphatab/src/ImporterSettings.ts @@ -17,11 +17,22 @@ export class ImporterSettings { * * * Guitar Pro 7 * * Guitar Pro 6 - * * Guitar Pro 3-5 * * MusicXML */ public encoding: string = 'utf-8'; + /** + * The text encoding to use when decoding strings within GuitarPro3-5 files. + * @since 1.9.0 + * @defaultValue `windows-1252` + * @category Importer + * @remarks + * Guitar Pro 3-5 encode strings as system specific ANSI encoding, typically Windows-1252 in western system cultures. + * This is different to the other typically used utf-8 encoding. + * Via this setting the Guitar Pro 3-5 specific decoding can be used. + */ + public gp3To5encoding: string = 'windows-1252'; + /** * If part-groups should be merged into a single track (MusicXML). * @since 0.9.6 diff --git a/packages/alphatab/src/generated/ImporterSettingsJson.ts b/packages/alphatab/src/generated/ImporterSettingsJson.ts index 582768163..80cb69ddb 100644 --- a/packages/alphatab/src/generated/ImporterSettingsJson.ts +++ b/packages/alphatab/src/generated/ImporterSettingsJson.ts @@ -23,10 +23,20 @@ export interface ImporterSettingsJson { * * * Guitar Pro 7 * * Guitar Pro 6 - * * Guitar Pro 3-5 * * MusicXML */ encoding?: string; + /** + * The text encoding to use when decoding strings within GuitarPro3-5 files. + * @since 1.9.0 + * @defaultValue `windows-1252` + * @category Importer + * @remarks + * Guitar Pro 3-5 encode strings as system specific ANSI encoding, typically Windows-1252 in western system cultures. + * This is different to the other typically used utf-8 encoding. + * Via this setting the Guitar Pro 3-5 specific decoding can be used. + */ + gp3To5encoding?: string; /** * If part-groups should be merged into a single track (MusicXML). * @since 0.9.6 diff --git a/packages/alphatab/src/generated/ImporterSettingsSerializer.ts b/packages/alphatab/src/generated/ImporterSettingsSerializer.ts index 66c17f71c..eaba13509 100644 --- a/packages/alphatab/src/generated/ImporterSettingsSerializer.ts +++ b/packages/alphatab/src/generated/ImporterSettingsSerializer.ts @@ -21,6 +21,7 @@ export class ImporterSettingsSerializer { } const o = new Map(); o.set("encoding", obj.encoding); + o.set("gp3to5encoding", obj.gp3To5encoding); o.set("mergepartgroupsinmusicxml", obj.mergePartGroupsInMusicXml); o.set("beattextaslyrics", obj.beatTextAsLyrics); o.set("maxdecodingbuffersize", obj.maxDecodingBufferSize); @@ -31,6 +32,9 @@ export class ImporterSettingsSerializer { case "encoding": obj.encoding = v! as string; return true; + case "gp3to5encoding": + obj.gp3To5encoding = v! as string; + return true; case "mergepartgroupsinmusicxml": obj.mergePartGroupsInMusicXml = v! as boolean; return true; diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index 415e28516..1676d6df3 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -92,6 +92,7 @@ export class Gp3To5Importer extends ScoreImporter { private _directionLookup: Map = new Map(); private _initialTempo: Automation | undefined; + private _stringEncoding = ''; public get name(): string { return 'Guitar Pro 3-5'; @@ -100,6 +101,13 @@ export class Gp3To5Importer extends ScoreImporter { public readScore(): Score { this._directionLookup.clear(); + // backwards compat: if users provide a custom encoding use it, otherwise use the new special ANSI encoding setting + if (this.settings.importer.encoding !== 'utf-8') { + this._stringEncoding = this.settings.importer.encoding; + } else { + this._stringEncoding = this.settings.importer.gp3To5encoding; + } + this.readVersion(); this._score = new Score(); // basic song info @@ -128,7 +136,7 @@ export class Gp3To5Importer extends ScoreImporter { this.readPageSetup(); this._initialTempo.text = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); } @@ -220,7 +228,7 @@ export class Gp3To5Importer extends ScoreImporter { } public readVersion(): void { - let version: string = GpBinaryHelpers.gpReadStringByteLength(this.data, 30, this.settings.importer.encoding); + let version: string = GpBinaryHelpers.gpReadStringByteLength(this.data, 30, this._stringEncoding); if (!version.startsWith(Gp3To5Importer._versionString)) { throw new UnsupportedFormatError('Unsupported format'); } @@ -234,50 +242,50 @@ export class Gp3To5Importer extends ScoreImporter { public readScoreInformation(): void { this._score.title = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); this._score.subTitle = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); this._score.artist = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); this._score.album = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); this._score.words = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); this._score.music = this._versionNumber >= 500 ? GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ) : this._score.words; this._score.copyright = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); this._score.tab = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); this._score.instructions = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); const noticeLines: number = IOHelper.readInt32LE(this.data); @@ -289,7 +297,7 @@ export class Gp3To5Importer extends ScoreImporter { } notice += GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize )?.toString(); } @@ -305,7 +313,7 @@ export class Gp3To5Importer extends ScoreImporter { // I haven't encountered such a long song in the wild. beyond 1000 bars something is clearly off private static readonly _maxBarCount = 1000; - + // I think GP5 itself limits already to ~10. 100 tracks is just unrealistic, proof me wrong private static readonly _maxTrackCount = 100; @@ -331,7 +339,7 @@ export class Gp3To5Importer extends ScoreImporter { lyrics.startBar = IOHelper.readInt32LE(this.data) - 1; lyrics.text = GpBinaryHelpers.gpReadStringInt( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); this._lyrics.push(lyrics); @@ -354,7 +362,7 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Title).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); @@ -363,7 +371,7 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.SubTitle).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); @@ -372,7 +380,7 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Artist).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); @@ -381,7 +389,7 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Album).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); @@ -390,7 +398,7 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Words).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); @@ -399,7 +407,7 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Music).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); @@ -408,7 +416,7 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.WordsAndMusic).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); @@ -417,7 +425,7 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Copyright).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); @@ -426,13 +434,13 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.CopyrightSecondLine).template = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); // page number format GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); } @@ -519,7 +527,7 @@ export class Gp3To5Importer extends ScoreImporter { const section: Section = new Section(); section.text = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); section.marker = ''; @@ -600,7 +608,7 @@ export class Gp3To5Importer extends ScoreImporter { // 128 - Show Tuning const flags: number = this.data.readByte(); - newTrack.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 40, this.settings.importer.encoding); + newTrack.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 40, this._stringEncoding); if ((flags & 0x01) !== 0) { mainStaff.isPercussion = true; } @@ -715,14 +723,14 @@ export class Gp3To5Importer extends ScoreImporter { // RSE: effect name GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); // RSE: effect category GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); } @@ -868,7 +876,7 @@ export class Gp3To5Importer extends ScoreImporter { if ((flags & 0x04) !== 0) { const text = GpBinaryHelpers.gpReadStringIntUnused( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); if (beatTextAsLyrics) { @@ -990,7 +998,7 @@ export class Gp3To5Importer extends ScoreImporter { const chordId: string = ModelUtils.newGuid(); if (this._versionNumber >= 500) { this.data.skip(17); - chord.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 21, this.settings.importer.encoding); + chord.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 21, this._stringEncoding); this.data.skip(4); chord.firstFret = IOHelper.readInt32LE(this.data); for (let i: number = 0; i < 7; i++) { @@ -1019,7 +1027,7 @@ export class Gp3To5Importer extends ScoreImporter { // Diminished/Augmented (4) // Add (1) this.data.skip(16); - chord.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 21, this.settings.importer.encoding); + chord.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 21, this._stringEncoding); // Unused (2) // Fifth (1) // Ninth (1) @@ -1048,7 +1056,7 @@ export class Gp3To5Importer extends ScoreImporter { } else { // unknown this.data.skip(25); - chord.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 34, this.settings.importer.encoding); + chord.name = GpBinaryHelpers.gpReadStringByteLength(this.data, 34, this._stringEncoding); chord.firstFret = IOHelper.readInt32LE(this.data); for (let i: number = 0; i < 6; i++) { const fret: number = IOHelper.readInt32LE(this.data); @@ -1063,7 +1071,7 @@ export class Gp3To5Importer extends ScoreImporter { const strings: number = this._versionNumber >= 406 ? 7 : 6; chord.name = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); chord.firstFret = IOHelper.readInt32LE(this.data); @@ -1252,7 +1260,7 @@ export class Gp3To5Importer extends ScoreImporter { if (this._versionNumber >= 500) { tableChange.tempoName = GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); } @@ -1302,12 +1310,12 @@ export class Gp3To5Importer extends ScoreImporter { if (this._versionNumber >= 510) { GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); GpBinaryHelpers.gpReadStringIntByte( this.data, - this.settings.importer.encoding, + this._stringEncoding, this.settings.importer.maxDecodingBufferSize ); } From 891ea11527369ebdd899d659a5fce91ff1ef4042 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 10 Jul 2026 14:22:08 +0200 Subject: [PATCH 5/8] fix: incorrect midi channel reading --- .../alphatab/src/importer/Gp3To5Importer.ts | 80 +++++++++++++------ 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index 1676d6df3..c9080e91a 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -81,7 +81,10 @@ export class Gp3To5Importer extends ScoreImporter { private _lyrics: Lyrics[] = []; private _barCount: number = 0; private _trackCount: number = 0; - private _playbackInfos: PlaybackInformation[] = []; + /** + * For 4 ports, 16 channels of information + */ + private _midiChannelInfo: Gp3To5MidiChannelInfo[][] = []; private _doubleBars: Set = new Set(); private _clefsPerTrack: Map = new Map(); private _keySignatures: Map = new Map< @@ -446,17 +449,25 @@ export class Gp3To5Importer extends ScoreImporter { } public readPlaybackInfos(): void { - this._playbackInfos = []; - let channel = 0; - for (let i: number = 0; i < 64; i++) { - const info: PlaybackInformation = new PlaybackInformation(); - info.primaryChannel = channel++; - info.secondaryChannel = channel++; - info.program = IOHelper.readInt32LE(this.data); - info.volume = this.data.readByte(); - info.balance = this.data.readByte(); - this.data.skip(6); - this._playbackInfos.push(info); + this._midiChannelInfo = []; + for (let port = 0; port < 4; port++) { + const portInfo: Gp3To5MidiChannelInfo[] = []; + this._midiChannelInfo.push(portInfo); + + for (let channel = 0; channel < 16; channel++) { + const info: Gp3To5MidiChannelInfo = { + program: IOHelper.readInt32LE(this.data), + volume: this.data.readByte(), + balance: this.data.readByte(), + chorus: this.data.readByte(), + reverb: this.data.readByte(), + phase: this.data.readByte(), + tremolo: this.data.readByte() + }; + // gp3 backwards compatibiliy (blanks) + this.data.skip(2); + portInfo.push(info); + } } } @@ -632,22 +643,29 @@ export class Gp3To5Importer extends ScoreImporter { } mainStaff.stringTuning.tunings = tuning; - const port: number = IOHelper.readInt32LE(this.data); - const index: number = IOHelper.readInt32LE(this.data) - 1; + const port: number = IOHelper.readInt32LE(this.data) - 1; + const channel: number = IOHelper.readInt32LE(this.data) - 1; const effectChannel: number = IOHelper.readInt32LE(this.data) - 1; this.data.skip(4); // Fretcount - if (index >= 0 && index < this._playbackInfos.length) { - const info: PlaybackInformation = this._playbackInfos[index]; - info.port = port; - info.isSolo = (flags & 0x10) !== 0; - info.isMute = (flags & 0x20) !== 0; - info.secondaryChannel = effectChannel; - if (GeneralMidi.isGuitar(info.program)) { - mainStaff.displayTranspositionPitch = -12; - } - newTrack.playbackInfo = info; + const mainMidiChannelInfo = this._midiChannelInfo[port][channel]; + // const effectMidiChannelInfo = this._midiChannelInfo[port][effectChannel]; + + const trackPlaybackInfo = new PlaybackInformation(); + trackPlaybackInfo.volume = mainMidiChannelInfo.volume; + trackPlaybackInfo.balance = mainMidiChannelInfo.balance; + trackPlaybackInfo.port = port; + trackPlaybackInfo.program = mainMidiChannelInfo.program; + trackPlaybackInfo.primaryChannel = channel; + trackPlaybackInfo.secondaryChannel = effectChannel; + trackPlaybackInfo.isSolo = (flags & 0x10) !== 0; + trackPlaybackInfo.isMute = (flags & 0x20) !== 0; + + if (GeneralMidi.isGuitar(trackPlaybackInfo.program)) { + mainStaff.displayTranspositionPitch = -12; } + newTrack.playbackInfo = trackPlaybackInfo; + mainStaff.capo = IOHelper.readInt32LE(this.data); newTrack.color = GpBinaryHelpers.gpReadColor(this.data, false); if (this._versionNumber >= 500) { @@ -1791,3 +1809,17 @@ class MixTableChange { public tempo: number = -1; public duration: number = -1; } + +/** + * The midi channel information + * @internal + */ +interface Gp3To5MidiChannelInfo { + program: number; + volume: number; + balance: number; + chorus: number; + reverb: number; + phase: number; + tremolo: number; +} From 381107149a580458b1e3f55b0c728519995d1ca3 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 10 Jul 2026 14:29:42 +0200 Subject: [PATCH 6/8] build: add `@record` marker --- packages/alphatab/src/importer/Gp3To5Importer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index c9080e91a..922d99d45 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -1813,6 +1813,7 @@ class MixTableChange { /** * The midi channel information * @internal + * @record */ interface Gp3To5MidiChannelInfo { program: number; From 1d48f895a7d32aa472f03fda11b64a77082043e0 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 10 Jul 2026 14:45:27 +0200 Subject: [PATCH 7/8] build(csharp): update test assertions and encoding lookup --- .../csharp/src/AlphaTab.Test/Test/Globals.cs | 4 ++++ .../src/AlphaTab/Core/EcmaScript/TextDecoder.cs | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/csharp/src/AlphaTab.Test/Test/Globals.cs b/packages/csharp/src/AlphaTab.Test/Test/Globals.cs index 1e7cf0f8c..2b6923e31 100644 --- a/packages/csharp/src/AlphaTab.Test/Test/Globals.cs +++ b/packages/csharp/src/AlphaTab.Test/Test/Globals.cs @@ -136,6 +136,10 @@ public void ToContain(object element) { CollectionAssert.DoesNotContain(collection, element, _message); } + else if (_actual is string str) + { + Assert.DoesNotContain((string)element, str); + } else { Assert.Fail("Contain can only be used with collection operands"); diff --git a/packages/csharp/src/AlphaTab/Core/EcmaScript/TextDecoder.cs b/packages/csharp/src/AlphaTab/Core/EcmaScript/TextDecoder.cs index c84b8fd37..9840424ec 100644 --- a/packages/csharp/src/AlphaTab/Core/EcmaScript/TextDecoder.cs +++ b/packages/csharp/src/AlphaTab/Core/EcmaScript/TextDecoder.cs @@ -1,14 +1,26 @@ -using System.Text; +using System.Collections.Concurrent; +using System.Text; namespace AlphaTab.Core.EcmaScript; internal class TextDecoder { + private static readonly ConcurrentDictionary EncodingCache = new(); private readonly Encoding _encoding; public TextDecoder(string encoding) { - _encoding = Encoding.GetEncoding(encoding); + _encoding = EncodingCache.GetOrAdd(encoding, s => + { + try + { + return Encoding.GetEncoding(encoding); + } + catch + { + return Encoding.Default; + } + }); } public string Decode(ArrayBuffer data) From be851d1f12bcc74dca49469214fbbafa73bcbd1d Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 10 Jul 2026 15:04:58 +0200 Subject: [PATCH 8/8] build: update kotlin test expector --- .../src/android/src/test/java/alphaTab/core/TestGlobals.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt index 104579d03..b2c09a588 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt @@ -100,7 +100,12 @@ class NotExpector(private val actual: T, private val message: String? = null) message ?: "Expected collection ${actual.joinToString(",")} to not contain $value", actual.contains(value) ) - } else { + } else if(actual is String) { + Assert.assertFalse( + message ?: "Expected string $actual to no contain $value", + actual.contains(value as String) + ) + }else { Assert.fail("toContain can only be used with Iterable operands"); } }