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/lib/head_music/language.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A language.
|
1
4
|
class HeadMusic::Language
|
2
5
|
include Comparable
|
3
6
|
include HeadMusic::NamedRudiment
|
@@ -10,22 +13,24 @@ class HeadMusic::Language
|
|
10
13
|
{ name: 'Italian', native_name: 'Italiano', abbreviation: 'it' },
|
11
14
|
{ name: 'Spanish', native_name: 'Español', abbreviation: 'es' },
|
12
15
|
{ name: 'Russian', native_name: 'русский', abbreviation: 'ru' },
|
13
|
-
]
|
16
|
+
].freeze
|
14
17
|
|
15
|
-
LANGUAGES.
|
16
|
-
|
17
|
-
|
18
|
+
LANGUAGES.
|
19
|
+
map { |language| ::HeadMusic::Utilities::HashKey.for(language[:name]) }.
|
20
|
+
each { |language_key| define_singleton_method(language_key) { HeadMusic::Language.get(language_key) } }
|
18
21
|
|
19
|
-
LANGUAGES.
|
20
|
-
|
21
|
-
|
22
|
+
LANGUAGES.
|
23
|
+
map { |language| ::HeadMusic::Utilities::HashKey.for(language[:native_name]) }.
|
24
|
+
reject(&:nil?).
|
25
|
+
each { |language_key| define_singleton_method(language_key) { HeadMusic::Language.get(language_key) } }
|
22
26
|
|
23
|
-
LANGUAGES.
|
24
|
-
|
25
|
-
|
27
|
+
LANGUAGES.
|
28
|
+
map { |language| ::HeadMusic::Utilities::HashKey.for(language[:short_name]) }.
|
29
|
+
reject(&:nil?).
|
30
|
+
each { |language_key| define_singleton_method(language_key) { HeadMusic::Language.get(language_key) } }
|
26
31
|
|
27
32
|
def self.default
|
28
|
-
|
33
|
+
english
|
29
34
|
end
|
30
35
|
|
31
36
|
def self.get(name)
|
@@ -36,9 +41,11 @@ class HeadMusic::Language
|
|
36
41
|
|
37
42
|
def initialize(identifier)
|
38
43
|
identifier_key = HeadMusic::Utilities::HashKey.for(identifier)
|
39
|
-
language_data = LANGUAGES.detect
|
40
|
-
[
|
41
|
-
|
44
|
+
language_data = LANGUAGES.detect do |data|
|
45
|
+
%i[name native_name short_name].
|
46
|
+
map { |key| HeadMusic::Utilities::HashKey.for(data[key]) }.
|
47
|
+
include?(identifier_key)
|
48
|
+
end
|
42
49
|
@name = language_data[:name]
|
43
50
|
@native_name = language_data[:native_name]
|
44
51
|
@short_name = language_data[:short_name]
|
@@ -1,5 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Music has seven lette names that are used to identify pitches and pitch classes.
|
1
4
|
class HeadMusic::LetterName
|
2
|
-
NAMES = %w[C D E F G A B]
|
5
|
+
NAMES = %w[C D E F G A B].freeze
|
3
6
|
|
4
7
|
NATURAL_PITCH_CLASS_NUMBERS = {
|
5
8
|
'C' => 0,
|
@@ -9,10 +12,10 @@ class HeadMusic::LetterName
|
|
9
12
|
'G' => 7,
|
10
13
|
'A' => 9,
|
11
14
|
'B' => 11,
|
12
|
-
}
|
15
|
+
}.freeze
|
13
16
|
|
14
17
|
def self.all
|
15
|
-
NAMES.map { |letter_name| get(letter_name)}
|
18
|
+
NAMES.map { |letter_name| get(letter_name) }
|
16
19
|
end
|
17
20
|
|
18
21
|
def self.get(identifier)
|
@@ -48,12 +51,12 @@ class HeadMusic::LetterName
|
|
48
51
|
HeadMusic::PitchClass.get(NATURAL_PITCH_CLASS_NUMBERS[name])
|
49
52
|
end
|
50
53
|
|
51
|
-
def ==(
|
52
|
-
to_s ==
|
54
|
+
def ==(other)
|
55
|
+
to_s == other.to_s
|
53
56
|
end
|
54
57
|
|
55
58
|
def position
|
56
|
-
NAMES.index(
|
59
|
+
NAMES.index(to_s) + 1
|
57
60
|
end
|
58
61
|
|
59
62
|
def steps(num)
|
@@ -73,11 +76,11 @@ class HeadMusic::LetterName
|
|
73
76
|
end
|
74
77
|
|
75
78
|
def cycle
|
76
|
-
cycle
|
77
|
-
|
78
|
-
cycle = cycle.rotate
|
79
|
+
@cycle ||= begin
|
80
|
+
cycle = NAMES
|
81
|
+
cycle = cycle.rotate while cycle.first != to_s
|
82
|
+
cycle
|
79
83
|
end
|
80
|
-
cycle
|
81
84
|
end
|
82
85
|
|
83
86
|
private_class_method :new
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A melodic interval is the distance between one note and the next.
|
1
4
|
class HeadMusic::MelodicInterval
|
2
|
-
attr_reader :
|
5
|
+
attr_reader :first_note, :second_note
|
3
6
|
|
4
|
-
def initialize(
|
5
|
-
@voice = voice
|
7
|
+
def initialize(note1, note2)
|
6
8
|
@first_note = note1
|
7
9
|
@second_note = note2
|
8
10
|
end
|
@@ -86,6 +88,10 @@ class HeadMusic::MelodicInterval
|
|
86
88
|
end
|
87
89
|
|
88
90
|
def method_missing(method_name, *args, &block)
|
89
|
-
functional_interval.send(method_name, *args, &block)
|
91
|
+
respond_to_missing?(method_name) ? functional_interval.send(method_name, *args, &block) : super
|
92
|
+
end
|
93
|
+
|
94
|
+
def respond_to_missing?(method_name, *_args)
|
95
|
+
functional_interval.respond_to?(method_name)
|
90
96
|
end
|
91
97
|
end
|
data/lib/head_music/meter.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Meter is the rhythmic size of a measure, such as 4/4 or 6/8
|
1
4
|
class HeadMusic::Meter
|
2
5
|
attr_reader :top_number, :bottom_number
|
3
6
|
|
4
7
|
NAMED = {
|
5
8
|
common_time: '4/4',
|
6
|
-
cut_time: '2/2'
|
7
|
-
}
|
9
|
+
cut_time: '2/2',
|
10
|
+
}.freeze
|
8
11
|
|
9
12
|
def self.get(identifier)
|
10
13
|
identifier = identifier.to_s
|
@@ -27,7 +30,8 @@ class HeadMusic::Meter
|
|
27
30
|
end
|
28
31
|
|
29
32
|
def initialize(top_number, bottom_number)
|
30
|
-
@top_number
|
33
|
+
@top_number = top_number
|
34
|
+
@bottom_number = bottom_number
|
31
35
|
end
|
32
36
|
|
33
37
|
def simple?
|
@@ -43,7 +47,7 @@ class HeadMusic::Meter
|
|
43
47
|
end
|
44
48
|
|
45
49
|
def triple?
|
46
|
-
top_number % 3
|
50
|
+
(top_number % 3).zero?
|
47
51
|
end
|
48
52
|
|
49
53
|
def quadruple?
|
@@ -77,8 +81,7 @@ class HeadMusic::Meter
|
|
77
81
|
def beat_unit
|
78
82
|
@beat_unit ||=
|
79
83
|
if compound?
|
80
|
-
|
81
|
-
HeadMusic::RhythmicValue.new(unit, dots: 1)
|
84
|
+
HeadMusic::RhythmicValue.new(HeadMusic::RhythmicUnit.for_denominator_value(bottom_number / 2), dots: 1)
|
82
85
|
else
|
83
86
|
HeadMusic::RhythmicValue.new(count_unit)
|
84
87
|
end
|
@@ -101,10 +104,7 @@ class HeadMusic::Meter
|
|
101
104
|
end
|
102
105
|
|
103
106
|
def strong_ticks
|
104
|
-
@strong_ticks ||=
|
105
|
-
[2,3,4].map do |sixths|
|
106
|
-
ticks_per_count * (sixths / 6.0)
|
107
|
-
end
|
107
|
+
@strong_ticks ||= [2, 3, 4].map { |sixths| ticks_per_count * (sixths / 6.0) }
|
108
108
|
end
|
109
109
|
|
110
110
|
private
|
@@ -122,10 +122,10 @@ class HeadMusic::Meter
|
|
122
122
|
end
|
123
123
|
|
124
124
|
def strong_beat_in_triple?(count, tick = 0)
|
125
|
-
beat?(tick) && counts_per_bar % 3
|
125
|
+
beat?(tick) && (counts_per_bar % 3).zero? && counts_per_bar > 6 && count % 3 == 1
|
126
126
|
end
|
127
127
|
|
128
128
|
def beat?(tick)
|
129
|
-
tick
|
129
|
+
tick.zero?
|
130
130
|
end
|
131
131
|
end
|
data/lib/head_music/motion.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Motion defines the relative pitch direction of the upper and lower voices of subsequence intervals.
|
1
4
|
class HeadMusic::Motion
|
2
5
|
attr_reader :first_harmonic_interval, :second_harmonic_interval
|
3
6
|
|
@@ -12,7 +15,7 @@ class HeadMusic::Motion
|
|
12
15
|
|
13
16
|
def oblique?
|
14
17
|
upper_melodic_interval.repetition? && lower_melodic_interval.moving? ||
|
15
|
-
|
18
|
+
lower_melodic_interval.repetition? && upper_melodic_interval.moving?
|
16
19
|
end
|
17
20
|
|
18
21
|
def direct?
|
@@ -21,19 +24,19 @@ class HeadMusic::Motion
|
|
21
24
|
|
22
25
|
def parallel?
|
23
26
|
upper_melodic_interval.moving? &&
|
24
|
-
|
25
|
-
|
27
|
+
upper_melodic_interval.direction == lower_melodic_interval.direction &&
|
28
|
+
upper_melodic_interval.steps == lower_melodic_interval.steps
|
26
29
|
end
|
27
30
|
|
28
31
|
def similar?
|
29
32
|
upper_melodic_interval.direction == lower_melodic_interval.direction &&
|
30
|
-
|
33
|
+
upper_melodic_interval.steps != lower_melodic_interval.steps
|
31
34
|
end
|
32
35
|
|
33
36
|
def contrary?
|
34
37
|
upper_melodic_interval.moving? &&
|
35
|
-
|
36
|
-
|
38
|
+
lower_melodic_interval.moving? &&
|
39
|
+
upper_melodic_interval.direction != lower_melodic_interval.direction
|
37
40
|
end
|
38
41
|
|
39
42
|
def notes
|
@@ -41,7 +44,7 @@ class HeadMusic::Motion
|
|
41
44
|
end
|
42
45
|
|
43
46
|
def contrapuntal_motion
|
44
|
-
[
|
47
|
+
%i[parallel similar oblique contrary repetition].detect do |motion_type|
|
45
48
|
send("#{motion_type}?")
|
46
49
|
end
|
47
50
|
end
|
@@ -53,11 +56,11 @@ class HeadMusic::Motion
|
|
53
56
|
private
|
54
57
|
|
55
58
|
def upper_melodic_interval
|
56
|
-
HeadMusic::MelodicInterval.new(upper_notes.first
|
59
|
+
HeadMusic::MelodicInterval.new(upper_notes.first, upper_notes.last)
|
57
60
|
end
|
58
61
|
|
59
62
|
def lower_melodic_interval
|
60
|
-
HeadMusic::MelodicInterval.new(lower_notes.first
|
63
|
+
HeadMusic::MelodicInterval.new(lower_notes.first, lower_notes.last)
|
61
64
|
end
|
62
65
|
|
63
66
|
def upper_notes
|
@@ -1,27 +1,29 @@
|
|
1
|
-
|
2
|
-
module NamedRudiment
|
3
|
-
attr_reader :name
|
4
|
-
delegate :to_s, to: :name
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
# NameRudiment is a module to be included in classes whose instances may be identified by name.
|
4
|
+
module HeadMusic::NamedRudiment
|
5
|
+
attr_reader :name
|
6
|
+
delegate :to_s, to: :name
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
def initialize(name)
|
9
|
+
@name = name.to_s
|
10
|
+
end
|
13
11
|
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
def hash_key
|
13
|
+
HeadMusic::Utilities::HashKey.for(name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
base.extend(ClassMethods)
|
18
|
+
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
# Adds .get_by_name to the including class.
|
21
|
+
module ClassMethods
|
22
|
+
def get_by_name(name)
|
23
|
+
name = name.to_s
|
24
|
+
@instances_by_name ||= {}
|
25
|
+
key = HeadMusic::Utilities::HashKey.for(name)
|
26
|
+
@instances_by_name[key] ||= new(name)
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
data/lib/head_music/note.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Note is like a placement, except:
|
2
4
|
# - always has a pitch
|
3
5
|
# - doesn't require voice and position
|
@@ -20,6 +22,10 @@ class HeadMusic::Note
|
|
20
22
|
end
|
21
23
|
|
22
24
|
def method_missing(method_name, *args, &block)
|
23
|
-
placement.send(method_name, *args, &block)
|
25
|
+
respond_to_missing?(method_name) ? placement.send(method_name, *args, &block) : super
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to_missing?(method_name, *_args)
|
29
|
+
placement.respond_to?(method_name)
|
24
30
|
end
|
25
31
|
end
|
data/lib/head_music/octave.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The Octave identifier is a number used in scientific pitch notation.
|
1
4
|
class HeadMusic::Octave
|
2
5
|
include Comparable
|
3
6
|
|
@@ -9,17 +12,16 @@ class HeadMusic::Octave
|
|
9
12
|
|
10
13
|
def self.from_number(identifier)
|
11
14
|
return nil unless identifier.to_s == identifier.to_i.to_s
|
12
|
-
return nil unless (-2..12).
|
15
|
+
return nil unless (-2..12).cover?(identifier.to_i)
|
13
16
|
@octaves ||= {}
|
14
17
|
@octaves[identifier.to_i] ||= new(identifier.to_i)
|
15
18
|
end
|
16
19
|
|
17
20
|
def self.from_name(string)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
21
|
+
return unless string.to_s.match?(HeadMusic::Spelling::MATCHER)
|
22
|
+
_letter, _sign, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
|
23
|
+
@octaves ||= {}
|
24
|
+
@octaves[octave_string.to_i] ||= new(octave_string.to_i) if octave_string
|
23
25
|
end
|
24
26
|
|
25
27
|
def self.default
|
@@ -34,7 +36,7 @@ class HeadMusic::Octave
|
|
34
36
|
end
|
35
37
|
|
36
38
|
def <=>(other)
|
37
|
-
|
39
|
+
to_i <=> other.to_i
|
38
40
|
end
|
39
41
|
|
40
42
|
private_class_method :new
|
data/lib/head_music/pitch.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A pitch is a named frequency represented by a spelling and an octive.
|
1
4
|
class HeadMusic::Pitch
|
2
5
|
include Comparable
|
3
6
|
|
@@ -15,6 +18,10 @@ class HeadMusic::Pitch
|
|
15
18
|
from_name(value) || from_number(value)
|
16
19
|
end
|
17
20
|
|
21
|
+
def self.middle_c
|
22
|
+
get('C4')
|
23
|
+
end
|
24
|
+
|
18
25
|
def self.from_name(name)
|
19
26
|
return nil unless name == name.to_s
|
20
27
|
fetch_or_create(HeadMusic::Spelling.get(name), HeadMusic::Octave.get(name).to_i)
|
@@ -29,21 +36,24 @@ class HeadMusic::Pitch
|
|
29
36
|
|
30
37
|
def self.from_number_and_letter(number, letter_name)
|
31
38
|
letter_name = HeadMusic::LetterName.get(letter_name)
|
32
|
-
natural_letter_pitch =
|
33
|
-
natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i).to_i >= 11
|
34
|
-
natural_letter_pitch = get(natural_letter_pitch)
|
39
|
+
natural_letter_pitch = natural_letter_pitch(number, letter_name)
|
35
40
|
sign_interval = natural_letter_pitch.smallest_interval_to(HeadMusic::PitchClass.get(number))
|
36
41
|
sign = HeadMusic::Sign.by(:semitones, sign_interval) if sign_interval != 0
|
37
42
|
spelling = HeadMusic::Spelling.fetch_or_create(letter_name, sign)
|
38
43
|
fetch_or_create(spelling, natural_letter_pitch.octave)
|
39
44
|
end
|
40
45
|
|
46
|
+
def self.natural_letter_pitch(number, letter_name)
|
47
|
+
natural_letter_pitch = get(HeadMusic::LetterName.get(letter_name).pitch_class)
|
48
|
+
natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i).to_i >= 11
|
49
|
+
get(natural_letter_pitch)
|
50
|
+
end
|
51
|
+
|
41
52
|
def self.fetch_or_create(spelling, octave)
|
53
|
+
return unless spelling && (-1..9).cover?(octave)
|
42
54
|
@pitches ||= {}
|
43
|
-
|
44
|
-
|
45
|
-
@pitches[hash_key] ||= new(spelling, octave)
|
46
|
-
end
|
55
|
+
hash_key = [spelling, octave].join
|
56
|
+
@pitches[hash_key] ||= new(spelling, octave)
|
47
57
|
end
|
48
58
|
|
49
59
|
def initialize(spelling, octave)
|
@@ -59,8 +69,8 @@ class HeadMusic::Pitch
|
|
59
69
|
(octave + 1) * 12 + letter_name.pitch_class.to_i + sign_semitones.to_i
|
60
70
|
end
|
61
71
|
|
62
|
-
|
63
|
-
|
72
|
+
alias midi midi_note_number
|
73
|
+
alias number midi_note_number
|
64
74
|
|
65
75
|
def to_s
|
66
76
|
name
|
@@ -71,34 +81,34 @@ class HeadMusic::Pitch
|
|
71
81
|
end
|
72
82
|
|
73
83
|
def natural
|
74
|
-
HeadMusic::Pitch.get(
|
84
|
+
HeadMusic::Pitch.get(to_s.gsub(/[#b]/, ''))
|
75
85
|
end
|
76
86
|
|
77
87
|
def enharmonic?(other)
|
78
|
-
|
88
|
+
midi_note_number == other.midi_note_number
|
79
89
|
end
|
80
90
|
|
81
|
-
def +(
|
82
|
-
HeadMusic::Pitch.get(
|
91
|
+
def +(other)
|
92
|
+
HeadMusic::Pitch.get(to_i + other.to_i)
|
83
93
|
end
|
84
94
|
|
85
|
-
def -(
|
86
|
-
if
|
95
|
+
def -(other)
|
96
|
+
if other.is_a?(HeadMusic::Pitch)
|
87
97
|
# return an interval
|
88
|
-
HeadMusic::Interval.get(
|
98
|
+
HeadMusic::Interval.get(to_i - other.to_i)
|
89
99
|
else
|
90
100
|
# assume value represents an interval in semitones and return another pitch
|
91
|
-
HeadMusic::Pitch.get(
|
101
|
+
HeadMusic::Pitch.get(to_i - other.to_i)
|
92
102
|
end
|
93
103
|
end
|
94
104
|
|
95
|
-
def ==(
|
96
|
-
other = HeadMusic::Pitch.get(
|
105
|
+
def ==(other)
|
106
|
+
other = HeadMusic::Pitch.get(other)
|
97
107
|
to_s == other.to_s
|
98
108
|
end
|
99
109
|
|
100
110
|
def <=>(other)
|
101
|
-
|
111
|
+
midi_note_number <=> other.midi_note_number
|
102
112
|
end
|
103
113
|
|
104
114
|
def scale(scale_type_name = nil)
|
@@ -106,16 +116,33 @@ class HeadMusic::Pitch
|
|
106
116
|
end
|
107
117
|
|
108
118
|
def natural_steps(num_steps)
|
109
|
-
target_letter_name
|
110
|
-
|
111
|
-
|
112
|
-
|
119
|
+
HeadMusic::Pitch.get([target_letter_name(num_steps), octave + octaves_delta(num_steps)].join)
|
120
|
+
end
|
121
|
+
|
122
|
+
private_class_method :new
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def octaves_delta(num_steps)
|
127
|
+
octaves_delta = (num_steps.abs / 7) * (num_steps >= 0 ? 1 : -1)
|
128
|
+
if wrapped_down?(num_steps)
|
113
129
|
octaves_delta -= 1
|
114
|
-
elsif num_steps
|
130
|
+
elsif wrapped_up?(num_steps)
|
115
131
|
octaves_delta += 1
|
116
132
|
end
|
117
|
-
|
133
|
+
octaves_delta
|
118
134
|
end
|
119
135
|
|
120
|
-
|
136
|
+
def wrapped_down?(num_steps)
|
137
|
+
num_steps.negative? && target_letter_name(num_steps).position > letter_name.position
|
138
|
+
end
|
139
|
+
|
140
|
+
def wrapped_up?(num_steps)
|
141
|
+
num_steps.positive? && target_letter_name(num_steps).position < letter_name.position
|
142
|
+
end
|
143
|
+
|
144
|
+
def target_letter_name(num_steps)
|
145
|
+
@target_letter_name ||= {}
|
146
|
+
@target_letter_name[num_steps] ||= letter_name.steps(num_steps)
|
147
|
+
end
|
121
148
|
end
|
@@ -1,18 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A pitch class is a set of notes separated by octaves.
|
1
4
|
class HeadMusic::PitchClass
|
2
5
|
attr_reader :number
|
3
6
|
|
4
|
-
SHARP_SPELLINGS = %w[C C# D D# E F F# G G# A A# B]
|
5
|
-
FLAT_SPELLINGS = %w[C Db D Eb E F Gb G Ab A Bb B]
|
7
|
+
SHARP_SPELLINGS = %w[C C# D D# E F F# G G# A A# B].freeze
|
8
|
+
FLAT_SPELLINGS = %w[C Db D Eb E F Gb G Ab A Bb B].freeze
|
6
9
|
|
7
10
|
def self.get(identifier)
|
8
11
|
@pitch_classes ||= {}
|
9
|
-
number = HeadMusic::Spelling.get(identifier).pitch_class.to_i if HeadMusic::Spelling.
|
12
|
+
number = HeadMusic::Spelling.get(identifier).pitch_class.to_i if HeadMusic::Spelling.matching_string(identifier)
|
10
13
|
number ||= identifier.to_i % 12
|
11
14
|
@pitch_classes[number] ||= new(number)
|
12
15
|
end
|
13
16
|
|
14
17
|
class << self
|
15
|
-
|
18
|
+
alias [] get
|
16
19
|
end
|
17
20
|
|
18
21
|
def initialize(pitch_class_or_midi_number)
|
@@ -23,22 +26,24 @@ class HeadMusic::PitchClass
|
|
23
26
|
number
|
24
27
|
end
|
25
28
|
|
26
|
-
|
27
|
-
|
29
|
+
# Pass in the number of semitones
|
30
|
+
def +(other)
|
31
|
+
HeadMusic::PitchClass.get(to_i + other.to_i)
|
28
32
|
end
|
29
33
|
|
30
|
-
|
31
|
-
|
34
|
+
# Pass in the number of semitones
|
35
|
+
def -(other)
|
36
|
+
HeadMusic::PitchClass.get(to_i - other.to_i)
|
32
37
|
end
|
33
38
|
|
34
|
-
def ==(
|
35
|
-
to_i ==
|
39
|
+
def ==(other)
|
40
|
+
to_i == other.to_i
|
36
41
|
end
|
37
|
-
|
42
|
+
alias enharmonic? ==
|
38
43
|
|
39
44
|
def intervals_to(other)
|
40
45
|
delta = other.to_i - to_i
|
41
|
-
inverse = delta
|
46
|
+
inverse = delta.positive? ? delta - 12 : delta + 12
|
42
47
|
[delta, inverse].sort_by(&:abs).map { |interval| HeadMusic::Interval.get(interval) }
|
43
48
|
end
|
44
49
|
|
data/lib/head_music/placement.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A placement is a note or rest at a position within a voice in a composition
|
1
4
|
class HeadMusic::Placement
|
2
5
|
include Comparable
|
3
6
|
|
@@ -22,13 +25,11 @@ class HeadMusic::Placement
|
|
22
25
|
end
|
23
26
|
|
24
27
|
def <=>(other)
|
25
|
-
|
28
|
+
position <=> other.position
|
26
29
|
end
|
27
30
|
|
28
31
|
def during?(other_placement)
|
29
|
-
(other_placement
|
30
|
-
(other_placement.next_position > position && other_placement.next_position <= next_position) ||
|
31
|
-
(other_placement.position <= position && other_placement.next_position >= next_position)
|
32
|
+
starts_during?(other_placement) || ends_during?(other_placement) || within?(other_placement)
|
32
33
|
end
|
33
34
|
|
34
35
|
def to_s
|
@@ -37,6 +38,18 @@ class HeadMusic::Placement
|
|
37
38
|
|
38
39
|
private
|
39
40
|
|
41
|
+
def starts_during?(other_placement)
|
42
|
+
(other_placement.next_position > position && other_placement.next_position <= next_position)
|
43
|
+
end
|
44
|
+
|
45
|
+
def ends_during?(other_placement)
|
46
|
+
(other_placement.position >= position && other_placement.position < next_position)
|
47
|
+
end
|
48
|
+
|
49
|
+
def within?(other_placement)
|
50
|
+
(other_placement.position <= position && other_placement.next_position >= next_position)
|
51
|
+
end
|
52
|
+
|
40
53
|
def ensure_attributes(voice, position, rhythmic_value, pitch)
|
41
54
|
@voice = voice
|
42
55
|
ensure_position(position)
|
@@ -45,10 +58,10 @@ class HeadMusic::Placement
|
|
45
58
|
end
|
46
59
|
|
47
60
|
def ensure_position(position)
|
48
|
-
if position.is_a?(HeadMusic::Position)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
61
|
+
@position = if position.is_a?(HeadMusic::Position)
|
62
|
+
position
|
63
|
+
else
|
64
|
+
HeadMusic::Position.new(composition, position)
|
65
|
+
end
|
53
66
|
end
|
54
67
|
end
|
data/lib/head_music/position.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A position is a moment in time within the rhythmic framework of a composition.
|
1
4
|
class HeadMusic::Position
|
2
5
|
include Comparable
|
3
6
|
|
@@ -35,10 +38,8 @@ class HeadMusic::Position
|
|
35
38
|
end
|
36
39
|
|
37
40
|
def <=>(other)
|
38
|
-
if other.is_a?(String) && other =~ /\D/
|
39
|
-
|
40
|
-
end
|
41
|
-
self.values <=> other.values
|
41
|
+
other = self.class.new(composition, other) if other.is_a?(String) && other =~ /\D/
|
42
|
+
values <=> other.values
|
42
43
|
end
|
43
44
|
|
44
45
|
def strength
|
@@ -53,11 +54,9 @@ class HeadMusic::Position
|
|
53
54
|
!strong?
|
54
55
|
end
|
55
56
|
|
56
|
-
def +(
|
57
|
-
if [HeadMusic::RhythmicUnit, Symbol, String].include?(
|
58
|
-
|
59
|
-
end
|
60
|
-
self.class.new(composition, bar_number, count, tick + rhythmic_value.ticks)
|
57
|
+
def +(other)
|
58
|
+
other = HeadMusic::RhythmicValue.new(other) if [HeadMusic::RhythmicUnit, Symbol, String].include?(other.class)
|
59
|
+
self.class.new(composition, bar_number, count, tick + other.ticks)
|
61
60
|
end
|
62
61
|
|
63
62
|
def start_of_next_bar
|