Skip to content

Commit ef91fc2

Browse files
authored
Introduce Two-Octave Major Arpeggios, Full Diatonic Arpeggios, and Comprehensive Test Suite (#21)
* Adding two octave arpeggios * Add test_get_practice_keys * Move get_practice_keys to MusicalConstants * Add full diatonic arpeggios with test suite * Add tests to ensure exercises exist for all practice keys * Cleanup
1 parent 3c874d6 commit ef91fc2

27 files changed

+892
-75
lines changed

app/scenes/piano.tscn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
[sub_resource type="ShaderMaterial" id="ShaderMaterial_s2q7l"]
1111
shader = ExtResource("6_v0a7s")
1212
shader_parameter/top_color = Color(0.241298, 0.45667, 0.596276, 1)
13-
shader_parameter/bottom_color = Color(0.428372, 0.418785, 0.605246, 1)
13+
shader_parameter/bottom_color = Color(0.607843, 0.419608, 0.603922, 1)
1414

1515
[node name="Piano" type="Control"]
1616
layout_mode = 3

app/scripts/autoload/musical_constants.gd

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,27 @@ const KEYS_PER_OCTAVE = 12
2020
const STARTING_MIDI_NOTE = 24 # Starting at C1
2121

2222
enum MusicKey {
23-
C,
24-
G,
25-
D,
26-
A,
27-
E,
28-
B,
29-
F_SHARP,
30-
C_SHARP,
31-
F,
32-
B_FLAT,
33-
E_FLAT,
34-
A_FLAT
23+
C, # No accidentals
24+
G, # 1 sharp
25+
D, # 2 sharps
26+
A, # 3 sharps
27+
E, # 4 sharps
28+
B, # 5 sharps
29+
F_SHARP, # 6 sharps
30+
C_SHARP, # 7 sharps
31+
G_SHARP, # 8 sharps
32+
D_SHARP, # 9 sharps
33+
A_SHARP, # 10 sharps
34+
E_SHARP, # 11 sharps
35+
B_SHARP, # 12 sharps
36+
F, # 1 flat
37+
B_FLAT, # 2 flats
38+
E_FLAT, # 3 flats
39+
A_FLAT, # 4 flats
40+
D_FLAT, # 5 flats
41+
G_FLAT, # 6 flats
42+
C_FLAT, # 7 flats
43+
F_FLAT # 8 flats
3544
}
3645

3746
const MUSIC_KEY_STRINGS = {
@@ -43,8 +52,48 @@ const MUSIC_KEY_STRINGS = {
4352
MusicKey.B: "B",
4453
MusicKey.F_SHARP: "F#",
4554
MusicKey.C_SHARP: "C#",
55+
MusicKey.G_SHARP: "G#",
56+
MusicKey.D_SHARP: "D#",
57+
MusicKey.A_SHARP: "A#",
58+
MusicKey.E_SHARP: "E#",
59+
MusicKey.B_SHARP: "B#",
4660
MusicKey.F: "F",
4761
MusicKey.B_FLAT: "Bb",
4862
MusicKey.E_FLAT: "Eb",
49-
MusicKey.A_FLAT: "Ab"
50-
}
63+
MusicKey.A_FLAT: "Ab",
64+
MusicKey.D_FLAT: "Db",
65+
MusicKey.G_FLAT: "Gb",
66+
MusicKey.C_FLAT: "Cb",
67+
MusicKey.F_FLAT: "Fb",
68+
}
69+
70+
const EXCLUDED_PRACTICE_KEYS = [
71+
MusicKey.G_SHARP, # 8 sharps (theoretical key, enharmonic with A♭)
72+
MusicKey.D_SHARP, # 9 sharps (theoretical key, enharmonic with E♭)
73+
MusicKey.A_SHARP, # 10 sharps (theoretical key, enharmonic with B♭)
74+
MusicKey.E_SHARP, # 11 sharps (theoretical key, enharmonic with F)
75+
MusicKey.B_SHARP, # 12 sharps (theoretical key, enharmonic with C)
76+
MusicKey.G_FLAT, # 6 flats (enharmonic with F♯, prefer F♯ in practice)
77+
MusicKey.C_FLAT, # 7 flats (theoretical key, enharmonic with B)
78+
MusicKey.F_FLAT # 8 flats (theoretical key, enharmonic with E)
79+
]
80+
81+
## Returns an array of musical keys suitable for practice and teaching.
82+
## Excludes impractical keys that:
83+
## - Require double accidentals (more than 7 sharps)
84+
## - Have enharmonic equivalents that are more commonly used
85+
## - Would be confusing for students learning music theory
86+
## Returns:
87+
## Array[MusicKey]: Practical keys ordered by complexity (following circle of fifths)
88+
static func get_practice_keys() -> Array[MusicKey]:
89+
# Create a list of all possible key values
90+
var all_keys: Array[MusicKey] = []
91+
for key_value in range(MusicKey.size()):
92+
all_keys.append(key_value)
93+
94+
# Filter out excluded keys using the predefined constant
95+
var practice_keys: Array[MusicKey] = all_keys.filter(
96+
func(key): return not (key in EXCLUDED_PRACTICE_KEYS)
97+
)
98+
99+
return practice_keys

app/scripts/exercise_manager.gd

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ signal add_finger_indicator(note: FingeredNote, is_current: bool)
2727
@onready var minor_arpeggios = preload("res://scripts/exercises/arpeggios_minor.gd").new()
2828
@onready var diminished_arpeggios = preload("res://scripts/exercises/arpeggios_diminished.gd").new()
2929
@onready var diatonic_arpeggios = preload("res://scripts/exercises/arpeggios_diatonic.gd").new()
30+
@onready var major_arpeggios_2_octave = preload("res://scripts/exercises/arpeggios_major_2_octave.gd").new()
3031

3132
# State tracking variables
3233
var current_sequence: PracticeSequence # The current exercise sequence
@@ -54,14 +55,22 @@ func _initialize_dropdowns():
5455
exercise_type_dropdown.add_item("Minor Arpeggios")
5556
exercise_type_dropdown.add_item("Diminished Arpeggios")
5657
exercise_type_dropdown.add_item("Diatonic Arpeggios")
58+
exercise_type_dropdown.add_item("Major Arpeggios 2 Octave")
5759

5860
_update_key_dropdown()
5961

6062
func _update_key_dropdown():
6163
music_key_dropdown.clear()
62-
for key in MusicalConstants.MusicKey.values():
63-
music_key_dropdown.add_item(MusicalConstants.MUSIC_KEY_STRINGS[key])
64-
64+
var practice_keys = MusicalConstants.get_practice_keys()
65+
for practice_key in practice_keys:
66+
var key_value = int(practice_key)
67+
if MusicalConstants.MUSIC_KEY_STRINGS.has(key_value):
68+
var key_string = MusicalConstants.MUSIC_KEY_STRINGS[key_value]
69+
print("Adding key: ", key_string, " (enum value: ", key_value, ")")
70+
music_key_dropdown.add_item(key_string)
71+
else:
72+
push_warning("Missing string representation for key value: " + str(key_value))
73+
6574
func _on_exercise_type_selected(_index):
6675
_update_key_dropdown()
6776
_update_exercise()
@@ -90,6 +99,8 @@ func _update_exercise():
9099
exercise_type = PracticeSequence.ExerciseType.DIMINISHED_ARPEGGIOS
91100
elif exercise_type_str == "Diatonic Arpeggios":
92101
exercise_type = PracticeSequence.ExerciseType.DIATONIC_ARPEGGIOS
102+
elif exercise_type_str == "Major Arpeggios 2 Octave":
103+
exercise_type = PracticeSequence.ExerciseType.MAJOR_ARPEGGIOS_2_OCTAVE
93104
else:
94105
exercise_type = PracticeSequence.ExerciseType.SCALES
95106

@@ -108,7 +119,8 @@ func _update_exercise():
108119
PracticeSequence.ExerciseType.MAJOR_ARPEGGIOS: "create_major_arpeggios",
109120
PracticeSequence.ExerciseType.MINOR_ARPEGGIOS: "create_minor_arpeggios",
110121
PracticeSequence.ExerciseType.DIMINISHED_ARPEGGIOS: "create_diminished_arpeggios",
111-
PracticeSequence.ExerciseType.DIATONIC_ARPEGGIOS: "create_diatonic_arpeggios"
122+
PracticeSequence.ExerciseType.DIATONIC_ARPEGGIOS: "create_diatonic_arpeggios",
123+
PracticeSequence.ExerciseType.MAJOR_ARPEGGIOS_2_OCTAVE: "create_major_arpeggios_2_octave"
112124
}
113125

114126
if exercises.has(exercise_type):
@@ -286,3 +298,13 @@ func create_diatonic_arpeggios(music_key: MusicalConstants.MusicKey, hand: Hand)
286298
practice_sequence.add_position(exercise_position)
287299

288300
return practice_sequence
301+
302+
func create_major_arpeggios_2_octave(music_key: MusicalConstants.MusicKey, hand: Hand) -> PracticeSequence:
303+
var practice_sequence = PracticeSequence.new()
304+
practice_sequence.exercise_type = PracticeSequence.ExerciseType.MAJOR_ARPEGGIOS_2_OCTAVE
305+
306+
var exercise = major_arpeggios_2_octave.get_exercise(music_key, hand)
307+
for exercise_position in exercise:
308+
practice_sequence.add_position(exercise_position)
309+
310+
return practice_sequence

app/scripts/exercises/arpeggios_diatonic.gd

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,33 @@ var exercises_rh = {
120120
"V": MajorArpeggiosRH.e_flat_major_rh_arpeggio,
121121
"vi": MinorArpeggiosRH.f_minor_rh_arpeggio,
122122
"vii°": DiminishedArpeggiosRH.g_diminished_rh_arpeggio
123+
},
124+
MusicalConstants.MusicKey.D_FLAT: {
125+
"I": MajorArpeggiosRH.d_flat_major_rh_arpeggio,
126+
"ii": MinorArpeggiosRH.e_flat_minor_rh_arpeggio,
127+
"iii": MinorArpeggiosRH.f_minor_rh_arpeggio,
128+
"IV": MajorArpeggiosRH.g_flat_major_rh_arpeggio,
129+
"V": MajorArpeggiosRH.a_flat_major_rh_arpeggio,
130+
"vi": MinorArpeggiosRH.b_flat_minor_rh_arpeggio,
131+
"vii°": DiminishedArpeggiosRH.c_diminished_rh_arpeggio
132+
},
133+
MusicalConstants.MusicKey.G_FLAT: {
134+
"I": MajorArpeggiosRH.g_flat_major_rh_arpeggio,
135+
"ii": MinorArpeggiosRH.a_flat_minor_rh_arpeggio,
136+
"iii": MinorArpeggiosRH.b_flat_minor_rh_arpeggio,
137+
"IV": MajorArpeggiosRH.c_flat_major_rh_arpeggio,
138+
"V": MajorArpeggiosRH.d_flat_major_rh_arpeggio,
139+
"vi": MinorArpeggiosRH.e_flat_minor_rh_arpeggio,
140+
"vii°": DiminishedArpeggiosRH.f_diminished_rh_arpeggio
141+
},
142+
MusicalConstants.MusicKey.C_FLAT: {
143+
"I": MajorArpeggiosRH.c_flat_major_rh_arpeggio,
144+
"ii": MinorArpeggiosRH.d_flat_minor_rh_arpeggio,
145+
"iii": MinorArpeggiosRH.e_flat_minor_rh_arpeggio,
146+
"IV": MajorArpeggiosRH.f_flat_major_rh_arpeggio,
147+
"V": MajorArpeggiosRH.g_flat_major_rh_arpeggio,
148+
"vi": MinorArpeggiosRH.a_flat_minor_rh_arpeggio,
149+
"vii°": DiminishedArpeggiosRH.b_flat_diminished_rh_arpeggio
123150
}
124151
}
125152

@@ -231,6 +258,33 @@ var exercises_lh = {
231258
"V": MajorArpeggiosLH.e_flat_major_lh_arpeggio,
232259
"vi": MinorArpeggiosLH.f_minor_lh_arpeggio,
233260
"vii°": DiminishedArpeggiosLH.g_diminished_lh_arpeggio
261+
},
262+
MusicalConstants.MusicKey.D_FLAT: {
263+
"I": MajorArpeggiosLH.d_flat_major_lh_arpeggio,
264+
"ii": MinorArpeggiosLH.e_flat_minor_lh_arpeggio,
265+
"iii": MinorArpeggiosLH.f_minor_lh_arpeggio,
266+
"IV": MajorArpeggiosLH.g_flat_major_lh_arpeggio,
267+
"V": MajorArpeggiosLH.a_flat_major_lh_arpeggio,
268+
"vi": MinorArpeggiosLH.b_flat_minor_lh_arpeggio,
269+
"vii°": DiminishedArpeggiosLH.c_diminished_lh_arpeggio
270+
},
271+
MusicalConstants.MusicKey.G_FLAT: {
272+
"I": MajorArpeggiosLH.g_flat_major_lh_arpeggio,
273+
"ii": MinorArpeggiosLH.a_flat_minor_lh_arpeggio,
274+
"iii": MinorArpeggiosLH.b_flat_minor_lh_arpeggio,
275+
"IV": MajorArpeggiosLH.c_flat_major_lh_arpeggio,
276+
"V": MajorArpeggiosLH.d_flat_major_lh_arpeggio,
277+
"vi": MinorArpeggiosLH.e_flat_minor_lh_arpeggio,
278+
"vii°": DiminishedArpeggiosLH.f_diminished_lh_arpeggio
279+
},
280+
MusicalConstants.MusicKey.C_FLAT: {
281+
"I": MajorArpeggiosLH.c_flat_major_lh_arpeggio,
282+
"ii": MinorArpeggiosLH.d_flat_minor_lh_arpeggio,
283+
"iii": MinorArpeggiosLH.e_flat_minor_lh_arpeggio,
284+
"IV": MajorArpeggiosLH.f_flat_major_lh_arpeggio,
285+
"V": MajorArpeggiosLH.g_flat_major_lh_arpeggio,
286+
"vi": MinorArpeggiosLH.a_flat_minor_lh_arpeggio,
287+
"vii°": DiminishedArpeggiosLH.b_flat_diminished_lh_arpeggio
234288
}
235289
}
236290

app/scripts/exercises/arpeggios_diminished.gd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ var exercises = {
5555
MusicalConstants.MusicKey.A_FLAT: {
5656
Hand.RIGHT_HAND: RH_Arpeggios.a_flat_diminished_rh_arpeggio,
5757
Hand.LEFT_HAND: LH_Arpeggios.a_flat_diminished_lh_arpeggio
58+
},
59+
MusicalConstants.MusicKey.C_FLAT: {
60+
Hand.RIGHT_HAND: RH_Arpeggios.c_flat_diminished_rh_arpeggio,
61+
Hand.LEFT_HAND: LH_Arpeggios.c_flat_diminished_lh_arpeggio
62+
},
63+
MusicalConstants.MusicKey.D_FLAT: {
64+
Hand.RIGHT_HAND: RH_Arpeggios.d_flat_diminished_rh_arpeggio,
65+
Hand.LEFT_HAND: LH_Arpeggios.d_flat_diminished_lh_arpeggio
5866
}
5967
}
6068

app/scripts/exercises/arpeggios_diminished_lh.gd

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,33 @@ static var c_diminished_lh_arpeggio = [
1313
FingeredNote.new("Eb3", Finger.MIDDLE, Hand.LEFT_HAND)
1414
]
1515

16+
static var c_flat_diminished_lh_arpeggio = [
17+
FingeredNote.new("Cb3", Finger.PINKY, Hand.LEFT_HAND),
18+
FingeredNote.new("Ebb3", Finger.MIDDLE, Hand.LEFT_HAND),
19+
FingeredNote.new("Gbb3", Finger.INDEX, Hand.LEFT_HAND),
20+
FingeredNote.new("Cb4", Finger.THUMB, Hand.LEFT_HAND),
21+
FingeredNote.new("Gbb3", Finger.INDEX, Hand.LEFT_HAND),
22+
FingeredNote.new("Ebb3", Finger.MIDDLE, Hand.LEFT_HAND)
23+
]
24+
25+
static var d_flat_diminished_lh_arpeggio = [
26+
FingeredNote.new("Db3", Finger.PINKY, Hand.LEFT_HAND),
27+
FingeredNote.new("Fb3", Finger.MIDDLE, Hand.LEFT_HAND),
28+
FingeredNote.new("Abb3", Finger.INDEX, Hand.LEFT_HAND),
29+
FingeredNote.new("Db4", Finger.THUMB, Hand.LEFT_HAND),
30+
FingeredNote.new("Abb3", Finger.INDEX, Hand.LEFT_HAND),
31+
FingeredNote.new("Fb3", Finger.MIDDLE, Hand.LEFT_HAND)
32+
]
33+
34+
static var g_flat_diminished_lh_arpeggio = [
35+
FingeredNote.new("Gb3", Finger.PINKY, Hand.LEFT_HAND),
36+
FingeredNote.new("Bbb3", Finger.MIDDLE, Hand.LEFT_HAND),
37+
FingeredNote.new("Dbb4", Finger.INDEX, Hand.LEFT_HAND),
38+
FingeredNote.new("Gb4", Finger.THUMB, Hand.LEFT_HAND),
39+
FingeredNote.new("Dbb4", Finger.INDEX, Hand.LEFT_HAND),
40+
FingeredNote.new("Bbb3", Finger.MIDDLE, Hand.LEFT_HAND)
41+
]
42+
1643
static var g_diminished_lh_arpeggio = [
1744
FingeredNote.new("G3", Finger.PINKY, Hand.LEFT_HAND),
1845
FingeredNote.new("Bb3", Finger.MIDDLE, Hand.LEFT_HAND),

app/scripts/exercises/arpeggios_diminished_rh.gd

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,33 @@ static var c_diminished_rh_arpeggio = [
1313
FingeredNote.new("Eb4", Finger.INDEX, Hand.RIGHT_HAND)
1414
]
1515

16+
static var c_flat_diminished_rh_arpeggio = [
17+
FingeredNote.new("Cb4", Finger.THUMB, Hand.RIGHT_HAND),
18+
FingeredNote.new("Ebb4", Finger.INDEX, Hand.RIGHT_HAND),
19+
FingeredNote.new("Gbb4", Finger.MIDDLE, Hand.RIGHT_HAND),
20+
FingeredNote.new("Cb5", Finger.PINKY, Hand.RIGHT_HAND),
21+
FingeredNote.new("Gbb4", Finger.MIDDLE, Hand.RIGHT_HAND),
22+
FingeredNote.new("Ebb4", Finger.INDEX, Hand.RIGHT_HAND)
23+
]
24+
25+
static var d_flat_diminished_rh_arpeggio = [
26+
FingeredNote.new("Db4", Finger.THUMB, Hand.RIGHT_HAND),
27+
FingeredNote.new("Fb4", Finger.INDEX, Hand.RIGHT_HAND),
28+
FingeredNote.new("Abb4", Finger.MIDDLE, Hand.RIGHT_HAND),
29+
FingeredNote.new("Db5", Finger.PINKY, Hand.RIGHT_HAND),
30+
FingeredNote.new("Abb4", Finger.MIDDLE, Hand.RIGHT_HAND),
31+
FingeredNote.new("Fb4", Finger.INDEX, Hand.RIGHT_HAND)
32+
]
33+
34+
static var g_flat_diminished_rh_arpeggio = [
35+
FingeredNote.new("Gb4", Finger.THUMB, Hand.RIGHT_HAND),
36+
FingeredNote.new("Bbb4", Finger.INDEX, Hand.RIGHT_HAND),
37+
FingeredNote.new("Dbb5", Finger.MIDDLE, Hand.RIGHT_HAND),
38+
FingeredNote.new("Gb5", Finger.PINKY, Hand.RIGHT_HAND),
39+
FingeredNote.new("Dbb5", Finger.MIDDLE, Hand.RIGHT_HAND),
40+
FingeredNote.new("Bbb4", Finger.INDEX, Hand.RIGHT_HAND)
41+
]
42+
1643
static var g_diminished_rh_arpeggio = [
1744
FingeredNote.new("G4", Finger.THUMB, Hand.RIGHT_HAND),
1845
FingeredNote.new("Bb4", Finger.INDEX, Hand.RIGHT_HAND),

app/scripts/exercises/arpeggios_major.gd

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
class_name ArpeggiosMajor
2+
13
const Hand = preload("res://scripts/constants/hand.gd").Hand
24
const Finger = preload("res://scripts/constants/finger.gd").Finger
35

@@ -52,6 +54,18 @@ var exercises = {
5254
MusicalConstants.MusicKey.A_FLAT: {
5355
Hand.RIGHT_HAND: MajorArpeggiosRH.a_flat_major_rh_arpeggio,
5456
Hand.LEFT_HAND: MajorArpeggiosLH.a_flat_major_lh_arpeggio
57+
},
58+
MusicalConstants.MusicKey.D_FLAT: {
59+
Hand.RIGHT_HAND: MajorArpeggiosRH.d_flat_major_rh_arpeggio,
60+
Hand.LEFT_HAND: MajorArpeggiosLH.d_flat_major_lh_arpeggio
61+
},
62+
MusicalConstants.MusicKey.G_FLAT: {
63+
Hand.RIGHT_HAND: MajorArpeggiosRH.g_flat_major_rh_arpeggio,
64+
Hand.LEFT_HAND: MajorArpeggiosLH.g_flat_major_lh_arpeggio
65+
},
66+
MusicalConstants.MusicKey.C_FLAT: {
67+
Hand.RIGHT_HAND: MajorArpeggiosRH.c_flat_major_rh_arpeggio,
68+
Hand.LEFT_HAND: MajorArpeggiosLH.c_flat_major_lh_arpeggio
5569
}
5670
}
5771

0 commit comments

Comments
 (0)