Skip to content
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
79 changes: 66 additions & 13 deletions packages/alphatab/src/exporter/GpifWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Duration } from '@coderline/alphatab/model/Duration';
import { DynamicValue } from '@coderline/alphatab/model/DynamicValue';
import { FadeType } from '@coderline/alphatab/model/FadeType';
import { type Fermata, FermataType } from '@coderline/alphatab/model/Fermata';
import { FingeringAssigner } from '@coderline/alphatab/model/FingeringAssigner';
import { Fingers } from '@coderline/alphatab/model/Fingers';
import { GolpeType } from '@coderline/alphatab/model/GolpeType';
import { GraceType } from '@coderline/alphatab/model/GraceType';
Expand All @@ -34,6 +35,7 @@ import type { Note } from '@coderline/alphatab/model/Note';
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament';
import { Ottavia } from '@coderline/alphatab/model/Ottavia';
import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper';
import { PickStroke } from '@coderline/alphatab/model/PickStroke';
import { Rasgueado } from '@coderline/alphatab/model/Rasgueado';
import type { Score } from '@coderline/alphatab/model/Score';
Expand All @@ -43,6 +45,7 @@ import { SlideOutType } from '@coderline/alphatab/model/SlideOutType';
import type { Staff } from '@coderline/alphatab/model/Staff';
import type { Track } from '@coderline/alphatab/model/Track';
import { TripletFeel } from '@coderline/alphatab/model/TripletFeel';
import { Tuning } from '@coderline/alphatab/model/Tuning';
import { VibratoType } from '@coderline/alphatab/model/VibratoType';
import type { Voice } from '@coderline/alphatab/model/Voice';
import { WahPedal } from '@coderline/alphatab/model/WahPedal';
Expand All @@ -63,11 +66,13 @@ export class GpifWriter {
private static readonly _sampleRate = 44100;

private _rhythmIdLookup: Map<string, string> = new Map<string, string>();
private _tuningByStaff: Map<Staff, number[]> = new Map<Staff, number[]>();

public writeXml(score: Score): string {
const xmlDocument = new XmlDocument();

this._rhythmIdLookup = new Map<string, string>();
this._tuningByStaff = new Map<Staff, number[]>();

this._writeDom(xmlDocument, score);

Expand Down Expand Up @@ -107,13 +112,38 @@ export class GpifWriter {

for (const tracks of score.tracks) {
for (const staff of tracks.staves) {
const needsFingering = ModelUtils.staffNotesAreNotStringed(staff);
const assignersByVoiceIndex = needsFingering ? new Map<number, FingeringAssigner>() : null;
const stringedTuning = needsFingering ? this._tuningByStaff.get(staff)! : null;
// Once the assigner sets note.string/note.fret, note.realValue
// routes through staff.tuning — needs to match the tuning we
// gave the assigner. Save + restore below leaves the input
// model untouched.
const savedTunings = staff.tuning;
if (needsFingering && stringedTuning !== null && savedTunings.length === 0) {
staff.stringTuning.tunings = stringedTuning.slice();
}

for (const bar of staff.bars) {
const activeVoices = this._writeBarNode(bars, bar);

for (const voice of activeVoices) {
let assigner: FingeringAssigner | null = null;
if (assignersByVoiceIndex !== null && stringedTuning !== null) {
if (assignersByVoiceIndex.has(voice.index)) {
assigner = assignersByVoiceIndex.get(voice.index)!;
} else {
assigner = new FingeringAssigner(stringedTuning, staff.capo, staff.transpositionPitch);
assignersByVoiceIndex.set(voice.index, assigner);
}
}

this._writeVoiceNode(voices, voice);

for (const beat of voice.beats) {
if (assigner !== null) {
assigner.assign(beat);
}
this._writeBeatNode(beats, beat, rhythms);

for (const note of beat.notes) {
Expand All @@ -122,6 +152,7 @@ export class GpifWriter {
}
}
}
staff.stringTuning.tunings = savedTunings;
}
}
}
Expand Down Expand Up @@ -282,7 +313,13 @@ export class GpifWriter {
this._writeConcertPitch(properties, note);
this._writeTransposedPitch(properties, note);

if (note.isStringed) {
if (note.isPercussion) {
const art = PercussionMapper.getArticulation(note);
const midi = art !== null ? art.outputMidiNumber : 0;
this._writeSimplePropertyNode(properties, 'String', 'String', (note.string - 1).toString());
this._writeSimplePropertyNode(properties, 'Fret', 'Fret', midi.toString());
this._writeSimplePropertyNode(properties, 'Midi', 'Number', midi.toString());
} else if (note.isStringed) {
this._writeSimplePropertyNode(properties, 'String', 'String', (note.string - 1).toString());
this._writeSimplePropertyNode(properties, 'Fret', 'Fret', note.fret.toString());
this._writeSimplePropertyNode(properties, 'Midi', 'Number', note.realValue.toString());
Expand All @@ -291,10 +328,6 @@ export class GpifWriter {
}
}

if (note.isPercussion) {
this._writeSimplePropertyNode(properties, 'String', 'String', (note.string - 1).toString());
}

if (note.isPiano) {
this._writeSimplePropertyNode(properties, 'Octave', 'Number', note.octave.toString());
this._writeSimplePropertyNode(properties, 'Tone', 'Step', note.tone.toString());
Expand Down Expand Up @@ -393,7 +426,7 @@ export class GpifWriter {

private _writeTransposedPitch(properties: XmlNode, note: Note) {
if (note.isPercussion) {
this._writePitch(properties, 'ConcertPitch', 'C', '-1', '');
this._writePitch(properties, 'TransposedPitch', 'C', '-1', '');
} else {
this._writePitchForValue(
properties,
Expand Down Expand Up @@ -998,7 +1031,7 @@ export class GpifWriter {
initialTempoAutomation.addElement('Bar').innerText = '0';
initialTempoAutomation.addElement('Position').innerText = '0';
initialTempoAutomation.addElement('Visible').innerText = 'true';
initialTempoAutomation.addElement('Value').innerText = `${score.tempo} 2`;
initialTempoAutomation.addElement('Value').innerText = `${score.tempo | 0} 2`;
if (score.tempoLabel) {
initialTempoAutomation.addElement('Text').innerText = score.tempoLabel;
}
Expand All @@ -1024,7 +1057,7 @@ export class GpifWriter {
tempoAutomation.addElement('Bar').innerText = mb.index.toString();
tempoAutomation.addElement('Position').innerText = automation.ratioPosition.toString();
tempoAutomation.addElement('Visible').innerText = automation.isVisible ? 'true' : 'false';
tempoAutomation.addElement('Value').innerText = `${automation.value} 2`;
tempoAutomation.addElement('Value').innerText = `${automation.value | 0} 2`;
if (automation.text) {
tempoAutomation.addElement('Text').innerText = automation.text;
}
Expand Down Expand Up @@ -1271,15 +1304,34 @@ export class GpifWriter {
this._writeSimplePropertyNode(properties, 'CapoFret', 'Fret', staff.capo.toString());
this._writeSimplePropertyNode(properties, 'FretCount', 'Fret', '24');

if (staff.tuning.length > 0) {
// GP7/8 requires every staff to carry a stringed tuning.
let tuning = staff.tuning;
let tuningName = staff.tuningName;
if (tuning.length === 0) {
if (staff.isPercussion) {
tuning = [0, 0, 0, 0, 0, 0];
tuningName = '';
} else if (ModelUtils.staffNotesAreNotStringed(staff)) {
const staffTuning =
staff.index === 0 ? Tuning.getDefaultTuningFor(6) : Tuning.getDefaultTuningFor(5);
tuning = staffTuning!.tunings;
tuningName = staffTuning!.name;
}
}
this._tuningByStaff.set(staff, tuning);

if (tuning.length > 0) {
const tuningProperty = properties.addElement('Property');
tuningProperty.attributes.set('name', 'Tuning');
tuningProperty.addElement('Pitches').innerText = staff.tuning.slice().reverse().join(' ');
tuningProperty.addElement('Label').setCData(staff.tuningName);
tuningProperty.addElement('LabelVisible').innerText = staff.tuningName ? 'true' : 'false';
tuningProperty.addElement('Pitches').innerText = tuning.slice().reverse().join(' ');
tuningProperty.addElement('Label').setCData(tuningName);
tuningProperty.addElement('LabelVisible').innerText = tuningName ? 'true' : 'false';
tuningProperty.addElement('Flat');

switch (staff.tuning.length) {
if (staff.isPercussion) {
tuningProperty.addElement('Instrument').innerText = 'Undefined';
} else {
switch (tuning.length) {
case 3:
tuningProperty.addElement('Instrument').innerText = 'Shamisen';
break;
Expand Down Expand Up @@ -1324,6 +1376,7 @@ export class GpifWriter {
default:
tuningProperty.addElement('Instrument').innerText = 'Guitar';
break;
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion packages/alphatab/src/importer/Gp3To5Importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
import { Note } from '@coderline/alphatab/model/Note';
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
import { Ottavia } from '@coderline/alphatab/model/Ottavia';
import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper';
import { PickStroke } from '@coderline/alphatab/model/PickStroke';
import { PlaybackInformation } from '@coderline/alphatab/model/PlaybackInformation';
import { Rasgueado } from '@coderline/alphatab/model/Rasgueado';
Expand Down Expand Up @@ -1480,9 +1481,13 @@ export class Gp3To5Importer extends ScoreImporter {
}

if (bar.staff.isPercussion) {
newNote.percussionArticulation = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret)
const midi = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret)
? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret)!
: newNote.fret;
const knownArticulation = PercussionMapper.getArticulationById(midi);
if (knownArticulation !== null) {
newNote.percussionArticulation = bar.staff.track.getOrRegisterPercussionArticulation(knownArticulation);
}
newNote.fret = Number.NaN;
}
if (swapAccidentals) {
Expand Down
76 changes: 75 additions & 1 deletion packages/alphatab/src/importer/GpifParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Duration } from '@coderline/alphatab/model/Duration';
import { DynamicValue } from '@coderline/alphatab/model/DynamicValue';
import { FadeType } from '@coderline/alphatab/model/FadeType';
import { Fermata, FermataType } from '@coderline/alphatab/model/Fermata';
import { FingeringAssigner } from '@coderline/alphatab/model/FingeringAssigner';
import { Fingers } from '@coderline/alphatab/model/Fingers';
import { GolpeType } from '@coderline/alphatab/model/GolpeType';
import { GraceType } from '@coderline/alphatab/model/GraceType';
Expand Down Expand Up @@ -134,6 +135,9 @@ export class GpifParser {
private _skipApplyLyrics: boolean = false;
private _backingTrackPadding: number = 0;

/** Marks the input as a Guitar Pro 6 file. Also auto-detected from the GPIF header. */
public isGp6: boolean = false;

private _doubleBars: Set<MasterBar> = new Set<MasterBar>();
private _keySignatures: Map<number, [KeySignature, KeySignatureType]> = new Map<
number,
Expand Down Expand Up @@ -174,6 +178,9 @@ export class GpifParser {
this._parseDom(dom);
this._buildModel();
ModelUtils.consolidate(this.score);
if (this.isGp6) {
this._assignFingeringForGp6();
}
this.score.finish(settings);
if (!this._skipApplyLyrics && this._lyricsByTrack.size > 0) {
for (const [t, lyrics] of this._lyricsByTrack) {
Expand All @@ -197,6 +204,16 @@ export class GpifParser {
// parse all children
for (const n of root.childElements()) {
switch (n.localName) {
case 'GPVersion':
if (n.innerText === '6') {
this.isGp6 = true;
}
break;
case 'Encoding':
if (n.findChildElement('EncodingDescription')?.innerText === 'GP6') {
this.isGp6 = true;
}
break;
case 'Score':
this._parseScoreNode(n);
break;
Expand Down Expand Up @@ -2559,7 +2576,7 @@ export class GpifParser {
note.addBendPoint(bendDestination);
}

// map GP6 element and variation combos to midi numbers
// Temporary MIDI id; normalised to a track-local index in `_attachNoteToBeat`.
if (element !== -1 && variation !== -1) {
note.percussionArticulation = PercussionMapper.articulationFromElementVariation(element, variation);
}
Expand Down Expand Up @@ -2733,6 +2750,63 @@ export class GpifParser {
if (this._tappedNotes.has(noteId)) {
beat.tap = true;
}
// Normalise `percussionArticulation` to the track-local index.
if (staff.isPercussion && note.percussionArticulation >= 0) {
const trackArticulations = staff.track.percussionArticulations;
let known: InstrumentArticulation | null = null;
if (note.percussionArticulation < trackArticulations.length) {
known = trackArticulations[note.percussionArticulation];
} else {
known = PercussionMapper.getArticulationById(note.percussionArticulation);
}
if (known !== null) {
note.percussionArticulation = staff.track.getOrRegisterPercussionArticulation(known);
}
}
}

private _assignFingeringForGp6(): void {
for (const track of this.score.tracks) {
for (const staff of track.staves) {
const isPercussion = staff.isPercussion;
const isPitchedOnly =
!isPercussion &&
staff.stringTuning.tunings.length === 0 &&
ModelUtils.staffNotesAreNotStringed(staff);
if (!isPercussion && !isPitchedOnly) {
continue;
}

if (isPercussion) {
staff.stringTuning.tunings = [0, 0, 0, 0, 0, 0];
} else {
const fallback =
staff.index === 0 ? Tuning.getDefaultTuningFor(6) : Tuning.getDefaultTuningFor(5);
if (fallback !== null) {
staff.stringTuning.tunings = fallback.tunings.slice();
staff.stringTuning.name = fallback.name;
}
}

const assignersByVoiceIndex = new Map<number, FingeringAssigner>();
for (const bar of staff.bars) {
for (const voice of bar.voices) {
let assigner: FingeringAssigner | undefined = assignersByVoiceIndex.get(voice.index);
if (assigner === undefined) {
assigner = new FingeringAssigner(
staff.stringTuning.tunings,
staff.capo,
staff.transpositionPitch
);
assignersByVoiceIndex.set(voice.index, assigner);
}
for (const beat of voice.beats) {
assigner.assign(beat);
}
}
}
}
}
}

private _buildModel(): void {
Expand Down
1 change: 1 addition & 0 deletions packages/alphatab/src/importer/GpxImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class GpxImporter 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.isGp6 = true;
gpifParser.parseXml(xml, this.settings);
Logger.debug(this.name, 'score.gpif parsed');
const score: Score = gpifParser.score;
Expand Down
2 changes: 1 addition & 1 deletion packages/alphatab/src/importer/MusicXmlImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2855,7 +2855,7 @@ export class MusicXmlImporter extends ScoreImporter {
} else if (note.beat.voice.bar.staff.isPercussion) {
const knownArticulation = PercussionMapper.getArticulationById(note.displayValue);
if (knownArticulation) {
note.percussionArticulation = knownArticulation.id;
note.percussionArticulation = track.getOrRegisterPercussionArticulation(knownArticulation);
}
}
}
Expand Down
Loading
Loading