head_music 0.20.0 → 0.22.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.
- checksums.yaml +5 -5
- data/.circleci/config.yml +22 -0
- data/.circleci/setup-rubygems.sh +3 -0
- data/.gitignore +1 -0
- data/.pairs +8 -0
- data/.rubocop.yml +11 -2
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +3 -6
- data/Gemfile +2 -2
- data/README.md +0 -1
- data/TODO.md +18 -0
- data/head_music.gemspec +1 -2
- data/lib/head_music/chord.rb +9 -2
- data/lib/head_music/circle.rb +1 -0
- data/lib/head_music/clef.rb +10 -5
- data/lib/head_music/consonance.rb +1 -1
- data/lib/head_music/content/composition.rb +1 -2
- data/lib/head_music/content/note.rb +2 -0
- data/lib/head_music/content/placement.rb +1 -1
- data/lib/head_music/content/rhythmic_value.rb +1 -0
- data/lib/head_music/content/voice.rb +4 -2
- data/lib/head_music/functional_interval.rb +43 -17
- data/lib/head_music/grand_staff.rb +2 -1
- data/lib/head_music/harmonic_interval.rb +1 -0
- data/lib/head_music/key_signature.rb +1 -0
- data/lib/head_music/letter_name.rb +20 -7
- data/lib/head_music/melodic_interval.rb +4 -2
- data/lib/head_music/meter.rb +1 -0
- data/lib/head_music/octave.rb +2 -0
- data/lib/head_music/pitch/enharmonic_equivalence.rb +28 -0
- data/lib/head_music/pitch/octave_equivalence.rb +24 -0
- data/lib/head_music/pitch.rb +41 -57
- data/lib/head_music/pitch_class.rb +5 -0
- data/lib/head_music/rhythmic_unit.rb +1 -0
- data/lib/head_music/scale.rb +7 -3
- data/lib/head_music/scale_degree.rb +2 -1
- data/lib/head_music/sign.rb +2 -1
- data/lib/head_music/spelling.rb +6 -2
- data/lib/head_music/style/analysis.rb +1 -0
- data/lib/head_music/style/annotation.rb +2 -2
- data/lib/head_music/style/guidelines/at_least_eight_notes.rb +1 -0
- data/lib/head_music/style/guidelines/direction_changes.rb +2 -0
- data/lib/head_music/style/guidelines/limit_octave_leaps.rb +1 -0
- data/lib/head_music/style/guidelines/mostly_conjunct.rb +1 -0
- data/lib/head_music/style/guidelines/notes_same_length.rb +2 -1
- data/lib/head_music/style/guidelines/one_to_one.rb +1 -0
- data/lib/head_music/style/guidelines/prefer_contrary_motion.rb +2 -0
- data/lib/head_music/style/guidelines/prefer_imperfect.rb +1 -0
- data/lib/head_music/style/guidelines/single_large_leaps.rb +1 -0
- data/lib/head_music/style/guidelines/start_on_perfect_consonance.rb +1 -0
- data/lib/head_music/style/guidelines/step_down_to_final_note.rb +1 -0
- data/lib/head_music/style/guidelines/step_out_of_unison.rb +1 -1
- data/lib/head_music/style/guidelines/step_up_to_final_note.rb +1 -0
- data/lib/head_music/style/mark.rb +3 -2
- data/lib/head_music/tuning.rb +6 -1
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +2 -0
- metadata +10 -4
- data/circle.yml +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c5e3afc9754608ddd1d1c10b191f356d4c200733ebfd9e9294693309d7ab53d9
|
4
|
+
data.tar.gz: f92fbe212261872b48e41d1189289f7a0b9aea7643e060a3e96ed223746f1089
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30fac39ee9b758610839343fb7e879ec5422485a83240be3b535be261fc5e31d9f6a2f66c8a34523f90bb95c9f6c3c836ba1a404d07a40634bc2963a1cb584c3
|
7
|
+
data.tar.gz: ef1240d4dcc7b9d159967a1dcad186867c2dc1f7cac8e33df295d77452bb75d0a1888d9d3a99800aeff60d7a00dca94d7a9ece30d8190769ebcacffbceccedf5
|
@@ -0,0 +1,22 @@
|
|
1
|
+
version: 2
|
2
|
+
jobs:
|
3
|
+
build:
|
4
|
+
docker:
|
5
|
+
- image: circleci/ruby:2.5.1
|
6
|
+
steps:
|
7
|
+
- checkout
|
8
|
+
- restore_cache:
|
9
|
+
keys:
|
10
|
+
- v1-gems-{{ checksum "Gemfile.lock" }}
|
11
|
+
- v1-gems-
|
12
|
+
- run:
|
13
|
+
name: Bundle Install
|
14
|
+
command: bundle check || bundle install
|
15
|
+
- save_cache:
|
16
|
+
key: v1-gems-{{ checksum "Gemfile.lock" }}
|
17
|
+
paths:
|
18
|
+
- vendor/bundle
|
19
|
+
- run:
|
20
|
+
command: bundle exec rspec
|
21
|
+
- store_test_results:
|
22
|
+
path: test_results
|
data/.gitignore
CHANGED
data/.pairs
ADDED
data/.rubocop.yml
CHANGED
@@ -8,7 +8,7 @@ Metrics/BlockLength:
|
|
8
8
|
- 'spec/**/*.rb'
|
9
9
|
|
10
10
|
Metrics/ClassLength:
|
11
|
-
Max:
|
11
|
+
Max: 155
|
12
12
|
|
13
13
|
Metrics/LineLength:
|
14
14
|
Max: 120
|
@@ -19,5 +19,14 @@ Style/SymbolArray:
|
|
19
19
|
Style/ClassAndModuleChildren:
|
20
20
|
EnforcedStyle: compact
|
21
21
|
|
22
|
-
Style/
|
22
|
+
Style/TrailingCommaInArrayLiteral:
|
23
23
|
EnforcedStyleForMultiline: consistent_comma
|
24
|
+
|
25
|
+
Style/TrailingCommaInHashLiteral:
|
26
|
+
EnforcedStyleForMultiline: consistent_comma
|
27
|
+
|
28
|
+
AllCops:
|
29
|
+
TargetRubyVersion: 2.4
|
30
|
+
|
31
|
+
Layout/EmptyLineAfterGuardClause:
|
32
|
+
Enabled: false
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.1
|
data/CODE_OF_CONDUCT.md
CHANGED
@@ -22,14 +22,11 @@ include:
|
|
22
22
|
|
23
23
|
Examples of unacceptable behavior by participants include:
|
24
24
|
|
25
|
-
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
-
advances
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
27
26
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
27
|
* Public or private harassment
|
29
|
-
* Publishing others' private information, such as a physical or electronic
|
30
|
-
|
31
|
-
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
-
professional setting
|
28
|
+
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
29
|
+
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
33
30
|
|
34
31
|
## Our Responsibilities
|
35
32
|
|
data/Gemfile
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
source 'https://rubygems.org'
|
4
4
|
|
5
|
-
ruby '2.
|
5
|
+
ruby '2.5.1'
|
6
6
|
|
7
7
|
# Specify your gem's dependencies in head_music.gemspec
|
8
8
|
gemspec
|
9
9
|
|
10
10
|
group :test do
|
11
11
|
gem 'codeclimate-test-reporter', '~> 1.0.0'
|
12
|
-
gem 'rubocop', require: false
|
12
|
+
gem 'rubocop', '>= 0.56.0', require: false
|
13
13
|
gem 'rubocop-rspec', require: false
|
14
14
|
gem 'simplecov'
|
15
15
|
end
|
data/README.md
CHANGED
data/TODO.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# TODO
|
2
|
+
|
3
|
+
## User stories
|
4
|
+
|
5
|
+
### Done
|
6
|
+
|
7
|
+
As a developer
|
8
|
+
Given a pitch
|
9
|
+
I want to be able to add a functional interval to get another pitch.
|
10
|
+
|
11
|
+
FunctionalInterval
|
12
|
+
- def above(pitch) -> pitch
|
13
|
+
FunctionalInterval
|
14
|
+
- def below(pitch) -> pitch
|
15
|
+
|
16
|
+
Pitch addition and subtraction
|
17
|
+
- define `Pitch#+`, `Pitch#-`
|
18
|
+
- use FunctionalInterval methods
|
data/head_music.gemspec
CHANGED
data/lib/head_music/chord.rb
CHANGED
@@ -6,23 +6,30 @@ class HeadMusic::Chord
|
|
6
6
|
|
7
7
|
def initialize(pitches)
|
8
8
|
raise ArgumentError if pitches.length < 3
|
9
|
+
|
9
10
|
@pitches = pitches.map { |pitch| HeadMusic::Pitch.get(pitch) }.sort
|
10
11
|
end
|
11
12
|
|
12
13
|
def consonant_triad?
|
13
|
-
return false unless three_pitches?
|
14
14
|
reduction.root_triad? || reduction.first_inversion_triad? || reduction.second_inversion_triad?
|
15
15
|
end
|
16
16
|
|
17
17
|
def root_triad?
|
18
|
+
return false unless triad?
|
19
|
+
|
18
20
|
intervals.map(&:shorthand).sort == %w[M3 m3]
|
19
21
|
end
|
20
22
|
|
23
|
+
# TODO: look up definition of first and second inversion triads. Can they be spread?
|
21
24
|
def first_inversion_triad?
|
25
|
+
return false unless triad?
|
26
|
+
|
22
27
|
invert.invert.intervals.map(&:shorthand).sort == %w[M3 m3]
|
23
28
|
end
|
24
29
|
|
25
30
|
def second_inversion_triad?
|
31
|
+
return false unless triad?
|
32
|
+
|
26
33
|
invert.intervals.map(&:shorthand).sort == %w[M3 m3]
|
27
34
|
end
|
28
35
|
|
@@ -30,7 +37,7 @@ class HeadMusic::Chord
|
|
30
37
|
@reduction ||= HeadMusic::Chord.new(reduction_pitches)
|
31
38
|
end
|
32
39
|
|
33
|
-
def
|
40
|
+
def triad?
|
34
41
|
pitches.length == 3
|
35
42
|
end
|
36
43
|
|
data/lib/head_music/circle.rb
CHANGED
data/lib/head_music/clef.rb
CHANGED
@@ -7,7 +7,7 @@ class HeadMusic::Clef
|
|
7
7
|
CLEFS = [
|
8
8
|
{ pitch: 'G4', line: 2, names: ['treble', 'G-clef'], modern: true },
|
9
9
|
{ pitch: 'G4', line: 1, names: ['French', 'French violin'] },
|
10
|
-
{ pitch: 'G3', line: 2, names: ['choral tenor', 'tenor'], modern: true },
|
10
|
+
{ pitch: 'G3', line: 2, names: ['choral tenor', 'tenor', 'tenor G-clef'], modern: true },
|
11
11
|
|
12
12
|
{ pitch: 'F3', line: 3, names: ['baritone'] },
|
13
13
|
{ pitch: 'F3', line: 4, names: ['bass', 'F-clef'], modern: true },
|
@@ -16,8 +16,8 @@ class HeadMusic::Clef
|
|
16
16
|
{ pitch: 'C4', line: 1, names: ['soprano'] },
|
17
17
|
{ pitch: 'C4', line: 2, names: ['mezzo-soprano'] },
|
18
18
|
{ pitch: 'C4', line: 3, names: ['alto', 'viola', 'counter-tenor', 'countertenor', 'C-clef'], modern: true },
|
19
|
-
{ pitch: 'C4', line: 4, names: ['tenor'], modern: true },
|
20
|
-
{ pitch: 'C4', line: 5, names: ['baritone'] },
|
19
|
+
{ pitch: 'C4', line: 4, names: ['tenor', 'tenor C-clef'], modern: true },
|
20
|
+
{ pitch: 'C4', line: 5, names: ['baritone', 'baritone C-clef'] },
|
21
21
|
|
22
22
|
{ pitch: nil, line: 3, names: %w[neutral percussion] },
|
23
23
|
].freeze
|
@@ -33,13 +33,14 @@ class HeadMusic::Clef
|
|
33
33
|
clef_data = CLEFS.detect { |clef| clef[:names].map(&:downcase).include?(name.downcase) }
|
34
34
|
@pitch = HeadMusic::Pitch.get(clef_data[:pitch])
|
35
35
|
@line = clef_data[:line]
|
36
|
+
@modern = clef_data[:modern]
|
36
37
|
end
|
37
38
|
|
38
39
|
def clef_type
|
39
40
|
"#{pitch.letter_name}-clef"
|
40
41
|
end
|
41
42
|
|
42
|
-
def
|
43
|
+
def pitch_for_line(line_number)
|
43
44
|
@line_pitches ||= {}
|
44
45
|
@line_pitches[line_number] ||= begin
|
45
46
|
steps = (line_number - line) * 2
|
@@ -47,7 +48,7 @@ class HeadMusic::Clef
|
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
|
-
def
|
51
|
+
def pitch_for_space(space_number)
|
51
52
|
@space_pitches ||= {}
|
52
53
|
@space_pitches[space_number] ||= begin
|
53
54
|
steps = (space_number - line) * 2 + 1
|
@@ -55,6 +56,10 @@ class HeadMusic::Clef
|
|
55
56
|
end
|
56
57
|
end
|
57
58
|
|
59
|
+
def modern?
|
60
|
+
@modern
|
61
|
+
end
|
62
|
+
|
58
63
|
def ==(other)
|
59
64
|
to_s == other.to_s
|
60
65
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Consonance
|
3
|
+
# Consonance describes a category or degree of harmonic pleasantness: perfect, imperfect, or dissonant
|
4
4
|
class HeadMusic::Consonance
|
5
5
|
LEVELS = %w[perfect imperfect dissonant].freeze
|
6
6
|
|
@@ -66,8 +66,7 @@ class HeadMusic::Composition
|
|
66
66
|
@name = name || 'Composition'
|
67
67
|
@key_signature = HeadMusic::KeySignature.get(key_signature) if key_signature
|
68
68
|
@key_signature ||= HeadMusic::KeySignature.default
|
69
|
-
@meter = HeadMusic::Meter.get(meter)
|
70
|
-
@meter ||= HeadMusic::Meter.default
|
69
|
+
@meter = meter ? HeadMusic::Meter.get(meter) : HeadMusic::Meter.default
|
71
70
|
end
|
72
71
|
|
73
72
|
def last_meter_change(bar_number)
|
@@ -38,11 +38,11 @@ class HeadMusic::Voice
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def highest_pitch
|
41
|
-
pitches.
|
41
|
+
pitches.max
|
42
42
|
end
|
43
43
|
|
44
44
|
def lowest_pitch
|
45
|
-
pitches.
|
45
|
+
pitches.min
|
46
46
|
end
|
47
47
|
|
48
48
|
def highest_notes
|
@@ -92,11 +92,13 @@ class HeadMusic::Voice
|
|
92
92
|
|
93
93
|
def earliest_bar_number
|
94
94
|
return 1 if notes.empty?
|
95
|
+
|
95
96
|
placements.first.position.bar_number
|
96
97
|
end
|
97
98
|
|
98
99
|
def latest_bar_number
|
99
100
|
return 1 if notes.empty?
|
101
|
+
|
100
102
|
placements.last.position.bar_number
|
101
103
|
end
|
102
104
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# A functional interval is the distance between two spelled pitches.
|
4
4
|
class HeadMusic::FunctionalInterval
|
5
5
|
include Comparable
|
6
6
|
|
@@ -61,7 +61,7 @@ class HeadMusic::FunctionalInterval
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def higher_letter
|
64
|
-
HeadMusic::Pitch.middle_c.letter_name.
|
64
|
+
HeadMusic::Pitch.middle_c.letter_name.steps_up(steps)
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
@@ -74,7 +74,7 @@ class HeadMusic::FunctionalInterval
|
|
74
74
|
end
|
75
75
|
|
76
76
|
def self.degree_quality_semitones
|
77
|
-
@
|
77
|
+
@degree_quality_semitones ||= begin
|
78
78
|
{}.tap do |degree_quality_semitones|
|
79
79
|
QUALITY_SEMITONES.each do |degree_name, qualities|
|
80
80
|
default_quality = qualities.keys.first
|
@@ -135,21 +135,20 @@ class HeadMusic::FunctionalInterval
|
|
135
135
|
@low_pitch, @high_pitch = *[pitch1, pitch2].sort
|
136
136
|
end
|
137
137
|
|
138
|
-
def
|
139
|
-
@
|
138
|
+
def number
|
139
|
+
@number ||= @low_pitch.steps_to(@high_pitch) + 1
|
140
140
|
end
|
141
141
|
|
142
|
-
def
|
143
|
-
@
|
142
|
+
def simple_number
|
143
|
+
@simple_number ||= octave_equivalent? ? 8 : (number - 1) % 7 + 1
|
144
144
|
end
|
145
145
|
|
146
|
-
|
147
|
-
|
148
|
-
simple_number + octaves * 7
|
146
|
+
def octaves
|
147
|
+
@octaves ||= number / 8
|
149
148
|
end
|
150
149
|
|
151
150
|
def simple?
|
152
|
-
|
151
|
+
number <= 8
|
153
152
|
end
|
154
153
|
|
155
154
|
def compound?
|
@@ -167,6 +166,12 @@ class HeadMusic::FunctionalInterval
|
|
167
166
|
def steps
|
168
167
|
number - 1
|
169
168
|
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def octave_equivalent?
|
173
|
+
number > 1 && ((number - 1) % 7).zero?
|
174
|
+
end
|
170
175
|
end
|
171
176
|
|
172
177
|
# Accepts a number and number of semitones and privides the naming methods.
|
@@ -183,7 +188,7 @@ class HeadMusic::FunctionalInterval
|
|
183
188
|
end
|
184
189
|
|
185
190
|
def simple_number
|
186
|
-
@simple_number ||= (number - 1) % 7 + 1
|
191
|
+
@simple_number ||= octave_equivalent? ? 8 : (number - 1) % 7 + 1
|
187
192
|
end
|
188
193
|
|
189
194
|
def simple_name
|
@@ -192,7 +197,8 @@ class HeadMusic::FunctionalInterval
|
|
192
197
|
|
193
198
|
def quality_name
|
194
199
|
starting_quality = QUALITY_SEMITONES[simple_number_name.to_sym].keys.first
|
195
|
-
delta = simple_semitones - QUALITY_SEMITONES[simple_number_name.to_sym][starting_quality]
|
200
|
+
delta = simple_semitones - (QUALITY_SEMITONES[simple_number_name.to_sym][starting_quality] % 12)
|
201
|
+
delta -= 12 while delta >= 6
|
196
202
|
HeadMusic::Quality.from(starting_quality, delta)
|
197
203
|
end
|
198
204
|
|
@@ -207,7 +213,7 @@ class HeadMusic::FunctionalInterval
|
|
207
213
|
def name
|
208
214
|
if named_number?
|
209
215
|
[quality_name, number_name].join(' ')
|
210
|
-
elsif simple_name == 'perfect
|
216
|
+
elsif simple_name == 'perfect octave'
|
211
217
|
"#{octaves.humanize} octaves"
|
212
218
|
else
|
213
219
|
"#{octaves.humanize} octaves and #{quality.article} #{simple_name}"
|
@@ -232,12 +238,20 @@ class HeadMusic::FunctionalInterval
|
|
232
238
|
def octaves
|
233
239
|
@octaves ||= semitones / 12
|
234
240
|
end
|
241
|
+
|
242
|
+
def octave_equivalent?
|
243
|
+
number > 1 && ((number - 1) % 7).zero?
|
244
|
+
end
|
235
245
|
end
|
236
246
|
|
237
247
|
delegate :step?, :skip?, :leap?, :large_leap?, to: :category
|
238
|
-
delegate :simple_number, :octaves, :number, :simple?, :compound?, :semitones, :simple_semitones, :steps, to: :size
|
239
248
|
delegate(
|
240
|
-
:
|
249
|
+
:simple_number, :octaves, :number, :simple?, :compound?, :semitones, :simple_semitones, :steps,
|
250
|
+
to: :size
|
251
|
+
)
|
252
|
+
delegate(
|
253
|
+
:simple_semitones, :simple_name, :quality_name, :simple_number_name, :number_name, :name, :shorthand,
|
254
|
+
to: :naming
|
241
255
|
)
|
242
256
|
|
243
257
|
# Accepts a name and returns the interval with middle c on the bottom
|
@@ -260,7 +274,9 @@ class HeadMusic::FunctionalInterval
|
|
260
274
|
|
261
275
|
def inversion
|
262
276
|
inverted_low_pitch = lower_pitch
|
263
|
-
|
277
|
+
while inverted_low_pitch < higher_pitch
|
278
|
+
inverted_low_pitch = HeadMusic::Pitch.fetch_or_create(lower_pitch.spelling, inverted_low_pitch.octave + 1)
|
279
|
+
end
|
264
280
|
HeadMusic::FunctionalInterval.new(higher_pitch, inverted_low_pitch)
|
265
281
|
end
|
266
282
|
alias invert inversion
|
@@ -288,6 +304,16 @@ class HeadMusic::FunctionalInterval
|
|
288
304
|
consonance(style).dissonant?
|
289
305
|
end
|
290
306
|
|
307
|
+
def above(pitch)
|
308
|
+
pitch = HeadMusic::Pitch.get(pitch)
|
309
|
+
HeadMusic::Pitch.from_number_and_letter(pitch + semitones, pitch.letter_name.steps_up(number - 1))
|
310
|
+
end
|
311
|
+
|
312
|
+
def below(pitch)
|
313
|
+
pitch = HeadMusic::Pitch.get(pitch)
|
314
|
+
HeadMusic::Pitch.from_number_and_letter(pitch - semitones, pitch.letter_name.steps_down(number - 1))
|
315
|
+
end
|
316
|
+
|
291
317
|
def <=>(other)
|
292
318
|
other = self.class.get(other) unless other.is_a?(HeadMusic::FunctionalInterval)
|
293
319
|
semitones <=> other.semitones
|
@@ -23,7 +23,8 @@ class HeadMusic::GrandStaff
|
|
23
23
|
def self.get(name)
|
24
24
|
@grand_staves ||= {}
|
25
25
|
hash_key = HeadMusic::Utilities::HashKey.for(name)
|
26
|
-
return nil unless GRAND_STAVES.
|
26
|
+
return nil unless GRAND_STAVES.key?(hash_key)
|
27
|
+
|
27
28
|
@grand_staves[hash_key] ||= new(hash_key)
|
28
29
|
end
|
29
30
|
|
@@ -15,6 +15,7 @@ class HeadMusic::KeySignature
|
|
15
15
|
|
16
16
|
def self.get(identifier)
|
17
17
|
return identifier if identifier.is_a?(HeadMusic::KeySignature)
|
18
|
+
|
18
19
|
@key_signatures ||= {}
|
19
20
|
tonic_spelling, scale_type_name = identifier.strip.split(/\s/)
|
20
21
|
hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, ' sharp').gsub(/(\w)[b♭]/, '\\1 flat'))
|
@@ -31,6 +31,7 @@ class HeadMusic::LetterName
|
|
31
31
|
def self.from_pitch_class(pitch_class)
|
32
32
|
@letter_names ||= {}
|
33
33
|
return nil if pitch_class.to_s == pitch_class
|
34
|
+
|
34
35
|
pitch_class = pitch_class.to_i % 12
|
35
36
|
name = NAMES.detect { |candidate| pitch_class == NATURAL_PITCH_CLASS_NUMBERS[candidate] }
|
36
37
|
name ||= HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_class].first
|
@@ -59,8 +60,12 @@ class HeadMusic::LetterName
|
|
59
60
|
NAMES.index(to_s) + 1
|
60
61
|
end
|
61
62
|
|
62
|
-
def
|
63
|
-
HeadMusic::LetterName.get(
|
63
|
+
def steps_up(num)
|
64
|
+
HeadMusic::LetterName.get(series_ascending[num % NAMES.length])
|
65
|
+
end
|
66
|
+
|
67
|
+
def steps_down(num)
|
68
|
+
HeadMusic::LetterName.get(series_descending[num % NAMES.length])
|
64
69
|
end
|
65
70
|
|
66
71
|
def steps_to(other, direction = :ascending)
|
@@ -75,11 +80,19 @@ class HeadMusic::LetterName
|
|
75
80
|
end
|
76
81
|
end
|
77
82
|
|
78
|
-
def
|
79
|
-
@
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
+
def series_ascending
|
84
|
+
@series_ascending ||= begin
|
85
|
+
series = NAMES
|
86
|
+
series = series.rotate while series.first != to_s
|
87
|
+
series
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def series_descending
|
92
|
+
@series_descending ||= begin
|
93
|
+
series = NAMES.reverse
|
94
|
+
series = series.rotate while series.first != to_s
|
95
|
+
series
|
83
96
|
end
|
84
97
|
end
|
85
98
|
|
@@ -62,11 +62,11 @@ class HeadMusic::MelodicInterval
|
|
62
62
|
end
|
63
63
|
|
64
64
|
def high_pitch
|
65
|
-
pitches.
|
65
|
+
pitches.max
|
66
66
|
end
|
67
67
|
|
68
68
|
def low_pitch
|
69
|
-
pitches.
|
69
|
+
pitches.min
|
70
70
|
end
|
71
71
|
|
72
72
|
def direction
|
@@ -82,8 +82,10 @@ class HeadMusic::MelodicInterval
|
|
82
82
|
|
83
83
|
def spells_consonant_triad_with?(other_interval)
|
84
84
|
return false if step? || other_interval.step?
|
85
|
+
|
85
86
|
combined_pitches = (pitches + other_interval.pitches).uniq
|
86
87
|
return false if combined_pitches.length < 3
|
88
|
+
|
87
89
|
HeadMusic::Chord.new(combined_pitches).consonant_triad?
|
88
90
|
end
|
89
91
|
|
data/lib/head_music/meter.rb
CHANGED
data/lib/head_music/octave.rb
CHANGED
@@ -13,12 +13,14 @@ class HeadMusic::Octave
|
|
13
13
|
def self.from_number(identifier)
|
14
14
|
return nil unless identifier.to_s == identifier.to_i.to_s
|
15
15
|
return nil unless (-2..12).cover?(identifier.to_i)
|
16
|
+
|
16
17
|
@octaves ||= {}
|
17
18
|
@octaves[identifier.to_i] ||= new(identifier.to_i)
|
18
19
|
end
|
19
20
|
|
20
21
|
def self.from_name(string)
|
21
22
|
return unless string.to_s.match?(HeadMusic::Spelling::MATCHER)
|
23
|
+
|
22
24
|
_letter, _sign, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
|
23
25
|
@octaves ||= {}
|
24
26
|
@octaves[octave_string.to_i] ||= new(octave_string.to_i) if octave_string
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# An enharmonic equivalent pitch is the same frequency spelled differently, such as D# and Eb.
|
4
|
+
class HeadMusic::Pitch::EnharmonicEquivalence
|
5
|
+
def self.get(pitch)
|
6
|
+
pitch = HeadMusic::Pitch.get(pitch)
|
7
|
+
@enharmonic_equivalences ||= {}
|
8
|
+
@enharmonic_equivalences[pitch.to_s] ||= new(pitch)
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :pitch
|
12
|
+
|
13
|
+
delegate :pitch_class, to: :pitch
|
14
|
+
|
15
|
+
def initialize(pitch)
|
16
|
+
@pitch = HeadMusic::Pitch.get(pitch)
|
17
|
+
end
|
18
|
+
|
19
|
+
def enharmonic_equivalent?(other)
|
20
|
+
other = HeadMusic::Pitch.get(other)
|
21
|
+
pitch.midi_note_number == other.midi_note_number && pitch.spelling != other.spelling
|
22
|
+
end
|
23
|
+
|
24
|
+
alias enharmonic? enharmonic_equivalent?
|
25
|
+
alias equivalent? enharmonic_equivalent?
|
26
|
+
|
27
|
+
private_class_method :new
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Octave equivalence is the functional equivalence of pitches with the same spelling separated by one or more octaves.
|
4
|
+
class HeadMusic::Pitch::OctaveEquivalence
|
5
|
+
def self.get(pitch)
|
6
|
+
@octave_equivalences ||= {}
|
7
|
+
@octave_equivalences[pitch.to_s] ||= new(pitch)
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :pitch
|
11
|
+
|
12
|
+
def initialize(pitch)
|
13
|
+
@pitch = pitch
|
14
|
+
end
|
15
|
+
|
16
|
+
def octave_equivalent?(other)
|
17
|
+
other = HeadMusic::Pitch.get(other)
|
18
|
+
pitch.spelling == other.spelling && pitch.octave != other.octave
|
19
|
+
end
|
20
|
+
|
21
|
+
alias equivalent? octave_equivalent?
|
22
|
+
|
23
|
+
private_class_method :new
|
24
|
+
end
|