head_music 17.2.0 → 17.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +1 -1
- data/lib/head_music/analysis/chord_analysis.rb +123 -0
- data/lib/head_music/analysis/circle.rb +11 -3
- data/lib/head_music/analysis/diatonic_interval.rb +14 -11
- data/lib/head_music/analysis/dyad/chord_implication.rb +101 -0
- data/lib/head_music/analysis/dyad.rb +24 -104
- data/lib/head_music/analysis/interval_cycle.rb +9 -3
- data/lib/head_music/analysis/pitch_collection.rb +33 -107
- data/lib/head_music/analysis/sonority.rb +11 -21
- data/lib/head_music/content/composition/hash_deserializer.rb +110 -0
- data/lib/head_music/content/composition/schema_values.rb +127 -0
- data/lib/head_music/content/composition.rb +0 -215
- data/lib/head_music/content/placement.rb +11 -42
- data/lib/head_music/content/sound_resolver.rb +54 -0
- data/lib/head_music/content/voice/melodic_line.rb +32 -0
- data/lib/head_music/content/voice/melodic_note_pair.rb +33 -0
- data/lib/head_music/content/voice.rb +7 -58
- data/lib/head_music/instruments/alternate_tuning.rb +4 -6
- data/lib/head_music/instruments/catalog_lookup.rb +18 -0
- data/lib/head_music/instruments/instrument.rb +17 -117
- data/lib/head_music/instruments/instrument_catalog.rb +48 -0
- data/lib/head_music/instruments/instrument_configuration.rb +4 -6
- data/lib/head_music/instruments/instrument_family.rb +3 -8
- data/lib/head_music/instruments/instrument_name.rb +49 -0
- data/lib/head_music/instruments/score_order.rb +52 -42
- data/lib/head_music/instruments/staff_profile.rb +71 -0
- data/lib/head_music/instruments/stringing.rb +4 -6
- data/lib/head_music/instruments/stringing_course.rb +4 -6
- data/lib/head_music/instruments/variant.rb +4 -6
- data/lib/head_music/named/locale.rb +32 -0
- data/lib/head_music/named/localized_name.rb +15 -0
- data/lib/head_music/named.rb +1 -45
- data/lib/head_music/notation/abc/body_lexer.rb +65 -24
- data/lib/head_music/notation/abc/duration_resolver.rb +11 -1
- data/lib/head_music/notation/abc/duration_writer.rb +1 -8
- data/lib/head_music/notation/abc/header.rb +27 -16
- data/lib/head_music/notation/abc/parser.rb +48 -159
- data/lib/head_music/notation/abc/preflight.rb +48 -0
- data/lib/head_music/notation/abc/voice_state.rb +161 -0
- data/lib/head_music/notation/abc/writer.rb +47 -16
- data/lib/head_music/notation/dotted_duration.rb +19 -0
- data/lib/head_music/notation/instrument_notation.rb +5 -7
- data/lib/head_music/notation/music_xml/beam_grouper.rb +108 -0
- data/lib/head_music/notation/music_xml/duration_writer.rb +3 -8
- data/lib/head_music/notation/music_xml/preflight.rb +90 -0
- data/lib/head_music/notation/music_xml/render_plan.rb +161 -0
- data/lib/head_music/notation/music_xml/writer.rb +31 -146
- data/lib/head_music/notation/placement_validation.rb +19 -0
- data/lib/head_music/rudiment/enharmonic_equivalence.rb +33 -0
- data/lib/head_music/rudiment/key.rb +8 -36
- data/lib/head_music/rudiment/key_signature/enharmonic_equivalence.rb +21 -15
- data/lib/head_music/rudiment/key_signature.rb +18 -19
- data/lib/head_music/rudiment/meter.rb +39 -6
- data/lib/head_music/rudiment/mode.rb +33 -65
- data/lib/head_music/rudiment/note.rb +18 -21
- data/lib/head_music/rudiment/pitch/enharmonic_equivalence.rb +4 -15
- data/lib/head_music/rudiment/pitch/natural_step.rb +36 -0
- data/lib/head_music/rudiment/pitch/parser.rb +16 -5
- data/lib/head_music/rudiment/pitch.rb +11 -33
- data/lib/head_music/rudiment/qualified_diatonic_context.rb +61 -0
- data/lib/head_music/rudiment/rhythmic_unit/parser.rb +16 -12
- data/lib/head_music/rudiment/rhythmic_unit.rb +21 -37
- data/lib/head_music/rudiment/rhythmic_value/parser.rb +53 -46
- data/lib/head_music/rudiment/spelling/enharmonic_equivalence.rb +13 -0
- data/lib/head_music/rudiment/spelling.rb +0 -25
- data/lib/head_music/rudiment/tuning/just_intonation.rb +2 -4
- data/lib/head_music/rudiment/tuning/meantone.rb +2 -4
- data/lib/head_music/rudiment/tuning/pythagorean.rb +2 -4
- data/lib/head_music/rudiment/tuning/tonal_centered.rb +10 -0
- data/lib/head_music/rudiment/unpitched_note.rb +3 -5
- data/lib/head_music/style/annotation/configured.rb +35 -0
- data/lib/head_music/style/annotation.rb +32 -44
- data/lib/head_music/style/guidelines/allowed_rhythmic_values_for_fifth_species.rb +0 -10
- data/lib/head_music/style/guidelines/contoured.rb +65 -53
- data/lib/head_music/style/guidelines/dissonance_figure_detection.rb +55 -16
- data/lib/head_music/style/guidelines/florid_dissonance_treatment.rb +0 -37
- data/lib/head_music/style/guidelines/large_leaps.rb +10 -2
- data/lib/head_music/style/guidelines/minimum_melodic_intervals.rb +3 -25
- data/lib/head_music/style/guidelines/minimum_notes.rb +3 -25
- data/lib/head_music/style/guidelines/minimum_threshold.rb +38 -0
- data/lib/head_music/style/guidelines/no_parallel_perfect.rb +31 -0
- data/lib/head_music/style/guidelines/no_parallel_perfect_on_downbeats.rb +3 -17
- data/lib/head_music/style/guidelines/no_parallel_perfect_with_syncopation.rb +3 -17
- data/lib/head_music/style/guidelines/prepare_octave_leaps.rb +10 -7
- data/lib/head_music/style/guidelines/singable_intervals.rb +22 -8
- data/lib/head_music/style/guidelines/suspension_treatment.rb +30 -12
- data/lib/head_music/style/guidelines/third_species_dissonance_treatment.rb +0 -7
- data/lib/head_music/style/guidelines/triple_meter_dissonance_treatment.rb +0 -12
- data/lib/head_music/style/guidelines/weak_beat_dissonance_treatment.rb +2 -42
- data/lib/head_music/style/guides/fifth_species_melody.rb +3 -13
- data/lib/head_music/style/guides/fourth_species_melody.rb +3 -13
- data/lib/head_music/style/guides/second_species_harmony.rb +2 -5
- data/lib/head_music/style/guides/second_species_melody.rb +2 -12
- data/lib/head_music/style/guides/species_harmony.rb +14 -0
- data/lib/head_music/style/guides/species_melody.rb +21 -0
- data/lib/head_music/style/guides/third_species_harmony.rb +2 -5
- data/lib/head_music/style/guides/third_species_melody.rb +3 -13
- data/lib/head_music/style/guides/third_species_triple_meter_harmony.rb +2 -5
- data/lib/head_music/style/guides/third_species_triple_meter_melody.rb +2 -12
- data/lib/head_music/time/conductor.rb +19 -125
- data/lib/head_music/time/musical_position.rb +26 -26
- data/lib/head_music/time/musical_time_converter.rb +104 -0
- data/lib/head_music/time/smpte_converter.rb +51 -0
- data/lib/head_music/time/smpte_timecode.rb +25 -23
- data/lib/head_music/time.rb +2 -0
- data/lib/head_music/value_equality.rb +37 -0
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +25 -1
- data/user-stories/done/abc-notation-beaming.md +235 -0
- data/user-stories/index.html +7 -3
- metadata +34 -2
|
@@ -2,37 +2,22 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# Represents a musical key (major or minor)
|
|
5
|
-
class HeadMusic::Rudiment::Key < HeadMusic::Rudiment::
|
|
6
|
-
include HeadMusic::Named
|
|
7
|
-
|
|
5
|
+
class HeadMusic::Rudiment::Key < HeadMusic::Rudiment::QualifiedDiatonicContext
|
|
8
6
|
QUALITIES = %i[major minor].freeze
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def self.get(identifier)
|
|
13
|
-
return identifier if identifier.is_a?(HeadMusic::Rudiment::Key)
|
|
14
|
-
|
|
15
|
-
@keys ||= {}
|
|
16
|
-
tonic_spelling, quality_name = parse_identifier(identifier)
|
|
17
|
-
hash_key = HeadMusic::Utilities::HashKey.for(identifier)
|
|
18
|
-
@keys[hash_key] ||= new(tonic_spelling, quality_name)
|
|
8
|
+
def self.default_qualifier
|
|
9
|
+
:major
|
|
19
10
|
end
|
|
20
11
|
|
|
21
|
-
def self.
|
|
22
|
-
|
|
23
|
-
quality_name ||= "major"
|
|
24
|
-
[tonic_spelling, quality_name]
|
|
12
|
+
def self.valid_qualifiers
|
|
13
|
+
QUALITIES
|
|
25
14
|
end
|
|
26
15
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
@quality = quality.to_s.downcase.to_sym
|
|
30
|
-
raise ArgumentError, "Quality must be :major or :minor" unless QUALITIES.include?(@quality)
|
|
16
|
+
def self.invalid_qualifier_message
|
|
17
|
+
"Quality must be :major or :minor"
|
|
31
18
|
end
|
|
32
19
|
|
|
33
|
-
|
|
34
|
-
@scale_type ||= HeadMusic::Rudiment::ScaleType.get(quality)
|
|
35
|
-
end
|
|
20
|
+
alias_method :quality, :qualifier
|
|
36
21
|
|
|
37
22
|
def major?
|
|
38
23
|
quality == :major
|
|
@@ -61,17 +46,4 @@ class HeadMusic::Rudiment::Key < HeadMusic::Rudiment::DiatonicContext
|
|
|
61
46
|
self.class.get("#{tonic_spelling} major")
|
|
62
47
|
end
|
|
63
48
|
end
|
|
64
|
-
|
|
65
|
-
def name
|
|
66
|
-
"#{tonic_spelling} #{quality}"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def to_s
|
|
70
|
-
name
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def ==(other)
|
|
74
|
-
other = self.class.get(other)
|
|
75
|
-
tonic_spelling == other.tonic_spelling && quality == other.quality
|
|
76
|
-
end
|
|
77
49
|
end
|
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
# Key signatures are enharmonic when they represent the same set of altered pitch classes but with different spellings.
|
|
2
|
-
class HeadMusic::Rudiment::KeySignature::EnharmonicEquivalence
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def self.get(key_signature)
|
|
6
|
-
key_signature = HeadMusic::Rudiment::KeySignature.get(key_signature)
|
|
7
|
-
@enharmonic_equivalences ||= {}
|
|
8
|
-
@enharmonic_equivalences[key_signature.to_s] ||= new(key_signature)
|
|
2
|
+
class HeadMusic::Rudiment::KeySignature::EnharmonicEquivalence < HeadMusic::Rudiment::EnharmonicEquivalence
|
|
3
|
+
def self.subject_class
|
|
4
|
+
HeadMusic::Rudiment::KeySignature
|
|
9
5
|
end
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
@key_signature = HeadMusic::Rudiment::KeySignature.get(key_signature)
|
|
13
|
-
end
|
|
7
|
+
alias_method :key_signature, :subject
|
|
14
8
|
|
|
15
9
|
def enharmonic_equivalent?(other)
|
|
16
10
|
other = HeadMusic::Rudiment::KeySignature.get(other)
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
key_signature.alterations.map(&:to_s).sort != other.alterations.map(&:to_s).sort
|
|
12
|
+
same_pitch_classes?(other) && different_spelling?(other)
|
|
20
13
|
end
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
alias_method :equivalent?, :enharmonic_equivalent?
|
|
15
|
+
private
|
|
24
16
|
|
|
25
|
-
|
|
17
|
+
def same_pitch_classes?(other)
|
|
18
|
+
sorted_pitch_class_numbers(key_signature) == sorted_pitch_class_numbers(other)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def different_spelling?(other)
|
|
22
|
+
sorted_alteration_names(key_signature) != sorted_alteration_names(other)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sorted_pitch_class_numbers(key_signature)
|
|
26
|
+
key_signature.pitch_classes.map(&:to_i).sort
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sorted_alteration_names(key_signature)
|
|
30
|
+
key_signature.alterations.map(&:to_s).sort
|
|
31
|
+
end
|
|
26
32
|
end
|
|
@@ -51,27 +51,19 @@ class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def sharps
|
|
54
|
-
|
|
55
|
-
ORDERED_LETTER_NAMES_OF_SHARPS.index(spelling.letter_name.to_s)
|
|
56
|
-
end
|
|
54
|
+
altered_spellings(:sharp?, ORDERED_LETTER_NAMES_OF_SHARPS)
|
|
57
55
|
end
|
|
58
56
|
|
|
59
57
|
def double_sharps
|
|
60
|
-
|
|
61
|
-
ORDERED_LETTER_NAMES_OF_SHARPS.index(spelling.letter_name.to_s)
|
|
62
|
-
end
|
|
58
|
+
altered_spellings(:double_sharp?, ORDERED_LETTER_NAMES_OF_SHARPS)
|
|
63
59
|
end
|
|
64
60
|
|
|
65
61
|
def flats
|
|
66
|
-
|
|
67
|
-
ORDERED_LETTER_NAMES_OF_FLATS.index(spelling.letter_name.to_s)
|
|
68
|
-
end
|
|
62
|
+
altered_spellings(:flat?, ORDERED_LETTER_NAMES_OF_FLATS)
|
|
69
63
|
end
|
|
70
64
|
|
|
71
65
|
def double_flats
|
|
72
|
-
|
|
73
|
-
ORDERED_LETTER_NAMES_OF_FLATS.index(spelling.letter_name.to_s)
|
|
74
|
-
end
|
|
66
|
+
altered_spellings(:double_flat?, ORDERED_LETTER_NAMES_OF_FLATS)
|
|
75
67
|
end
|
|
76
68
|
|
|
77
69
|
def num_sharps
|
|
@@ -102,13 +94,10 @@ class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
|
|
|
102
94
|
end
|
|
103
95
|
|
|
104
96
|
def to_s
|
|
105
|
-
if sharps.any?
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
else
|
|
110
|
-
"no sharps or flats"
|
|
111
|
-
end
|
|
97
|
+
return pluralize(sharps.length, "sharp") if sharps.any?
|
|
98
|
+
return pluralize(flats.length, "flat") if flats.any?
|
|
99
|
+
|
|
100
|
+
"no sharps or flats"
|
|
112
101
|
end
|
|
113
102
|
|
|
114
103
|
def enharmonic_equivalent?(other)
|
|
@@ -117,6 +106,16 @@ class HeadMusic::Rudiment::KeySignature < HeadMusic::Rudiment::Base
|
|
|
117
106
|
|
|
118
107
|
private
|
|
119
108
|
|
|
109
|
+
def altered_spellings(predicate, letter_name_order)
|
|
110
|
+
spellings.select(&predicate).sort_by do |spelling|
|
|
111
|
+
letter_name_order.index(spelling.letter_name.to_s)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def pluralize(count, noun)
|
|
116
|
+
(count == 1) ? "1 #{noun}" : "#{count} #{noun}s"
|
|
117
|
+
end
|
|
118
|
+
|
|
120
119
|
def enharmonic_equivalence
|
|
121
120
|
@enharmonic_equivalence ||= HeadMusic::Rudiment::KeySignature::EnharmonicEquivalence.get(self)
|
|
122
121
|
end
|
|
@@ -88,17 +88,26 @@ class HeadMusic::Rudiment::Meter < HeadMusic::Rudiment::Base
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def beat_value
|
|
91
|
-
@beat_value ||=
|
|
92
|
-
if compound?
|
|
93
|
-
HeadMusic::Rudiment::RhythmicValue.new(HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(bottom_number / 2), dots: 1)
|
|
94
|
-
else
|
|
95
|
-
HeadMusic::Rudiment::RhythmicValue.new(count_unit)
|
|
96
|
-
end
|
|
91
|
+
@beat_value ||= compound? ? dotted_denominator_value(bottom_number / 2) : count_unit_value
|
|
97
92
|
end
|
|
98
93
|
|
|
99
94
|
# for consistency with conversational usage
|
|
100
95
|
alias_method :beat_unit, :beat_value
|
|
101
96
|
|
|
97
|
+
# The span of one default beam group for this meter, as a RhythmicValue.
|
|
98
|
+
# This is a distinct concern from #beat_value: it is the boundary unit used
|
|
99
|
+
# to decide where beams break.
|
|
100
|
+
def beam_group_unit
|
|
101
|
+
@beam_group_unit ||=
|
|
102
|
+
if compound?
|
|
103
|
+
beat_value
|
|
104
|
+
elsif bottom_number >= 8
|
|
105
|
+
whole_bar_beam_group_unit
|
|
106
|
+
else
|
|
107
|
+
count_unit_value
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
102
111
|
def to_s
|
|
103
112
|
[top_number, bottom_number].join("/")
|
|
104
113
|
end
|
|
@@ -119,6 +128,30 @@ class HeadMusic::Rudiment::Meter < HeadMusic::Rudiment::Base
|
|
|
119
128
|
|
|
120
129
|
private
|
|
121
130
|
|
|
131
|
+
# For simple meters with an eighth-or-shorter denominator (e.g. 2/8, 3/8),
|
|
132
|
+
# the whole bar is a single beam group. A triple grouping (3/8) is a dotted
|
|
133
|
+
# value of the half-denominator unit; a duple grouping (2/8) spans the bar.
|
|
134
|
+
# Asymmetric meters (5/8, 7/16, ...) are out of scope: their bar has no
|
|
135
|
+
# single power-of-two unit, so the group unit falls back to the count unit
|
|
136
|
+
# (each count its own group) rather than raising on a nil unit.
|
|
137
|
+
def whole_bar_beam_group_unit
|
|
138
|
+
return dotted_denominator_value(bottom_number / 2) if (top_number % 3).zero?
|
|
139
|
+
|
|
140
|
+
unit = HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(bottom_number / top_number)
|
|
141
|
+
HeadMusic::Rudiment::RhythmicValue.new(unit || count_unit)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def count_unit_value
|
|
145
|
+
HeadMusic::Rudiment::RhythmicValue.new(count_unit)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A dotted value of the rhythmic unit for the given denominator (e.g. the
|
|
149
|
+
# dotted-quarter beat of a 6/8 meter for denominator 4).
|
|
150
|
+
def dotted_denominator_value(denominator)
|
|
151
|
+
unit = HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(denominator)
|
|
152
|
+
HeadMusic::Rudiment::RhythmicValue.new(unit, dots: 1)
|
|
153
|
+
end
|
|
154
|
+
|
|
122
155
|
def downbeat?(count, tick = 0)
|
|
123
156
|
beat?(tick) && count == 1
|
|
124
157
|
end
|
|
@@ -2,59 +2,38 @@
|
|
|
2
2
|
module HeadMusic::Rudiment; end
|
|
3
3
|
|
|
4
4
|
# Represents a musical mode (church modes)
|
|
5
|
-
class HeadMusic::Rudiment::Mode < HeadMusic::Rudiment::
|
|
6
|
-
include HeadMusic::Named
|
|
7
|
-
|
|
5
|
+
class HeadMusic::Rudiment::Mode < HeadMusic::Rudiment::QualifiedDiatonicContext
|
|
8
6
|
MODES = %i[ionian dorian phrygian lydian mixolydian aeolian locrian].freeze
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def self.get(identifier)
|
|
13
|
-
return identifier if identifier.is_a?(HeadMusic::Rudiment::Mode)
|
|
14
|
-
|
|
15
|
-
@modes ||= {}
|
|
16
|
-
tonic_spelling, mode_name = parse_identifier(identifier)
|
|
17
|
-
hash_key = HeadMusic::Utilities::HashKey.for(identifier)
|
|
18
|
-
@modes[hash_key] ||= new(tonic_spelling, mode_name)
|
|
8
|
+
def self.default_qualifier
|
|
9
|
+
:ionian
|
|
19
10
|
end
|
|
20
11
|
|
|
21
|
-
def self.
|
|
22
|
-
|
|
23
|
-
parts = identifier.split(/\s+/)
|
|
24
|
-
tonic_spelling = parts[0]
|
|
25
|
-
mode_name = parts[1] || "ionian"
|
|
26
|
-
[tonic_spelling, mode_name]
|
|
12
|
+
def self.valid_qualifiers
|
|
13
|
+
MODES
|
|
27
14
|
end
|
|
28
15
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
@mode_name = mode_name.to_s.downcase.to_sym
|
|
32
|
-
raise ArgumentError, "Mode must be one of: #{MODES.join(", ")}" unless MODES.include?(@mode_name)
|
|
16
|
+
def self.invalid_qualifier_message
|
|
17
|
+
"Mode must be one of: #{MODES.join(", ")}"
|
|
33
18
|
end
|
|
34
19
|
|
|
35
|
-
|
|
36
|
-
@scale_type ||= HeadMusic::Rudiment::ScaleType.get(mode_name)
|
|
37
|
-
end
|
|
20
|
+
alias_method :mode_name, :qualifier
|
|
38
21
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
when :phrygian
|
|
46
|
-
relative_pitch = tonic_pitch + -4
|
|
47
|
-
when :lydian
|
|
48
|
-
relative_pitch = tonic_pitch + -5
|
|
49
|
-
when :mixolydian
|
|
50
|
-
relative_pitch = tonic_pitch + -7
|
|
51
|
-
when :aeolian
|
|
52
|
-
relative_pitch = tonic_pitch + -9
|
|
53
|
-
when :locrian
|
|
54
|
-
relative_pitch = tonic_pitch + -11
|
|
55
|
-
end
|
|
22
|
+
# Semitones from a mode's tonic down to its relative major tonic.
|
|
23
|
+
# Ionian is omitted because its own tonic spelling is the relative major.
|
|
24
|
+
RELATIVE_MAJOR_SEMITONES_BELOW_TONIC = {
|
|
25
|
+
dorian: -2, phrygian: -4, lydian: -5,
|
|
26
|
+
mixolydian: -7, aeolian: -9, locrian: -11
|
|
27
|
+
}.freeze
|
|
56
28
|
|
|
57
|
-
|
|
29
|
+
# The major or minor quality of each mode's parallel key.
|
|
30
|
+
PARALLEL_QUALITIES = {
|
|
31
|
+
ionian: :major, dorian: :minor, phrygian: :minor, lydian: :major,
|
|
32
|
+
mixolydian: :major, aeolian: :minor, locrian: :minor
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def relative_major
|
|
36
|
+
HeadMusic::Rudiment::Key.get("#{relative_major_tonic_spelling} major")
|
|
58
37
|
end
|
|
59
38
|
|
|
60
39
|
def relative
|
|
@@ -62,31 +41,20 @@ class HeadMusic::Rudiment::Mode < HeadMusic::Rudiment::DiatonicContext
|
|
|
62
41
|
end
|
|
63
42
|
|
|
64
43
|
def parallel
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
when :ionian
|
|
68
|
-
HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
69
|
-
when :aeolian
|
|
70
|
-
HeadMusic::Rudiment::Key.get("#{tonic_spelling} minor")
|
|
71
|
-
when :dorian, :phrygian
|
|
72
|
-
HeadMusic::Rudiment::Key.get("#{tonic_spelling} minor")
|
|
73
|
-
when :lydian, :mixolydian
|
|
74
|
-
HeadMusic::Rudiment::Key.get("#{tonic_spelling} major")
|
|
75
|
-
when :locrian
|
|
76
|
-
HeadMusic::Rudiment::Key.get("#{tonic_spelling} minor")
|
|
77
|
-
end
|
|
78
|
-
end
|
|
44
|
+
quality = PARALLEL_QUALITIES[mode_name]
|
|
45
|
+
return unless quality
|
|
79
46
|
|
|
80
|
-
|
|
81
|
-
"#{tonic_spelling} #{mode_name}"
|
|
47
|
+
HeadMusic::Rudiment::Key.get("#{tonic_spelling} #{quality}")
|
|
82
48
|
end
|
|
83
49
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def relative_major_tonic_spelling
|
|
53
|
+
return tonic_spelling if mode_name == :ionian
|
|
87
54
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
55
|
+
offset = RELATIVE_MAJOR_SEMITONES_BELOW_TONIC[mode_name]
|
|
56
|
+
relative_pitch = tonic_pitch + offset if offset
|
|
57
|
+
# An unrecognized mode leaves relative_pitch nil, raising NoMethodError (preserved behavior).
|
|
58
|
+
relative_pitch.spelling
|
|
91
59
|
end
|
|
92
60
|
end
|
|
@@ -22,30 +22,27 @@ class HeadMusic::Rudiment::Note < HeadMusic::Rudiment::RhythmicElement
|
|
|
22
22
|
|
|
23
23
|
def self.get(pitch, rhythmic_value = nil)
|
|
24
24
|
return pitch if pitch.is_a?(HeadMusic::Rudiment::Note)
|
|
25
|
+
return get_from_string(pitch) if rhythmic_value.nil? && pitch.is_a?(String)
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
pitch_str = match[1] # The full pitch part
|
|
31
|
-
rhythmic_value_str = match[5] # The rhythmic value part
|
|
32
|
-
|
|
33
|
-
pitch_obj = HeadMusic::Rudiment::Pitch.get(pitch_str)
|
|
34
|
-
rhythmic_value_obj = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value_str)
|
|
27
|
+
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
28
|
+
rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value || :quarter)
|
|
29
|
+
fetch_or_create(pitch, rhythmic_value)
|
|
30
|
+
end
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
# Parse a lone string as "pitch rhythmic_value" (e.g., "F#4 dotted-quarter"),
|
|
33
|
+
# falling back to just a pitch with a default quarter note.
|
|
34
|
+
def self.get_from_string(string)
|
|
35
|
+
match = string.match(MATCHER)
|
|
36
|
+
if match
|
|
37
|
+
pitch_obj = HeadMusic::Rudiment::Pitch.get(match[1])
|
|
38
|
+
rhythmic_value_obj = HeadMusic::Rudiment::RhythmicValue.get(match[5])
|
|
39
|
+
return fetch_or_create(pitch_obj, rhythmic_value_obj) if pitch_obj && rhythmic_value_obj
|
|
40
|
+
end
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return fetch_or_create(pitch_obj, HeadMusic::Rudiment::RhythmicValue.get(:quarter)) if pitch_obj
|
|
42
|
+
pitch_obj = HeadMusic::Rudiment::Pitch.get(string)
|
|
43
|
+
return fetch_or_create(pitch_obj, HeadMusic::Rudiment::RhythmicValue.get(:quarter)) if pitch_obj
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
else
|
|
45
|
-
pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
46
|
-
rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value || :quarter)
|
|
47
|
-
fetch_or_create(pitch, rhythmic_value)
|
|
48
|
-
end
|
|
45
|
+
nil
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
def self.fetch_or_create(pitch, rhythmic_value)
|
|
@@ -108,5 +105,5 @@ class HeadMusic::Rudiment::Note < HeadMusic::Rudiment::RhythmicElement
|
|
|
108
105
|
true
|
|
109
106
|
end
|
|
110
107
|
|
|
111
|
-
private_class_method :fetch_or_create
|
|
108
|
+
private_class_method :fetch_or_create, :get_from_string
|
|
112
109
|
end
|
|
@@ -1,26 +1,15 @@
|
|
|
1
1
|
# An enharmonic equivalent pitch is the same frequency spelled differently, such as D# and Eb.
|
|
2
|
-
class HeadMusic::Rudiment::Pitch::EnharmonicEquivalence
|
|
3
|
-
def self.
|
|
4
|
-
|
|
5
|
-
@enharmonic_equivalences ||= {}
|
|
6
|
-
@enharmonic_equivalences[pitch.to_s] ||= new(pitch)
|
|
2
|
+
class HeadMusic::Rudiment::Pitch::EnharmonicEquivalence < HeadMusic::Rudiment::EnharmonicEquivalence
|
|
3
|
+
def self.subject_class
|
|
4
|
+
HeadMusic::Rudiment::Pitch
|
|
7
5
|
end
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
alias_method :pitch, :subject
|
|
10
8
|
|
|
11
9
|
delegate :pitch_class, to: :pitch
|
|
12
10
|
|
|
13
|
-
def initialize(pitch)
|
|
14
|
-
@pitch = HeadMusic::Rudiment::Pitch.get(pitch)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
11
|
def enharmonic_equivalent?(other)
|
|
18
12
|
other = HeadMusic::Rudiment::Pitch.get(other)
|
|
19
13
|
pitch.midi_note_number == other.midi_note_number && pitch.spelling != other.spelling
|
|
20
14
|
end
|
|
21
|
-
|
|
22
|
-
alias_method :enharmonic?, :enharmonic_equivalent?
|
|
23
|
-
alias_method :equivalent?, :enharmonic_equivalent?
|
|
24
|
-
|
|
25
|
-
private_class_method :new
|
|
26
15
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class HeadMusic::Rudiment::Pitch
|
|
2
|
+
# Computes where a signed number of diatonic (letter-name) steps lands
|
|
3
|
+
# relative to a starting letter name: the destination letter and how many
|
|
4
|
+
# octaves the move crosses. Register-agnostic — it works purely from the
|
|
5
|
+
# letter name's position in the scale, so the Pitch supplies the register.
|
|
6
|
+
class NaturalStep
|
|
7
|
+
attr_reader :letter_name, :num_steps
|
|
8
|
+
|
|
9
|
+
def initialize(letter_name, num_steps)
|
|
10
|
+
@letter_name = letter_name
|
|
11
|
+
@num_steps = num_steps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def target_letter_name
|
|
15
|
+
@target_letter_name ||= letter_name.steps_up(num_steps)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def octaves_delta
|
|
19
|
+
whole_octaves = (num_steps.abs / 7) * (num_steps.negative? ? -1 : 1)
|
|
20
|
+
return whole_octaves - 1 if wrapped_down?
|
|
21
|
+
return whole_octaves + 1 if wrapped_up?
|
|
22
|
+
|
|
23
|
+
whole_octaves
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def wrapped_down?
|
|
29
|
+
num_steps.negative? && target_letter_name.position > letter_name.position
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def wrapped_up?
|
|
33
|
+
num_steps.positive? && target_letter_name.position < letter_name.position
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -42,11 +42,22 @@ class HeadMusic::Rudiment::Pitch::Parser
|
|
|
42
42
|
|
|
43
43
|
def parse_components
|
|
44
44
|
match = identifier.match(PATTERN)
|
|
45
|
+
return unless match
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
@letter_name = parse_letter_name(match[1])
|
|
48
|
+
@alteration = parse_alteration(match[2])
|
|
49
|
+
@register = parse_register(match[3])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_letter_name(token)
|
|
53
|
+
LetterName.get(token.upcase) unless token.to_s.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_alteration(token)
|
|
57
|
+
Alteration.get(token || "") unless token.to_s.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_register(token)
|
|
61
|
+
Register.get(token.to_i) unless token.to_s.empty?
|
|
51
62
|
end
|
|
52
63
|
end
|
|
@@ -137,16 +137,16 @@ class HeadMusic::Rudiment::Pitch < HeadMusic::Rudiment::Base
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def -(other)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
HeadMusic::Rudiment::ChromaticInterval.get(
|
|
140
|
+
# a diatonic interval below this pitch resolves to another pitch
|
|
141
|
+
return other.below(self) if other.is_a?(HeadMusic::Analysis::DiatonicInterval)
|
|
142
|
+
|
|
143
|
+
difference = to_i - other.to_i
|
|
144
|
+
if other.is_a?(HeadMusic::Rudiment::Pitch)
|
|
145
|
+
# the distance between two pitches is an interval
|
|
146
|
+
HeadMusic::Rudiment::ChromaticInterval.get(difference)
|
|
147
147
|
else
|
|
148
|
-
#
|
|
149
|
-
HeadMusic::Rudiment::Pitch.get(
|
|
148
|
+
# otherwise treat the value as a number of semitones and return another pitch
|
|
149
|
+
HeadMusic::Rudiment::Pitch.get(difference)
|
|
150
150
|
end
|
|
151
151
|
end
|
|
152
152
|
|
|
@@ -164,7 +164,8 @@ class HeadMusic::Rudiment::Pitch < HeadMusic::Rudiment::Base
|
|
|
164
164
|
end
|
|
165
165
|
|
|
166
166
|
def natural_steps(num_steps)
|
|
167
|
-
|
|
167
|
+
step = NaturalStep.new(letter_name, num_steps)
|
|
168
|
+
HeadMusic::Rudiment::Pitch.get([step.target_letter_name, register + step.octaves_delta].join)
|
|
168
169
|
end
|
|
169
170
|
|
|
170
171
|
def frequency
|
|
@@ -202,29 +203,6 @@ class HeadMusic::Rudiment::Pitch < HeadMusic::Rudiment::Base
|
|
|
202
203
|
@tuning ||= HeadMusic::Rudiment::Tuning.new
|
|
203
204
|
end
|
|
204
205
|
|
|
205
|
-
def octaves_delta(num_steps)
|
|
206
|
-
octaves_delta = (num_steps.abs / 7) * ((num_steps >= 0) ? 1 : -1)
|
|
207
|
-
if wrapped_down?(num_steps)
|
|
208
|
-
octaves_delta -= 1
|
|
209
|
-
elsif wrapped_up?(num_steps)
|
|
210
|
-
octaves_delta += 1
|
|
211
|
-
end
|
|
212
|
-
octaves_delta
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def wrapped_down?(num_steps)
|
|
216
|
-
num_steps.negative? && target_letter_name(num_steps).position > letter_name.position
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def wrapped_up?(num_steps)
|
|
220
|
-
num_steps.positive? && target_letter_name(num_steps).position < letter_name.position
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def target_letter_name(num_steps)
|
|
224
|
-
@target_letter_name ||= {}
|
|
225
|
-
@target_letter_name[num_steps] ||= letter_name.steps_up(num_steps)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
206
|
def helmholtz_letter_name
|
|
229
207
|
spelling_str = spelling.to_s
|
|
230
208
|
return spelling_str.downcase if HeadMusic::Rudiment::Register.get(register).helmholtz_case == :lower
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# A module for music rudiments
|
|
2
|
+
module HeadMusic::Rudiment; end
|
|
3
|
+
|
|
4
|
+
# Shared behavior for diatonic contexts identified by a tonic spelling and a
|
|
5
|
+
# named qualifier -- a key's quality (major/minor) or a mode's name.
|
|
6
|
+
# Subclasses declare their valid qualifiers, default qualifier, and the
|
|
7
|
+
# message raised for an invalid qualifier, and implement #relative and #parallel.
|
|
8
|
+
class HeadMusic::Rudiment::QualifiedDiatonicContext < HeadMusic::Rudiment::DiatonicContext
|
|
9
|
+
include HeadMusic::Named
|
|
10
|
+
|
|
11
|
+
def self.get(identifier)
|
|
12
|
+
return identifier if identifier.is_a?(self)
|
|
13
|
+
|
|
14
|
+
@cache ||= {}
|
|
15
|
+
tonic_spelling, qualifier = parse_identifier(identifier)
|
|
16
|
+
hash_key = HeadMusic::Utilities::HashKey.for(identifier)
|
|
17
|
+
@cache[hash_key] ||= new(tonic_spelling, qualifier)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.parse_identifier(identifier)
|
|
21
|
+
tonic_spelling, qualifier = identifier.to_s.strip.split(/\s+/)
|
|
22
|
+
[tonic_spelling, qualifier || default_qualifier.to_s]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.default_qualifier
|
|
26
|
+
raise NotImplementedError, "Subclasses must implement .default_qualifier"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.valid_qualifiers
|
|
30
|
+
raise NotImplementedError, "Subclasses must implement .valid_qualifiers"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.invalid_qualifier_message
|
|
34
|
+
raise NotImplementedError, "Subclasses must implement .invalid_qualifier_message"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :qualifier
|
|
38
|
+
|
|
39
|
+
def initialize(tonic_spelling, qualifier = nil)
|
|
40
|
+
super(tonic_spelling)
|
|
41
|
+
@qualifier = (qualifier || self.class.default_qualifier).to_s.downcase.to_sym
|
|
42
|
+
raise ArgumentError, self.class.invalid_qualifier_message unless self.class.valid_qualifiers.include?(@qualifier)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def scale_type
|
|
46
|
+
@scale_type ||= HeadMusic::Rudiment::ScaleType.get(qualifier)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def name
|
|
50
|
+
"#{tonic_spelling} #{qualifier}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_s
|
|
54
|
+
name
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ==(other)
|
|
58
|
+
other = self.class.get(other)
|
|
59
|
+
tonic_spelling == other.tonic_spelling && qualifier == other.qualifier
|
|
60
|
+
end
|
|
61
|
+
end
|