Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: Add Backing Track and SyncPoint reading to GP importer
  • Loading branch information
Danielku15 committed May 4, 2025
commit 3d5fb4ebd116417597d7df8a1f29180c49025b3c
8 changes: 8 additions & 0 deletions src/importer/Gp7To8Importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export class Gp7To8Importer extends ScoreImporter {
let binaryStylesheetData: Uint8Array | null = null;
let partConfigurationData: Uint8Array | null = null;
let layoutConfigurationData: Uint8Array | null = null;
const entryLookup = new Map<string, ZipEntry>();
for (const entry of entries) {
entryLookup.set(entry.fullName, entry);
switch (entry.fileName) {
case 'score.gpif':
xml = IOHelper.toString(entry.data, this.settings.importer.encoding);
Expand All @@ -63,6 +65,12 @@ export class Gp7To8Importer extends ScoreImporter {
// the score information as XML we need to parse.
Logger.debug(this.name, 'Start Parsing score.gpif');
const gpifParser: GpifParser = new GpifParser();
gpifParser.loadAsset = (fileName) => {
if(entryLookup.has(fileName)) {
return entryLookup.get(fileName)!.data;
};
return undefined;
};
gpifParser.parseXml(xml, this.settings);
Logger.debug(this.name, 'score.gpif parsed');
const score: Score = gpifParser.score;
Expand Down
135 changes: 126 additions & 9 deletions src/importer/GpifParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError';
import { AccentuationType } from '@src/model/AccentuationType';
import { Automation, AutomationType } from '@src/model/Automation';
import { Automation, AutomationType, SyncPointData } from '@src/model/Automation';
import { Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar';
import { Beat, BeatBeamingMode } from '@src/model/Beat';
import { BendPoint } from '@src/model/BendPoint';
Expand Down Expand Up @@ -54,6 +54,7 @@ import { NoteOrnament } from '@src/model/NoteOrnament';
import { Rasgueado } from '@src/model/Rasgueado';
import { Direction } from '@src/model/Direction';
import { ModelUtils } from '@src/model/ModelUtils';
import { BackingTrack } from '@src/model/BackingTrack';

/**
* This structure represents a duration within a gpif
Expand Down Expand Up @@ -96,6 +97,7 @@ export class GpifParser {
private static readonly BendPointValueFactor: number = 1 / 25.0;

public score!: Score;
private _backingTrackAssetId: string | undefined;

private _masterTrackAutomations!: Map<number, Automation[]>;
private _automationsPerTrackIdAndBarIndex!: Map<string, Map<number, Automation[]>>;
Expand Down Expand Up @@ -126,6 +128,8 @@ export class GpifParser {
[KeySignature, KeySignatureType]
>();

public loadAsset?: (fileName: string) => Uint8Array | undefined;

public parseXml(xml: string, settings: Settings): void {
this._masterTrackAutomations = new Map<number, Automation[]>();
this._automationsPerTrackIdAndBarIndex = new Map<string, Map<number, Automation[]>>();
Expand Down Expand Up @@ -187,6 +191,9 @@ export class GpifParser {
case 'MasterTrack':
this.parseMasterTrackNode(n);
break;
case 'BackingTrack':
this.parseBackingTrackNode(n);
break;
case 'Tracks':
this.parseTracksNode(n);
break;
Expand All @@ -208,13 +215,48 @@ export class GpifParser {
case 'Rhythms':
this.parseRhythms(n);
break;
case 'Assets':
this.parseAssets(n);
break;
}
}
} else {
throw new UnsupportedFormatError('Root node of XML was not GPIF');
}
}

private parseAssets(element: XmlNode) {
for (const c of element.childElements()) {
switch (c.localName) {
case 'Asset':
if (c.getAttribute('id') === this._backingTrackAssetId) {
this.parseBackingTrackAsset(c);
}
break;
}
}
}
private parseBackingTrackAsset(element: XmlNode) {
let embeddedFilePath = '';
for (const c of element.childElements()) {
switch (c.localName) {
case 'EmbeddedFilePath':
embeddedFilePath = c.innerText;
break;
}
}

const loadAsset = this.loadAsset;
if (loadAsset) {
const assetData = loadAsset(embeddedFilePath);
if (assetData) {
this.score.backingTrack!.rawAudioFile = assetData;
} else {
this.score.backingTrack = undefined;
}
}
}

//
// <Score>...</Score>
//
Expand Down Expand Up @@ -303,7 +345,43 @@ export class GpifParser {
return [];
}

return text.split(separator).map(t => t.trim()).filter(t => t.length > 0);
return text
.split(separator)
.map(t => t.trim())
.filter(t => t.length > 0);
}

//
// <BackingTrack>...</BackingTrack>
//
private parseBackingTrackNode(node: XmlNode): void {
const backingTrack = new BackingTrack();
let enabled = false;
let source = '';
let assetId = '';
for (const c of node.childElements()) {
switch (c.localName) {
case 'Enabled':
enabled = c.innerText === 'true';
break;
case 'Source':
source = c.innerText;
break;
case 'AssetId':
assetId = c.innerText;
break;
case 'FramePadding':
backingTrack.framePadding = GpifParser.parseIntSafe(c.innerText, 0);
break;
}
}

// only local (contained backing tracks are supported)
// remote / youtube links seem to come in future releases according to the gpif tags.
if (enabled && source === 'Local') {
this.score.backingTrack = backingTrack;
this._backingTrackAssetId = assetId; // when the Asset tag is parsed this ID is used to load the raw data
}
}

//
Expand Down Expand Up @@ -354,6 +432,8 @@ export class GpifParser {
let textValue: string | null = null;
let reference: number = 0;
let text: string | null = null;
let syncPointValue: SyncPointData | undefined = undefined;

for (const c of node.childElements()) {
switch (c.localName) {
case 'Type':
Expand All @@ -371,6 +451,31 @@ export class GpifParser {
case 'Value':
if (c.firstElement && c.firstElement.nodeType === XmlNodeType.CDATA) {
textValue = c.innerText;
} else if (
c.firstElement &&
c.firstElement.nodeType === XmlNodeType.Element &&
type === 'SyncPoint'
) {
syncPointValue = new SyncPointData();
for (const vc of c.childElements()) {
switch (vc.localName) {
case 'BarIndex':
barIndex = GpifParser.parseIntSafe(vc.innerText, 0);
break;
case 'BarOccurrence':
syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
break;
case 'ModifiedTempo':
syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
break;
case 'OriginalTempo':
syncPointValue.originalTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
break;
case 'FrameOffset':
syncPointValue.frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
break;
}
}
} else {
const parts: string[] = GpifParser.splitSafe(c.innerText);
// Issue 391: Some GPX files might have
Expand All @@ -397,6 +502,13 @@ export class GpifParser {
case 'Tempo':
automation = Automation.buildTempoAutomation(isLinear, ratioPosition, numberValue, reference);
break;
case 'SyncPoint':
automation = new Automation();
automation.type = AutomationType.SyncPoint;
automation.isLinear = isLinear;
automation.ratioPosition = ratioPosition;
automation.syncPointValue = syncPointValue;
break;
case 'Sound':
if (textValue && sounds && sounds.has(textValue)) {
automation = Automation.buildInstrumentAutomation(
Expand Down Expand Up @@ -2619,14 +2731,19 @@ export class GpifParser {
const masterBar: MasterBar = this.score.masterBars[barNumber];
for (let i: number = 0, j: number = automations.length; i < j; i++) {
const automation: Automation = automations[i];
if (automation.type === AutomationType.Tempo) {
if (barNumber === 0) {
this.score.tempo = automation.value | 0;
if (automation.text) {
this.score.tempoLabel = automation.text;
switch (automation.type) {
case AutomationType.Tempo:
if (barNumber === 0) {
this.score.tempo = automation.value | 0;
if (automation.text) {
this.score.tempoLabel = automation.text;
}
}
}
masterBar.tempoAutomations.push(automation);
masterBar.tempoAutomations.push(automation);
break;
case AutomationType.SyncPoint:
masterBar.addSyncPoint(automation);
break;
}
}
}
Expand Down
Binary file added test-data/guitarpro8/canon-audio-track.gp
Binary file not shown.
9 changes: 9 additions & 0 deletions test/importer/Gp8Importer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,13 @@ describe('Gp8ImporterTest', () => {
}
}
});

it('audio-track', async () => {
const score = (await prepareImporterWithFile('guitarpro8/canon-audio-track.gp')).readScore();

// track data not relevant for snapshots
score.tracks = [];

expect(score).to.toMatchSnapshot();
});
});
Loading