Skip to content

Commit 85fb44b

Browse files
authored
Changed tuplet grouping behavior (#342)
1 parent 7550bff commit 85fb44b

File tree

4 files changed

+75
-41
lines changed

4 files changed

+75
-41
lines changed

Source/AlphaTab.Test/VisualTests/Features/EffectsAndAnnotationsTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ public void Tuplets()
114114
RunVisualTest(settings, new[] { 0 }, "TestFiles/Docs/Features/Tuplets.gp5", "TestFiles/VisualTests/Features/EffectsAndAnnotations/Tuplets.png");
115115
}
116116
[TestMethod, AsyncTestMethod]
117+
public void TupletsAdvanced()
118+
{
119+
var settings = new Settings();
120+
RunVisualTest(settings, new[] { 0, 1, 2}, "TestFiles/VisualTests/Features/EffectsAndAnnotations/TupletsAdvanced.gp", "TestFiles/VisualTests/Features/EffectsAndAnnotations/TupletsAdvanced.png");
121+
}
122+
[TestMethod, AsyncTestMethod]
117123
public void Fingering()
118124
{
119125
var settings = new Settings();

Source/AlphaTab/Model/TupletGroup.cs

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using AlphaTab.Audio;
2-
using AlphaTab.Collections;
1+
using AlphaTab.Collections;
32

43
namespace AlphaTab.Model
54
{
@@ -8,6 +7,9 @@ namespace AlphaTab.Model
87
/// </summary>
98
public class TupletGroup
109
{
10+
public int _totalDuration;
11+
private bool _isEqualLengthTuplet = true;
12+
1113
/// <summary>
1214
/// Gets or sets the list of beats contained in this group.
1315
/// </summary>
@@ -18,15 +20,6 @@ public class TupletGroup
1820
/// </summary>
1921
public Voice Voice { get; set; }
2022

21-
/// <summary>
22-
/// Gets the absolute midi tick start when this tuplet group starts.
23-
/// </summary>
24-
public int TupletStart { get; set; }
25-
26-
/// <summary>
27-
/// Gets the absolute midi tick start when this tuplet group ends.
28-
/// </summary>
29-
public int TupletEnd { get; set; }
3023

3124
/// <summary>
3225
/// Initializes a new instance of the <see cref="TupletGroup"/> class.
@@ -39,58 +32,93 @@ public TupletGroup(Voice voice)
3932
}
4033

4134
/// <summary>
42-
/// Gets a value indicating whether the tuplet group is fully filled.
35+
/// Gets a value indicating whether the tuplet group is fully filled.
4336
/// </summary>
4437
public bool IsFull
4538
{
4639
get;
4740
private set;
4841
}
4942

50-
private static readonly int FullThreshold = Duration.OneHundredTwentyEighth.ToTicks();
43+
44+
private const int HalfTicks = 1920;
45+
private const int QuarterTicks = 960;
46+
private const int EighthTicks = 480;
47+
private const int SixteenthTicks = 240;
48+
private const int ThirtySecondTicks = 120;
49+
private const int SixtyFourthTicks = 60;
50+
private const int OneHundredTwentyEighthTicks = 30;
51+
private const int TwoHundredFiftySixthTicks = 15;
52+
53+
54+
private static readonly int[] AllTicks =
55+
{
56+
HalfTicks, QuarterTicks, EighthTicks, SixteenthTicks, ThirtySecondTicks, SixtyFourthTicks,
57+
OneHundredTwentyEighthTicks, TwoHundredFiftySixthTicks
58+
};
5159

5260
internal bool Check(Beat beat)
5361
{
5462
if (Beats.Count == 0)
5563
{
56-
// calculate the range for which the tuplet will be valid. ("N notes sound like M")
57-
// via this time range check we can have also tuplets with different durations like:
58-
// /-----4:2-----\
59-
// 4 2 16 16 16 16
60-
// - the tuplet is filled fully according to the duration and displayed accordingly
61-
//
62-
// while with a pure counting we would have
63-
// /--4:2--\ 4:2 4:2
64-
// 4 2 16 16 16 16
65-
// - the first tuplet would be treated as full because there are 4 notes in it, but
66-
// duration wise it does not fill 2 quarter notes.
67-
TupletStart = beat.AbsolutePlaybackStart;
68-
var beatDuration = beat.PlaybackDuration;
69-
if (beat.GraceType == GraceType.None)
70-
{
71-
beatDuration = MidiUtils.RemoveTuplet(beatDuration, beat.TupletNumerator, beat.TupletDenominator);
72-
}
73-
74-
TupletEnd = TupletStart + beatDuration * beat.TupletDenominator;
64+
// accept first beat
65+
Beats.Add(beat);
66+
_totalDuration += beat.PlaybackDuration;
67+
return true;
7568
}
76-
else if (beat.GraceType != GraceType.None)
69+
70+
if (beat.GraceType != GraceType.None)
7771
{
78-
// grace notes do not break tuplet group, but also do not contribute to them.
72+
// grace notes do not break tuplet group, but also do not contribute to them.
7973
return true;
8074
}
81-
else if (beat.Voice != Voice || IsFull
82-
|| beat.TupletNumerator != Beats[0].TupletNumerator ||
83-
beat.TupletDenominator != Beats[0].TupletDenominator
84-
|| beat.AbsolutePlaybackStart > TupletEnd)
75+
76+
if (beat.Voice != Voice
77+
|| IsFull
78+
|| beat.TupletNumerator != Beats[0].TupletNumerator
79+
|| beat.TupletDenominator != Beats[0].TupletDenominator)
8580
{
81+
// only same tuplets are potentially accepted
8682
return false;
8783
}
8884

85+
// TBH: I do not really know how the 100% tuplet grouping of Guitar Pro might work
86+
// it sometimes has really strange rules where notes filling 3 quarters, are considered a full 3:2 tuplet
87+
88+
// in alphaTab we have now 2 rules where we consider a tuplet full:
89+
// 1. if all beats have the same length, the tuplet must contain N notes of an N:M tuplet
90+
// 2. if we have mixed beats, we check if the current set of beats, matches a N:M tuplet
91+
// by checking all potential note durations.
92+
93+
// this logic is very likely not 100% correct but for most cases the tuplets
94+
// appeared correct.
95+
96+
if (beat.PlaybackDuration != Beats[0].PlaybackDuration)
97+
{
98+
_isEqualLengthTuplet = false;
99+
}
100+
89101
Beats.Add(beat);
90-
var beatEnd = beat.AbsolutePlaybackStart + beat.PlaybackDuration;
91-
if (TupletEnd < beatEnd + FullThreshold)
102+
_totalDuration += beat.PlaybackDuration;
103+
104+
if (_isEqualLengthTuplet)
92105
{
93-
IsFull = true;
106+
if (Beats.Count == Beats[0].TupletNumerator)
107+
{
108+
IsFull = true;
109+
}
110+
}
111+
else
112+
{
113+
var factor = Beats[0].TupletNumerator / Beats[0].TupletDenominator;
114+
foreach (var potentialMatch in AllTicks)
115+
{
116+
if (_totalDuration == potentialMatch * factor)
117+
{
118+
IsFull = true;
119+
break;
120+
}
121+
}
94122
}
95123

96124
return true;
Binary file not shown.
78.8 KB
Loading

0 commit comments

Comments
 (0)