head_music 0.11.9 → 0.13.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/head_music/bar.rb +9 -7
- data/lib/head_music/clef.rb +4 -6
- data/lib/head_music/composition.rb +40 -9
- data/lib/head_music/functional_interval.rb +34 -40
- data/lib/head_music/harmonic_interval.rb +49 -0
- data/lib/head_music/instrument.rb +3 -10
- data/lib/head_music/melodic_interval.rb +36 -1
- data/lib/head_music/meter.rb +33 -17
- data/lib/head_music/motion.rb +70 -0
- data/lib/head_music/named_rudiment.rb +27 -0
- data/lib/head_music/placement.rb +7 -1
- data/lib/head_music/position.rb +10 -5
- data/lib/head_music/rhythmic_unit.rb +21 -10
- data/lib/head_music/spelling.rb +1 -1
- data/lib/head_music/style/analysis.rb +12 -1
- data/lib/head_music/style/annotation.rb +72 -1
- data/lib/head_music/style/annotations/always_move.rb +1 -3
- data/lib/head_music/style/annotations/approach_perfection_contrarily.rb +24 -0
- data/lib/head_music/style/annotations/at_least_eight_notes.rb +5 -8
- data/lib/head_music/style/annotations/avoid_crossing_voices.rb +40 -0
- data/lib/head_music/style/annotations/avoid_overlapping_voices.rb +42 -0
- data/lib/head_music/style/annotations/consonant_climax.rb +3 -5
- data/lib/head_music/style/annotations/consonant_downbeats.rb +22 -0
- data/lib/head_music/style/annotations/diatonic.rb +1 -3
- data/lib/head_music/style/annotations/direction_changes.rb +1 -3
- data/lib/head_music/style/annotations/end_on_perfect_consonance.rb +2 -12
- data/lib/head_music/style/annotations/end_on_tonic.rb +6 -8
- data/lib/head_music/style/annotations/limit_octave_leaps.rb +20 -0
- data/lib/head_music/style/annotations/mostly_conjunct.rb +1 -3
- data/lib/head_music/style/annotations/no_rests.rb +2 -4
- data/lib/head_music/style/annotations/no_unisons_in_middle.rb +39 -0
- data/lib/head_music/style/annotations/notes_same_length.rb +1 -3
- data/lib/head_music/style/annotations/one_to_one.rb +3 -13
- data/lib/head_music/style/annotations/prefer_contrary_motion.rb +23 -0
- data/lib/head_music/style/annotations/prefer_imperfect.rb +23 -0
- data/lib/head_music/style/annotations/recover_large_leaps.rb +2 -4
- data/lib/head_music/style/annotations/singable_intervals.rb +1 -3
- data/lib/head_music/style/annotations/{limit_range.rb → singable_range.rb} +2 -4
- data/lib/head_music/style/annotations/start_on_perfect_consonance.rb +3 -13
- data/lib/head_music/style/annotations/start_on_tonic.rb +1 -11
- data/lib/head_music/style/annotations/step_down_to_final_note.rb +1 -3
- data/lib/head_music/style/annotations/step_out_of_unison.rb +38 -0
- data/lib/head_music/style/annotations/step_up_to_final_note.rb +1 -3
- data/lib/head_music/style/annotations/up_to_thirteen_notes.rb +2 -4
- data/lib/head_music/style/rulesets/cantus_firmus.rb +2 -2
- data/lib/head_music/style/rulesets/first_species_harmony.rb +8 -1
- data/lib/head_music/style/rulesets/first_species_melody.rb +10 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music/voice.rb +31 -1
- data/lib/head_music.rb +14 -1
- metadata +15 -3
data/lib/head_music/spelling.rb
CHANGED
@@ -8,12 +8,23 @@ module HeadMusic
|
|
8
8
|
@subject = subject
|
9
9
|
end
|
10
10
|
|
11
|
+
def messages
|
12
|
+
annotations.reject(&:perfect?).map(&:message)
|
13
|
+
end
|
14
|
+
|
11
15
|
def annotations
|
12
16
|
@annotations ||= @ruleset.analyze(subject)
|
13
17
|
end
|
14
18
|
|
15
19
|
def fitness
|
16
|
-
annotations.
|
20
|
+
return 1.0 if annotations.length == 0
|
21
|
+
fitness_scores.inject(:+).to_f / fitness_scores.length
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def fitness_scores
|
27
|
+
annotations.map(&:fitness)
|
17
28
|
end
|
18
29
|
end
|
19
30
|
end
|
@@ -1,8 +1,12 @@
|
|
1
1
|
class HeadMusic::Style::Annotation
|
2
|
+
MESSAGE = 'Write music.'
|
3
|
+
|
2
4
|
attr_reader :voice
|
3
5
|
|
4
6
|
delegate(
|
5
7
|
:composition,
|
8
|
+
:highest_pitch,
|
9
|
+
:lowest_pitch,
|
6
10
|
:highest_notes,
|
7
11
|
:lowest_notes,
|
8
12
|
:melodic_intervals,
|
@@ -14,6 +18,9 @@ class HeadMusic::Style::Annotation
|
|
14
18
|
to: :voice
|
15
19
|
)
|
16
20
|
|
21
|
+
delegate :key_signature, to: :composition
|
22
|
+
delegate :tonic_spelling, to: :key_signature
|
23
|
+
|
17
24
|
def initialize(voice)
|
18
25
|
@voice = voice
|
19
26
|
end
|
@@ -39,6 +46,70 @@ class HeadMusic::Style::Annotation
|
|
39
46
|
end
|
40
47
|
|
41
48
|
def message
|
42
|
-
|
49
|
+
self.class::MESSAGE
|
50
|
+
end
|
51
|
+
|
52
|
+
def first_note
|
53
|
+
notes && notes.first
|
54
|
+
end
|
55
|
+
|
56
|
+
def last_note
|
57
|
+
notes && notes.last
|
58
|
+
end
|
59
|
+
|
60
|
+
def voices
|
61
|
+
@voices ||= voice.composition.voices
|
62
|
+
end
|
63
|
+
|
64
|
+
def other_voices
|
65
|
+
@other_voices ||= voices.select { |part| part != voice }
|
66
|
+
end
|
67
|
+
|
68
|
+
def cantus_firmus
|
69
|
+
@cantus_firmus ||= other_voices.detect(&:cantus_firmus?) || other_voices.first
|
70
|
+
end
|
71
|
+
|
72
|
+
def higher_voices
|
73
|
+
@higher_voices ||= unsorted_higher_voices.sort_by(&:highest_pitch).reverse
|
74
|
+
end
|
75
|
+
|
76
|
+
def lower_voices
|
77
|
+
@lower_voices ||= unsorted_lower_voices.sort_by(&:lowest_pitch).reverse
|
78
|
+
end
|
79
|
+
|
80
|
+
def functional_interval_from_tonic(note)
|
81
|
+
HeadMusic::FunctionalInterval.new(tonic_spelling, note.spelling)
|
82
|
+
end
|
83
|
+
|
84
|
+
def bass_voice?
|
85
|
+
lower_voices.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def starts_on_tonic?
|
89
|
+
tonic_spelling == first_note.spelling
|
90
|
+
end
|
91
|
+
|
92
|
+
def motions
|
93
|
+
downbeat_harmonic_intervals.map.with_index do |harmonic_interval, i|
|
94
|
+
next_harmonic_interval = downbeat_harmonic_intervals[i+1]
|
95
|
+
HeadMusic::Motion.new(harmonic_interval, next_harmonic_interval) if next_harmonic_interval
|
96
|
+
end.reject(&:nil?)
|
97
|
+
end
|
98
|
+
|
99
|
+
def downbeat_harmonic_intervals
|
100
|
+
@downbeat_harmonic_intervals ||= cantus_firmus.notes.map do |cantus_firmus_note|
|
101
|
+
interval = HeadMusic::HarmonicInterval.new(cantus_firmus_note.voice, voice, cantus_firmus_note.position)
|
102
|
+
interval.notes.length == 2 ? interval : nil
|
103
|
+
end.reject(&:nil?)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def unsorted_higher_voices
|
109
|
+
other_voices.select { |part| part.highest_pitch > highest_pitch }
|
110
|
+
end
|
111
|
+
|
112
|
+
def unsorted_lower_voices
|
113
|
+
other_voices.select { |part| part.lowest_pitch < lowest_pitch }
|
43
114
|
end
|
44
115
|
end
|
@@ -2,9 +2,7 @@ module HeadMusic::Style::Annotations
|
|
2
2
|
end
|
3
3
|
|
4
4
|
class HeadMusic::Style::Annotations::AlwaysMove < HeadMusic::Style::Annotation
|
5
|
-
|
6
|
-
"Always move to a different note."
|
7
|
-
end
|
5
|
+
MESSAGE = "Always move to a different note."
|
8
6
|
|
9
7
|
def marks
|
10
8
|
melodic_intervals.map.with_index do |interval, i|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module HeadMusic::Style::Annotations
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Annotations::ApproachPerfectionContrarily < HeadMusic::Style::Annotation
|
5
|
+
MESSAGE = 'Step down to final note.'
|
6
|
+
|
7
|
+
def marks
|
8
|
+
motions_to_perfect_consonance_approached_directly.map do |bad_motion|
|
9
|
+
HeadMusic::Style::Mark.for_all(bad_motion.notes)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def motions_to_perfect_consonance_approached_directly
|
16
|
+
motions_to_perfect_consonance.select(&:direct?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def motions_to_perfect_consonance
|
20
|
+
motions.select do |motion|
|
21
|
+
motion.second_harmonic_interval.perfect_consonance?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -4,9 +4,7 @@ end
|
|
4
4
|
class HeadMusic::Style::Annotations::AtLeastEightNotes < HeadMusic::Style::Annotation
|
5
5
|
MINIMUM_NOTES = 8
|
6
6
|
|
7
|
-
|
8
|
-
"Write at least eight notes."
|
9
|
-
end
|
7
|
+
MESSAGE = "Write at least eight notes."
|
10
8
|
|
11
9
|
def marks
|
12
10
|
placements.empty? ? no_placements_mark : deficiency_mark
|
@@ -15,17 +13,16 @@ class HeadMusic::Style::Annotations::AtLeastEightNotes < HeadMusic::Style::Annot
|
|
15
13
|
private
|
16
14
|
|
17
15
|
def no_placements_mark
|
18
|
-
|
16
|
+
HeadMusic::Style::Mark.new(
|
19
17
|
HeadMusic::Position.new(composition, "1:1"),
|
20
18
|
HeadMusic::Position.new(composition, "2:1"),
|
21
|
-
fitness:
|
19
|
+
fitness: 0
|
22
20
|
)
|
23
21
|
end
|
24
22
|
|
25
23
|
def deficiency_mark
|
26
|
-
|
27
|
-
|
28
|
-
HeadMusic::Style::Mark.for_all(placements, fitness: HeadMusic::PENALTY_FACTOR**deficiency)
|
24
|
+
if notes.length < MINIMUM_NOTES
|
25
|
+
HeadMusic::Style::Mark.for_all(placements, fitness: notes.length.to_f / MINIMUM_NOTES)
|
29
26
|
end
|
30
27
|
end
|
31
28
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module HeadMusic::Style::Annotations
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Annotations::AvoidCrossingVoices < HeadMusic::Style::Annotation
|
5
|
+
MESSAGE = "Avoid crossing voices."
|
6
|
+
|
7
|
+
def marks
|
8
|
+
crossings
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def crossings
|
14
|
+
crossings_of_lower_voices + crossings_of_higher_voices
|
15
|
+
end
|
16
|
+
|
17
|
+
def crossings_of_lower_voices
|
18
|
+
[].tap do |marks|
|
19
|
+
lower_voices.each do |lower_voice|
|
20
|
+
lower_voice.notes.each do |lower_voice_note|
|
21
|
+
notes_during = voice.notes_during(lower_voice_note)
|
22
|
+
crossed_notes = notes_during.select { |note| note.pitch < lower_voice_note.pitch }
|
23
|
+
marks << HeadMusic::Style::Mark.for_all(crossed_notes)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def crossings_of_higher_voices
|
30
|
+
[].tap do |marks|
|
31
|
+
higher_voices.each do |higher_voice|
|
32
|
+
higher_voice.notes.each do |higher_voice_note|
|
33
|
+
notes_during = voice.notes_during(higher_voice_note)
|
34
|
+
crossed_notes = notes_during.select { |note| note.pitch > lower_voice_note.pitch }
|
35
|
+
marks << HeadMusic::Style::Mark.for_all(crossed_notes)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module HeadMusic::Style::Annotations
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Annotations::AvoidOverlappingVoices < HeadMusic::Style::Annotation
|
5
|
+
MESSAGE = "Avoid overlapping voices."
|
6
|
+
|
7
|
+
def marks
|
8
|
+
overlappings
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def overlappings
|
14
|
+
overlappings_of_lower_voices + overlappings_of_higher_voices
|
15
|
+
end
|
16
|
+
|
17
|
+
def overlappings_of_lower_voices
|
18
|
+
[].tap do |marks|
|
19
|
+
lower_voices.each do |lower_voice|
|
20
|
+
overlapped_notes = voice.notes.select do |note|
|
21
|
+
preceding_note = lower_voice.note_preceding(note.position)
|
22
|
+
following_note = lower_voice.note_following(note.position)
|
23
|
+
(preceding_note && preceding_note.pitch > note.pitch) || (following_note && following_note.pitch > note.pitch)
|
24
|
+
end
|
25
|
+
marks << HeadMusic::Style::Mark.for_each(overlapped_notes)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def overlappings_of_higher_voices
|
31
|
+
[].tap do |marks|
|
32
|
+
higher_voices.each do |higher_voice|
|
33
|
+
overlapped_notes = voice.notes.select do |note|
|
34
|
+
preceding_note = higher_voice.note_preceding(note.position)
|
35
|
+
following_note = lower_voice.note_following(note.position)
|
36
|
+
(preceding_note && preceding_note.pitch < note.pitch) || (following_note && following_note.pitch < note.pitch)
|
37
|
+
end
|
38
|
+
marks << HeadMusic::Style::Mark.for_each(overlapped_notes)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -2,16 +2,14 @@ module HeadMusic::Style::Annotations
|
|
2
2
|
end
|
3
3
|
|
4
4
|
class HeadMusic::Style::Annotations::ConsonantClimax < HeadMusic::Style::Annotation
|
5
|
-
|
6
|
-
"Peak on a consonant high note one time."
|
7
|
-
end
|
5
|
+
MESSAGE = "Peak on a consonant high note one time."
|
8
6
|
|
9
7
|
def marks
|
10
8
|
if notes
|
11
9
|
improper_climaxes = highest_notes.select.with_index do |note, i|
|
12
|
-
tonic_pitch = HeadMusic::Pitch.get(
|
10
|
+
tonic_pitch = HeadMusic::Pitch.get(tonic_spelling)
|
13
11
|
interval = HeadMusic::FunctionalInterval.new(tonic_pitch, note.pitch)
|
14
|
-
interval.consonance.dissonant? || i > 0
|
12
|
+
interval.consonance(:melodic).dissonant? || i > 0
|
15
13
|
end
|
16
14
|
HeadMusic::Style::Mark.for_each(improper_climaxes)
|
17
15
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module HeadMusic::Style::Annotations
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Annotations::ConsonantDownbeats < HeadMusic::Style::Annotation
|
5
|
+
MESSAGE = "Use consonant harmonic intervals."
|
6
|
+
|
7
|
+
def marks
|
8
|
+
dissonant_pairs.map do |dissonant_pair|
|
9
|
+
HeadMusic::Style::Mark.for_all(dissonant_pair)
|
10
|
+
end.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def dissonant_pairs
|
16
|
+
dissonant_intervals.map(&:notes).reject(&:nil?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def dissonant_intervals
|
20
|
+
downbeat_harmonic_intervals.select { |interval| interval.dissonance?(:two_part_harmony) }
|
21
|
+
end
|
22
|
+
end
|
@@ -2,9 +2,7 @@ module HeadMusic::Style::Annotations
|
|
2
2
|
end
|
3
3
|
|
4
4
|
class HeadMusic::Style::Annotations::Diatonic < HeadMusic::Style::Annotation
|
5
|
-
|
6
|
-
"Use only notes in the key signature."
|
7
|
-
end
|
5
|
+
MESSAGE = "Use only notes in the key signature."
|
8
6
|
|
9
7
|
def marks
|
10
8
|
HeadMusic::Style::Mark.for_each(notes_not_in_key)
|
@@ -4,9 +4,7 @@ end
|
|
4
4
|
class HeadMusic::Style::Annotations::DirectionChanges < HeadMusic::Style::Annotation
|
5
5
|
MAXIMUM_NOTES_PER_DIRECTION = 3
|
6
6
|
|
7
|
-
|
8
|
-
"Balance ascending and descending motion."
|
9
|
-
end
|
7
|
+
MESSAGE = "Balance ascending and descending motion."
|
10
8
|
|
11
9
|
def marks
|
12
10
|
if overage > 0
|
@@ -3,9 +3,7 @@ end
|
|
3
3
|
|
4
4
|
# marks the voice if the first note is not the first or fifth scale degree of the key.
|
5
5
|
class HeadMusic::Style::Annotations::EndOnPerfectConsonance < HeadMusic::Style::Annotation
|
6
|
-
|
7
|
-
'End on the tonic or a perfect consonance above the tonic.'
|
8
|
-
end
|
6
|
+
MESSAGE = 'End on the tonic or the fifth scale degree.'
|
9
7
|
|
10
8
|
def marks
|
11
9
|
if last_note && !ends_on_perfect_consonance?
|
@@ -16,14 +14,6 @@ class HeadMusic::Style::Annotations::EndOnPerfectConsonance < HeadMusic::Style::
|
|
16
14
|
private
|
17
15
|
|
18
16
|
def ends_on_perfect_consonance?
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
def functional_interval
|
23
|
-
HeadMusic::FunctionalInterval.new(composition.key_signature.tonic_spelling, last_note.spelling)
|
24
|
-
end
|
25
|
-
|
26
|
-
def last_note
|
27
|
-
notes.last
|
17
|
+
functional_interval_from_tonic(last_note).perfect_consonance?(:two_part_harmony)
|
28
18
|
end
|
29
19
|
end
|
@@ -2,9 +2,7 @@ module HeadMusic::Style::Annotations
|
|
2
2
|
end
|
3
3
|
|
4
4
|
class HeadMusic::Style::Annotations::EndOnTonic < HeadMusic::Style::Annotation
|
5
|
-
|
6
|
-
'End on the tonic.'
|
7
|
-
end
|
5
|
+
MESSAGE = 'End on the tonic.'
|
8
6
|
|
9
7
|
def marks
|
10
8
|
if !notes.empty? && !ends_on_tonic?
|
@@ -15,10 +13,10 @@ class HeadMusic::Style::Annotations::EndOnTonic < HeadMusic::Style::Annotation
|
|
15
13
|
private
|
16
14
|
|
17
15
|
def ends_on_tonic?
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
tonic_spelling == last_note_spelling
|
17
|
+
end
|
18
|
+
|
19
|
+
def last_note_spelling
|
20
|
+
last_note && last_note.spelling
|
23
21
|
end
|
24
22
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module HeadMusic::Style::Annotations
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Annotations::LimitOctaveLeaps < HeadMusic::Style::Annotation
|
5
|
+
MESSAGE = "Use a maximum of one octave skip."
|
6
|
+
|
7
|
+
def marks
|
8
|
+
if octave_leaps.length > 1
|
9
|
+
octave_leaps.map do |leap|
|
10
|
+
HeadMusic::Style::Mark.for_all(leap.notes)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def octave_leaps
|
18
|
+
melodic_intervals.select(&:octave?)
|
19
|
+
end
|
20
|
+
end
|
@@ -2,9 +2,7 @@ module HeadMusic::Style::Annotations
|
|
2
2
|
end
|
3
3
|
|
4
4
|
class HeadMusic::Style::Annotations::MostlyConjunct < HeadMusic::Style::Annotation
|
5
|
-
|
6
|
-
"Use mostly conjunct motion."
|
7
|
-
end
|
5
|
+
MESSAGE = "Use mostly conjunct motion."
|
8
6
|
|
9
7
|
def marks
|
10
8
|
marks_for_skips_and_leaps if conjunct_ratio <= 0.5
|
@@ -2,11 +2,9 @@ module HeadMusic::Style::Annotations
|
|
2
2
|
end
|
3
3
|
|
4
4
|
class HeadMusic::Style::Annotations::NoRests < HeadMusic::Style::Annotation
|
5
|
-
|
6
|
-
"Use only notes."
|
7
|
-
end
|
5
|
+
MESSAGE = "Use only notes."
|
8
6
|
|
9
7
|
def marks
|
10
|
-
|
8
|
+
HeadMusic::Style::Mark.for_each(rests)
|
11
9
|
end
|
12
10
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module HeadMusic::Style::Annotations
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Annotations::NoUnisonsInMiddle < HeadMusic::Style::Annotation
|
5
|
+
MESSAGE = "Unisons may only be used in the first and last note."
|
6
|
+
|
7
|
+
def marks
|
8
|
+
unison_pairs.map do |notes|
|
9
|
+
HeadMusic::Style::Mark.for_all(notes)
|
10
|
+
end.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def unison_pairs
|
16
|
+
middle_unisons.map(&:notes).reject(&:nil?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def middle_unisons
|
20
|
+
middle_intervals.select { |interval| interval.perfect_consonance? && interval.unison? }
|
21
|
+
end
|
22
|
+
|
23
|
+
def middle_intervals
|
24
|
+
[harmonic_intervals[1..-2]].flatten.reject(&:nil?)
|
25
|
+
end
|
26
|
+
|
27
|
+
def harmonic_intervals
|
28
|
+
cantus_firmus.notes.map do |cantus_firmus_note|
|
29
|
+
counterpoint_notes = voice.notes_during(cantus_firmus_note)
|
30
|
+
counterpoint_notes.map { |note|
|
31
|
+
HarmonicInterval.new(cantus_firmus_note.voice, voice, note.position)
|
32
|
+
}
|
33
|
+
end.flatten
|
34
|
+
end
|
35
|
+
|
36
|
+
def positions
|
37
|
+
voices.map(:notes).flatten.map(&:position).sort.uniq
|
38
|
+
end
|
39
|
+
end
|
@@ -2,9 +2,7 @@ module HeadMusic::Style::Annotations
|
|
2
2
|
end
|
3
3
|
|
4
4
|
class HeadMusic::Style::Annotations::NotesSameLength < HeadMusic::Style::Annotation
|
5
|
-
|
6
|
-
'Use consistent rhythmic unit.'
|
7
|
-
end
|
5
|
+
MESSAGE = 'Use consistent rhythmic unit.'
|
8
6
|
|
9
7
|
def marks
|
10
8
|
preferred_value = first_most_common_rhythmic_value
|
@@ -2,14 +2,12 @@ module HeadMusic::Style::Annotations
|
|
2
2
|
end
|
3
3
|
|
4
4
|
class HeadMusic::Style::Annotations::OneToOne < HeadMusic::Style::Annotation
|
5
|
-
|
6
|
-
'Place a note for each note in the other voice.'
|
7
|
-
end
|
5
|
+
MESSAGE = 'Place a note for each note in the other voice.'
|
8
6
|
|
9
7
|
def marks
|
10
|
-
if
|
8
|
+
if cantus_firmus && cantus_firmus.notes.length > 0
|
11
9
|
HeadMusic::Style::Mark.for_each(
|
12
|
-
notes_without_match(voice,
|
10
|
+
notes_without_match(voice, cantus_firmus) + notes_without_match(cantus_firmus, voice)
|
13
11
|
)
|
14
12
|
end
|
15
13
|
end
|
@@ -21,12 +19,4 @@ class HeadMusic::Style::Annotations::OneToOne < HeadMusic::Style::Annotation
|
|
21
19
|
voice2.notes.map(&:position).include?(voice1_note.position)
|
22
20
|
end
|
23
21
|
end
|
24
|
-
|
25
|
-
def other_voice
|
26
|
-
other_voices.detect(&:cantus_firmus?) || other_voices.first
|
27
|
-
end
|
28
|
-
|
29
|
-
def other_voices
|
30
|
-
@other_voices ||= voice.composition.voices.select { |part| part != voice }
|
31
|
-
end
|
32
22
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HeadMusic::Style::Annotations
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Annotations::PreferContraryMotion < HeadMusic::Style::Annotation
|
5
|
+
MESSAGE = "Prefer contrary motion."
|
6
|
+
|
7
|
+
def marks
|
8
|
+
return nil if notes.length < 2
|
9
|
+
return nil if direct_motion_ratio <= 0.5
|
10
|
+
direct_motions.map { |motion| HeadMusic::Style::Mark.for_all(motion.notes) }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def direct_motions
|
16
|
+
motions.select(&:direct?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def direct_motion_ratio
|
20
|
+
return 0 if motions.empty?
|
21
|
+
direct_motions.count / motions.count.to_f
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HeadMusic::Style::Annotations
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Annotations::PreferImperfect < HeadMusic::Style::Annotation
|
5
|
+
MESSAGE = "Use consonant harmonic intervals."
|
6
|
+
|
7
|
+
def marks
|
8
|
+
if ratio_of_perfect_intervals > 0.5
|
9
|
+
HeadMusic::Style::Mark.for_all(perfect_intervals.map(&:notes).flatten)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def ratio_of_perfect_intervals
|
16
|
+
return 0 if downbeat_harmonic_intervals.empty?
|
17
|
+
perfect_intervals.length.to_f / downbeat_harmonic_intervals.length
|
18
|
+
end
|
19
|
+
|
20
|
+
def perfect_intervals
|
21
|
+
downbeat_harmonic_intervals.select { |interval| interval.perfect_consonance?(:two_part_harmony) }
|
22
|
+
end
|
23
|
+
end
|
@@ -6,9 +6,7 @@ end
|
|
6
6
|
# unless another leap (in either direction) creates a consonant triad.
|
7
7
|
# - Brian
|
8
8
|
class HeadMusic::Style::Annotations::RecoverLargeLeaps < HeadMusic::Style::Annotation
|
9
|
-
|
10
|
-
"Recover leaps by step in the opposite direction."
|
11
|
-
end
|
9
|
+
MESSAGE = "Recover leaps by step in the opposite direction."
|
12
10
|
|
13
11
|
def marks
|
14
12
|
melodic_intervals.drop(1).to_a.map.with_index do |interval, i|
|
@@ -22,7 +20,7 @@ class HeadMusic::Style::Annotations::RecoverLargeLeaps < HeadMusic::Style::Annot
|
|
22
20
|
private
|
23
21
|
|
24
22
|
def unrecovered_leap?(first_interval, second_interval)
|
25
|
-
first_interval.
|
23
|
+
first_interval.large_leap? &&
|
26
24
|
!spelling_consonant_triad?(first_interval, second_interval) &&
|
27
25
|
(
|
28
26
|
!direction_changed?(first_interval, second_interval) ||
|
@@ -5,9 +5,7 @@ class HeadMusic::Style::Annotations::SingableIntervals < HeadMusic::Style::Annot
|
|
5
5
|
PERMITTED_ASCENDING = %w[m2 M2 m3 M3 P4 P5 m6 P8]
|
6
6
|
PERMITTED_DESCENDING = %w[m2 M2 m3 M3 P4 P5 P8]
|
7
7
|
|
8
|
-
|
9
|
-
"Use only m2, M2, m3, M3, P4, P5, m6 (ascending), P8."
|
10
|
-
end
|
8
|
+
MESSAGE = "Use only m2, M2, m3, M3, P4, P5, m6 (ascending), P8."
|
11
9
|
|
12
10
|
def marks
|
13
11
|
melodic_intervals.reject { |interval| permitted?(interval) }.map do |unpermitted_interval|
|
@@ -1,12 +1,10 @@
|
|
1
1
|
module HeadMusic::Style::Annotations
|
2
2
|
end
|
3
3
|
|
4
|
-
class HeadMusic::Style::Annotations::
|
4
|
+
class HeadMusic::Style::Annotations::SingableRange < HeadMusic::Style::Annotation
|
5
5
|
MAXIMUM_RANGE = 10
|
6
6
|
|
7
|
-
|
8
|
-
'Limit melodic range to a 10th.'
|
9
|
-
end
|
7
|
+
MESSAGE = 'Limit melodic range to a 10th.'
|
10
8
|
|
11
9
|
def marks
|
12
10
|
if overage > 0
|
@@ -3,12 +3,10 @@ end
|
|
3
3
|
|
4
4
|
# marks the voice if the first note is not the first or fifth scale degree of the key.
|
5
5
|
class HeadMusic::Style::Annotations::StartOnPerfectConsonance < HeadMusic::Style::Annotation
|
6
|
-
|
7
|
-
'Start on the tonic or a perfect consonance above the tonic.'
|
8
|
-
end
|
6
|
+
MESSAGE = 'Start on the tonic or a perfect consonance above the tonic.'
|
9
7
|
|
10
8
|
def marks
|
11
|
-
if first_note && !starts_on_perfect_consonance?
|
9
|
+
if first_note && ((bass_voice? && !starts_on_tonic?) || !starts_on_perfect_consonance?)
|
12
10
|
HeadMusic::Style::Mark.for(first_note)
|
13
11
|
end
|
14
12
|
end
|
@@ -16,14 +14,6 @@ class HeadMusic::Style::Annotations::StartOnPerfectConsonance < HeadMusic::Style
|
|
16
14
|
private
|
17
15
|
|
18
16
|
def starts_on_perfect_consonance?
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
def functional_interval
|
23
|
-
HeadMusic::FunctionalInterval.new(composition.key_signature.tonic_spelling, first_note.spelling)
|
24
|
-
end
|
25
|
-
|
26
|
-
def first_note
|
27
|
-
notes.first
|
17
|
+
functional_interval_from_tonic(first_note).perfect_consonance?(:two_part_harmony)
|
28
18
|
end
|
29
19
|
end
|