stave 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +71 -0
- data/LICENSE.txt +21 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/lib/stave/core/degree_collection.rb +51 -0
- data/lib/stave/core/lookup.rb +110 -0
- data/lib/stave/core/note_collection.rb +47 -0
- data/lib/stave/core/option_merger.rb +25 -0
- data/lib/stave/core/scale_harmoniser.rb +34 -0
- data/lib/stave/theory/accidental.rb +23 -0
- data/lib/stave/theory/chord.rb +15 -0
- data/lib/stave/theory/chord_inversion.rb +15 -0
- data/lib/stave/theory/chord_inversion_type.rb +32 -0
- data/lib/stave/theory/chord_type.rb +70 -0
- data/lib/stave/theory/circle.rb +22 -0
- data/lib/stave/theory/circle_type.rb +8 -0
- data/lib/stave/theory/degree.rb +117 -0
- data/lib/stave/theory/interval.rb +185 -0
- data/lib/stave/theory/key_signature.rb +41 -0
- data/lib/stave/theory/mode.rb +6 -0
- data/lib/stave/theory/mode_type.rb +19 -0
- data/lib/stave/theory/note.rb +140 -0
- data/lib/stave/theory/scale.rb +38 -0
- data/lib/stave/theory/scale_type.rb +77 -0
- data/lib/stave/version.rb +3 -0
- data/lib/stave.rb +13 -0
- data/sig/stave.rbs +3 -0
- data/stave.gemspec +29 -0
- metadata +88 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
module Stave
|
2
|
+
module Theory
|
3
|
+
class Degree < Core::Lookup
|
4
|
+
with_options position: 1 do
|
5
|
+
variant :root,
|
6
|
+
interval: Interval.perfect_unison,
|
7
|
+
accidental: Accidental.natural
|
8
|
+
|
9
|
+
variant :octave,
|
10
|
+
interval: Interval.perfect_octave,
|
11
|
+
accidental: Accidental.natural
|
12
|
+
end
|
13
|
+
|
14
|
+
with_options position: 2 do
|
15
|
+
variant :flat_two,
|
16
|
+
interval: Interval.minor_second,
|
17
|
+
accidental: Accidental.flat
|
18
|
+
|
19
|
+
variant :two,
|
20
|
+
interval: Interval.major_second,
|
21
|
+
accidental: Accidental.natural
|
22
|
+
|
23
|
+
variant :sharp_two,
|
24
|
+
interval: Interval.augmented_second,
|
25
|
+
accidental: Accidental.sharp
|
26
|
+
end
|
27
|
+
|
28
|
+
with_options position: 3 do
|
29
|
+
variant :flat_three,
|
30
|
+
interval: Interval.minor_third,
|
31
|
+
accidental: Accidental.flat
|
32
|
+
|
33
|
+
variant :three,
|
34
|
+
interval: Interval.major_third,
|
35
|
+
accidental: Accidental.natural
|
36
|
+
|
37
|
+
variant :sharp_three,
|
38
|
+
interval: Interval.augmented_third,
|
39
|
+
accidental: Accidental.sharp
|
40
|
+
end
|
41
|
+
|
42
|
+
with_options position: 4 do
|
43
|
+
variant :flat_four,
|
44
|
+
interval: Interval.diminished_fourth,
|
45
|
+
accidental: Accidental.flat
|
46
|
+
|
47
|
+
variant :four,
|
48
|
+
interval: Interval.perfect_fourth,
|
49
|
+
accidental: Accidental.natural
|
50
|
+
|
51
|
+
variant :sharp_four,
|
52
|
+
interval: Interval.augmented_fourth,
|
53
|
+
accidental: Accidental.sharp
|
54
|
+
end
|
55
|
+
|
56
|
+
with_options position: 5 do
|
57
|
+
variant :flat_five,
|
58
|
+
interval: Interval.diminished_fifth,
|
59
|
+
accidental: Accidental.flat
|
60
|
+
|
61
|
+
variant :five,
|
62
|
+
interval: Interval.perfect_fifth,
|
63
|
+
accidental: Accidental.natural
|
64
|
+
|
65
|
+
variant :sharp_five,
|
66
|
+
interval: Interval.augmented_fifth,
|
67
|
+
accidental: Accidental.sharp
|
68
|
+
end
|
69
|
+
|
70
|
+
with_options position: 6 do
|
71
|
+
variant :flat_six,
|
72
|
+
interval: Interval.minor_sixth,
|
73
|
+
accidental: Accidental.flat
|
74
|
+
|
75
|
+
variant :six,
|
76
|
+
interval: Interval.major_sixth,
|
77
|
+
accidental: Accidental.natural
|
78
|
+
|
79
|
+
variant :sharp_six,
|
80
|
+
interval: Interval.augmented_sixth,
|
81
|
+
accidental: Accidental.sharp
|
82
|
+
end
|
83
|
+
|
84
|
+
with_options position: 7 do
|
85
|
+
variant :flat_seven,
|
86
|
+
interval: Interval.minor_seventh,
|
87
|
+
accidental: Accidental.flat
|
88
|
+
|
89
|
+
variant :seven,
|
90
|
+
interval: Interval.major_seventh,
|
91
|
+
accidental: Accidental.natural
|
92
|
+
end
|
93
|
+
|
94
|
+
def symbol
|
95
|
+
return "R" if (to_i % 12).zero?
|
96
|
+
|
97
|
+
"#{accidental.symbol}#{interval.number.symbol}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_i
|
101
|
+
interval.to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
def +(other)
|
105
|
+
new_interval = interval + other.interval
|
106
|
+
|
107
|
+
Degree.find_by(interval: new_interval)
|
108
|
+
end
|
109
|
+
|
110
|
+
def -(other)
|
111
|
+
new_interval = interval - other.interval
|
112
|
+
|
113
|
+
Degree.find_by(interval: new_interval)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module Stave
|
2
|
+
module Theory
|
3
|
+
class Interval < Core::Lookup
|
4
|
+
class Quality < Core::Lookup
|
5
|
+
variant :diminished,
|
6
|
+
transform: { major: -2, perfect: -1 },
|
7
|
+
inversion: :augmented,
|
8
|
+
symbol: "d"
|
9
|
+
|
10
|
+
variant :minor,
|
11
|
+
transform: { major: -1, perfect: nil },
|
12
|
+
inversion: :major,
|
13
|
+
symbol: "m"
|
14
|
+
|
15
|
+
variant :major,
|
16
|
+
transform: { major: 0, perfect: nil },
|
17
|
+
inversion: :minor,
|
18
|
+
symbol: "M"
|
19
|
+
|
20
|
+
variant :perfect,
|
21
|
+
transform: { major: nil, perfect: 0 },
|
22
|
+
inversion: :perfect,
|
23
|
+
symbol: "P"
|
24
|
+
|
25
|
+
variant :augmented,
|
26
|
+
transform: { major: 1, perfect: 1 },
|
27
|
+
inversion: :diminished,
|
28
|
+
symbol: "A"
|
29
|
+
|
30
|
+
def invert!
|
31
|
+
Quality.new(inversion)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Number < Core::Lookup
|
36
|
+
variant :one, to_i: 1, size: 0, perfect?: true
|
37
|
+
variant :two, to_i: 2, size: 2, perfect?: false
|
38
|
+
variant :three, to_i: 3, size: 4, perfect?: false
|
39
|
+
variant :four, to_i: 4, size: 5, perfect?: true
|
40
|
+
variant :five, to_i: 5, size: 7, perfect?: true
|
41
|
+
variant :six, to_i: 6, size: 9, perfect?: false
|
42
|
+
variant :seven, to_i: 7, size: 11, perfect?: false
|
43
|
+
variant :eight, to_i: 8, size: 12, perfect?: true
|
44
|
+
variant :nine, to_i: 9, size: 14, perfect?: false
|
45
|
+
variant :eleven, to_i: 11, size: 17, perfect?: true
|
46
|
+
variant :thirteen, to_i: 13, size: 21, perfect?: false
|
47
|
+
|
48
|
+
def symbol = to_i.to_s
|
49
|
+
|
50
|
+
def compound? = size >= 12
|
51
|
+
|
52
|
+
def octave? = (size % 12).zero?
|
53
|
+
|
54
|
+
def degree = compound? ? to_i - 7 : to_i
|
55
|
+
|
56
|
+
def offset = to_i - 1
|
57
|
+
|
58
|
+
def +(other)
|
59
|
+
target_i = to_i + other.relative.offset
|
60
|
+
target_i -= 7 while target_i >= 8
|
61
|
+
|
62
|
+
Number.find_by(to_i: target_i)
|
63
|
+
end
|
64
|
+
|
65
|
+
def relative
|
66
|
+
target_i = to_i
|
67
|
+
target_i -= 7 while target_i >= 8
|
68
|
+
|
69
|
+
Number.find_by(to_i: target_i)
|
70
|
+
end
|
71
|
+
|
72
|
+
def invert!
|
73
|
+
return self if octave?
|
74
|
+
|
75
|
+
Number.find_by(degree: 9 - degree)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.between(note, other_note)
|
79
|
+
target = other_note.pitch_class.index - note.pitch_class.index + 1
|
80
|
+
target += 7 unless target.positive?
|
81
|
+
|
82
|
+
find_by(to_i: target)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
with_options number: Number.one, scope: :unisons do
|
87
|
+
variant :perfect_unison, quality: Quality.perfect
|
88
|
+
end
|
89
|
+
|
90
|
+
with_options number: Number.two, scope: :seconds do
|
91
|
+
variant :minor_second, quality: Quality.minor
|
92
|
+
variant :major_second, quality: Quality.major
|
93
|
+
variant :augmented_second, quality: Quality.augmented
|
94
|
+
end
|
95
|
+
|
96
|
+
with_options number: Number.three, scope: :thirds do
|
97
|
+
variant :diminished_third, quality: Quality.diminished
|
98
|
+
variant :minor_third, quality: Quality.minor
|
99
|
+
variant :major_third, quality: Quality.major
|
100
|
+
variant :augmented_third, quality: Quality.augmented
|
101
|
+
end
|
102
|
+
|
103
|
+
with_options number: Number.four, scope: :fourths do
|
104
|
+
variant :diminished_fourth, quality: Quality.diminished
|
105
|
+
variant :perfect_fourth, quality: Quality.perfect
|
106
|
+
variant :augmented_fourth, quality: Quality.augmented
|
107
|
+
end
|
108
|
+
|
109
|
+
with_options number: Number.five, scope: :fifths do
|
110
|
+
variant :diminished_fifth, quality: Quality.diminished
|
111
|
+
variant :perfect_fifth, quality: Quality.perfect
|
112
|
+
variant :augmented_fifth, quality: Quality.augmented
|
113
|
+
end
|
114
|
+
|
115
|
+
with_options number: Number.six, scope: :sixths do
|
116
|
+
variant :diminished_sixth, quality: Quality.diminished
|
117
|
+
variant :minor_sixth, quality: Quality.minor
|
118
|
+
variant :major_sixth, quality: Quality.major
|
119
|
+
variant :augmented_sixth, quality: Quality.augmented
|
120
|
+
end
|
121
|
+
|
122
|
+
with_options number: Number.seven, scope: :sevenths do
|
123
|
+
variant :diminished_seventh, quality: Quality.diminished
|
124
|
+
variant :minor_seventh, quality: Quality.minor
|
125
|
+
variant :major_seventh, quality: Quality.major
|
126
|
+
end
|
127
|
+
|
128
|
+
with_options number: Number.eight, scope: :octaves do
|
129
|
+
variant :perfect_octave, quality: Quality.perfect
|
130
|
+
end
|
131
|
+
|
132
|
+
with_options number: Number.nine, scope: :ninths do
|
133
|
+
variant :minor_ninth, quality: Quality.minor
|
134
|
+
variant :major_ninth, quality: Quality.major
|
135
|
+
variant :augmented_ninth, quality: Quality.augmented
|
136
|
+
end
|
137
|
+
|
138
|
+
with_options number: Number.eleven, scope: :elevenths do
|
139
|
+
variant :diminished_eleventh, quality: Quality.diminished
|
140
|
+
variant :perfect_eleventh, quality: Quality.perfect
|
141
|
+
variant :augmented_eleventh, quality: Quality.augmented
|
142
|
+
end
|
143
|
+
|
144
|
+
with_options number: Number.thirteen, scope: :thirteenths do
|
145
|
+
variant :diminished_thirteenth, quality: Quality.diminished
|
146
|
+
variant :minor_thirteenth, quality: Quality.minor
|
147
|
+
variant :major_thirteenth, quality: Quality.major
|
148
|
+
variant :augmented_thirteenth, quality: Quality.augmented
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_i
|
152
|
+
transform_key = number.perfect? ? :perfect : :major
|
153
|
+
|
154
|
+
number.size + quality.transform[transform_key]
|
155
|
+
end
|
156
|
+
|
157
|
+
def symbol
|
158
|
+
"#{quality.symbol}#{number.symbol}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def +(other)
|
162
|
+
target_number = number + other.number
|
163
|
+
target_i = (to_i + other.to_i) % 12
|
164
|
+
|
165
|
+
Interval.find_by(number: target_number, to_i: target_i)
|
166
|
+
end
|
167
|
+
|
168
|
+
def -(other)
|
169
|
+
self + other.invert!
|
170
|
+
end
|
171
|
+
|
172
|
+
def invert!
|
173
|
+
Interval.find_by(number: number.invert!, quality: quality.invert!)
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.between(note, other_note)
|
177
|
+
number = Number.between(note, other_note)
|
178
|
+
to_i = other_note.to_i - note.to_i
|
179
|
+
to_i += 12 if to_i.negative?
|
180
|
+
|
181
|
+
Interval.find_by(number:, to_i:)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Stave
|
2
|
+
module Theory
|
3
|
+
class KeySignature < Core::Lookup
|
4
|
+
class Group < Core::Lookup
|
5
|
+
variant :flats, accidental: Accidental.flat, name: "flats"
|
6
|
+
variant :natural, accidental: Accidental.natural, name: ""
|
7
|
+
variant :sharps, accidental: Accidental.sharp, name: "sharps"
|
8
|
+
end
|
9
|
+
|
10
|
+
variant :natural, group: Group.natural, flat_count: 0, sharp_count: 0
|
11
|
+
|
12
|
+
with_options group: Group.flats, sharp_count: 0 do
|
13
|
+
variant :one_flat, flat_count: 1
|
14
|
+
variant :two_flats, flat_count: 2
|
15
|
+
variant :three_flats, flat_count: 3
|
16
|
+
variant :four_flats, flat_count: 4
|
17
|
+
variant :five_flats, flat_count: 5
|
18
|
+
variant :six_flats, flat_count: 6
|
19
|
+
variant :seven_flats, flat_count: 7
|
20
|
+
end
|
21
|
+
|
22
|
+
with_options group: Group.sharps, flat_count: 0 do
|
23
|
+
variant :one_sharp, sharp_count: 1
|
24
|
+
variant :two_sharps, sharp_count: 2
|
25
|
+
variant :three_sharps, sharp_count: 3
|
26
|
+
variant :four_sharps, sharp_count: 4
|
27
|
+
variant :five_sharps, sharp_count: 5
|
28
|
+
variant :six_sharps, sharp_count: 6
|
29
|
+
variant :seven_sharps, sharp_count: 7
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.parse(scale)
|
33
|
+
accidentals = scale.uniq.map(&:accidental)
|
34
|
+
flat_count = accidentals.count(&:flat?)
|
35
|
+
sharp_count = accidentals.count(&:sharp?)
|
36
|
+
|
37
|
+
find_by(flat_count:, sharp_count:)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Stave
|
2
|
+
module Theory
|
3
|
+
class ModeType < Core::DegreeCollection
|
4
|
+
with_options scale_type: ScaleType.major do
|
5
|
+
variant :ionian, position: 1
|
6
|
+
variant :dorian, position: 2
|
7
|
+
variant :phrygian, position: 3
|
8
|
+
variant :lydian, position: 4
|
9
|
+
variant :mixolydian, position: 5
|
10
|
+
variant :aeolian, position: 6
|
11
|
+
variant :locrian, position: 7
|
12
|
+
end
|
13
|
+
|
14
|
+
def degrees
|
15
|
+
scale_type.relative_rotate(position)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Stave
|
2
|
+
module Theory
|
3
|
+
class Note < Core::Lookup
|
4
|
+
class PitchClass < Core::Lookup
|
5
|
+
variant :c, index: 0, symbol: "C", to_i: 0
|
6
|
+
variant :d, index: 1, symbol: "D", to_i: 2
|
7
|
+
variant :e, index: 2, symbol: "E", to_i: 4
|
8
|
+
variant :f, index: 3, symbol: "F", to_i: 5
|
9
|
+
variant :g, index: 4, symbol: "G", to_i: 7
|
10
|
+
variant :a, index: 5, symbol: "A", to_i: 9
|
11
|
+
variant :b, index: 6, symbol: "B", to_i: 11
|
12
|
+
|
13
|
+
def +(other)
|
14
|
+
case other
|
15
|
+
when Integer then PitchClass.find_by(index: (index + other) % 7)
|
16
|
+
else raise TypeError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def -(other)
|
21
|
+
case other
|
22
|
+
when Integer then PitchClass.find_by(index: (index - other) % 7)
|
23
|
+
else raise TypeError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
with_options pitch_class: PitchClass.a do
|
29
|
+
variant :a_double_flat, accidental: Accidental.double_flat
|
30
|
+
variant :a_flat, accidental: Accidental.flat
|
31
|
+
variant :a_natural, accidental: Accidental.natural
|
32
|
+
variant :a_sharp, accidental: Accidental.sharp
|
33
|
+
variant :a_double_sharp, accidental: Accidental.double_sharp
|
34
|
+
end
|
35
|
+
|
36
|
+
with_options pitch_class: PitchClass.b do
|
37
|
+
variant :b_double_flat, accidental: Accidental.double_flat
|
38
|
+
variant :b_flat, accidental: Accidental.flat
|
39
|
+
variant :b_natural, accidental: Accidental.natural
|
40
|
+
variant :b_sharp, accidental: Accidental.sharp
|
41
|
+
variant :b_double_sharp, accidental: Accidental.double_sharp
|
42
|
+
end
|
43
|
+
|
44
|
+
with_options pitch_class: PitchClass.c do
|
45
|
+
variant :c_double_flat, accidental: Accidental.double_flat
|
46
|
+
variant :c_flat, accidental: Accidental.flat
|
47
|
+
variant :c_natural, accidental: Accidental.natural
|
48
|
+
variant :c_sharp, accidental: Accidental.sharp
|
49
|
+
variant :c_double_sharp, accidental: Accidental.double_sharp
|
50
|
+
end
|
51
|
+
|
52
|
+
with_options pitch_class: PitchClass.d do
|
53
|
+
variant :d_double_flat, accidental: Accidental.double_flat
|
54
|
+
variant :d_flat, accidental: Accidental.flat
|
55
|
+
variant :d_natural, accidental: Accidental.natural
|
56
|
+
variant :d_sharp, accidental: Accidental.sharp
|
57
|
+
variant :d_double_sharp, accidental: Accidental.double_sharp
|
58
|
+
end
|
59
|
+
|
60
|
+
with_options pitch_class: PitchClass.e do
|
61
|
+
variant :e_double_flat, accidental: Accidental.double_flat
|
62
|
+
variant :e_flat, accidental: Accidental.flat
|
63
|
+
variant :e_natural, accidental: Accidental.natural
|
64
|
+
variant :e_sharp, accidental: Accidental.sharp
|
65
|
+
variant :e_double_sharp, accidental: Accidental.double_sharp
|
66
|
+
end
|
67
|
+
|
68
|
+
with_options pitch_class: PitchClass.f do
|
69
|
+
variant :f_double_flat, accidental: Accidental.double_flat
|
70
|
+
variant :f_flat, accidental: Accidental.flat
|
71
|
+
variant :f_natural, accidental: Accidental.natural
|
72
|
+
variant :f_sharp, accidental: Accidental.sharp
|
73
|
+
variant :f_double_sharp, accidental: Accidental.double_sharp
|
74
|
+
end
|
75
|
+
|
76
|
+
with_options pitch_class: PitchClass.g do
|
77
|
+
variant :g_double_flat, accidental: Accidental.double_flat
|
78
|
+
variant :g_flat, accidental: Accidental.flat
|
79
|
+
variant :g_natural, accidental: Accidental.natural
|
80
|
+
variant :g_sharp, accidental: Accidental.sharp
|
81
|
+
variant :g_double_sharp, accidental: Accidental.double_sharp
|
82
|
+
end
|
83
|
+
|
84
|
+
def +(other)
|
85
|
+
case other
|
86
|
+
when Interval then note_above(other)
|
87
|
+
when Integer then Note.find_by(to_i: (to_i + other) % 12)
|
88
|
+
else raise
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def -(other)
|
93
|
+
case other
|
94
|
+
when Interval then note_below(other)
|
95
|
+
when Integer then Note.find_by(to_i: (to_i - other) % 12)
|
96
|
+
else raise
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def symbol
|
101
|
+
"#{pitch_class.symbol}#{accidental.symbol}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_i
|
105
|
+
(pitch_class.to_i + accidental.transform) % 12
|
106
|
+
end
|
107
|
+
|
108
|
+
def note_above(interval)
|
109
|
+
target_pitch_class = pitch_class + interval.number.offset
|
110
|
+
target_integer = (to_i + interval.to_i) % 12
|
111
|
+
|
112
|
+
Note.find_by(pitch_class: target_pitch_class, to_i: target_integer)
|
113
|
+
end
|
114
|
+
|
115
|
+
def note_below(interval)
|
116
|
+
note_above(interval.invert!)
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.flats
|
120
|
+
where(accidental: Accidental.flat)
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.naturals
|
124
|
+
where(accidental: Accidental.natural)
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.sharps
|
128
|
+
where(accidental: Accidental.sharp)
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.single_accidental
|
132
|
+
flats + naturals + sharps
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.circle_of_fifths
|
136
|
+
Circle.new(type: CircleType.fifths, root: Note.c_natural).notes
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Stave
|
2
|
+
module Theory
|
3
|
+
class Scale < Core::NoteCollection
|
4
|
+
def key_signature
|
5
|
+
KeySignature.parse(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def triads
|
9
|
+
return unless type.hexatonic?
|
10
|
+
|
11
|
+
harmonise!(type.triad_types)
|
12
|
+
end
|
13
|
+
|
14
|
+
def sevenths
|
15
|
+
return unless type.hexatonic?
|
16
|
+
|
17
|
+
harmonise!(type.seventh_types)
|
18
|
+
end
|
19
|
+
|
20
|
+
def mode(position)
|
21
|
+
return if type.mode_types.empty?
|
22
|
+
|
23
|
+
Mode.new(
|
24
|
+
type: type.mode_type_at(position),
|
25
|
+
root: note_at(position)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def harmonise!(chord_types)
|
32
|
+
chord_types.zip(notes).map do |chord_type, note|
|
33
|
+
Chord.new(type: chord_type, root: note)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Stave
|
2
|
+
module Theory
|
3
|
+
class ScaleType < Core::DegreeCollection
|
4
|
+
variant :major, degrees: [
|
5
|
+
Degree.root,
|
6
|
+
Degree.two,
|
7
|
+
Degree.three,
|
8
|
+
Degree.four,
|
9
|
+
Degree.five,
|
10
|
+
Degree.six,
|
11
|
+
Degree.seven,
|
12
|
+
Degree.octave
|
13
|
+
]
|
14
|
+
|
15
|
+
variant :minor, degrees: [
|
16
|
+
Degree.root,
|
17
|
+
Degree.two,
|
18
|
+
Degree.flat_three,
|
19
|
+
Degree.four,
|
20
|
+
Degree.five,
|
21
|
+
Degree.flat_six,
|
22
|
+
Degree.flat_seven,
|
23
|
+
Degree.octave
|
24
|
+
]
|
25
|
+
|
26
|
+
with_options suffix: :pentatonic do
|
27
|
+
variant :major, degrees: [
|
28
|
+
Degree.root,
|
29
|
+
Degree.two,
|
30
|
+
Degree.three,
|
31
|
+
Degree.five,
|
32
|
+
Degree.six,
|
33
|
+
Degree.octave
|
34
|
+
]
|
35
|
+
|
36
|
+
variant :minor, degrees: [
|
37
|
+
Degree.root,
|
38
|
+
Degree.flat_three,
|
39
|
+
Degree.four,
|
40
|
+
Degree.five,
|
41
|
+
Degree.flat_seven,
|
42
|
+
Degree.octave
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
def pentatonic?
|
47
|
+
count == 5
|
48
|
+
end
|
49
|
+
|
50
|
+
def hexatonic?
|
51
|
+
count == 7
|
52
|
+
end
|
53
|
+
|
54
|
+
def triad_types
|
55
|
+
Core::ScaleHarmoniser
|
56
|
+
.new(scale_type: self, chord_set: ChordType::Set.triad)
|
57
|
+
.harmonise!
|
58
|
+
end
|
59
|
+
|
60
|
+
def seventh_types
|
61
|
+
Core::ScaleHarmoniser
|
62
|
+
.new(scale_type: self, chord_set: ChordType::Set.seventh)
|
63
|
+
.harmonise!
|
64
|
+
end
|
65
|
+
|
66
|
+
def mode_types
|
67
|
+
ModeType.where(scale_type: self).sort_by(&:position)
|
68
|
+
end
|
69
|
+
|
70
|
+
def mode_type_at(position)
|
71
|
+
position -= 7 while position >= 8
|
72
|
+
|
73
|
+
mode_types[position - 1]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/stave.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "zeitwerk"
|
3
|
+
|
4
|
+
module Stave
|
5
|
+
INFLECTIONS = {}.freeze
|
6
|
+
|
7
|
+
COLLAPSED_DIRS = [].freeze
|
8
|
+
|
9
|
+
Zeitwerk::Loader.for_gem(warn_on_extra_files: false).tap do |loader|
|
10
|
+
loader.inflector.inflect(INFLECTIONS)
|
11
|
+
loader.collapse(COLLAPSED_DIRS)
|
12
|
+
end.setup
|
13
|
+
end
|
data/sig/stave.rbs
ADDED
data/stave.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative "lib/stave/version"
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "stave"
|
5
|
+
spec.version = Stave::VERSION
|
6
|
+
spec.authors = ["Chris Welham"]
|
7
|
+
spec.email = ["71787007+apexatoll@users.noreply.github.com"]
|
8
|
+
|
9
|
+
spec.summary = "Music theory object library"
|
10
|
+
spec.description = "A Ruby gem that abstracts and models music theory"
|
11
|
+
spec.homepage = "https://github.com/apexatoll/stave"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = ">= 3.3.0"
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
17
|
+
|
18
|
+
spec.files = Dir.chdir(__dir__) do
|
19
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
next false if File.expand_path(f) == __FILE__
|
21
|
+
|
22
|
+
f.start_with?(*%w[bin/ spec/ .git Gemfile])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_dependency "zeitwerk"
|
29
|
+
end
|