head_music 17.3.0 → 17.5.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/.github/workflows/ci.yml +3 -3
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/security.yml +2 -1
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +68 -68
- data/head_music.gemspec +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 +35 -109
- data/lib/head_music/analysis/sonority.rb +11 -21
- data/lib/head_music/content/composition/hash_deserializer.rb +113 -0
- data/lib/head_music/content/composition/schema_values.rb +167 -0
- data/lib/head_music/content/composition.rb +0 -216
- data/lib/head_music/content/placement.rb +31 -42
- data/lib/head_music/content/sound_resolver.rb +54 -0
- data/lib/head_music/content/syllable.rb +38 -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 +8 -59
- 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 +48 -23
- 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 +44 -183
- 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 +34 -17
- 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 +2 -2
- data/lib/head_music/notation/music_xml/duration_writer.rb +4 -9
- 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 +66 -209
- 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 +17 -13
- 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/notes_same_length.rb +1 -1
- 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 +14 -26
- data/lib/head_music/time/musical_time_converter.rb +104 -0
- data/lib/head_music/time/radix_carry.rb +22 -0
- data/lib/head_music/time/smpte_converter.rb +51 -0
- data/lib/head_music/time/smpte_timecode.rb +13 -23
- data/lib/head_music/time.rb +3 -0
- data/lib/head_music/value_equality.rb +37 -0
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music.rb +26 -1
- data/user-stories/done/lyrics.md +162 -0
- data/user-stories/index.html +7 -3
- metadata +45 -6
|
@@ -31,11 +31,7 @@ class HeadMusic::Analysis::Dyad
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def possible_trichords
|
|
34
|
-
|
|
35
|
-
trichords = generate_possible_trichords
|
|
36
|
-
trichords = filter_by_key(trichords) if key
|
|
37
|
-
sort_by_diatonic_agreement(trichords)
|
|
38
|
-
end
|
|
34
|
+
chord_implication.trichords
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
def possible_triads
|
|
@@ -43,11 +39,7 @@ class HeadMusic::Analysis::Dyad
|
|
|
43
39
|
end
|
|
44
40
|
|
|
45
41
|
def possible_seventh_chords
|
|
46
|
-
|
|
47
|
-
seventh_chords = generate_possible_seventh_chords
|
|
48
|
-
seventh_chords = filter_by_key(seventh_chords) if key
|
|
49
|
-
sort_by_diatonic_agreement(seventh_chords)
|
|
50
|
-
end
|
|
42
|
+
chord_implication.seventh_chords
|
|
51
43
|
end
|
|
52
44
|
|
|
53
45
|
def enharmonic_respellings
|
|
@@ -68,111 +60,39 @@ class HeadMusic::Analysis::Dyad
|
|
|
68
60
|
|
|
69
61
|
private
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
%w[m3 P5], # minor triad
|
|
74
|
-
%w[m3 d5], # diminished triad
|
|
75
|
-
%w[M3 A5], # augmented triad
|
|
76
|
-
%w[P4 P5], # sus4 (not a triad)
|
|
77
|
-
%w[M2 P5] # sus2 (not a triad)
|
|
78
|
-
].freeze
|
|
79
|
-
|
|
80
|
-
SEVENTH_CHORD_INTERVALS = [
|
|
81
|
-
%w[M3 P5 M7], # major seventh
|
|
82
|
-
%w[M3 P5 m7], # dominant seventh (major-minor)
|
|
83
|
-
%w[m3 P5 m7], # minor seventh
|
|
84
|
-
%w[m3 P5 M7], # minor-major seventh
|
|
85
|
-
%w[m3 d5 m7], # half-diminished seventh
|
|
86
|
-
%w[m3 d5 d7], # diminished seventh
|
|
87
|
-
%w[M2 M3 P5 m7], # dominant ninth
|
|
88
|
-
%w[m2 M3 P5 m7], # dominant minor ninth
|
|
89
|
-
%w[M2 m3 P5 m7], # minor ninth
|
|
90
|
-
%w[M2 M3 P5 M7] # major ninth
|
|
91
|
-
].freeze
|
|
92
|
-
|
|
93
|
-
def generate_possible_trichords
|
|
94
|
-
generate_possible_chords(TRICHORD_INTERVALS)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def generate_possible_seventh_chords
|
|
98
|
-
generate_possible_chords(SEVENTH_CHORD_INTERVALS)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def generate_possible_chords(interval_sets)
|
|
102
|
-
dyad_pitch_classes = [lower_pitch.pitch_class, upper_pitch.pitch_class]
|
|
103
|
-
chords = []
|
|
104
|
-
|
|
105
|
-
HeadMusic::Rudiment::Spelling::CHROMATIC_SPELLINGS.each do |root_spelling|
|
|
106
|
-
root_pitch = HeadMusic::Rudiment::Pitch.get("#{root_spelling}4")
|
|
107
|
-
|
|
108
|
-
interval_sets.each do |intervals|
|
|
109
|
-
chord_pitches = [root_pitch] + intervals.map { |name| HeadMusic::Analysis::DiatonicInterval.get(name).above(root_pitch) }
|
|
110
|
-
pitch_collection = HeadMusic::Analysis::PitchCollection.new(chord_pitches)
|
|
111
|
-
|
|
112
|
-
if dyad_pitch_classes.all? { |pc| pitch_collection.pitch_classes.include?(pc) }
|
|
113
|
-
chords << pitch_collection
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
chords.uniq { |chord| chord.pitch_classes.sort.map(&:to_i) }
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def filter_by_key(pitch_collections)
|
|
122
|
-
return pitch_collections unless key
|
|
123
|
-
|
|
124
|
-
pitch_collections.select do |pitch_collection|
|
|
125
|
-
pitch_collection.pitches.all? { |pitch| diatonic_spellings.include?(pitch.spelling) }
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def sort_by_diatonic_agreement(pitch_collections)
|
|
130
|
-
return pitch_collections unless key
|
|
131
|
-
|
|
132
|
-
pitch_collections.sort_by do |pitch_collection|
|
|
133
|
-
-pitch_collection.pitches.count { |pitch| diatonic_spellings.include?(pitch.spelling) }
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def diatonic_spellings
|
|
138
|
-
@diatonic_spellings ||= key.scale.spellings
|
|
63
|
+
def chord_implication
|
|
64
|
+
@chord_implication ||= ChordImplication.new([lower_pitch.pitch_class, upper_pitch.pitch_class], key)
|
|
139
65
|
end
|
|
140
66
|
|
|
141
67
|
def generate_enharmonic_respellings
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# Get enharmonic equivalents for each pitch
|
|
145
|
-
pitch1_equivalents = enharmonic_equivalents_for(pitch1)
|
|
146
|
-
pitch2_equivalents = enharmonic_equivalents_for(pitch2)
|
|
68
|
+
lower_equivalents = enharmonic_equivalents_for(pitch1)
|
|
69
|
+
upper_equivalents = enharmonic_equivalents_for(pitch2)
|
|
147
70
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
respellings << self.class.new(lower, upper, key: key)
|
|
154
|
-
end
|
|
155
|
-
end
|
|
71
|
+
lower_equivalents.product(upper_equivalents)
|
|
72
|
+
.reject { |lower, upper| original_spelling?(lower, upper) }
|
|
73
|
+
.map { |lower, upper| self.class.new(lower, upper, key: key) }
|
|
74
|
+
end
|
|
156
75
|
|
|
157
|
-
|
|
76
|
+
def original_spelling?(lower, upper)
|
|
77
|
+
lower.spelling == pitch1.spelling && upper.spelling == pitch2.spelling
|
|
158
78
|
end
|
|
159
79
|
|
|
160
80
|
ALTERATION_SIGNS = {-2 => "bb", -1 => "b", 0 => "", 1 => "#", 2 => "##"}.freeze
|
|
161
81
|
|
|
162
82
|
def enharmonic_equivalents_for(pitch)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
HeadMusic::Rudiment::LetterName.all.each do |letter_name|
|
|
167
|
-
ALTERATION_SIGNS.each_value do |sign|
|
|
168
|
-
spelling = HeadMusic::Rudiment::Spelling.get("#{letter_name}#{sign}")
|
|
169
|
-
next unless spelling && spelling.pitch_class == target_pitch_class
|
|
170
|
-
next if equivalents.any? { |equiv| equiv.spelling == spelling }
|
|
171
|
-
|
|
172
|
-
equivalents << HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, pitch.register)
|
|
173
|
-
end
|
|
83
|
+
equivalent_pitches = enharmonic_spellings_for(pitch.pitch_class).map do |spelling|
|
|
84
|
+
HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, pitch.register)
|
|
174
85
|
end
|
|
86
|
+
[pitch, *equivalent_pitches].uniq(&:spelling)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def enharmonic_spellings_for(target_pitch_class)
|
|
90
|
+
all_spellings.select { |spelling| spelling.pitch_class == target_pitch_class }
|
|
91
|
+
end
|
|
175
92
|
|
|
176
|
-
|
|
93
|
+
def all_spellings
|
|
94
|
+
HeadMusic::Rudiment::LetterName.all.flat_map do |letter_name|
|
|
95
|
+
ALTERATION_SIGNS.each_value.map { |sign| HeadMusic::Rudiment::Spelling.get("#{letter_name}#{sign}") }
|
|
96
|
+
end.compact
|
|
177
97
|
end
|
|
178
98
|
end
|
|
@@ -41,15 +41,21 @@ class HeadMusic::Analysis::IntervalCycle
|
|
|
41
41
|
def pitches_up
|
|
42
42
|
@pitches_up ||= [starting_pitch].tap do |list|
|
|
43
43
|
loop do
|
|
44
|
-
next_pitch = list.last
|
|
45
|
-
|
|
46
|
-
break if next_pitch.pitch_class == list.first.pitch_class
|
|
44
|
+
next_pitch = folded_up(list.last)
|
|
45
|
+
break if next_pitch.pitch_class == starting_pitch.pitch_class
|
|
47
46
|
|
|
48
47
|
list << next_pitch
|
|
49
48
|
end
|
|
50
49
|
end
|
|
51
50
|
end
|
|
52
51
|
|
|
52
|
+
# The next pitch a cycle-interval above, folded down by octaves to stay within an octave of the start.
|
|
53
|
+
def folded_up(pitch)
|
|
54
|
+
next_pitch = pitch + interval
|
|
55
|
+
next_pitch -= octave while next_pitch - starting_pitch > 12
|
|
56
|
+
next_pitch
|
|
57
|
+
end
|
|
58
|
+
|
|
53
59
|
def octave
|
|
54
60
|
@octave ||= HeadMusic::Analysis::DiatonicInterval.get(:perfect_octave)
|
|
55
61
|
end
|
|
@@ -6,15 +6,6 @@ module HeadMusic::Analysis; end
|
|
|
6
6
|
# as distinct from a "pitch-class set" which abstracts away register and octave equivalence.
|
|
7
7
|
# See also: PitchClassSet
|
|
8
8
|
class HeadMusic::Analysis::PitchCollection
|
|
9
|
-
TERTIAN_SONORITIES = {
|
|
10
|
-
implied_triad: [3],
|
|
11
|
-
triad: [3, 5],
|
|
12
|
-
seventh_chord: [3, 5, 7],
|
|
13
|
-
ninth_chord: [2, 3, 5, 7],
|
|
14
|
-
eleventh_chord: [2, 3, 4, 5, 7],
|
|
15
|
-
thirteenth_chord: [2, 3, 4, 5, 6, 7] # a.k.a. diatonic scale
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
9
|
attr_reader :pitches
|
|
19
10
|
|
|
20
11
|
delegate :diatonic_intervals, to: :reduction, prefix: true
|
|
@@ -24,6 +15,17 @@ class HeadMusic::Analysis::PitchCollection
|
|
|
24
15
|
delegate :heptachord?, :octachord?, :nonachord?, :decachord?, :undecachord?, :dodecachord?, to: :pitch_class_set
|
|
25
16
|
delegate :size, to: :pitch_class_set, prefix: true
|
|
26
17
|
|
|
18
|
+
# Tertian chord classification (triad/seventh/extended, quality, inversion).
|
|
19
|
+
delegate(
|
|
20
|
+
:triad?, :consonant_triad?,
|
|
21
|
+
:major_triad?, :minor_triad?, :diminished_triad?, :augmented_triad?,
|
|
22
|
+
:root_position_triad?, :first_inversion_triad?, :second_inversion_triad?,
|
|
23
|
+
:seventh_chord?, :root_position_seventh_chord?, :first_inversion_seventh_chord?,
|
|
24
|
+
:second_inversion_seventh_chord?, :third_inversion_seventh_chord?,
|
|
25
|
+
:ninth_chord?, :eleventh_chord?, :thirteenth_chord?, :tertian?,
|
|
26
|
+
to: :chord_analysis
|
|
27
|
+
)
|
|
28
|
+
|
|
27
29
|
def initialize(pitches)
|
|
28
30
|
@pitches = pitches.map { |pitch| HeadMusic::Rudiment::Pitch.get(pitch) }.sort.uniq
|
|
29
31
|
end
|
|
@@ -60,20 +62,17 @@ class HeadMusic::Analysis::PitchCollection
|
|
|
60
62
|
# questions:
|
|
61
63
|
# - should this be absolute? 0 = C?
|
|
62
64
|
# - should there be both absolute and relative versions?
|
|
63
|
-
@integer_notation ||=
|
|
64
|
-
return [] if pitches.empty?
|
|
65
|
-
diatonic_intervals_above_bass_pitch.map { |interval| interval.semitones % 12 }.flatten.sort.unshift(0).uniq
|
|
66
|
-
end
|
|
65
|
+
@integer_notation ||= integer_notation_values
|
|
67
66
|
end
|
|
68
67
|
|
|
69
68
|
def invert
|
|
70
|
-
inverted_pitch = pitches
|
|
69
|
+
inverted_pitch = pitches.first + octave
|
|
71
70
|
new_pitches = pitches.drop(1) + [inverted_pitch]
|
|
72
71
|
HeadMusic::Analysis::PitchCollection.new(new_pitches)
|
|
73
72
|
end
|
|
74
73
|
|
|
75
74
|
def uninvert
|
|
76
|
-
inverted_pitch = pitches
|
|
75
|
+
inverted_pitch = pitches.last - octave
|
|
77
76
|
new_pitches = [inverted_pitch] + pitches[0..-2]
|
|
78
77
|
HeadMusic::Analysis::PitchCollection.new(new_pitches)
|
|
79
78
|
end
|
|
@@ -83,11 +82,11 @@ class HeadMusic::Analysis::PitchCollection
|
|
|
83
82
|
end
|
|
84
83
|
|
|
85
84
|
def inspect
|
|
86
|
-
pitches.
|
|
85
|
+
pitches.join(" ")
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
def to_s
|
|
90
|
-
pitches.
|
|
89
|
+
pitches.join(" ")
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
def ==(other)
|
|
@@ -102,92 +101,6 @@ class HeadMusic::Analysis::PitchCollection
|
|
|
102
101
|
pitches.length
|
|
103
102
|
end
|
|
104
103
|
|
|
105
|
-
def triad?
|
|
106
|
-
trichord? && tertian?
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def consonant_triad?
|
|
110
|
-
major_triad? || minor_triad?
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
TRIAD_PATTERNS = {
|
|
114
|
-
major: [%w[M3 m3], %w[m3 P4], %w[P4 M3]],
|
|
115
|
-
minor: [%w[m3 M3], %w[M3 P4], %w[P4 m3]],
|
|
116
|
-
diminished: [%w[m3 m3], %w[m3 A4], %w[A4 m3]],
|
|
117
|
-
augmented: [%w[M3 M3], %w[M3 d4], %w[d4 M3]]
|
|
118
|
-
}.freeze
|
|
119
|
-
|
|
120
|
-
def major_triad?
|
|
121
|
-
triad_type?(:major)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def minor_triad?
|
|
125
|
-
triad_type?(:minor)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def diminished_triad?
|
|
129
|
-
triad_type?(:diminished)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def augmented_triad?
|
|
133
|
-
triad_type?(:augmented)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def root_position_triad?
|
|
137
|
-
trichord? && reduction_diatonic_intervals.all?(&:third?)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def first_inversion_triad?
|
|
141
|
-
trichord? && reduction.uninvert.diatonic_intervals.all?(&:third?)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def second_inversion_triad?
|
|
145
|
-
trichord? && reduction.invert.diatonic_intervals.all?(&:third?)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def seventh_chord?
|
|
149
|
-
tetrachord? && tertian?
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def root_position_seventh_chord?
|
|
153
|
-
tetrachord? && reduction_diatonic_intervals.all?(&:third?)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def first_inversion_seventh_chord?
|
|
157
|
-
tetrachord? && reduction.uninvert.diatonic_intervals.all?(&:third?)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def second_inversion_seventh_chord?
|
|
161
|
-
tetrachord? && reduction.uninvert.uninvert.diatonic_intervals.all?(&:third?)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def third_inversion_seventh_chord?
|
|
165
|
-
tetrachord? && reduction.invert.diatonic_intervals.all?(&:third?)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def ninth_chord?
|
|
169
|
-
pentachord? && tertian?
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def eleventh_chord?
|
|
173
|
-
hexachord? && tertian?
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def thirteenth_chord?
|
|
177
|
-
heptachord? && tertian?
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def tertian?
|
|
181
|
-
return false unless diatonic_intervals.any?
|
|
182
|
-
|
|
183
|
-
inversion = reduction
|
|
184
|
-
pitches.length.times do
|
|
185
|
-
return true if TERTIAN_SONORITIES.value?(inversion.scale_degrees_above_bass_pitch)
|
|
186
|
-
inversion = inversion.invert
|
|
187
|
-
end
|
|
188
|
-
false
|
|
189
|
-
end
|
|
190
|
-
|
|
191
104
|
def scale_degrees
|
|
192
105
|
@scale_degrees ||= pitches.empty? ? [] : scale_degrees_above_bass_pitch.unshift(1)
|
|
193
106
|
end
|
|
@@ -200,16 +113,29 @@ class HeadMusic::Analysis::PitchCollection
|
|
|
200
113
|
@sonority ||= HeadMusic::Analysis::Sonority.new(self)
|
|
201
114
|
end
|
|
202
115
|
|
|
116
|
+
def chord_analysis
|
|
117
|
+
@chord_analysis ||= HeadMusic::Analysis::ChordAnalysis.new(self)
|
|
118
|
+
end
|
|
119
|
+
|
|
203
120
|
private
|
|
204
121
|
|
|
205
|
-
def
|
|
206
|
-
|
|
122
|
+
def octave
|
|
123
|
+
@octave ||= HeadMusic::Analysis::DiatonicInterval.get("perfect octave")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def integer_notation_values
|
|
127
|
+
return [] if pitches.empty?
|
|
128
|
+
|
|
129
|
+
diatonic_intervals_above_bass_pitch.map { |interval| interval.semitones % 12 }.sort.unshift(0).uniq
|
|
207
130
|
end
|
|
208
131
|
|
|
209
132
|
def reduction_pitches
|
|
210
|
-
pitches.map
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
133
|
+
pitches.map { |pitch| folded_into_reduction(pitch) }.sort
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def folded_into_reduction(pitch)
|
|
137
|
+
ceiling = bass_pitch + 12
|
|
138
|
+
pitch = HeadMusic::Rudiment::Pitch.fetch_or_create(pitch.spelling, pitch.register - 1) while pitch > ceiling
|
|
139
|
+
pitch
|
|
214
140
|
end
|
|
215
141
|
end
|
|
@@ -73,6 +73,7 @@ class HeadMusic::Analysis::Sonority
|
|
|
73
73
|
delegate :heptachord?, :octachord?, :nonachord?, :decachord?, :undecachord?, :dodecachord?, to: :pitch_collection
|
|
74
74
|
delegate :pitch_class_set, :pitch_class_set_size, to: :pitch_collection
|
|
75
75
|
delegate :scale_degrees_above_bass_pitch, to: :pitch_collection
|
|
76
|
+
delegate :triad?, :seventh_chord?, :tertian?, to: :pitch_collection
|
|
76
77
|
|
|
77
78
|
def initialize(pitch_collection)
|
|
78
79
|
@pitch_collection = pitch_collection
|
|
@@ -117,32 +118,13 @@ class HeadMusic::Analysis::Sonority
|
|
|
117
118
|
root_position.diatonic_intervals_above_bass_pitch.all?(&:consonant?)
|
|
118
119
|
end
|
|
119
120
|
|
|
120
|
-
def triad?
|
|
121
|
-
@triad ||= trichord? && tertian?
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def seventh_chord?
|
|
125
|
-
@seventh_chord ||= tetrachord? && tertian?
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def tertian?
|
|
129
|
-
@tertian ||= inversions.detect do |inversion|
|
|
130
|
-
inversion.diatonic_intervals.count(&:third?).to_f / inversion.diatonic_intervals.length > 0.5 ||
|
|
131
|
-
(scale_degrees_above_bass_pitch && [3, 5, 7]).length == 3
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
121
|
def secundal?
|
|
136
|
-
@secundal ||= inversions.detect
|
|
137
|
-
inversion.diatonic_intervals.count(&:second?).to_f / inversion.diatonic_intervals.length > 0.5
|
|
138
|
-
end
|
|
122
|
+
@secundal ||= inversions.detect { |inversion| interval_ratio(inversion, &:second?) > 0.5 }
|
|
139
123
|
end
|
|
140
124
|
|
|
141
125
|
def quartal?
|
|
142
126
|
@quartal ||= inversions.detect do |inversion|
|
|
143
|
-
inversion
|
|
144
|
-
interval.fourth? || interval.fifth?
|
|
145
|
-
end.to_f / inversion.diatonic_intervals.length > 0.5
|
|
127
|
+
interval_ratio(inversion) { |interval| interval.fourth? || interval.fifth? } > 0.5
|
|
146
128
|
end
|
|
147
129
|
end
|
|
148
130
|
alias_method :quintal?, :quartal?
|
|
@@ -159,4 +141,12 @@ class HeadMusic::Analysis::Sonority
|
|
|
159
141
|
other = self.class.new(other) if other.is_a?(HeadMusic::Analysis::PitchCollection)
|
|
160
142
|
identifier == other.identifier
|
|
161
143
|
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Fraction of an inversion's diatonic intervals that satisfy the given predicate.
|
|
148
|
+
def interval_ratio(inversion, &predicate)
|
|
149
|
+
intervals = inversion.diatonic_intervals
|
|
150
|
+
intervals.count(&predicate).to_f / intervals.length
|
|
151
|
+
end
|
|
162
152
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
class HeadMusic::Content::Composition
|
|
2
|
+
# Rebuilds a composition from a schema v3 hash by replaying the public
|
|
3
|
+
# builder API in dependency order: meter and key changes first (position
|
|
4
|
+
# strings roll counts and ticks over via the meter map), then placements,
|
|
5
|
+
# then repeat flags (a pickup-bar flag needs its bar allocated), then
|
|
6
|
+
# comments. Raw values are validated at the boundary by SchemaValues so
|
|
7
|
+
# corrupted input raises ArgumentError with path context instead of
|
|
8
|
+
# silently deserializing wrong.
|
|
9
|
+
class HashDeserializer
|
|
10
|
+
def initialize(hash)
|
|
11
|
+
raise ArgumentError, "expected a Hash, got #{hash.class}" unless hash.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
@hash = hash.deep_transform_keys(&:to_s)
|
|
14
|
+
validate_schema_version
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def composition
|
|
18
|
+
@composition ||= build_base_composition.tap do |composition|
|
|
19
|
+
apply_bar_changes(composition)
|
|
20
|
+
build_voices(composition)
|
|
21
|
+
apply_repeat_flags(composition)
|
|
22
|
+
add_comments(composition)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :hash
|
|
29
|
+
|
|
30
|
+
def values
|
|
31
|
+
@values ||= SchemaValues.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_schema_version
|
|
35
|
+
version = hash["schema_version"]
|
|
36
|
+
return if version.is_a?(Integer) && version == SCHEMA_VERSION
|
|
37
|
+
|
|
38
|
+
message = "unsupported schema_version: #{version.inspect} (supported: #{SCHEMA_VERSION})"
|
|
39
|
+
if version == 2
|
|
40
|
+
message += "; migrate v2 \"pitches\" arrays to v3 \"sounds\" arrays " \
|
|
41
|
+
"(pitch strings unchanged; unpitched sounds are {\"unpitched\" => name_key} objects)"
|
|
42
|
+
end
|
|
43
|
+
raise ArgumentError, message
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_base_composition
|
|
47
|
+
HeadMusic::Content::Composition.new(
|
|
48
|
+
name: hash["name"],
|
|
49
|
+
key_signature: values.key_signature(hash["key_signature"], "key_signature"),
|
|
50
|
+
meter: values.meter(hash["meter"], "meter"),
|
|
51
|
+
composer: hash["composer"],
|
|
52
|
+
origin: hash["origin"]
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def bar_hashes
|
|
57
|
+
@bar_hashes ||= Array(hash["bars"])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def apply_bar_changes(composition)
|
|
61
|
+
bar_hashes.each_with_index do |bar_hash, index|
|
|
62
|
+
number = values.bar_number(bar_hash, index)
|
|
63
|
+
path = "bars[#{index}]"
|
|
64
|
+
key_signature = values.key_signature(bar_hash["key_signature"], path)
|
|
65
|
+
meter = values.meter(bar_hash["meter"], path)
|
|
66
|
+
composition.change_key_signature(number, key_signature) if key_signature
|
|
67
|
+
composition.change_meter(number, meter) if meter
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_voices(composition)
|
|
72
|
+
Array(hash["voices"]).each_with_index do |voice_hash, voice_index|
|
|
73
|
+
voice = composition.add_voice(role: voice_hash["role"])
|
|
74
|
+
Array(voice_hash["placements"]).each_with_index do |placement_hash, placement_index|
|
|
75
|
+
path = "voices[#{voice_index}].placements[#{placement_index}]"
|
|
76
|
+
position = values.position(placement_hash["position"], path)
|
|
77
|
+
rhythmic_value = values.rhythmic_value(placement_hash["rhythmic_value"], path)
|
|
78
|
+
sounds = values.placement_sounds(placement_hash, path)
|
|
79
|
+
placement = voice.place(position, rhythmic_value, sounds)
|
|
80
|
+
placement.beam_break_before = placement_hash["beam_break_before"] if placement_hash.key?("beam_break_before")
|
|
81
|
+
values.placement_syllables(placement_hash, path).each do |syllable|
|
|
82
|
+
placement.sing(syllable.text, verse: syllable.verse, hyphen_after: syllable.hyphen_after)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def apply_repeat_flags(composition)
|
|
89
|
+
bar_hashes.each_with_index do |bar_hash, index|
|
|
90
|
+
next unless repeat_state?(bar_hash)
|
|
91
|
+
|
|
92
|
+
bar = composition.bars(values.bar_number(bar_hash, index)).last
|
|
93
|
+
bar.starts_repeat = true if bar_hash["starts_repeat"]
|
|
94
|
+
ends_repeat = bar_hash["ends_repeat_after_num_plays"]
|
|
95
|
+
bar.ends_repeat_after_num_plays = ends_repeat if ends_repeat
|
|
96
|
+
plays_on_passes = bar_hash["plays_on_passes"]
|
|
97
|
+
bar.plays_on_passes = plays_on_passes if plays_on_passes
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def add_comments(composition)
|
|
102
|
+
Array(hash["comments"]).each_with_index do |comment_hash, index|
|
|
103
|
+
raw_position = comment_hash["position"]
|
|
104
|
+
position = values.position(raw_position, "comments[#{index}]") if raw_position
|
|
105
|
+
composition.add_comment(comment_hash["text"], position)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def repeat_state?(bar_hash)
|
|
110
|
+
bar_hash["starts_repeat"] || bar_hash["ends_repeat_after_num_plays"] || bar_hash["plays_on_passes"]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|