stave 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,6 @@
1
+ module Stave
2
+ module Theory
3
+ class Mode < Core::NoteCollection
4
+ end
5
+ end
6
+ 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
@@ -0,0 +1,3 @@
1
+ module Stave
2
+ VERSION = "0.1.0".freeze
3
+ 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
@@ -0,0 +1,3 @@
1
+ module Stave
2
+ VERSION: String
3
+ end
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