head_music 0.20.0 → 0.22.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|