Skip to content

Commit bc7e236

Browse files
committed
test: add test cases for new calculations
1 parent a49bdc2 commit bc7e236

File tree

8 files changed

+340
-6
lines changed

8 files changed

+340
-6
lines changed

packages/alphatab/src/model/ModelUtils.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -968,14 +968,14 @@ export class ModelUtils {
968968
[2, AccidentalType.DoubleSharp]
969969
]);
970970

971-
private static readonly _forcedAccidentalOffsetByMode = new Map<NoteAccidentalMode, number | null>([
971+
private static readonly _forcedAccidentalOffsetByMode = new Map<NoteAccidentalMode, number>([
972972
[NoteAccidentalMode.ForceSharp, 1],
973973
[NoteAccidentalMode.ForceDoubleSharp, 2],
974974
[NoteAccidentalMode.ForceFlat, -1],
975975
[NoteAccidentalMode.ForceDoubleFlat, -2],
976976
[NoteAccidentalMode.ForceNatural, 0],
977977
[NoteAccidentalMode.ForceNone, 0],
978-
[NoteAccidentalMode.Default, null]
978+
[NoteAccidentalMode.Default, Number.NaN]
979979
]);
980980

981981
private static _buildKeySignatureAccidentalByDegree(): number[][] {
@@ -1008,10 +1008,12 @@ export class ModelUtils {
10081008
const chroma = ModelUtils.flooredDivision(noteValue, 12);
10091009

10101010
const preferred = ModelUtils._getPreferredSpellingForKeySignature(keySignature, chroma);
1011-
const desiredOffset = ModelUtils._forcedAccidentalOffsetByMode.get(accidentalMode) ?? null;
1011+
const desiredOffset = ModelUtils._forcedAccidentalOffsetByMode.has(accidentalMode)
1012+
? ModelUtils._forcedAccidentalOffsetByMode.get(accidentalMode)!
1013+
: Number.NaN;
10121014

10131015
let spelling: SpellingBase = preferred;
1014-
if (desiredOffset !== null) {
1016+
if (!Number.isNaN(desiredOffset)) {
10151017
const candidates = ModelUtils._spellingCandidates[chroma];
10161018
const exact = candidates.find(c => c.accidentalOffset === desiredOffset);
10171019
if (exact) {
@@ -1085,7 +1087,9 @@ export class ModelUtils {
10851087
}
10861088

10871089
public static accidentalOffsetToType(offset: number): AccidentalType {
1088-
return ModelUtils._accidentalOffsetToType.get(offset) ?? AccidentalType.None;
1090+
return ModelUtils._accidentalOffsetToType.has(offset)
1091+
? ModelUtils._accidentalOffsetToType.get(offset)!
1092+
: AccidentalType.None;
10891093
}
10901094

10911095
private static _getPreferredSpellingForKeySignature(keySignature: KeySignature, chroma: number): SpellingBase {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { AccidentalType } from '@coderline/alphatab/model/AccidentalType';
2+
import { KeySignature } from '@coderline/alphatab/model/KeySignature';
3+
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
4+
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
5+
import { expect } from 'chai';
6+
7+
describe('AccidentalResolutionEdgeTests', () => {
8+
it('spells B# in C# major for pitch C natural', () => {
9+
const ks = KeySignature.CSharp;
10+
const noteValue = 60; // C4
11+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.Default);
12+
expect(spelling.degree).to.equal(6); // B
13+
expect(spelling.accidentalOffset).to.equal(1); // B#
14+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.Default, spelling, false, null);
15+
expect(accidental).to.equal(AccidentalType.None);
16+
});
17+
18+
it('spells Fb in Cb major for pitch E natural', () => {
19+
const ks = KeySignature.Cb;
20+
const noteValue = 64; // E4
21+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.Default);
22+
expect(spelling.degree).to.equal(3); // F
23+
expect(spelling.accidentalOffset).to.equal(-1); // Fb
24+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.Default, spelling, false, null);
25+
expect(accidental).to.equal(AccidentalType.None);
26+
});
27+
28+
it('forces double sharp spelling when requested', () => {
29+
const ks = KeySignature.C;
30+
const noteValue = 62; // D
31+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.ForceDoubleSharp);
32+
expect(spelling.degree).to.equal(0); // C
33+
expect(spelling.accidentalOffset).to.equal(2); // C##
34+
const accidental = ModelUtils.computeAccidentalForSpelling(
35+
ks,
36+
NoteAccidentalMode.ForceDoubleSharp,
37+
spelling,
38+
false,
39+
null
40+
);
41+
expect(accidental).to.equal(AccidentalType.DoubleSharp);
42+
});
43+
44+
it('forces double flat spelling when requested', () => {
45+
const ks = KeySignature.C;
46+
const noteValue = 62; // D
47+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.ForceDoubleFlat);
48+
expect(spelling.degree).to.equal(2); // E
49+
expect(spelling.accidentalOffset).to.equal(-2); // Ebb
50+
const accidental = ModelUtils.computeAccidentalForSpelling(
51+
ks,
52+
NoteAccidentalMode.ForceDoubleFlat,
53+
spelling,
54+
false,
55+
null
56+
);
57+
expect(accidental).to.equal(AccidentalType.DoubleFlat);
58+
});
59+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { AccidentalType } from '@coderline/alphatab/model/AccidentalType';
2+
import { KeySignature } from '@coderline/alphatab/model/KeySignature';
3+
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
4+
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
5+
import { expect } from 'chai';
6+
7+
describe('AccidentalResolutionTests', () => {
8+
const degreeSemitones = [0, 2, 4, 5, 7, 9, 11];
9+
10+
function noteValueForDegree(keySignature: KeySignature, degree: number, octave: number): number {
11+
const ksOffset = ModelUtils.getKeySignatureAccidentalOffset(keySignature, degree);
12+
const baseSemitone = degreeSemitones[degree] + ksOffset;
13+
return (octave + 1) * 12 + baseSemitone;
14+
}
15+
16+
const allKeySignatures: KeySignature[] = [
17+
KeySignature.Cb,
18+
KeySignature.Gb,
19+
KeySignature.Db,
20+
KeySignature.Ab,
21+
KeySignature.Eb,
22+
KeySignature.Bb,
23+
KeySignature.F,
24+
KeySignature.C,
25+
KeySignature.G,
26+
KeySignature.D,
27+
KeySignature.A,
28+
KeySignature.E,
29+
KeySignature.B,
30+
KeySignature.FSharp,
31+
KeySignature.CSharp
32+
];
33+
34+
it('diatonic notes require no accidental in each key signature', () => {
35+
for (const ks of allKeySignatures) {
36+
for (let degree = 0; degree < 7; degree++) {
37+
const noteValue = noteValueForDegree(ks, degree, 4);
38+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.Default);
39+
expect(spelling.degree, `ks=${ks} degree=${degree}`).to.equal(degree);
40+
expect(spelling.accidentalOffset, `ks=${ks} degree=${degree}`).to.equal(
41+
ModelUtils.getKeySignatureAccidentalOffset(ks, degree)
42+
);
43+
44+
const accidental = ModelUtils.computeAccidentalForSpelling(
45+
ks,
46+
NoteAccidentalMode.Default,
47+
spelling,
48+
false,
49+
null
50+
);
51+
expect(accidental, `ks=${ks} degree=${degree}`).to.equal(AccidentalType.None);
52+
}
53+
}
54+
});
55+
56+
it('spells E# in F# major for pitch F natural', () => {
57+
const ks = KeySignature.FSharp;
58+
const noteValue = 65; // F natural
59+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.Default);
60+
expect(spelling.degree).to.equal(2); // E
61+
expect(spelling.accidentalOffset).to.equal(1); // E#
62+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.Default, spelling, false, null);
63+
expect(accidental).to.equal(AccidentalType.None);
64+
});
65+
66+
it('spells Cb in Cb major for pitch B natural', () => {
67+
const ks = KeySignature.Cb;
68+
const noteValue = 59; // B natural
69+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.Default);
70+
expect(spelling.degree).to.equal(0); // C
71+
expect(spelling.accidentalOffset).to.equal(-1); // Cb
72+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.Default, spelling, false, null);
73+
expect(accidental).to.equal(AccidentalType.None);
74+
});
75+
76+
it('forces flat spelling preference when requested', () => {
77+
const ks = KeySignature.C;
78+
const noteValue = 61; // C# / Db
79+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.ForceFlat);
80+
expect(spelling.degree).to.equal(1); // D
81+
expect(spelling.accidentalOffset).to.equal(-1); // Db
82+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.ForceFlat, spelling, false, null);
83+
expect(accidental).to.equal(AccidentalType.Flat);
84+
});
85+
86+
it('forces sharp spelling preference when requested', () => {
87+
const ks = KeySignature.C;
88+
const noteValue = 61; // C# / Db
89+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.ForceSharp);
90+
expect(spelling.degree).to.equal(0); // C
91+
expect(spelling.accidentalOffset).to.equal(1); // C#
92+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.ForceSharp, spelling, false, null);
93+
expect(accidental).to.equal(AccidentalType.Sharp);
94+
});
95+
96+
it('force natural displays a natural accidental when key signature would otherwise apply one', () => {
97+
const ks = KeySignature.D; // F#, C#
98+
const noteValue = 65; // F natural
99+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.ForceNatural);
100+
expect(spelling.degree).to.equal(3); // F
101+
expect(spelling.accidentalOffset).to.equal(0); // natural
102+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.ForceNatural, spelling, false, null);
103+
expect(accidental).to.equal(AccidentalType.Natural);
104+
});
105+
106+
it('force none suppresses accidentals regardless of spelling', () => {
107+
const ks = KeySignature.C;
108+
const noteValue = 61; // C#
109+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.ForceNone);
110+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.ForceNone, spelling, false, null);
111+
expect(accidental).to.equal(AccidentalType.None);
112+
});
113+
114+
it('no accidental when current accidental already matches', () => {
115+
const ks = KeySignature.C;
116+
const noteValue = 61; // C#
117+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.Default);
118+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.Default, spelling, false, 1);
119+
expect(accidental).to.equal(AccidentalType.None);
120+
});
121+
122+
it('quarter tone accidentals are chosen when quarter bend is true', () => {
123+
const ks = KeySignature.C;
124+
const noteValue = 61; // C# -> requires sharp
125+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.Default);
126+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.Default, spelling, true, null);
127+
expect(accidental).to.equal(AccidentalType.SharpQuarterNoteUp);
128+
});
129+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { KeySignature } from '@coderline/alphatab/model/KeySignature';
2+
import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType';
3+
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
4+
import { expect } from 'chai';
5+
6+
describe('KeySignatureUtilsTests', () => {
7+
it('sharp key signatures apply accidentals in F C G D A E B order', () => {
8+
const ksG = KeySignature.G; // 1 sharp -> F#
9+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksG, 3)).to.equal(1); // F
10+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksG, 0)).to.equal(0); // C
11+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksG, 6)).to.equal(0); // B
12+
13+
const ksD = KeySignature.D; // 2 sharps -> F#, C#
14+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksD, 3)).to.equal(1); // F
15+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksD, 0)).to.equal(1); // C
16+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksD, 4)).to.equal(0); // G
17+
});
18+
19+
it('flat key signatures apply accidentals in B E A D G C F order', () => {
20+
const ksF = KeySignature.F; // 1 flat -> Bb
21+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksF, 6)).to.equal(-1); // B
22+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksF, 2)).to.equal(0); // E
23+
24+
const ksBb = KeySignature.Bb; // 2 flats -> Bb, Eb
25+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksBb, 6)).to.equal(-1); // B
26+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksBb, 2)).to.equal(-1); // E
27+
expect(ModelUtils.getKeySignatureAccidentalOffset(ksBb, 5)).to.equal(0); // A
28+
});
29+
30+
it('major tonic degree matches expected scale degree', () => {
31+
expect(ModelUtils.getKeySignatureTonicDegree(KeySignature.C, KeySignatureType.Major)).to.equal(0); // C
32+
expect(ModelUtils.getKeySignatureTonicDegree(KeySignature.G, KeySignatureType.Major)).to.equal(4); // G
33+
expect(ModelUtils.getKeySignatureTonicDegree(KeySignature.Eb, KeySignatureType.Major)).to.equal(2); // Eb
34+
});
35+
36+
it('minor tonic degree matches expected scale degree', () => {
37+
expect(ModelUtils.getKeySignatureTonicDegree(KeySignature.C, KeySignatureType.Minor)).to.equal(5); // A
38+
expect(ModelUtils.getKeySignatureTonicDegree(KeySignature.G, KeySignatureType.Minor)).to.equal(2); // E
39+
expect(ModelUtils.getKeySignatureTonicDegree(KeySignature.Bb, KeySignatureType.Minor)).to.equal(4); // G (relative minor)
40+
});
41+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Clef } from '@coderline/alphatab/model/Clef';
2+
import { KeySignature } from '@coderline/alphatab/model/KeySignature';
3+
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
4+
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
5+
import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper';
6+
import { expect } from 'chai';
7+
8+
describe('NoteStepsTests', () => {
9+
it('calculates known steps for C4 in G2 and F4 clef', () => {
10+
const spelling = ModelUtils.resolveSpelling(KeySignature.C, 60, NoteAccidentalMode.Default); // C4
11+
const stepsG2 = AccidentalHelper.calculateNoteSteps(Clef.G2, spelling);
12+
const stepsF4 = AccidentalHelper.calculateNoteSteps(Clef.F4, spelling);
13+
14+
expect(stepsG2).to.equal(10);
15+
expect(stepsF4).to.equal(-2);
16+
});
17+
18+
it('octave shift changes steps by 7', () => {
19+
const spellingC4 = ModelUtils.resolveSpelling(KeySignature.C, 60, NoteAccidentalMode.Default); // C4
20+
const spellingC5 = ModelUtils.resolveSpelling(KeySignature.C, 72, NoteAccidentalMode.Default); // C5
21+
const stepsC4 = AccidentalHelper.calculateNoteSteps(Clef.G2, spellingC4);
22+
const stepsC5 = AccidentalHelper.calculateNoteSteps(Clef.G2, spellingC5);
23+
expect(stepsC4 - stepsC5).to.equal(7);
24+
});
25+
26+
it('adjacent diatonic degrees differ by one step', () => {
27+
const spellingC4 = ModelUtils.resolveSpelling(KeySignature.C, 60, NoteAccidentalMode.Default); // C4
28+
const spellingD4 = ModelUtils.resolveSpelling(KeySignature.C, 62, NoteAccidentalMode.Default); // D4
29+
const stepsC4 = AccidentalHelper.calculateNoteSteps(Clef.G2, spellingC4);
30+
const stepsD4 = AccidentalHelper.calculateNoteSteps(Clef.G2, spellingD4);
31+
expect(stepsC4 - stepsD4).to.equal(1);
32+
});
33+
34+
it('same pitch with different spelling yields different steps', () => {
35+
const noteValue = 61; // C# / Db
36+
const spellingSharp = ModelUtils.resolveSpelling(KeySignature.C, noteValue, NoteAccidentalMode.ForceSharp);
37+
const spellingFlat = ModelUtils.resolveSpelling(KeySignature.C, noteValue, NoteAccidentalMode.ForceFlat);
38+
39+
const stepsSharp = AccidentalHelper.calculateNoteSteps(Clef.G2, spellingSharp);
40+
const stepsFlat = AccidentalHelper.calculateNoteSteps(Clef.G2, spellingFlat);
41+
42+
expect(stepsSharp - stepsFlat).to.equal(1); // C# (degree 0) above Db (degree 1)
43+
});
44+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Clef } from '@coderline/alphatab/model/Clef';
2+
import { KeySignature } from '@coderline/alphatab/model/KeySignature';
3+
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
4+
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
5+
import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper';
6+
import { expect } from 'chai';
7+
8+
describe('NoteStepsAdditionalTests', () => {
9+
it('same pitch yields different steps across clefs', () => {
10+
const spelling = ModelUtils.resolveSpelling(KeySignature.C, 60, NoteAccidentalMode.Default); // C4
11+
const stepsG2 = AccidentalHelper.calculateNoteSteps(Clef.G2, spelling);
12+
const stepsC4 = AccidentalHelper.calculateNoteSteps(Clef.C4, spelling);
13+
expect(stepsG2 - stepsC4).to.equal(8);
14+
});
15+
16+
it('enharmonic spelling changes steps (C# vs Db)', () => {
17+
const noteValue = 61; // C#/Db
18+
const spellingSharp = ModelUtils.resolveSpelling(KeySignature.C, noteValue, NoteAccidentalMode.ForceSharp);
19+
const spellingFlat = ModelUtils.resolveSpelling(KeySignature.C, noteValue, NoteAccidentalMode.ForceFlat);
20+
21+
const stepsSharp = AccidentalHelper.calculateNoteSteps(Clef.G2, spellingSharp);
22+
const stepsFlat = AccidentalHelper.calculateNoteSteps(Clef.G2, spellingFlat);
23+
24+
expect(stepsSharp - stepsFlat).to.equal(1);
25+
});
26+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { AccidentalType } from '@coderline/alphatab/model/AccidentalType';
2+
import { KeySignature } from '@coderline/alphatab/model/KeySignature';
3+
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
4+
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
5+
import { expect } from 'chai';
6+
7+
describe('QuarterToneAccidentalsTests', () => {
8+
it('uses natural quarter tone when no key signature offset is required', () => {
9+
const ks = KeySignature.C;
10+
const noteValue = 60; // C4
11+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.Default);
12+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.Default, spelling, true, null);
13+
expect(accidental).to.equal(AccidentalType.NaturalQuarterNoteUp);
14+
});
15+
16+
it('uses sharp quarter tone for positive offset', () => {
17+
const ks = KeySignature.C;
18+
const noteValue = 61; // C#
19+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.ForceSharp);
20+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.ForceSharp, spelling, true, null);
21+
expect(accidental).to.equal(AccidentalType.SharpQuarterNoteUp);
22+
});
23+
24+
it('uses flat quarter tone for negative offset', () => {
25+
const ks = KeySignature.C;
26+
const noteValue = 61; // Db
27+
const spelling = ModelUtils.resolveSpelling(ks, noteValue, NoteAccidentalMode.ForceFlat);
28+
const accidental = ModelUtils.computeAccidentalForSpelling(ks, NoteAccidentalMode.ForceFlat, spelling, true, null);
29+
expect(accidental).to.equal(AccidentalType.FlatQuarterNoteUp);
30+
});
31+
});

packages/transpiler/src/csharp/CSharpAstTransformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1214,7 +1214,7 @@ export default class CSharpAstTransformer {
12141214
nodeType: cs.SyntaxKind.PropertyDeclaration,
12151215
isAbstract: false,
12161216
isOverride: false,
1217-
isStatic: false,
1217+
isStatic: true,
12181218
isVirtual: false,
12191219
name: this.context.toPascalCase(d.name.getText()),
12201220
type: this.createUnresolvedTypeNode(null, d.type ?? d, type),

0 commit comments

Comments
 (0)