head_music 0.26.3 → 0.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +1 -1
- data/.rubocop.yml +11 -37
- data/.ruby-version +1 -1
- data/.travis.yml +1 -1
- data/Gemfile +8 -6
- data/Rakefile +5 -4
- data/bin/console +3 -3
- data/head_music.gemspec +21 -22
- data/lib/head_music/chromatic_interval.rb +3 -3
- data/lib/head_music/circle.rb +18 -14
- data/lib/head_music/clef.rb +3 -3
- data/lib/head_music/content/bar.rb +1 -1
- data/lib/head_music/content/composition.rb +1 -1
- data/lib/head_music/content/note.rb +1 -1
- data/lib/head_music/content/placement.rb +5 -5
- data/lib/head_music/content/position.rb +3 -3
- data/lib/head_music/content/rhythmic_value.rb +8 -8
- data/lib/head_music/content/voice.rb +1 -1
- data/lib/head_music/diatonic_interval.rb +41 -43
- data/lib/head_music/grand_staff.rb +8 -8
- data/lib/head_music/instrument.rb +8 -8
- data/lib/head_music/interval_cycle.rb +10 -12
- data/lib/head_music/key_signature.rb +12 -13
- data/lib/head_music/letter_name.rb +7 -7
- data/lib/head_music/melodic_interval.rb +1 -1
- data/lib/head_music/meter.rb +17 -12
- data/lib/head_music/named.rb +3 -3
- data/lib/head_music/pitch/enharmonic_equivalence.rb +2 -2
- data/lib/head_music/pitch/octave_equivalence.rb +1 -1
- data/lib/head_music/pitch.rb +24 -9
- data/lib/head_music/pitch_class.rb +18 -4
- data/lib/head_music/pitch_class_set.rb +5 -5
- data/lib/head_music/pitch_set.rb +9 -6
- data/lib/head_music/quality.rb +15 -14
- data/lib/head_music/reference_pitch.rb +23 -23
- data/lib/head_music/register.rb +13 -0
- data/lib/head_music/rhythmic_unit.rb +9 -9
- data/lib/head_music/scale.rb +4 -4
- data/lib/head_music/scale_degree.rb +2 -2
- data/lib/head_music/scale_type.rb +13 -13
- data/lib/head_music/sign.rb +12 -11
- data/lib/head_music/solmization.rb +3 -3
- data/lib/head_music/sonority.rb +2 -2
- data/lib/head_music/spelling.rb +4 -6
- data/lib/head_music/staff.rb +1 -1
- data/lib/head_music/style/analysis.rb +1 -1
- data/lib/head_music/style/annotation.rb +7 -7
- data/lib/head_music/style/guidelines/always_move.rb +4 -4
- data/lib/head_music/style/guidelines/approach_perfection_contrarily.rb +1 -1
- data/lib/head_music/style/guidelines/at_least_eight_notes.rb +3 -3
- data/lib/head_music/style/guidelines/avoid_crossing_voices.rb +6 -6
- data/lib/head_music/style/guidelines/avoid_overlapping_voices.rb +1 -1
- data/lib/head_music/style/guidelines/consonant_climax.rb +1 -1
- data/lib/head_music/style/guidelines/consonant_downbeats.rb +1 -1
- data/lib/head_music/style/guidelines/diatonic.rb +2 -2
- data/lib/head_music/style/guidelines/end_on_perfect_consonance.rb +1 -1
- data/lib/head_music/style/guidelines/end_on_tonic.rb +1 -1
- data/lib/head_music/style/guidelines/frequent_direction_changes.rb +1 -1
- data/lib/head_music/style/guidelines/limit_octave_leaps.rb +1 -1
- data/lib/head_music/style/guidelines/moderate_direction_changes.rb +1 -1
- data/lib/head_music/style/guidelines/mostly_conjunct.rb +4 -4
- data/lib/head_music/style/guidelines/no_rests.rb +1 -1
- data/lib/head_music/style/guidelines/no_unisons_in_middle.rb +1 -1
- data/lib/head_music/style/guidelines/notes_same_length.rb +4 -4
- data/lib/head_music/style/guidelines/one_to_one.rb +1 -1
- data/lib/head_music/style/guidelines/prefer_contrary_motion.rb +1 -1
- data/lib/head_music/style/guidelines/prefer_imperfect.rb +1 -1
- data/lib/head_music/style/guidelines/prepare_octave_leaps.rb +1 -1
- data/lib/head_music/style/guidelines/recover_large_leaps.rb +1 -1
- data/lib/head_music/style/guidelines/singable_intervals.rb +1 -1
- data/lib/head_music/style/guidelines/singable_range.rb +1 -1
- data/lib/head_music/style/guidelines/single_large_leaps.rb +1 -1
- data/lib/head_music/style/guidelines/start_on_perfect_consonance.rb +1 -1
- data/lib/head_music/style/guidelines/start_on_tonic.rb +1 -1
- data/lib/head_music/style/guidelines/step_down_to_final_note.rb +2 -2
- data/lib/head_music/style/guidelines/step_out_of_unison.rb +1 -1
- data/lib/head_music/style/guidelines/step_to_final_note.rb +2 -2
- data/lib/head_music/style/guidelines/step_up_to_final_note.rb +2 -2
- data/lib/head_music/style/guidelines/up_to_fourteen_notes.rb +2 -2
- data/lib/head_music/style/guides/first_species_harmony.rb +9 -1
- data/lib/head_music/style/guides/first_species_melody.rb +1 -1
- data/lib/head_music/style/guides/fux_cantus_firmus.rb +1 -1
- data/lib/head_music/style/guides/modern_cantus_firmus.rb +1 -1
- data/lib/head_music/style/mark.rb +1 -1
- data/lib/head_music/utilities/hash_key.rb +1 -1
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +91 -91
- metadata +6 -20
@@ -4,7 +4,7 @@
|
|
4
4
|
class HeadMusic::Instrument
|
5
5
|
include HeadMusic::Named
|
6
6
|
|
7
|
-
INSTRUMENTS = YAML.load_file(File.expand_path(
|
7
|
+
INSTRUMENTS = YAML.load_file(File.expand_path("data/instruments.yml", __dir__)).freeze
|
8
8
|
|
9
9
|
def self.get(name)
|
10
10
|
return get_by_name(name) if get_by_name(name)
|
@@ -59,20 +59,20 @@ class HeadMusic::Instrument
|
|
59
59
|
|
60
60
|
def record_for_key(key)
|
61
61
|
INSTRUMENTS.each do |name_key, data|
|
62
|
-
return data.merge!(
|
62
|
+
return data.merge!("name_key" => name_key) if name_key.to_s == key.to_s
|
63
63
|
end
|
64
64
|
nil
|
65
65
|
end
|
66
66
|
|
67
67
|
def initialize_data_from_record(record)
|
68
|
-
@name_key = record[
|
69
|
-
@family = record[
|
70
|
-
@standard_staves = record[
|
71
|
-
@classifications = record[
|
72
|
-
self.name = I18n.translate(name_key, scope:
|
68
|
+
@name_key = record["name_key"].to_sym
|
69
|
+
@family = record["family"]
|
70
|
+
@standard_staves = record["standard_staves"] || []
|
71
|
+
@classifications = record["classifications"] || []
|
72
|
+
self.name = I18n.translate(name_key, scope: "instruments", locale: "en", default: inferred_name)
|
73
73
|
end
|
74
74
|
|
75
75
|
def inferred_name
|
76
|
-
name_key.to_s.
|
76
|
+
name_key.to_s.tr("_", " ")
|
77
77
|
end
|
78
78
|
end
|
@@ -6,12 +6,12 @@ class HeadMusic::IntervalCycle
|
|
6
6
|
|
7
7
|
def self.get(interval = 7)
|
8
8
|
@interval_cycles ||= {}
|
9
|
-
interval = interval.to_s.gsub(/^C/i,
|
9
|
+
interval = interval.to_s.gsub(/^C/i, "").to_i
|
10
10
|
interval = HeadMusic::ChromaticInterval.get(interval)
|
11
|
-
@interval_cycles[interval.to_i] ||= new(interval: interval, starting_pitch:
|
11
|
+
@interval_cycles[interval.to_i] ||= new(interval: interval, starting_pitch: "C4")
|
12
12
|
end
|
13
13
|
|
14
|
-
def initialize(interval:, starting_pitch:
|
14
|
+
def initialize(interval:, starting_pitch: "C4")
|
15
15
|
@interval = interval if interval.is_a?(HeadMusic::DiatonicInterval)
|
16
16
|
@interval ||= interval if interval.is_a?(HeadMusic::ChromaticInterval)
|
17
17
|
@interval ||= HeadMusic::ChromaticInterval.get(interval) if interval.to_s.match?(/\d/)
|
@@ -38,15 +38,13 @@ class HeadMusic::IntervalCycle
|
|
38
38
|
protected
|
39
39
|
|
40
40
|
def pitches_up
|
41
|
-
@pitches_up ||=
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
list << next_pitch
|
49
|
-
end
|
41
|
+
@pitches_up ||= [starting_pitch].tap do |list|
|
42
|
+
loop do
|
43
|
+
next_pitch = list.last + interval
|
44
|
+
next_pitch -= octave while next_pitch - starting_pitch > 12
|
45
|
+
break if next_pitch.pitch_class == list.first.pitch_class
|
46
|
+
|
47
|
+
list << next_pitch
|
50
48
|
end
|
51
49
|
end
|
52
50
|
end
|
@@ -1,16 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Represents a key signature.
|
4
|
+
# In French, sharps and flats in the key signature are called "altérations".
|
4
5
|
class HeadMusic::KeySignature
|
5
|
-
attr_reader :tonic_spelling
|
6
|
-
attr_reader :scale_type
|
7
|
-
attr_reader :scale
|
6
|
+
attr_reader :tonic_spelling, :scale_type, :scale
|
8
7
|
|
9
8
|
ORDERED_LETTER_NAMES_OF_SHARPS = %w[F C G D A E B].freeze
|
10
9
|
ORDERED_LETTER_NAMES_OF_FLATS = ORDERED_LETTER_NAMES_OF_SHARPS.reverse.freeze
|
11
10
|
|
12
11
|
def self.default
|
13
|
-
@default ||= new(
|
12
|
+
@default ||= new("C", :major)
|
14
13
|
end
|
15
14
|
|
16
15
|
def self.get(identifier)
|
@@ -18,7 +17,7 @@ class HeadMusic::KeySignature
|
|
18
17
|
|
19
18
|
@key_signatures ||= {}
|
20
19
|
tonic_spelling, scale_type_name = identifier.strip.split(/\s/)
|
21
|
-
hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/,
|
20
|
+
hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, " sharp").gsub(/(\w)[b♭]/, '\\1 flat'))
|
22
21
|
@key_signatures[hash_key] ||= new(tonic_spelling, scale_type_name)
|
23
22
|
end
|
24
23
|
|
@@ -74,11 +73,11 @@ class HeadMusic::KeySignature
|
|
74
73
|
flats.any? ? flats : sharps
|
75
74
|
end
|
76
75
|
|
77
|
-
|
78
|
-
|
76
|
+
alias_method :sharps_and_flats, :signs
|
77
|
+
alias_method :accidentals, :signs
|
79
78
|
|
80
79
|
def name
|
81
|
-
[tonic_spelling, scale_type].join(
|
80
|
+
[tonic_spelling, scale_type].join(" ")
|
82
81
|
end
|
83
82
|
|
84
83
|
def ==(other)
|
@@ -87,11 +86,11 @@ class HeadMusic::KeySignature
|
|
87
86
|
|
88
87
|
def to_s
|
89
88
|
if sharps.any?
|
90
|
-
sharps.length == 1 ?
|
89
|
+
(sharps.length == 1) ? "1 sharp" : "#{sharps.length} sharps"
|
91
90
|
elsif flats.any?
|
92
|
-
flats.length == 1 ?
|
91
|
+
(flats.length == 1) ? "1 flat" : "#{flats.length} flats"
|
93
92
|
else
|
94
|
-
|
93
|
+
"no sharps or flats"
|
95
94
|
end
|
96
95
|
end
|
97
96
|
|
@@ -125,8 +124,8 @@ class HeadMusic::KeySignature
|
|
125
124
|
(key_signature.signs | other.signs).map(&:to_s).uniq.length == 12
|
126
125
|
end
|
127
126
|
|
128
|
-
|
129
|
-
|
127
|
+
alias_method :enharmonic?, :enharmonic_equivalent?
|
128
|
+
alias_method :equivalent?, :enharmonic_equivalent?
|
130
129
|
|
131
130
|
private_class_method :new
|
132
131
|
end
|
@@ -5,13 +5,13 @@ class HeadMusic::LetterName
|
|
5
5
|
NAMES = %w[C D E F G A B].freeze
|
6
6
|
|
7
7
|
NATURAL_PITCH_CLASS_NUMBERS = {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
"C" => 0,
|
9
|
+
"D" => 2,
|
10
|
+
"E" => 4,
|
11
|
+
"F" => 5,
|
12
|
+
"G" => 7,
|
13
|
+
"A" => 9,
|
14
|
+
"B" => 11
|
15
15
|
}.freeze
|
16
16
|
|
17
17
|
def self.all
|
data/lib/head_music/meter.rb
CHANGED
@@ -5,8 +5,8 @@ class HeadMusic::Meter
|
|
5
5
|
attr_reader :top_number, :bottom_number
|
6
6
|
|
7
7
|
NAMED = {
|
8
|
-
common_time:
|
9
|
-
cut_time:
|
8
|
+
common_time: "4/4",
|
9
|
+
cut_time: "2/2"
|
10
10
|
}.freeze
|
11
11
|
|
12
12
|
def self.get(identifier)
|
@@ -14,11 +14,11 @@ class HeadMusic::Meter
|
|
14
14
|
hash_key = HeadMusic::Utilities::HashKey.for(identifier)
|
15
15
|
time_signature_string = NAMED[hash_key] || identifier
|
16
16
|
@meters ||= {}
|
17
|
-
@meters[hash_key] ||= new(*time_signature_string.split(
|
17
|
+
@meters[hash_key] ||= new(*time_signature_string.split("/").map(&:to_i))
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.default
|
21
|
-
get(
|
21
|
+
get("4/4")
|
22
22
|
end
|
23
23
|
|
24
24
|
def self.common_time
|
@@ -39,7 +39,7 @@ class HeadMusic::Meter
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def compound?
|
42
|
-
top_number > 3 && top_number
|
42
|
+
top_number > 3 && (top_number % 3).zero?
|
43
43
|
end
|
44
44
|
|
45
45
|
def duple?
|
@@ -89,7 +89,7 @@ class HeadMusic::Meter
|
|
89
89
|
end
|
90
90
|
|
91
91
|
def to_s
|
92
|
-
[top_number, bottom_number].join(
|
92
|
+
[top_number, bottom_number].join("/")
|
93
93
|
end
|
94
94
|
|
95
95
|
def ==(other)
|
@@ -97,10 +97,8 @@ class HeadMusic::Meter
|
|
97
97
|
end
|
98
98
|
|
99
99
|
def strong_counts
|
100
|
-
@strong_counts ||=
|
101
|
-
(
|
102
|
-
downbeat?(count) || strong_beat_in_duple?(count) || strong_beat_in_triple?(count)
|
103
|
-
end
|
100
|
+
@strong_counts ||= (1..counts_per_bar).select do |count|
|
101
|
+
downbeat?(count) || strong_beat_in_duple?(count) || strong_beat_in_triple?(count)
|
104
102
|
end
|
105
103
|
end
|
106
104
|
|
@@ -119,11 +117,18 @@ class HeadMusic::Meter
|
|
119
117
|
end
|
120
118
|
|
121
119
|
def strong_beat_in_duple?(count, tick = 0)
|
122
|
-
beat?(tick)
|
120
|
+
return false unless beat?(tick)
|
121
|
+
return false unless counts_per_bar.even?
|
122
|
+
|
123
|
+
(count - 1) == counts_per_bar / 2
|
123
124
|
end
|
124
125
|
|
125
126
|
def strong_beat_in_triple?(count, tick = 0)
|
126
|
-
beat?(tick)
|
127
|
+
return false unless beat?(tick)
|
128
|
+
return false unless (counts_per_bar % 3).zero?
|
129
|
+
return false if counts_per_bar < 6
|
130
|
+
|
131
|
+
count % 3 == 1
|
127
132
|
end
|
128
133
|
|
129
134
|
def beat?(tick)
|
data/lib/head_music/named.rb
CHANGED
@@ -16,10 +16,10 @@ module HeadMusic::Named
|
|
16
16
|
|
17
17
|
def self.get(code)
|
18
18
|
@locales ||= {}
|
19
|
-
parts = code.to_s.split(/[_
|
19
|
+
parts = code.to_s.split(/[_-]/)
|
20
20
|
language = parts[0].downcase
|
21
21
|
region = parts[1]&.upcase
|
22
|
-
key = [language, region].compact.join(
|
22
|
+
key = [language, region].compact.join("_").to_sym
|
23
23
|
@locales[key] ||= new(language: language, region: region)
|
24
24
|
end
|
25
25
|
|
@@ -29,7 +29,7 @@ module HeadMusic::Named
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def code
|
32
|
-
@code ||= [@language, @region].compact.join(
|
32
|
+
@code ||= [@language, @region].compact.join("_")
|
33
33
|
end
|
34
34
|
|
35
35
|
private_class_method :new
|
@@ -21,8 +21,8 @@ class HeadMusic::Pitch::EnharmonicEquivalence
|
|
21
21
|
pitch.midi_note_number == other.midi_note_number && pitch.spelling != other.spelling
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
24
|
+
alias_method :enharmonic?, :enharmonic_equivalent?
|
25
|
+
alias_method :equivalent?, :enharmonic_equivalent?
|
26
26
|
|
27
27
|
private_class_method :new
|
28
28
|
end
|
data/lib/head_music/pitch.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# A pitch is a named frequency represented by a spelling and
|
3
|
+
# A pitch is a named frequency represented by a spelling and a register.
|
4
4
|
class HeadMusic::Pitch
|
5
5
|
include Comparable
|
6
6
|
|
@@ -34,11 +34,11 @@ class HeadMusic::Pitch
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def self.middle_c
|
37
|
-
get(
|
37
|
+
get("C4")
|
38
38
|
end
|
39
39
|
|
40
40
|
def self.concert_a
|
41
|
-
get(
|
41
|
+
get("A4")
|
42
42
|
end
|
43
43
|
|
44
44
|
def self.from_pitch_class(pitch_class)
|
@@ -97,8 +97,8 @@ class HeadMusic::Pitch
|
|
97
97
|
(register + 1) * 12 + letter_name.pitch_class.to_i + sign_semitones.to_i
|
98
98
|
end
|
99
99
|
|
100
|
-
|
101
|
-
|
100
|
+
alias_method :midi, :midi_note_number
|
101
|
+
alias_method :number, :midi_note_number
|
102
102
|
|
103
103
|
def to_s
|
104
104
|
name
|
@@ -108,8 +108,12 @@ class HeadMusic::Pitch
|
|
108
108
|
midi_note_number
|
109
109
|
end
|
110
110
|
|
111
|
+
def helmholtz_notation
|
112
|
+
helmholtz_letter_name + helmholtz_marks
|
113
|
+
end
|
114
|
+
|
111
115
|
def natural
|
112
|
-
HeadMusic::Pitch.get(to_s.gsub(HeadMusic::Sign.matcher,
|
116
|
+
HeadMusic::Pitch.get(to_s.gsub(HeadMusic::Sign.matcher, ""))
|
113
117
|
end
|
114
118
|
|
115
119
|
def +(other)
|
@@ -123,10 +127,11 @@ class HeadMusic::Pitch
|
|
123
127
|
end
|
124
128
|
|
125
129
|
def -(other)
|
126
|
-
|
130
|
+
case other
|
131
|
+
when HeadMusic::DiatonicInterval
|
127
132
|
# return a pitch
|
128
133
|
other.below(self)
|
129
|
-
|
134
|
+
when HeadMusic::Pitch
|
130
135
|
# return an interval
|
131
136
|
HeadMusic::ChromaticInterval.get(to_i - other.to_i)
|
132
137
|
else
|
@@ -190,7 +195,7 @@ class HeadMusic::Pitch
|
|
190
195
|
end
|
191
196
|
|
192
197
|
def octaves_delta(num_steps)
|
193
|
-
octaves_delta = (num_steps.abs / 7) * (num_steps >= 0 ? 1 : -1)
|
198
|
+
octaves_delta = (num_steps.abs / 7) * ((num_steps >= 0) ? 1 : -1)
|
194
199
|
if wrapped_down?(num_steps)
|
195
200
|
octaves_delta -= 1
|
196
201
|
elsif wrapped_up?(num_steps)
|
@@ -211,4 +216,14 @@ class HeadMusic::Pitch
|
|
211
216
|
@target_letter_name ||= {}
|
212
217
|
@target_letter_name[num_steps] ||= letter_name.steps_up(num_steps)
|
213
218
|
end
|
219
|
+
|
220
|
+
def helmholtz_letter_name
|
221
|
+
return spelling.to_s.downcase if HeadMusic::Register.get(register).helmholtz_case == :lower
|
222
|
+
|
223
|
+
spelling.to_s
|
224
|
+
end
|
225
|
+
|
226
|
+
def helmholtz_marks
|
227
|
+
HeadMusic::Register.get(register).helmholtz_marks
|
228
|
+
end
|
214
229
|
end
|
@@ -4,11 +4,11 @@
|
|
4
4
|
class HeadMusic::PitchClass
|
5
5
|
include Comparable
|
6
6
|
|
7
|
-
attr_reader :number
|
8
|
-
attr_reader :spelling
|
7
|
+
attr_reader :number, :spelling
|
9
8
|
|
10
9
|
SHARP_SPELLINGS = %w[C C♯ D D♯ E F F♯ G G♯ A A♯ B].freeze
|
11
10
|
FLAT_SPELLINGS = %w[C D♭ D E♭ E F G♭ G A♭ A B♭ B].freeze
|
11
|
+
FLATTER_SPELLINGS = %w[C D♭ D E♭ F♭ F G♭ G A♭ A B♭ C♭].freeze
|
12
12
|
INTEGER_NOTATION = %w[0 1 2 3 4 5 6 7 8 9 t e].freeze
|
13
13
|
|
14
14
|
def self.get(identifier)
|
@@ -22,7 +22,7 @@ class HeadMusic::PitchClass
|
|
22
22
|
end
|
23
23
|
|
24
24
|
class << self
|
25
|
-
|
25
|
+
alias_method :[], :get
|
26
26
|
end
|
27
27
|
|
28
28
|
def initialize(pitch_class_or_midi_number)
|
@@ -45,6 +45,20 @@ class HeadMusic::PitchClass
|
|
45
45
|
FLAT_SPELLINGS[number]
|
46
46
|
end
|
47
47
|
|
48
|
+
def flatter_spelling
|
49
|
+
FLATTER_SPELLINGS[number]
|
50
|
+
end
|
51
|
+
|
52
|
+
def smart_spelling(max_sharps_in_major_key_signature: 6)
|
53
|
+
sharp_key = HeadMusic::KeySignature.get(sharp_spelling)
|
54
|
+
return HeadMusic::Spelling.get(sharp_spelling) if sharp_key.num_sharps <= max_sharps_in_major_key_signature
|
55
|
+
|
56
|
+
flat_key = HeadMusic::KeySignature.get(flat_spelling)
|
57
|
+
return HeadMusic::Spelling.get(flat_spelling) if flat_key.num_sharps <= max_sharps_in_major_key_signature
|
58
|
+
|
59
|
+
HeadMusic::Spelling.get(flatter_spelling)
|
60
|
+
end
|
61
|
+
|
48
62
|
# Pass in the number of semitones
|
49
63
|
def +(other)
|
50
64
|
HeadMusic::PitchClass.get(to_i + other.to_i)
|
@@ -58,7 +72,7 @@ class HeadMusic::PitchClass
|
|
58
72
|
def ==(other)
|
59
73
|
to_i == other.to_i
|
60
74
|
end
|
61
|
-
|
75
|
+
alias_method :enharmonic?, :==
|
62
76
|
|
63
77
|
def <=>(other)
|
64
78
|
to_i <=> other.to_i
|
@@ -6,18 +6,18 @@ class HeadMusic::PitchClassSet
|
|
6
6
|
attr_reader :pitch_classes
|
7
7
|
|
8
8
|
delegate :empty?, to: :pitch_classes
|
9
|
-
|
9
|
+
alias_method :empty_set?, :empty?
|
10
10
|
|
11
11
|
def initialize(identifiers)
|
12
12
|
@pitch_classes = identifiers.map { |identifier| HeadMusic::PitchClass.get(identifier) }.uniq.sort
|
13
13
|
end
|
14
14
|
|
15
15
|
def inspect
|
16
|
-
pitch_classes.map(&:to_s).join(
|
16
|
+
pitch_classes.map(&:to_s).join(" ")
|
17
17
|
end
|
18
18
|
|
19
19
|
def to_s
|
20
|
-
pitch_classes.map(&:to_s).join(
|
20
|
+
pitch_classes.map(&:to_s).join(" ")
|
21
21
|
end
|
22
22
|
|
23
23
|
def ==(other)
|
@@ -35,12 +35,12 @@ class HeadMusic::PitchClassSet
|
|
35
35
|
def monochord?
|
36
36
|
pitch_classes.length == 1
|
37
37
|
end
|
38
|
-
|
38
|
+
alias_method :monad?, :monochord?
|
39
39
|
|
40
40
|
def dichord?
|
41
41
|
pitch_classes.length == 2
|
42
42
|
end
|
43
|
-
|
43
|
+
alias_method :dyad?, :dichord?
|
44
44
|
|
45
45
|
def trichord?
|
46
46
|
pitch_classes.length == 3
|
data/lib/head_music/pitch_set.rb
CHANGED
@@ -9,7 +9,7 @@ class HeadMusic::PitchSet
|
|
9
9
|
seventh_chord: [3, 5, 7],
|
10
10
|
ninth_chord: [2, 3, 5, 7],
|
11
11
|
eleventh_chord: [2, 3, 4, 5, 7],
|
12
|
-
thirteenth_chord: [2, 3, 4, 5, 6, 7]
|
12
|
+
thirteenth_chord: [2, 3, 4, 5, 6, 7] # a.k.a. diatonic scale
|
13
13
|
}.freeze
|
14
14
|
|
15
15
|
attr_reader :pitches
|
@@ -54,20 +54,23 @@ class HeadMusic::PitchSet
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def integer_notation
|
57
|
+
# questions:
|
58
|
+
# - should this be absolute? 0 = C?
|
59
|
+
# - should there be both absolute and relative versions?
|
57
60
|
@integer_notation ||= begin
|
58
61
|
return [] if pitches.empty?
|
59
|
-
diatonic_intervals_above_bass_pitch.map { |interval| interval.semitones % 12 }.flatten.sort.unshift(0)
|
62
|
+
diatonic_intervals_above_bass_pitch.map { |interval| interval.semitones % 12 }.flatten.sort.unshift(0).uniq
|
60
63
|
end
|
61
64
|
end
|
62
65
|
|
63
66
|
def invert
|
64
|
-
inverted_pitch = pitches[0] + HeadMusic::DiatonicInterval.get(
|
67
|
+
inverted_pitch = pitches[0] + HeadMusic::DiatonicInterval.get("perfect octave")
|
65
68
|
new_pitches = pitches.drop(1) + [inverted_pitch]
|
66
69
|
HeadMusic::PitchSet.new(new_pitches)
|
67
70
|
end
|
68
71
|
|
69
72
|
def uninvert
|
70
|
-
inverted_pitch = pitches[-1] - HeadMusic::DiatonicInterval.get(
|
73
|
+
inverted_pitch = pitches[-1] - HeadMusic::DiatonicInterval.get("perfect octave")
|
71
74
|
new_pitches = [inverted_pitch] + pitches[0..-2]
|
72
75
|
HeadMusic::PitchSet.new(new_pitches)
|
73
76
|
end
|
@@ -77,11 +80,11 @@ class HeadMusic::PitchSet
|
|
77
80
|
end
|
78
81
|
|
79
82
|
def inspect
|
80
|
-
pitches.map(&:to_s).join(
|
83
|
+
pitches.map(&:to_s).join(" ")
|
81
84
|
end
|
82
85
|
|
83
86
|
def to_s
|
84
|
-
pitches.map(&:to_s).join(
|
87
|
+
pitches.map(&:to_s).join(" ")
|
85
88
|
end
|
86
89
|
|
87
90
|
def ==(other)
|
data/lib/head_music/quality.rb
CHANGED
@@ -3,13 +3,13 @@
|
|
3
3
|
# A quality is a categorization of an interval.
|
4
4
|
class HeadMusic::Quality
|
5
5
|
SHORTHAND = {
|
6
|
-
perfect:
|
7
|
-
major:
|
8
|
-
minor:
|
9
|
-
diminished:
|
10
|
-
augmented:
|
11
|
-
doubly_diminished:
|
12
|
-
doubly_augmented:
|
6
|
+
perfect: "P",
|
7
|
+
major: "M",
|
8
|
+
minor: "m",
|
9
|
+
diminished: "d",
|
10
|
+
augmented: "A",
|
11
|
+
doubly_diminished: "dd",
|
12
|
+
doubly_augmented: "AA"
|
13
13
|
}.freeze
|
14
14
|
NAMES = SHORTHAND.keys
|
15
15
|
|
@@ -18,7 +18,7 @@ class HeadMusic::Quality
|
|
18
18
|
-1 => :diminished,
|
19
19
|
0 => :perfect,
|
20
20
|
1 => :augmented,
|
21
|
-
2 => :doubly_augmented
|
21
|
+
2 => :doubly_augmented
|
22
22
|
}.freeze
|
23
23
|
|
24
24
|
MAJOR_INTERVAL_MODIFICATION = {
|
@@ -26,7 +26,7 @@ class HeadMusic::Quality
|
|
26
26
|
-1 => :minor,
|
27
27
|
0 => :major,
|
28
28
|
1 => :augmented,
|
29
|
-
2 => :doubly_augmented
|
29
|
+
2 => :doubly_augmented
|
30
30
|
}.freeze
|
31
31
|
|
32
32
|
def self.get(identifier)
|
@@ -36,10 +36,11 @@ class HeadMusic::Quality
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def self.from(starting_quality, delta)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
case starting_quality
|
40
|
+
when :perfect
|
41
|
+
PERFECT_INTERVAL_MODIFICATION[delta].to_s.gsub(/_+/, " ")
|
42
|
+
when :major
|
43
|
+
MAJOR_INTERVAL_MODIFICATION[delta].to_s.gsub(/_+/, " ")
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
@@ -60,7 +61,7 @@ class HeadMusic::Quality
|
|
60
61
|
end
|
61
62
|
|
62
63
|
def article
|
63
|
-
%w[a e i o u h].include?(name.to_s.first) ?
|
64
|
+
%w[a e i o u h].include?(name.to_s.first) ? "an" : "a"
|
64
65
|
end
|
65
66
|
|
66
67
|
NAMES.each do |method_name|
|