diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index 9d2805579..b5eb34d08 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -5,7 +5,6 @@ import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi'; import { MidiFileGenerator } from '@coderline/alphatab/midi/MidiFileGenerator'; import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; -import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; import { type Automation, AutomationType } from '@coderline/alphatab/model/Automation'; import { type Bar, SustainPedalMarkerType } from '@coderline/alphatab/model/Bar'; import { BarreShape } from '@coderline/alphatab/model/BarreShape'; @@ -32,7 +31,7 @@ import type { MasterBar } from '@coderline/alphatab/model/MasterBar'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; +import type { 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'; @@ -451,7 +450,8 @@ export class GpifWriter { } } - private static readonly _defaultSteps: string[] = ['C', 'C', 'D', 'D', 'E', 'F', 'F', 'G', 'G', 'A', 'A', 'B']; + private static readonly _steps: string[] = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; + private static readonly _accidentals: string[] = ['bb', 'b', '', '#', 'x']; private _writePitchForValue( properties: XmlNode, @@ -460,74 +460,15 @@ export class GpifWriter { accidentalMode: NoteAccidentalMode, keySignature: KeySignature ) { - let index = 0; - let octave = 0; - - let step = ''; - let accidental = ''; - - const updateParts: () => void = () => { - index = value % 12; - octave = (value / 12) | 0; - - step = GpifWriter._defaultSteps[index]; - switch (ModelUtils.computeAccidental(keySignature, NoteAccidentalMode.Default, value, false)) { - case AccidentalType.None: - case AccidentalType.Natural: - accidental = ''; - break; - case AccidentalType.Sharp: - accidental = '#'; - break; - case AccidentalType.Flat: - accidental = 'b'; - break; - case AccidentalType.DoubleSharp: - accidental = 'x'; - break; - case AccidentalType.DoubleFlat: - accidental = 'bb'; - break; - } - }; - updateParts(); - - switch (accidentalMode) { - case NoteAccidentalMode.Default: - break; - case NoteAccidentalMode.ForceNone: - accidental = ''; - break; - case NoteAccidentalMode.ForceNatural: - accidental = ''; - break; - case NoteAccidentalMode.ForceSharp: - accidental = '#'; - break; - case NoteAccidentalMode.ForceDoubleSharp: - if (accidental === '#') { - value -= 2; - updateParts(); - } - accidental = 'x'; - break; - case NoteAccidentalMode.ForceFlat: - if (accidental === '#') { - value += 1; - updateParts(); - } - accidental = 'b'; - break; - case NoteAccidentalMode.ForceDoubleFlat: - if (accidental === '#') { - value += 2; - updateParts(); - } - accidental = 'bb'; - break; - } - - this._writePitch(properties, propertyName, step, octave.toString(), accidental); + // Guitar Pro stores the spelled pitch, the octave belongs to the step (B#4 = C5, Cb5 = B4) + const spelling = ModelUtils.resolveSpelling(keySignature, value, accidentalMode); + this._writePitch( + properties, + propertyName, + GpifWriter._steps[spelling.degree], + (spelling.octave + 1).toString(), + GpifWriter._accidentals[spelling.accidentalOffset + 2] + ); } private _writePitch(properties: XmlNode, propertyName: string, step: string, octave: string, accidental: string) { diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 9af55e8af..d0e417eb1 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -4085,25 +4085,28 @@ export class MusicXmlImporter extends ScoreImporter { } } + /** + * The spelling of the note is defined by its ``, the `` only describes the printed sign + * which is computed during rendering. We only report signs which contradict the pitch. + * https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/accidental/ + */ private _parseAccidental(element: XmlNode, note: Note) { - // NOTE: this can currently lead to wrong notes shown, - // TODO: check partwise-complex-measures.xml where accidentals and notes get wrong - // in combination with key signatures + let accidentalMode = NoteAccidentalMode.Default; switch (element.innerText) { case 'sharp': - note.accidentalMode = NoteAccidentalMode.ForceSharp; + accidentalMode = NoteAccidentalMode.ForceSharp; break; case 'natural': - note.accidentalMode = NoteAccidentalMode.ForceNatural; + accidentalMode = NoteAccidentalMode.ForceNatural; break; case 'flat': - note.accidentalMode = NoteAccidentalMode.ForceFlat; + accidentalMode = NoteAccidentalMode.ForceFlat; break; case 'double-sharp': - note.accidentalMode = NoteAccidentalMode.ForceDoubleSharp; + accidentalMode = NoteAccidentalMode.ForceDoubleSharp; break; case 'flat-flat': - note.accidentalMode = NoteAccidentalMode.ForceDoubleFlat; + accidentalMode = NoteAccidentalMode.ForceDoubleFlat; break; // case 'sharp-sharp': Not supported // case 'natural-sharp': Not supported @@ -4147,6 +4150,17 @@ export class MusicXmlImporter extends ScoreImporter { // Logger.warning('MusicXML', `Unsupported accidental ${element.innerText}`); // break; } + + if ( + accidentalMode !== NoteAccidentalMode.Default && + note.accidentalMode !== NoteAccidentalMode.Default && + accidentalMode !== note.accidentalMode + ) { + Logger.warning( + 'MusicXML', + `Accidental '${element.innerText}' does not match the pitch of the note, the pitch is used` + ); + } } private _calculatePitchedNoteValue(note: Note) { @@ -4214,10 +4228,34 @@ export class MusicXmlImporter extends ScoreImporter { note.octave = (value / 12) | 0; note.tone = value - note.octave * 12; + note.accidentalMode = MusicXmlImporter._accidentalModeForAlter(semitones); return note; } + /** + * The `` and `` define the spelling of the note (e.g. F# vs Gb), also if no `` is printed. + * https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/pitch/ + */ + private static _accidentalModeForAlter(alter: number): NoteAccidentalMode { + if (alter === 0) { + return NoteAccidentalMode.ForceNatural; + } + if (alter === 1) { + return NoteAccidentalMode.ForceSharp; + } + if (alter === 2) { + return NoteAccidentalMode.ForceDoubleSharp; + } + if (alter === -1) { + return NoteAccidentalMode.ForceFlat; + } + if (alter === -2) { + return NoteAccidentalMode.ForceDoubleFlat; + } + return NoteAccidentalMode.Default; + } + private _applyNoteHead( note: Note, beatDuration: Duration, diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index d2d4865a2..c09255fb3 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -60,7 +60,6 @@ import { Fingers } from '@coderline/alphatab/model/Fingers'; import { GolpeType } from '@coderline/alphatab/model/GolpeType'; import { GraceType } from '@coderline/alphatab/model/GraceType'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; -import { KeySignature } from '@coderline/alphatab/model/KeySignature'; import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; import { Lyrics } from '@coderline/alphatab/model/Lyrics'; import { BeamingRules, type MasterBar } from '@coderline/alphatab/model/MasterBar'; @@ -3437,18 +3436,14 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler Atnf.prop(properties, 'slur', Atnf.identValue(slurId)); } - // NOTE: it would be better to check via accidentalhelper what accidentals we really need to force - const skipAccidental = - note.accidentalMode === NoteAccidentalMode.Default || - (note.beat.voice.bar.keySignature === KeySignature.C && - note.accidentalMode === NoteAccidentalMode.ForceNatural); - - if (!skipAccidental) { - Atnf.prop( - properties, - 'acc', - Atnf.identValue(ModelUtils.reverseAccidentalModeMapping.get(note.accidentalMode)!) - ); + // only the spelling hints which change the rendering are needed + const accidentalMode = ModelUtils.simplifyAccidentalMode( + note.beat.voice.bar.keySignature, + note.displayValue, + note.accidentalMode + ); + if (accidentalMode !== NoteAccidentalMode.Default) { + Atnf.prop(properties, 'acc', Atnf.identValue(ModelUtils.reverseAccidentalModeMapping.get(accidentalMode)!)); } switch (note.ornament) { diff --git a/packages/alphatab/src/model/ModelUtils.ts b/packages/alphatab/src/model/ModelUtils.ts index 64c93147c..df3d97f03 100644 --- a/packages/alphatab/src/model/ModelUtils.ts +++ b/packages/alphatab/src/model/ModelUtils.ts @@ -1046,6 +1046,33 @@ export class ModelUtils { }; } + /** + * Returns the simplest accidental mode which results in the same rendering as the given one. + * The forced modes are only spelling hints: when the hint resolves to the spelling which + * {@link NoteAccidentalMode.Default} would choose anyhow, the hint is not needed. + * The rendering only uses the accidental mode via {@link resolveSpelling} and checks for + * {@link NoteAccidentalMode.ForceNone}. If this changes, this method has to be adapted. + * @param keySignature The key signature of the bar holding the note. + * @param noteValue The display value of the note. + * @param accidentalMode The accidental mode of the note. + */ + public static simplifyAccidentalMode( + keySignature: KeySignature, + noteValue: number, + accidentalMode: NoteAccidentalMode + ): NoteAccidentalMode { + if (accidentalMode === NoteAccidentalMode.Default || accidentalMode === NoteAccidentalMode.ForceNone) { + return accidentalMode; + } + + const forced = ModelUtils.resolveSpelling(keySignature, noteValue, accidentalMode); + const preferred = ModelUtils.resolveSpelling(keySignature, noteValue, NoteAccidentalMode.Default); + if (forced.degree === preferred.degree && forced.accidentalOffset === preferred.accidentalOffset) { + return NoteAccidentalMode.Default; + } + return accidentalMode; + } + public static computeAccidental( keySignature: KeySignature, accidentalMode: NoteAccidentalMode, diff --git a/packages/alphatab/test-data/exporter/notation-legend-formatted.atex b/packages/alphatab/test-data/exporter/notation-legend-formatted.atex index 0acfdbd6f..e1bc7785b 100644 --- a/packages/alphatab/test-data/exporter/notation-legend-formatted.atex +++ b/packages/alphatab/test-data/exporter/notation-legend-formatted.atex @@ -144,17 +144,17 @@ 7.3{be (prebendbend 0 4 15 6)}.1{txt "grad." beam Down} | // Bar 16 / Voice 1 contents - (7.2{be (bend 0 0 14.399999999999999 2) acc #} 7.3{be (bend 0 0 15 4)}).1{beam Down} + (7.2{be (bend 0 0 14.399999999999999 2)} 7.3{be (bend 0 0 15 4)}).1{beam Down} | // Bar 17 / Voice 1 contents - (7.2{be (bend 0 0 14.399999999999999 2) acc #} 7.3{be (bend 0 0 15 4)}).4{beam Down} + (7.2{be (bend 0 0 14.399999999999999 2)} 7.3{be (bend 0 0 15 4)}).4{beam Down} (7.2{be (release 0 2 15 0) t} 7.3{be (release 0 4 15 0) t}).4{beam Down} - (7.3{t} 7.2{t acc #}).2{beam Down} + (7.3{t} 7.2{t}).2{beam Down} | // Bar 18 / Voice 1 contents - (7.2{be (bend 0 0 14.399999999999999 2) acc #} 7.3{be (bend 0 0 15 4)}).8{gr onbeat beam Up} + (7.2{be (bend 0 0 14.399999999999999 2)} 7.3{be (bend 0 0 15 4)}).8{gr onbeat beam Up} (7.2{be (release 0 2 15 0) t} 7.3{be (release 0 4 15 0) t}).4{beam Down} - (7.3{t} 7.2{t acc #}).2{d beam Down} + (7.3{t} 7.2{t}).2{d beam Down} | // Bar 19 / Voice 1 contents (10.2 12.3{be (bend 0 0 15 4)}).1{beam Down} @@ -315,20 +315,20 @@ // Bar 51 / Voice 1 contents 5.3{h}.8{beam Down} 7.3{sl}.8{beam Down} - 8.3{sl acc #}.8{beam Down} + 8.3{sl}.8{beam Down} 7.3{h}.8{beam Down} 5.3{sl}.8{beam Down} - 3.3{h acc #}.8{beam Down} + 3.3{h}.8{beam Down} 5.3{h}.8{beam Down} - 3.3{acc #}.8{beam Down} + 3.3.8{beam Down} | // Bar 52 / Voice 1 contents 5.3{h}.8{beam Down} - 6.3{h acc #}.8{beam Down} + 6.3{h}.8{beam Down} 7.3{h}.8{beam Down} - 8.3{h acc #}.8{beam Down} + 8.3{h}.8{beam Down} 7.3{h}.8{beam Down} - 6.3{h acc #}.8{beam Down} + 6.3{h}.8{beam Down} 5.3.4{beam Down} | // Bar 53 / Voice 1 contents @@ -377,7 +377,7 @@ // Bar 58 / Voice 1 contents 7.5.4{tp 3 beam Up} 8.5.4{tp 3 beam Up} - 9.5{acc #}.2{tp 3 beam Up} + 9.5.2{tp 3 beam Up} | // Bar 59 / Voice 1 contents 7.3{tr (9 16)}.1{beam Down} @@ -393,7 +393,7 @@ 7.5{x}.8{beam Up} | // Bar 61 / Voice 1 contents - (7.5{psd x} 6.4{psd x acc #}).1{beam Up} + (7.5{psd x} 6.4{psd x}).1{beam Up} | // Bar 62 / Voice 1 contents 12.2{nh}.4{beam Down} @@ -409,13 +409,13 @@ | // Bar 64 / Voice 1 contents 5.2{ph 12}.4{beam Down} - 7.2{ph 12 acc #}.4{beam Down} + 7.2{ph 12}.4{beam Down} 8.2.4{beam Down} 10.2{ph 12}.4{beam Down} | // Bar 65 / Voice 1 contents 5.2{th 12}.4{beam Down} - 7.2{th 12 acc #}.4{beam Down} + 7.2{th 12}.4{beam Down} 8.2.4{beam Down} 10.2{th 12}.4{beam Down} | @@ -430,10 +430,10 @@ | // Bar 69 / Voice 1 contents 5.6{h}.16{tu (6 4) beam Up} - 9.6{h acc #}.16{tu (6 4) beam Up} + 9.6{h}.16{tu (6 4) beam Up} 12.6{h}.16{tt tu (6 4) beam Up} 7.4.16{tu (6 4) beam Up} - 9.6{h acc #}.16{tu (6 4) beam Up} + 9.6{h}.16{tu (6 4) beam Up} 12.6.16{tt tu (6 4) beam Up} r.2{d beam Up} | @@ -453,18 +453,18 @@ \simile none // Bar 73 / Voice 1 contents 1.1.8{dy mf beam Up} - 2.1{acc #}.8{beam Up} + 2.1.8{beam Up} 3.1.8{beam Up} - 4.1{acc #}.8{beam Up} + 4.1.8{beam Up} 3.1.4{beam Up} 1.1.8{beam Up} - 4.1{acc #}.8{beam Up} + 4.1.8{beam Up} | // Bar 74 / Voice 1 contents - (0.6 2.5 4.4{acc #} 0.3 0.2 0.1).1{ad 0 beam Up} + (0.6 2.5 4.4 0.3 0.2 0.1).1{ad 0 beam Up} | // Bar 75 / Voice 1 contents - (0.6 2.5 4.4{acc #} 0.3 0.2 0.1).1{au 0 beam Up} + (0.6 2.5 4.4 0.3 0.2 0.1).1{au 0 beam Up} | // Masterbar 76 Metadata \tf triplet8th @@ -472,23 +472,23 @@ 0.6.8{tu (3 2) beam Up} 0.6.8{tu (3 2) beam Up} 0.6.8{tu (3 2) beam Up} - 4.6{acc #}.8{beam Up} - 4.6{acc #}.8{beam Up} + 4.6.8{beam Up} + 4.6.8{beam Up} 5.6.8{beam Up} 5.6.8{beam Up} - 1.5{acc #}.8{tu (3 2) beam Up} - 1.5{acc #}.8{tu (3 2) beam Up} - 1.5{acc #}.8{tu (3 2) beam Up} + 1.5.8{tu (3 2) beam Up} + 1.5.8{tu (3 2) beam Up} + 1.5.8{tu (3 2) beam Up} | // Bar 77 / Voice 1 contents 2.5.8{beam Up} 2.5.8{beam Up} 0.5.8{beam Up} 0.5.8{beam Up} - 4.6{acc #}.8{beam Up} - 4.6{acc #}.8{beam Up} - 2.6{acc #}.8{beam Up} - 2.6{acc #}.8{beam Up} + 4.6.8{beam Up} + 4.6.8{beam Up} + 2.6.8{beam Up} + 2.6.8{beam Up} | // Bar 78 / Voice 1 contents 0.6.1{fermata (medium 0.5) beam Up} @@ -496,14 +496,14 @@ // Masterbar 79 Metadata \tf none // Bar 79 / Voice 1 contents - 21.1{acc #}.8{ot 8va beam Down} - 21.1{acc #}.8{ot 8va beam Down} - 21.1{acc #}.8{ot 8va beam Down} - 21.1{acc #}.8{ot 8va beam Down} - 21.1{acc #}.8{beam Down} - 21.1{acc #}.8{ot 8va beam Down} - 21.1{acc #}.8{beam Down} - 21.1{acc #}.8{ot 8va beam Down} + 21.1.8{ot 8va beam Down} + 21.1.8{ot 8va beam Down} + 21.1.8{ot 8va beam Down} + 21.1.8{ot 8va beam Down} + 21.1.8{beam Down} + 21.1.8{ot 8va beam Down} + 21.1.8{beam Down} + 21.1.8{ot 8va beam Down} | // Bar 80 / Voice 1 contents 24.1.1{ot 15ma beam Down} @@ -557,29 +557,29 @@ (3.1 3.2 0.3 5.4 5.5 3.6{lf 1}).1{beam Up} | // Bar 86 / Voice 1 contents - (0.5 2.4 2.3 2.2{acc #} 0.1).8{beam Up} - (0.5 2.4 2.3 2.2{acc #} 0.1).8{slashed beam Up} - (0.5 2.4 2.3 2.2{acc #} 0.1).4{slashed beam Up} - (0.5 2.4 2.3 2.2{acc #} 0.1).4{slashed beam Up} - (0.5 2.4 2.3 2.2{acc #} 0.1).8{slashed beam Up} - (0.5 2.4 2.3 2.2{acc #} 0.1).8{slashed beam Up} + (0.5 2.4 2.3 2.2 0.1).8{beam Up} + (0.5 2.4 2.3 2.2 0.1).8{slashed beam Up} + (0.5 2.4 2.3 2.2 0.1).4{slashed beam Up} + (0.5 2.4 2.3 2.2 0.1).4{slashed beam Up} + (0.5 2.4 2.3 2.2 0.1).8{slashed beam Up} + (0.5 2.4 2.3 2.2 0.1).8{slashed beam Up} | // Bar 87 / Voice 1 contents 5.5.8{txt "V." beam Up} - 6.5{acc #}.8{beam Up} + 6.5.8{beam Up} 8.5.8{beam Up} 10.5.8{txt "X." beam Up} 12.5.4{beam Up} - 13.5{acc #}.4{beam Up} + 13.5.4{beam Up} | // Bar 88 / Voice 1 contents - (3.5 2.4 3.3{acc #} 3.2 3.1).4{ch "C9" beam Up} + (3.5 2.4 3.3 3.2 3.1).4{ch "C9" beam Up} (3.5 2.4 0.3 1.2 0.1).8{ch "C" beam Up} (3.5 2.4 0.3 1.2 0.1).8{beam Up} (3.6 2.5 0.4 0.3 0.2 3.1).8{ch "G" beam Up} (3.6 2.5 0.4 0.3 0.2 3.1).8{beam Up} - (1.6 3.5 3.4 1.3{acc #} 3.2 1.1).8{ch "Fm6" beam Up} - (1.6 3.5 3.4 1.3{acc #} 3.2 1.1).8{beam Up} + (1.6 3.5 3.4 1.3 3.2 1.1).8{ch "Fm6" beam Up} + (1.6 3.5 3.4 1.3 3.2 1.1).8{beam Up} | // Bar 89 / Voice 1 contents (3.5 5.4 3.3{acc b} 6.2 3.1).4{ch "C7sus4" beam Up} @@ -588,22 +588,22 @@ | // Bar 90 / Voice 1 contents 5.5.8{beam Up} - 6.5{acc #}.8{beam Up} + 6.5.8{beam Up} 8.5.8{beam Up} 10.5.8{beam Up} 12.5.4{beam Up} - 13.5{acc #}.4{beam Up} + 13.5.4{beam Up} | // Bar 91 / Voice 1 contents 5.5.8{waho beam Up} - 6.5{acc #}.8{wahc beam Up} + 6.5.8{wahc beam Up} 8.5.8{waho beam Up} 10.5.8{wahc beam Up} 12.5.4{waho beam Up} - 13.5{acc #}.4{wahc beam Up} + 13.5.4{wahc beam Up} | // Bar 92 / Voice 1 contents - 4.4{acc #}.8{dy ppp beam Up} + 4.4.8{dy ppp beam Up} 5.4.8{dy pp beam Up} 5.4.8{dy p beam Up} 5.4.8{dy mp beam Up} @@ -630,38 +630,38 @@ 8.2.32{sd beam Down} 8.1.32{sd beam Down} 7.1.32{su beam Down} - 7.2{acc #}.32{su beam Down} - 8.3{acc #}.32{su beam Down} + 7.2.32{su beam Down} + 8.3.32{su beam Down} 9.4.32{su beam Down} 9.4.32{sd beam Down} - 8.3{acc #}.32{sd beam Down} - 7.2{acc #}.32{sd beam Down} + 8.3.32{sd beam Down} + 7.2.32{sd beam Down} 7.1.32{sd beam Down} - 6.1{acc #}.32{su beam Down} + 6.1.32{su beam Down} 6.2.32{su beam Down} 7.3.32{su beam Down} - 8.4{acc #}.32{su beam Down} - 8.4{acc #}.32{sd beam Down} + 8.4.32{su beam Down} + 8.4.32{sd beam Down} 7.3.32{sd beam Down} 6.2.32{sd beam Down} - 6.1{acc #}.32{sd beam Down} + 6.1.32{sd beam Down} | // Bar 94 / Voice 1 contents 5.5{lf 2}.8{dy mf beam Up} - 6.5{lf 3 acc #}.8{beam Up} + 6.5{lf 3}.8{beam Up} 8.5{lf 5}.8{beam Up} 10.5{lf 2}.8{beam Up} 12.5{lf 4}.4{beam Up} - 13.5{lf 5 acc #}.4{beam Up} + 13.5{lf 5}.4{beam Up} | // Bar 95 / Voice 1 contents 1.1{lf 2}.8{beam Up} - 2.1{lf 3 acc #}.8{beam Up} + 2.1{lf 3}.8{beam Up} 3.1{lf 3}.8{beam Up} - 4.1{lf 5 acc #}.8{beam Up} + 4.1{lf 5}.8{beam Up} 3.1{lf 3}.4{beam Up} 1.1{lf 2}.8{beam Up} - 4.1{lf 5 acc #}.8{beam Up} + 4.1{lf 5}.8{beam Up} | // Bar 96 / Voice 1 contents (5.2 5.3 5.4).4{tbe (dive default 0 0 45 -4) beam Down} @@ -720,13 +720,13 @@ | // Bar 106 / Voice 1 contents 7.3.4{tbe (dip default 0 0 7.199999999999999 -4 7.8 -4 13.799999999999999 0) beam Down} - 8.3{acc #}.4{tbe (dip default 0 0 5.3999999999999995 -4 6 -4 12 0) beam Down} + 8.3.4{tbe (dip default 0 0 5.3999999999999995 -4 6 -4 12 0) beam Down} 10.3.4{tbe (dip default 0 0 6.6 -4 7.199999999999999 -4 13.799999999999999 0) beam Down} 12.3.4{tbe (dip default 0 0 15 -4 30 0) beam Down} | // Bar 107 / Voice 1 contents 7.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} - 8.3{acc #}.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 8.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} 10.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} 12.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} r.2{beam Down} @@ -755,19 +755,19 @@ 0.1{lr}.8{beam Up} | // Bar 112 / Voice 1 contents - (1.4{acc #} 3.3{acc #} 4.2{acc #}).4{beam Up} + (1.4 3.3 4.2).4{beam Up} 3.1{lr}.16{beam Down} - 4.2{lr acc #}.16{beam Down} - 3.3{lr acc #}.16{beam Down} - 4.2{lr acc #}.16{beam Down} + 4.2{lr}.16{beam Down} + 3.3{lr}.16{beam Down} + 4.2{lr}.16{beam Down} 3.1{lr}.16{beam Down} - 4.2{lr acc #}.16{beam Down} - 3.3{lr acc #}.16{beam Down} - 4.2{lr acc #}.16{beam Down} + 4.2{lr}.16{beam Down} + 3.3{lr}.16{beam Down} + 4.2{lr}.16{beam Down} 3.1{lr}.16{beam Down} - 4.2{lr acc #}.16{beam Down} - 3.3{lr acc #}.16{beam Down} - 4.2{lr acc #}.16{beam Down} + 4.2{lr}.16{beam Down} + 3.3{lr}.16{beam Down} + 4.2{lr}.16{beam Down} | // Bar 113 / Voice 1 contents r.1{beam Down} @@ -783,19 +783,19 @@ (0.1{lr} 0.6{t} 0.3{t}).8{beam Up} | // Bar 115 / Voice 1 contents - (1.4{acc #} 3.3{acc #} 4.2{acc #}).4{beam Up} + (1.4 3.3 4.2).4{beam Up} 3.1{lr}.16{beam Down} - (4.2{lr acc #} 3.1{t}).16{beam Down} - (3.3{lr acc #} 3.1{t} 4.2{t acc #}).16{beam Down} - (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} - (3.1{lr} 4.2{t acc #} 3.3{t acc #}).16{beam Down} - (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} - (3.3{lr acc #} 3.1{t} 4.2{t acc #}).16{beam Down} - (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} - (3.1{lr} 4.2{t acc #} 3.3{t acc #}).16{beam Down} - (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} - (3.3{lr acc #} 3.1{t} 4.2{t acc #}).16{beam Down} - (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} + (4.2{lr} 3.1{t}).16{beam Down} + (3.3{lr} 3.1{t} 4.2{t}).16{beam Down} + (4.2{lr} 3.1{t} 3.3{t}).16{beam Down} + (3.1{lr} 4.2{t} 3.3{t}).16{beam Down} + (4.2{lr} 3.1{t} 3.3{t}).16{beam Down} + (3.3{lr} 3.1{t} 4.2{t}).16{beam Down} + (4.2{lr} 3.1{t} 3.3{t}).16{beam Down} + (3.1{lr} 4.2{t} 3.3{t}).16{beam Down} + (4.2{lr} 3.1{t} 3.3{t}).16{beam Down} + (3.3{lr} 3.1{t} 4.2{t}).16{beam Down} + (4.2{lr} 3.1{t} 3.3{t}).16{beam Down} | // Bar 116 / Voice 1 contents r.1{beam Down} @@ -812,20 +812,20 @@ (0.3{lr} 0.6{lr t} 0.2{lr t} 0.1{lr t}).8{beam Up} | // Bar 118 / Voice 1 contents - 1.5{lr acc #}.8{beam Up} - (3.4{lr} 1.5{lr t acc #}).8{beam Up} - (3.3{lr acc #} 1.5{lr t acc #} 3.4{lr t}).8{beam Up} - (1.5{lr t acc #} 3.4{lr} 3.3{lr t acc #}).8{beam Up} - (1.5{lr acc #} 3.4{lr t} 3.3{lr t acc #}).8{beam Up} - (3.4{lr} 1.5{lr t acc #} 3.3{lr t acc #}).8{beam Up} + 1.5{lr}.8{beam Up} + (3.4{lr} 1.5{lr t}).8{beam Up} + (3.3{lr} 1.5{lr t} 3.4{lr t}).8{beam Up} + (1.5{lr t} 3.4{lr} 3.3{lr t}).8{beam Up} + (1.5{lr} 3.4{lr t} 3.3{lr t}).8{beam Up} + (3.4{lr} 1.5{lr t} 3.3{lr t}).8{beam Up} | // Bar 119 / Voice 1 contents - 2.1{lr acc #}.8{beam Down} - (3.2{lr} 2.1{lr t acc #}).8{beam Down} - (2.3{lr} 2.1{lr t acc #} 3.2{lr t}).8{beam Down} - (3.2{lr} 2.1{lr t acc #} 2.3{lr t}).8{beam Down} - (2.1{lr acc #} 3.2{lr t} 2.3{lr t}).8{beam Down} - (3.2{lr} 2.1{lr t acc #} 2.3{lr t}).8{beam Down} + 2.1{lr}.8{beam Down} + (3.2{lr} 2.1{lr t}).8{beam Down} + (2.3{lr} 2.1{lr t} 3.2{lr t}).8{beam Down} + (3.2{lr} 2.1{lr t} 2.3{lr t}).8{beam Down} + (2.1{lr} 3.2{lr t} 2.3{lr t}).8{beam Down} + (3.2{lr} 2.1{lr t} 2.3{lr t}).8{beam Down} | // Bar 120 / Voice 1 contents r.2{d beam Down} @@ -952,7 +952,7 @@ | // Bar 24 / Voice 2 contents r.4{beam Down} - 9.2{acc #}.2{d beam Down} + 9.2.2{d beam Down} | // Bar 25 / Voice 2 contents r.4{beam Down} @@ -1065,12 +1065,12 @@ // Bar 73 Metadata \simile none // Bar 73 / Voice 2 contents - 3.3{acc #}.4{dy mf beam Down} + 3.3.4{dy mf beam Down} 4.3.4{beam Down} r.8{beam Down} 5.3.8{beam Down} - 3.3{acc #}.8{beam Down} - 1.3{acc #}.8{beam Down} + 3.3.8{beam Down} + 1.3.8{beam Down} | // Bar 74 / Voice 2 no contents | @@ -1115,11 +1115,11 @@ // Bar 94 / Voice 2 no contents | // Bar 95 / Voice 2 contents - 3.3{lf 4 acc #}.4{dy mf beam Down} + 3.3{lf 4}.4{dy mf beam Down} 4.3{lf 4}.4{beam Down} r.8{beam Down} 5.3{lf 5}.8{beam Down} - 3.3{lf 4 acc #}.4{beam Down} + 3.3{lf 4}.4{beam Down} | // Bar 96 / Voice 2 no contents | diff --git a/packages/alphatab/test-data/musicxml-samples/Binchois.png b/packages/alphatab/test-data/musicxml-samples/Binchois.png index 47ed0f9f1..39f3cfadd 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Binchois.png and b/packages/alphatab/test-data/musicxml-samples/Binchois.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png b/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png index be9f92a36..fab4fc93e 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png and b/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png b/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png index 3de78728b..4ed343c6c 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png and b/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png b/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png index 3044bd34b..ddad88851 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png and b/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png b/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png index 107ba1b1d..b41b49433 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png and b/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png b/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png index 181fe9b5e..61b612fce 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png and b/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/01d-Pitches-Microtones.png b/packages/alphatab/test-data/musicxml-testsuite/01d-Pitches-Microtones.png index 452aa63c1..ca6e01ef0 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/01d-Pitches-Microtones.png and b/packages/alphatab/test-data/musicxml-testsuite/01d-Pitches-Microtones.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png b/packages/alphatab/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png index 7e748416d..af4669ffe 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png and b/packages/alphatab/test-data/musicxml-testsuite/01f-Pitches-ParenthesizedMicrotoneAccidentals.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/13a-KeySignatures.png b/packages/alphatab/test-data/musicxml-testsuite/13a-KeySignatures.png index 2ce51b8d9..933c9cd6c 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/13a-KeySignatures.png and b/packages/alphatab/test-data/musicxml-testsuite/13a-KeySignatures.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/34b-Colors.png b/packages/alphatab/test-data/musicxml-testsuite/34b-Colors.png index 607bd0de9..9e5eb0a0f 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/34b-Colors.png and b/packages/alphatab/test-data/musicxml-testsuite/34b-Colors.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/34c-Font-Size.png b/packages/alphatab/test-data/musicxml-testsuite/34c-Font-Size.png index 58f1a6efa..7390e1528 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/34c-Font-Size.png and b/packages/alphatab/test-data/musicxml-testsuite/34c-Font-Size.png differ diff --git a/packages/alphatab/test-data/musicxml4/accidental-spelling.png b/packages/alphatab/test-data/musicxml4/accidental-spelling.png new file mode 100644 index 000000000..0dd848d1f Binary files /dev/null and b/packages/alphatab/test-data/musicxml4/accidental-spelling.png differ diff --git a/packages/alphatab/test-data/musicxml4/accidental-spelling.xml b/packages/alphatab/test-data/musicxml4/accidental-spelling.xml new file mode 100644 index 000000000..8c6e0480a --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/accidental-spelling.xml @@ -0,0 +1,133 @@ + + + + + Music + + + + + + + 1 + + -1 + + + + G + 2 + + + + F14 + 1 + 1 + quarter + sharp + + + F14 + 1 + 1 + quarter + + + G4 + 2 + 1 + half + + + + + + F14 + 4 + + 1 + whole + sharp + + + + + + F14 + 4 + + 1 + whole + + + + + + + + -6 + + + + B4 + 2 + 1 + half + natural + + + B4 + 2 + 1 + half + + + + + + + 0 + + + + F14 + 4 + 1 + whole + flat + + + + + + B14 + 1 + 1 + quarter + sharp + + + C5 + 1 + 1 + quarter + + + C-15 + 1 + 1 + quarter + flat + + + + 1 + 1 + quarter + + + + diff --git a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts index 6e0de8e76..f262ba4da 100644 --- a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts +++ b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { AlphaTexExporter } from '@coderline/alphatab/exporter/AlphaTexExporter'; import { AlphaTexErrorWithDiagnostics } from '@coderline/alphatab/importer/AlphaTexImporter'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; @@ -191,4 +191,19 @@ describe('AlphaTexExporterTest', () => { it('gp7-to-alphaTex', async () => { await testRoundTripEqual(`conversion/full-song.gp`); }); + + it('accidental-mode-only-when-needed', () => { + // in F major: F# needs a hint (default spelling is Gb), Gb, Bb and B natural are spelled like this by default + const score = ScoreLoader.loadAlphaTex( + '\\instrument piano \\tuning piano \\ks f . F#4.4 Gb4.4 Bb4.4 B4{acc n}.4' + ); + const exported = exportAlphaTex(score); + + expect(exported).toContain('Gb4{acc #}.4'); + const firstAccidental = exported.indexOf('acc '); + expect(exported.indexOf('acc ', firstAccidental + 1)).toBe(-1); + + const reimported = ScoreLoader.loadAlphaTex(exported); + ComparisonHelpers.alphaTexExportRoundtripEqual('accidental-mode-only-when-needed', reimported, score); + }); }); diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index 7d5ee9f17..af7ded391 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -15,6 +15,7 @@ import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Score } from '@coderline/alphatab/model/Score'; import { Settings } from '@coderline/alphatab/Settings'; import { XmlDocument } from '@coderline/alphatab/xml/XmlDocument'; +import type { XmlNode } from '@coderline/alphatab/xml/XmlNode'; import { ZipReader } from '@coderline/alphatab/zip/ZipReader'; import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { TestPlatform } from 'test/TestPlatform'; @@ -57,6 +58,8 @@ describe('Gp7ExporterTest', () => { const exported = exportGp7(expected); const actual = prepareImporterWithBytes(exported).readScore(); + ComparisonHelpers.simplifyAccidentalModes(expected); + ComparisonHelpers.simplifyAccidentalModes(actual); const expectedJson = JsonConverter.scoreToJsObject(expected); const actualJson = JsonConverter.scoreToJsObject(actual); @@ -115,7 +118,6 @@ describe('Gp7ExporterTest', () => { it('gp5-to-gp7', async () => { await testRoundTripEqual('conversion/full-song.gp5', [ - 'accidentalmode', // gets upgraded from default 'percussionarticulations', // gets added 'automations' // volume automations are not yet supported in gpif ]); @@ -123,7 +125,6 @@ describe('Gp7ExporterTest', () => { it('gp6-to-gp7', async () => { await testRoundTripEqual('conversion/full-song.gpx', [ - 'accidentalmode', // gets upgraded from default 'percussionarticulations', // gets added 'percussionarticulation' // gets added ]); @@ -149,10 +150,12 @@ describe('Gp7ExporterTest', () => { const actual = prepareImporterWithBytes(exported).readScore(); + ComparisonHelpers.simplifyAccidentalModes(expected); + ComparisonHelpers.simplifyAccidentalModes(actual); const expectedJson = JsonConverter.scoreToJsObject(expected); const actualJson = JsonConverter.scoreToJsObject(actual); - ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', ['accidentalmode']); + ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', null); }); it('alphatex-to-gp7-score-system-layout-as-text', () => { @@ -195,10 +198,12 @@ describe('Gp7ExporterTest', () => { const actual = prepareImporterWithBytes(exported).readScore(); + ComparisonHelpers.simplifyAccidentalModes(expected); + ComparisonHelpers.simplifyAccidentalModes(actual); const expectedJson = JsonConverter.scoreToJsObject(expected); const actualJson = JsonConverter.scoreToJsObject(actual); - ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', ['accidentalmode']); + ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', null); expect(actual.tracks[0].percussionArticulations.length).toBe(2); expect(actual.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).toBe(0); @@ -417,6 +422,86 @@ describe('Gp7ExporterTest', () => { expect(second).toBe(first); }); + /** + * Reads the concert and transposed pitch of all notes in score order as `step accidental octave`. + */ + function readNotePitches(gpif: string): string[][] { + const xml = new XmlDocument(); + xml.parse(gpif); + const root = xml.findChildElement('GPIF')!; + + const lookup = (section: string) => { + const elements = new Map(); + for (const e of root.findChildElement(section)!.childElements()) { + elements.set(e.getAttribute('id'), e); + } + return elements; + }; + const ids = (e: XmlNode, name: string) => { + const result: string[] = []; + for (const id of e.findChildElement(name)!.innerText.trim().split(/\s+/)) { + if (id.length > 0 && id !== '-1') { + result.push(id); + } + } + return result; + }; + const pitch = (note: XmlNode, name: string) => { + for (const property of note.findChildElement('Properties')!.childElements()) { + if (property.getAttribute('name') === name) { + const p = property.findChildElement('Pitch')!; + return `${p.findChildElement('Step')!.innerText} ${p.findChildElement('Accidental')!.innerText} ${p.findChildElement('Octave')!.innerText}`; + } + } + return ''; + }; + + const bars = lookup('Bars'); + const voices = lookup('Voices'); + const beats = lookup('Beats'); + const notes = lookup('Notes'); + const result: string[][] = []; + for (const masterBar of root.findChildElement('MasterBars')!.childElements()) { + for (const bar of ids(masterBar, 'Bars')) { + for (const voice of ids(bars.get(bar)!, 'Voices')) { + for (const beat of ids(voices.get(voice)!, 'Beats')) { + if (!beats.get(beat)!.findChildElement('Notes')) { + continue; + } + for (const note of ids(beats.get(beat)!, 'Notes')) { + const n = notes.get(note)!; + result.push([pitch(n, 'ConcertPitch'), pitch(n, 'TransposedPitch')]); + } + } + } + } + } + return result; + } + + async function testPitchSpelling(name: string) { + const data = await TestPlatform.loadFile(`test-data/${name}`); + const expected = readNotePitches(readExportedGpif(data)); + const actual = readNotePitches(readExportedGpif(exportGp7(ScoreLoader.loadScoreFromBytes(data)))); + + expect(actual.length).toBe(expected.length); + for (let i = 0; i < expected.length; i++) { + expect(actual[i][1], `TransposedPitch of note ${i}`).toBe(expected[i][1]); + // without transposition the concert pitch has the same spelling + if (expected[i][0] === expected[i][1]) { + expect(actual[i][0], `ConcertPitch of note ${i}`).toBe(expected[i][0]); + } + } + } + + it('pitch-spelling-key-signatures', async () => { + await testPitchSpelling('visual-tests/music-notation/key-signatures.gp'); + }); + + it('pitch-spelling-accidentals', async () => { + await testPitchSpelling('visual-tests/music-notation/accidentals-advanced.gp'); + }); + /** * This test generates the articulations code needed for the PercussionMapper. * To update the code there, run this test and copy the source code from the written file. diff --git a/packages/alphatab/test/importer/MusicXmlImporter.test.ts b/packages/alphatab/test/importer/MusicXmlImporter.test.ts index 24f37bf33..fd4cc3c1d 100644 --- a/packages/alphatab/test/importer/MusicXmlImporter.test.ts +++ b/packages/alphatab/test/importer/MusicXmlImporter.test.ts @@ -4,6 +4,7 @@ import { Direction } from '@coderline/alphatab/model/Direction'; import { Fingers } from '@coderline/alphatab/model/Fingers'; import type { Note } from '@coderline/alphatab/model/Note'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; import type { Score } from '@coderline/alphatab/model/Score'; import { MusicXmlImporterTestHelper } from 'test/importer/MusicXmlImporterTestHelper'; @@ -330,6 +331,46 @@ describe('MusicXmlImporterTests', () => { expect(notes[1].percussionArticulation).toBeGreaterThanOrEqual(0); }); + it('accidental-spelling', async () => { + const score = await MusicXmlImporterTestHelper.testReferenceFile('test-data/musicxml4/accidental-spelling.xml'); + + // written spelling of all notes as [bar, degree, accidental offset, octave] + const expected = [ + [0, 3, 1, 4], // F#4 + [0, 3, 1, 4], // F#4 without printed sign + [0, 4, 0, 4], // G4 + [1, 3, 1, 4], // F#4 tied + [2, 3, 1, 4], // F#4 tie destination without printed sign + [3, 6, 0, 4], // B4 in Gb major + [3, 6, 0, 4], // B4 in Gb major without printed sign + [4, 3, 1, 4], // F#4 with a contradicting flat sign + [5, 6, 1, 4], // B#4 + [5, 0, 0, 5], // C5 + [5, 0, -1, 5] // Cb5 + ]; + + let i = 0; + for (const bar of score.tracks[0].staves[0].bars) { + for (const beat of bar.voices[0].beats) { + for (const note of beat.notes) { + const spelling = ModelUtils.resolveSpelling( + bar.keySignature, + note.displayValue, + note.accidentalMode + ); + const context = `note ${i}`; + expect(bar.index, context).toBe(expected[i][0]); + expect(spelling.degree, context).toBe(expected[i][1]); + expect(spelling.accidentalOffset, context).toBe(expected[i][2]); + expect(spelling.octave, context).toBe(expected[i][3]); + i++; + } + } + } + expect(i).toBe(expected.length); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].notes[0].isTieDestination).toBe(true); + }); + it('transposed-tie', async () => { const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/transposed-tie.xml'); const notes = score.tracks[0].staves[0].bars[0].voices[0].beats.map(b => b.notes[0]); diff --git a/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap index 0054d7caf..16c292baf 100644 --- a/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap @@ -42,6 +42,7 @@ Map { "id" => 0, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "automations" => [ @@ -611,6 +612,7 @@ Map { "id" => 0, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "automations" => [ @@ -640,6 +642,7 @@ Map { "id" => 1, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "tremolopicking" => Map { @@ -661,6 +664,7 @@ Map { "id" => 2, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "tremolopicking" => Map { @@ -682,6 +686,7 @@ Map { "id" => 3, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "tremolopicking" => Map { @@ -1408,12 +1413,14 @@ Map { "id" => 0, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, Map { "__kind" => "Note", "id" => 1, "octave" => 5, "tone" => 2, + "accidentalmode" => 2, }, ], "automations" => [ @@ -1439,6 +1446,7 @@ Map { "id" => 2, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "displaystart" => 960, @@ -1467,6 +1475,7 @@ Map { "id" => 3, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "duration" => 16, @@ -1497,12 +1506,14 @@ Map { "id" => 6, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, Map { "__kind" => "Note", "id" => 7, "octave" => 5, "tone" => 2, + "accidentalmode" => 2, }, ], "displayduration" => 960, @@ -1518,6 +1529,7 @@ Map { "id" => 8, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "displaystart" => 960, @@ -1546,6 +1558,7 @@ Map { "id" => 9, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "duration" => 16, @@ -1591,6 +1604,7 @@ Map { "id" => 4, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "displaystart" => 2880, @@ -1623,6 +1637,7 @@ Map { "id" => 5, "octave" => 6, "tone" => 0, + "accidentalmode" => 2, }, ], "duration" => 2, @@ -1662,6 +1677,7 @@ Map { "id" => 10, "octave" => 5, "tone" => 0, + "accidentalmode" => 2, }, ], "displaystart" => 2880, @@ -1694,6 +1710,7 @@ Map { "id" => 11, "octave" => 6, "tone" => 0, + "accidentalmode" => 2, }, ], "duration" => 2, @@ -1759,6 +1776,7 @@ Map { "id" => 12, "octave" => 6, "tone" => 0, + "accidentalmode" => 2, }, ], "duration" => 2, @@ -1792,6 +1810,7 @@ Map { "id" => 13, "octave" => 6, "tone" => 0, + "accidentalmode" => 2, }, ], "displaystart" => 960, @@ -1824,6 +1843,7 @@ Map { "id" => 14, "octave" => 6, "tone" => 0, + "accidentalmode" => 2, }, ], "duration" => 8, @@ -1864,6 +1884,7 @@ Map { "id" => 15, "octave" => 6, "tone" => 0, + "accidentalmode" => 2, }, ], "duration" => 2, @@ -1897,6 +1918,7 @@ Map { "id" => 16, "octave" => 6, "tone" => 0, + "accidentalmode" => 2, }, ], "displaystart" => 960, @@ -1929,6 +1951,7 @@ Map { "id" => 17, "octave" => 6, "tone" => 0, + "accidentalmode" => 2, }, ], "duration" => 8, @@ -2033,6 +2056,7 @@ Map { "id" => 11, "octave" => 5, "tone" => 3, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2053,6 +2077,7 @@ Map { "id" => 12, "octave" => 4, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2094,6 +2119,7 @@ Map { "id" => 14, "octave" => 5, "tone" => 3, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2143,6 +2169,7 @@ Map { "id" => 0, "octave" => 3, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2184,6 +2211,7 @@ Map { "id" => 2, "octave" => 4, "tone" => 3, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2204,6 +2232,7 @@ Map { "id" => 3, "octave" => 4, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2224,6 +2253,7 @@ Map { "id" => 4, "octave" => 4, "tone" => 0, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2244,6 +2274,7 @@ Map { "id" => 5, "octave" => 4, "tone" => 3, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2264,6 +2295,7 @@ Map { "id" => 6, "octave" => 4, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2305,6 +2337,7 @@ Map { "id" => 8, "octave" => 4, "tone" => 3, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2325,6 +2358,7 @@ Map { "id" => 9, "octave" => 4, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2345,6 +2379,7 @@ Map { "id" => 10, "octave" => 5, "tone" => 0, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2392,6 +2427,7 @@ Map { "octave" => 3, "tone" => 8, "isstaccato" => true, + "accidentalmode" => 3, }, ], "duration" => 8, @@ -2413,6 +2449,7 @@ Map { "octave" => 3, "tone" => 0, "isstaccato" => true, + "accidentalmode" => 3, }, ], "duration" => 8, @@ -2434,6 +2471,7 @@ Map { "octave" => 3, "tone" => 8, "isstaccato" => true, + "accidentalmode" => 3, }, ], "duration" => 8, @@ -2455,6 +2493,7 @@ Map { "octave" => 3, "tone" => 0, "isstaccato" => true, + "accidentalmode" => 3, }, ], "duration" => 8, @@ -2476,6 +2515,7 @@ Map { "octave" => 3, "tone" => 8, "isstaccato" => true, + "accidentalmode" => 3, }, ], "duration" => 8, @@ -2497,6 +2537,7 @@ Map { "octave" => 3, "tone" => 0, "isstaccato" => true, + "accidentalmode" => 3, }, ], "duration" => 8, @@ -2518,6 +2559,7 @@ Map { "octave" => 3, "tone" => 8, "isstaccato" => true, + "accidentalmode" => 3, }, ], "duration" => 8, @@ -2596,6 +2638,7 @@ Map { "id" => 30, "octave" => 5, "tone" => 1, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2636,6 +2679,7 @@ Map { "id" => 33, "octave" => 5, "tone" => 1, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2656,6 +2700,7 @@ Map { "id" => 34, "octave" => 5, "tone" => 4, + "accidentalmode" => 2, }, ], "duration" => 16, @@ -2676,6 +2721,7 @@ Map { "id" => 35, "octave" => 4, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2696,6 +2742,7 @@ Map { "id" => 36, "octave" => 5, "tone" => 1, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2716,6 +2763,7 @@ Map { "id" => 37, "octave" => 5, "tone" => 4, + "accidentalmode" => 2, }, ], "duration" => 16, @@ -2763,6 +2811,7 @@ Map { "id" => 23, "octave" => 3, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2783,6 +2832,7 @@ Map { "id" => 24, "octave" => 4, "tone" => 1, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2803,6 +2853,7 @@ Map { "id" => 25, "octave" => 4, "tone" => 4, + "accidentalmode" => 2, }, ], "duration" => 16, @@ -2823,6 +2874,7 @@ Map { "id" => 26, "octave" => 4, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2843,6 +2895,7 @@ Map { "id" => 27, "octave" => 4, "tone" => 1, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2863,6 +2916,7 @@ Map { "id" => 28, "octave" => 4, "tone" => 4, + "accidentalmode" => 2, }, ], "duration" => 16, @@ -2883,6 +2937,7 @@ Map { "id" => 29, "octave" => 4, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, @@ -2914,6 +2969,7 @@ Map { "id" => 31, "octave" => 4, "tone" => 4, + "accidentalmode" => 2, }, ], "duration" => 16, @@ -2934,6 +2990,7 @@ Map { "id" => 32, "octave" => 4, "tone" => 8, + "accidentalmode" => 3, }, ], "duration" => 16, diff --git a/packages/alphatab/test/model/AccidentalResolutionTests.test.ts b/packages/alphatab/test/model/AccidentalResolutionTests.test.ts index c2d0dcfb8..72538607e 100644 --- a/packages/alphatab/test/model/AccidentalResolutionTests.test.ts +++ b/packages/alphatab/test/model/AccidentalResolutionTests.test.ts @@ -125,4 +125,59 @@ describe('AccidentalResolutionTests', () => { const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.Default, spelling, true, null); expect(accidental).toBe(AccidentalType.SharpQuarterNoteUp); }); + + it('forced modes keep every written spelling', () => { + // index: accidental offset + 2 + const modes = [ + NoteAccidentalMode.ForceDoubleFlat, + NoteAccidentalMode.ForceFlat, + NoteAccidentalMode.ForceNatural, + NoteAccidentalMode.ForceSharp, + NoteAccidentalMode.ForceDoubleSharp + ]; + for (const ks of allKeySignatures) { + for (let degree = 0; degree < 7; degree++) { + for (let offset = -2; offset <= 2; offset++) { + for (let octave = 0; octave < 9; octave++) { + const noteValue = (octave + 1) * 12 + degreeSemitones[degree] + offset; + const spelling = ModelUtils.resolveSpelling(ks, noteValue, modes[offset + 2]); + const context = `ks=${ks} degree=${degree} offset=${offset} octave=${octave}`; + expect(spelling.degree, context).toBe(degree); + expect(spelling.accidentalOffset, context).toBe(offset); + expect(spelling.octave, context).toBe(octave); + } + } + } + } + }); + + it('simplify keeps only spelling hints which change the spelling', () => { + // F# in F major: default spelling is Gb + expect(ModelUtils.simplifyAccidentalMode(KeySignature.F, 66, NoteAccidentalMode.ForceSharp)).toBe( + NoteAccidentalMode.ForceSharp + ); + // Bb in F major: default spelling is Bb + expect(ModelUtils.simplifyAccidentalMode(KeySignature.F, 70, NoteAccidentalMode.ForceFlat)).toBe( + NoteAccidentalMode.Default + ); + // B natural in Gb major: default spelling is Cb + expect(ModelUtils.simplifyAccidentalMode(KeySignature.Gb, 71, NoteAccidentalMode.ForceNatural)).toBe( + NoteAccidentalMode.ForceNatural + ); + // C natural in C major + expect(ModelUtils.simplifyAccidentalMode(KeySignature.C, 60, NoteAccidentalMode.ForceNatural)).toBe( + NoteAccidentalMode.Default + ); + // G with a sharp hint has no sharp spelling, the default spelling is used anyhow + expect(ModelUtils.simplifyAccidentalMode(KeySignature.C, 67, NoteAccidentalMode.ForceSharp)).toBe( + NoteAccidentalMode.Default + ); + // ForceNone affects the accidental, not the spelling + expect(ModelUtils.simplifyAccidentalMode(KeySignature.C, 61, NoteAccidentalMode.ForceNone)).toBe( + NoteAccidentalMode.ForceNone + ); + expect(ModelUtils.simplifyAccidentalMode(KeySignature.C, 61, NoteAccidentalMode.Default)).toBe( + NoteAccidentalMode.Default + ); + }); }); \ No newline at end of file diff --git a/packages/alphatab/test/model/ComparisonHelpers.ts b/packages/alphatab/test/model/ComparisonHelpers.ts index 0ca8b167a..ea3f069db 100644 --- a/packages/alphatab/test/model/ComparisonHelpers.ts +++ b/packages/alphatab/test/model/ComparisonHelpers.ts @@ -48,6 +48,30 @@ export class ComparisonHelpers { } } + /** + * Accidental modes are spelling hints, formats store them differently (e.g. Guitar Pro always stores the spelling). + * This reduces them to the hints which matter for the rendering to compare them across formats. + */ + public static simplifyAccidentalModes(score: Score) { + for (const track of score.tracks) { + for (const staff of track.staves) { + for (const bar of staff.bars) { + for (const voice of bar.voices) { + for (const beat of voice.beats) { + for (const note of beat.notes) { + note.accidentalMode = ModelUtils.simplifyAccidentalMode( + bar.keySignature, + note.displayValue, + note.accidentalMode + ); + } + } + } + } + } + } + } + public static alphaTexExportRoundtripPrepare(expected: Score) { // the exporter will clear out empty voices and bars, to have correct assertions we do that before ModelUtils.trimEmptyBarsAtEnd(expected); @@ -68,6 +92,8 @@ export class ComparisonHelpers { expected: Score, ignoreKeys: string[] | null = null ) { + ComparisonHelpers.simplifyAccidentalModes(expected); + ComparisonHelpers.simplifyAccidentalModes(actual); const expectedJson = JsonConverter.scoreToJsObject(expected); const actualJson = JsonConverter.scoreToJsObject(actual); @@ -97,7 +123,6 @@ export class ComparisonHelpers { // note level 'ratioposition', 'percussionarticulation', - 'accidentalmode', // we need a better way to check defaults against forced modes // for now ignore the automations as they get reorganized from beat to masterbar level // which messes with the 1:1 validation