head_music 0.17.0 → 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -1145
- data/Gemfile +8 -2
- data/Rakefile +7 -5
- data/bin/console +4 -3
- data/circle.yml +1 -1
- data/head_music.gemspec +19 -17
- data/lib/head_music/bar.rb +2 -0
- data/lib/head_music/chord.rb +24 -6
- data/lib/head_music/circle.rb +5 -0
- data/lib/head_music/clef.rb +5 -2
- data/lib/head_music/composition.rb +3 -0
- data/lib/head_music/consonance.rb +5 -2
- data/lib/head_music/functional_interval.rb +84 -47
- data/lib/head_music/grand_staff.rb +11 -11
- data/lib/head_music/harmonic_interval.rb +13 -7
- data/lib/head_music/instrument.rb +9 -6
- data/lib/head_music/interval.rb +13 -7
- data/lib/head_music/key_signature.rb +8 -5
- data/lib/head_music/language.rb +21 -14
- data/lib/head_music/letter_name.rb +13 -10
- data/lib/head_music/melodic_interval.rb +10 -4
- data/lib/head_music/meter.rb +12 -12
- data/lib/head_music/motion.rb +12 -9
- data/lib/head_music/named_rudiment.rb +22 -20
- data/lib/head_music/note.rb +7 -1
- data/lib/head_music/octave.rb +9 -7
- data/lib/head_music/pitch.rb +54 -27
- data/lib/head_music/pitch_class.rb +17 -12
- data/lib/head_music/placement.rb +22 -9
- data/lib/head_music/position.rb +8 -9
- data/lib/head_music/quality.rb +9 -6
- data/lib/head_music/rhythm.rb +2 -0
- data/lib/head_music/rhythmic_unit.rb +29 -19
- data/lib/head_music/rhythmic_value.rb +5 -2
- data/lib/head_music/scale.rb +65 -45
- data/lib/head_music/scale_degree.rb +9 -6
- data/lib/head_music/scale_type.rb +70 -30
- data/lib/head_music/sign.rb +18 -13
- data/lib/head_music/spelling.rb +14 -10
- data/lib/head_music/staff.rb +4 -1
- data/lib/head_music/style/analysis.rb +36 -34
- data/lib/head_music/style/annotation.rb +14 -13
- data/lib/head_music/style/annotations/always_move.rb +7 -6
- data/lib/head_music/style/annotations/approach_perfection_contrarily.rb +5 -2
- data/lib/head_music/style/annotations/at_least_eight_notes.rb +10 -8
- data/lib/head_music/style/annotations/avoid_crossing_voices.rb +11 -8
- data/lib/head_music/style/annotations/avoid_overlapping_voices.rb +17 -10
- data/lib/head_music/style/annotations/consonant_climax.rb +18 -15
- data/lib/head_music/style/annotations/consonant_downbeats.rb +6 -3
- data/lib/head_music/style/annotations/diatonic.rb +8 -5
- data/lib/head_music/style/annotations/direction_changes.rb +8 -6
- data/lib/head_music/style/annotations/end_on_perfect_consonance.rb +5 -5
- data/lib/head_music/style/annotations/end_on_tonic.rb +7 -6
- data/lib/head_music/style/annotations/frequent_direction_changes.rb +6 -3
- data/lib/head_music/style/annotations/limit_octave_leaps.rb +9 -7
- data/lib/head_music/style/annotations/moderate_direction_changes.rb +6 -3
- data/lib/head_music/style/annotations/mostly_conjunct.rb +8 -5
- data/lib/head_music/style/annotations/no_rests.rb +6 -3
- data/lib/head_music/style/annotations/no_unisons_in_middle.rb +6 -3
- data/lib/head_music/style/annotations/notes_same_length.rb +9 -6
- data/lib/head_music/style/annotations/one_to_one.rb +10 -7
- data/lib/head_music/style/annotations/prefer_contrary_motion.rb +6 -3
- data/lib/head_music/style/annotations/prefer_imperfect.rb +7 -6
- data/lib/head_music/style/annotations/prepare_octave_leaps.rb +21 -12
- data/lib/head_music/style/annotations/recover_large_leaps.rb +17 -12
- data/lib/head_music/style/annotations/singable_intervals.rb +8 -5
- data/lib/head_music/style/annotations/singable_range.rb +7 -6
- data/lib/head_music/style/annotations/single_large_leaps.rb +6 -3
- data/lib/head_music/style/annotations/start_on_perfect_consonance.rb +6 -5
- data/lib/head_music/style/annotations/start_on_tonic.rb +6 -5
- data/lib/head_music/style/annotations/step_down_to_final_note.rb +12 -12
- data/lib/head_music/style/annotations/step_out_of_unison.rb +10 -7
- data/lib/head_music/style/annotations/step_to_final_note.rb +6 -3
- data/lib/head_music/style/annotations/step_up_to_final_note.rb +12 -12
- data/lib/head_music/style/annotations/up_to_fourteen_notes.rb +6 -5
- data/lib/head_music/style/mark.rb +10 -4
- data/lib/head_music/style/rulesets/first_species_harmony.rb +6 -3
- data/lib/head_music/style/rulesets/first_species_melody.rb +6 -3
- data/lib/head_music/style/rulesets/fux_cantus_firmus.rb +6 -3
- data/lib/head_music/style/rulesets/modern_cantus_firmus.rb +6 -3
- data/lib/head_music/utilities/hash_key.rb +9 -7
- data/lib/head_music/version.rb +3 -1
- data/lib/head_music/voice.rb +7 -3
- data/lib/head_music.rb +2 -0
- metadata +4 -4
data/Gemfile
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
source 'https://rubygems.org'
|
2
4
|
|
5
|
+
ruby '2.4.0'
|
6
|
+
|
3
7
|
# Specify your gem's dependencies in head_music.gemspec
|
4
8
|
gemspec
|
5
9
|
|
6
10
|
group :test do
|
7
|
-
gem
|
8
|
-
gem
|
11
|
+
gem 'codeclimate-test-reporter', '~> 1.0.0'
|
12
|
+
gem 'rubocop', require: false
|
13
|
+
gem 'rubocop-rspec', require: false
|
14
|
+
gem 'simplecov'
|
9
15
|
end
|
data/Rakefile
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
3
5
|
|
4
6
|
RSpec::Core::RakeTask.new(:spec)
|
5
7
|
|
6
|
-
task :
|
8
|
+
task default: :spec
|
7
9
|
|
8
|
-
desc
|
10
|
+
desc 'Open an irb session preloaded with this library'
|
9
11
|
task :console do
|
10
|
-
sh
|
12
|
+
sh 'irb -rubygems -I lib -r head_music.rb'
|
11
13
|
end
|
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'head_music'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "head_music"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start
|
data/circle.yml
CHANGED
data/head_music.gemspec
CHANGED
@@ -1,33 +1,35 @@
|
|
1
|
-
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
lib = File.expand_path('../lib', __FILE__)
|
3
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
6
|
require 'head_music/version'
|
5
7
|
|
6
8
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
9
|
+
spec.name = 'head_music'
|
8
10
|
spec.version = HeadMusic::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
+
spec.authors = ['Rob Head']
|
12
|
+
spec.email = ['robert.head@gmail.com']
|
11
13
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
14
|
-
spec.homepage =
|
15
|
-
spec.license =
|
14
|
+
spec.summary = 'The rudiments of western music theory.'
|
15
|
+
spec.description = 'Work with the elements of western music theory, such as pitches, scales, intervals, and chords.'
|
16
|
+
spec.homepage = 'https://github.com/roberthead/head_music'
|
17
|
+
spec.license = 'MIT'
|
16
18
|
|
17
19
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
20
|
f.match(%r{^(test|spec|features)/})
|
19
21
|
end
|
20
|
-
spec.bindir =
|
22
|
+
spec.bindir = 'exe'
|
21
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
-
spec.require_paths = [
|
24
|
+
spec.require_paths = ['lib']
|
23
25
|
|
24
|
-
spec.required_ruby_version = '
|
26
|
+
spec.required_ruby_version = '>= 2.4.0'
|
25
27
|
|
26
|
-
spec.add_runtime_dependency
|
27
|
-
spec.add_runtime_dependency
|
28
|
+
spec.add_runtime_dependency 'activesupport', '~> 5.0'
|
29
|
+
spec.add_runtime_dependency 'humanize', '~> 1.3'
|
28
30
|
|
29
|
-
spec.add_development_dependency
|
30
|
-
spec.add_development_dependency
|
31
|
-
spec.add_development_dependency
|
32
|
-
spec.add_development_dependency
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
34
|
+
spec.add_development_dependency 'rspec-its', '~> 1.2'
|
33
35
|
end
|
data/lib/head_music/bar.rb
CHANGED
data/lib/head_music/chord.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A Chord is a collection of three or more pitches
|
1
4
|
class HeadMusic::Chord
|
2
5
|
attr_reader :pitches
|
3
6
|
|
@@ -7,12 +10,27 @@ class HeadMusic::Chord
|
|
7
10
|
end
|
8
11
|
|
9
12
|
def consonant_triad?
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
)
|
13
|
+
return false unless three_pitches?
|
14
|
+
root_triad? || first_inversion_triad? || second_inversion_triad?
|
15
|
+
end
|
16
|
+
|
17
|
+
def root_triad?
|
18
|
+
intervals.map(&:shorthand).sort == %w[M3 m3]
|
19
|
+
end
|
20
|
+
|
21
|
+
def first_inversion_triad?
|
22
|
+
invert.invert.intervals.map(&:shorthand).sort == %w[M3 m3]
|
23
|
+
end
|
24
|
+
|
25
|
+
def second_inversion_triad?
|
26
|
+
invert.intervals.map(&:shorthand).sort == %w[M3 m3]
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO
|
30
|
+
def reduction; end
|
31
|
+
|
32
|
+
def three_pitches?
|
33
|
+
pitches.length == 3
|
16
34
|
end
|
17
35
|
|
18
36
|
def intervals
|
data/lib/head_music/circle.rb
CHANGED
data/lib/head_music/clef.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A clef assigns pitches to the lines and spaces of a staff.
|
1
4
|
class HeadMusic::Clef
|
2
5
|
include HeadMusic::NamedRudiment
|
3
6
|
|
@@ -16,8 +19,8 @@ class HeadMusic::Clef
|
|
16
19
|
{ pitch: 'C4', line: 4, names: ['tenor'], modern: true },
|
17
20
|
{ pitch: 'C4', line: 5, names: ['baritone'] },
|
18
21
|
|
19
|
-
{ pitch: nil, line: 3, names: [
|
20
|
-
]
|
22
|
+
{ pitch: nil, line: 3, names: %w[neutral percussion] },
|
23
|
+
].freeze
|
21
24
|
|
22
25
|
def self.get(name)
|
23
26
|
get_by_name(name)
|
@@ -1,5 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Consonance is a description of intervals that sound more pleasing.
|
1
4
|
class HeadMusic::Consonance
|
2
|
-
LEVELS = %w[perfect imperfect dissonant]
|
5
|
+
LEVELS = %w[perfect imperfect dissonant].freeze
|
3
6
|
|
4
7
|
def self.get(name)
|
5
8
|
@consonances ||= {}
|
@@ -15,7 +18,7 @@ class HeadMusic::Consonance
|
|
15
18
|
end
|
16
19
|
|
17
20
|
def ==(other)
|
18
|
-
|
21
|
+
to_s == other.to_s
|
19
22
|
end
|
20
23
|
|
21
24
|
LEVELS.each do |method_name|
|
@@ -1,8 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Represents a functional interval.
|
1
4
|
class HeadMusic::FunctionalInterval
|
2
5
|
include Comparable
|
3
6
|
|
4
|
-
NUMBER_NAMES = %w[
|
5
|
-
|
7
|
+
NUMBER_NAMES = %w[
|
8
|
+
unison second third fourth fifth sixth seventh octave
|
9
|
+
ninth tenth eleventh twelfth thirteenth fourteenth fifteenth
|
10
|
+
sixteenth seventeenth
|
11
|
+
].freeze
|
12
|
+
NAME_SUFFIXES = Hash.new('th').merge(1 => 'st', 2 => 'nd', 3 => 'rd').freeze
|
6
13
|
|
7
14
|
QUALITY_SEMITONES = {
|
8
15
|
unison: { perfect: 0 },
|
@@ -21,43 +28,75 @@ class HeadMusic::FunctionalInterval
|
|
21
28
|
fourteenth: { major: 23 },
|
22
29
|
fifteenth: { perfect: 24 },
|
23
30
|
sixteenth: { major: 26 },
|
24
|
-
seventeenth: { major: 28 }
|
25
|
-
}
|
31
|
+
seventeenth: { major: 28 },
|
32
|
+
}.freeze
|
26
33
|
|
27
34
|
attr_reader :lower_pitch, :higher_pitch
|
28
35
|
|
29
36
|
delegate :to_s, to: :name
|
30
37
|
delegate :perfect?, :major?, :minor?, :diminished?, :augmented?, :doubly_diminished?, :doubly_augmented?, to: :quality
|
31
38
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
39
|
+
# Representation of the name of the functional interval
|
40
|
+
class Name
|
41
|
+
attr_reader :identifier
|
42
|
+
|
43
|
+
def initialize(identifier)
|
44
|
+
@identifier = identifier
|
45
|
+
end
|
46
|
+
|
47
|
+
def words
|
48
|
+
identifier.to_s.split(/[_ ]+/)
|
49
|
+
end
|
50
|
+
|
51
|
+
def quality_name
|
52
|
+
words[0..-2].join(' ').to_sym
|
53
|
+
end
|
54
|
+
|
55
|
+
def degree_name
|
56
|
+
words.last
|
57
|
+
end
|
58
|
+
|
59
|
+
def steps
|
60
|
+
NUMBER_NAMES.index(degree_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def higher_letter
|
64
|
+
HeadMusic::Pitch.middle_c.letter_name.steps(steps)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.get(identifier)
|
69
|
+
name = Name.new(identifier)
|
70
|
+
semitones = _degree_quality_semitones.dig(name.degree_name.to_sym, name.quality_name)
|
71
|
+
higher_pitch = HeadMusic::Pitch.from_number_and_letter(HeadMusic::Pitch.middle_c + semitones, name.higher_letter)
|
72
|
+
new(HeadMusic::Pitch.middle_c, higher_pitch)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self._degree_quality_semitones
|
76
|
+
@_degree_quality_semitones ||= begin
|
77
|
+
{}.tap do |degree_quality_semitones|
|
78
|
+
QUALITY_SEMITONES.each do |degree_name, qualities|
|
79
|
+
default_quality = qualities.keys.first
|
80
|
+
default_semitones = qualities[default_quality]
|
81
|
+
degree_quality_semitones[degree_name] = _semitones_for_degree(default_quality, default_semitones)
|
58
82
|
end
|
59
83
|
end
|
60
|
-
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def self._semitones_for_degree(quality, default_semitones)
|
88
|
+
{}.tap do |semitones|
|
89
|
+
_degree_quality_modifications(quality).each do |current_quality, delta|
|
90
|
+
semitones[current_quality] = default_semitones + delta
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self._degree_quality_modifications(quality)
|
96
|
+
if quality == :perfect
|
97
|
+
HeadMusic::Quality::PERFECT_INTERVAL_MODIFICATION.invert
|
98
|
+
else
|
99
|
+
HeadMusic::Quality::MAJOR_INTERVAL_MODIFICATION.invert
|
61
100
|
end
|
62
101
|
end
|
63
102
|
|
@@ -96,7 +135,7 @@ class HeadMusic::FunctionalInterval
|
|
96
135
|
end
|
97
136
|
|
98
137
|
def simple?
|
99
|
-
octaves
|
138
|
+
octaves.zero?
|
100
139
|
end
|
101
140
|
|
102
141
|
def simple_name
|
@@ -104,7 +143,7 @@ class HeadMusic::FunctionalInterval
|
|
104
143
|
end
|
105
144
|
|
106
145
|
def name
|
107
|
-
if
|
146
|
+
if named_number?
|
108
147
|
[quality_name, number_name].join(' ')
|
109
148
|
elsif simple_name == 'perfect unison'
|
110
149
|
"#{octaves.humanize} octaves"
|
@@ -125,7 +164,7 @@ class HeadMusic::FunctionalInterval
|
|
125
164
|
def quality_name
|
126
165
|
starting_quality = QUALITY_SEMITONES[simple_number_name.to_sym].keys.first
|
127
166
|
delta = simple_semitones - QUALITY_SEMITONES[simple_number_name.to_sym][starting_quality]
|
128
|
-
HeadMusic::Quality
|
167
|
+
HeadMusic::Quality.from(starting_quality, delta)
|
129
168
|
end
|
130
169
|
|
131
170
|
def simple_number_name
|
@@ -133,16 +172,12 @@ class HeadMusic::FunctionalInterval
|
|
133
172
|
end
|
134
173
|
|
135
174
|
def number_name
|
136
|
-
NUMBER_NAMES[number - 1] ||
|
137
|
-
number.to_s + NAME_SUFFIXES[number % 10]
|
138
|
-
end
|
175
|
+
NUMBER_NAMES[number - 1] || (number.to_s + NAME_SUFFIXES[number % 10])
|
139
176
|
end
|
140
177
|
|
141
178
|
def inversion
|
142
179
|
inverted_low_pitch = lower_pitch
|
143
|
-
while inverted_low_pitch < higher_pitch
|
144
|
-
inverted_low_pitch += 12
|
145
|
-
end
|
180
|
+
inverted_low_pitch += 12 while inverted_low_pitch < higher_pitch
|
146
181
|
HeadMusic::FunctionalInterval.new(higher_pitch, inverted_low_pitch)
|
147
182
|
end
|
148
183
|
|
@@ -169,11 +204,11 @@ class HeadMusic::FunctionalInterval
|
|
169
204
|
end
|
170
205
|
|
171
206
|
def step?
|
172
|
-
number
|
207
|
+
number == 2
|
173
208
|
end
|
174
209
|
|
175
210
|
def skip?
|
176
|
-
number
|
211
|
+
number == 3
|
177
212
|
end
|
178
213
|
|
179
214
|
def leap?
|
@@ -185,10 +220,8 @@ class HeadMusic::FunctionalInterval
|
|
185
220
|
end
|
186
221
|
|
187
222
|
def <=>(other)
|
188
|
-
|
189
|
-
|
190
|
-
end
|
191
|
-
self.semitones <=> other.semitones
|
223
|
+
other = self.class.get(other) unless other.is_a?(HeadMusic::FunctionalInterval)
|
224
|
+
semitones <=> other.semitones
|
192
225
|
end
|
193
226
|
|
194
227
|
NUMBER_NAMES.each do |interval_name|
|
@@ -201,12 +234,16 @@ class HeadMusic::FunctionalInterval
|
|
201
234
|
|
202
235
|
private
|
203
236
|
|
237
|
+
def named_number?
|
238
|
+
number < NUMBER_NAMES.length
|
239
|
+
end
|
240
|
+
|
204
241
|
def consonance_for_perfect(style = :standard_practice)
|
205
242
|
HeadMusic::Consonance.get(dissonant_fourth?(style) ? :dissonant : :perfect) if perfect?
|
206
243
|
end
|
207
244
|
|
208
245
|
def consonance_for_major_and_minor
|
209
|
-
HeadMusic::Consonance.get(
|
246
|
+
HeadMusic::Consonance.get(third_or_compound? || sixth_or_compound? ? :imperfect : :dissonant) if major? || minor?
|
210
247
|
end
|
211
248
|
|
212
249
|
def dissonant_fourth?(style = :standard_practice)
|
@@ -1,21 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A grand staff is a group of staves for a single instrument, such as a piano.
|
1
4
|
class HeadMusic::GrandStaff
|
2
5
|
GRAND_STAVES = {
|
3
6
|
piano: {
|
4
7
|
instrument: :piano,
|
5
8
|
staves: [
|
6
9
|
{ clef: :treble, instrument: :piano },
|
7
|
-
{ clef: :bass, instrument: :piano }
|
8
|
-
]
|
10
|
+
{ clef: :bass, instrument: :piano },
|
11
|
+
],
|
9
12
|
},
|
10
13
|
organ: {
|
11
14
|
instrument: :organ,
|
12
15
|
staves: [
|
13
16
|
{ clef: :treble, instrument: :organ },
|
14
17
|
{ clef: :bass, instrument: :organ },
|
15
|
-
{ clef: :bass, instrument: :pedals }
|
16
|
-
]
|
17
|
-
}
|
18
|
-
}
|
18
|
+
{ clef: :bass, instrument: :pedals },
|
19
|
+
],
|
20
|
+
},
|
21
|
+
}.freeze
|
19
22
|
|
20
23
|
def self.get(name)
|
21
24
|
@grand_staves ||= {}
|
@@ -36,11 +39,8 @@ class HeadMusic::GrandStaff
|
|
36
39
|
end
|
37
40
|
|
38
41
|
def staves
|
39
|
-
@staves ||=
|
40
|
-
data[:staves].map { |staff|
|
41
|
-
HeadMusic::Staff.new(staff[:clef], instrument: staff[:instrument] || instrument)
|
42
|
-
}
|
43
|
-
end
|
42
|
+
@staves ||=
|
43
|
+
data[:staves].map { |staff| HeadMusic::Staff.new(staff[:clef], instrument: staff[:instrument] || instrument) }
|
44
44
|
end
|
45
45
|
|
46
46
|
def brace_staves_index_first
|
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A harmonic interval is the functional interval between two notes sounding together.
|
1
4
|
class HeadMusic::HarmonicInterval
|
2
5
|
attr_reader :voice1, :voice2, :position
|
3
6
|
|
@@ -40,12 +43,11 @@ class HeadMusic::HarmonicInterval
|
|
40
43
|
end
|
41
44
|
|
42
45
|
def pitch_orientation
|
43
|
-
if lower_pitch
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
46
|
+
return if lower_pitch == upper_pitch
|
47
|
+
if lower_note.voice == voice1
|
48
|
+
:up
|
49
|
+
elsif lower_note.voice == voice2
|
50
|
+
:down
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
@@ -54,6 +56,10 @@ class HeadMusic::HarmonicInterval
|
|
54
56
|
end
|
55
57
|
|
56
58
|
def method_missing(method_name, *args, &block)
|
57
|
-
functional_interval.send(method_name, *args, &block)
|
59
|
+
respond_to_missing?(method_name) ? functional_interval.send(method_name, *args, &block) : super
|
60
|
+
end
|
61
|
+
|
62
|
+
def respond_to_missing?(method_name, *_args)
|
63
|
+
functional_interval.respond_to?(method_name)
|
58
64
|
end
|
59
65
|
end
|
@@ -1,18 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# An instrument can be assigned to a staff.
|
1
4
|
class HeadMusic::Instrument
|
2
5
|
include HeadMusic::NamedRudiment
|
3
6
|
|
4
7
|
INSTRUMENTS = {
|
5
8
|
violin: {
|
6
|
-
name:
|
9
|
+
name: 'violin',
|
7
10
|
family: :string,
|
8
|
-
default_clef: :treble
|
11
|
+
default_clef: :treble,
|
9
12
|
},
|
10
13
|
piano: {
|
11
|
-
name:
|
14
|
+
name: 'piano',
|
12
15
|
family: :string,
|
13
|
-
default_system: [
|
14
|
-
}
|
15
|
-
}
|
16
|
+
default_system: %i[treble bass],
|
17
|
+
},
|
18
|
+
}.freeze
|
16
19
|
|
17
20
|
def self.get(name)
|
18
21
|
get_by_name(name)
|
data/lib/head_music/interval.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# An interval is the distance between two pitches.
|
1
4
|
class HeadMusic::Interval
|
2
5
|
include Comparable
|
3
6
|
|
4
7
|
private_class_method :new
|
5
8
|
|
6
|
-
NAMES = %w
|
9
|
+
NAMES = %w[
|
10
|
+
perfect_unison minor_second major_second minor_third major_third perfect_fourth tritone perfect_fifth
|
11
|
+
minor_sixth major_sixth minor_seventh major_seventh perfect_octave
|
12
|
+
].freeze
|
7
13
|
|
8
14
|
attr_reader :semitones
|
9
15
|
|
@@ -23,7 +29,7 @@ class HeadMusic::Interval
|
|
23
29
|
end
|
24
30
|
|
25
31
|
def simple?
|
26
|
-
(0..12).
|
32
|
+
(0..12).cover?(semitones)
|
27
33
|
end
|
28
34
|
|
29
35
|
def compound?
|
@@ -34,15 +40,15 @@ class HeadMusic::Interval
|
|
34
40
|
semitones
|
35
41
|
end
|
36
42
|
|
37
|
-
def +(
|
38
|
-
HeadMusic::Interval.get(to_i +
|
43
|
+
def +(other)
|
44
|
+
HeadMusic::Interval.get(to_i + other.to_i)
|
39
45
|
end
|
40
46
|
|
41
|
-
def -(
|
42
|
-
HeadMusic::Interval.get((to_i -
|
47
|
+
def -(other)
|
48
|
+
HeadMusic::Interval.get((to_i - other.to_i).abs)
|
43
49
|
end
|
44
50
|
|
45
51
|
def <=>(other)
|
46
|
-
|
52
|
+
to_i <=> other.to_i
|
47
53
|
end
|
48
54
|
end
|
@@ -1,10 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Represents a key signature.
|
1
4
|
class HeadMusic::KeySignature
|
2
5
|
attr_reader :tonic_spelling
|
3
6
|
attr_reader :scale_type
|
4
7
|
attr_reader :scale
|
5
8
|
|
6
|
-
SHARPS = %w
|
7
|
-
FLATS = %w
|
9
|
+
SHARPS = %w[F♯ C♯ G♯ D♯ A♯ E♯ B♯].freeze
|
10
|
+
FLATS = %w[B♭ E♭ A♭ D♭ G♭ C♭ F♭].freeze
|
8
11
|
|
9
12
|
def self.default
|
10
13
|
@default ||= new('C', :major)
|
@@ -50,8 +53,8 @@ class HeadMusic::KeySignature
|
|
50
53
|
flats.length
|
51
54
|
end
|
52
55
|
|
53
|
-
def
|
54
|
-
flats.
|
56
|
+
def signs
|
57
|
+
!flats.empty? ? flats : sharps
|
55
58
|
end
|
56
59
|
|
57
60
|
def name
|
@@ -59,6 +62,6 @@ class HeadMusic::KeySignature
|
|
59
62
|
end
|
60
63
|
|
61
64
|
def ==(other)
|
62
|
-
|
65
|
+
signs == self.class.get(other).signs
|
63
66
|
end
|
64
67
|
end
|