stave 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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