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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/head_music/bar.rb +9 -7
  4. data/lib/head_music/clef.rb +4 -6
  5. data/lib/head_music/composition.rb +40 -9
  6. data/lib/head_music/functional_interval.rb +34 -40
  7. data/lib/head_music/harmonic_interval.rb +49 -0
  8. data/lib/head_music/instrument.rb +3 -10
  9. data/lib/head_music/melodic_interval.rb +36 -1
  10. data/lib/head_music/meter.rb +33 -17
  11. data/lib/head_music/motion.rb +70 -0
  12. data/lib/head_music/named_rudiment.rb +27 -0
  13. data/lib/head_music/placement.rb +7 -1
  14. data/lib/head_music/position.rb +10 -5
  15. data/lib/head_music/rhythmic_unit.rb +21 -10
  16. data/lib/head_music/spelling.rb +1 -1
  17. data/lib/head_music/style/analysis.rb +12 -1
  18. data/lib/head_music/style/annotation.rb +72 -1
  19. data/lib/head_music/style/annotations/always_move.rb +1 -3
  20. data/lib/head_music/style/annotations/approach_perfection_contrarily.rb +24 -0
  21. data/lib/head_music/style/annotations/at_least_eight_notes.rb +5 -8
  22. data/lib/head_music/style/annotations/avoid_crossing_voices.rb +40 -0
  23. data/lib/head_music/style/annotations/avoid_overlapping_voices.rb +42 -0
  24. data/lib/head_music/style/annotations/consonant_climax.rb +3 -5
  25. data/lib/head_music/style/annotations/consonant_downbeats.rb +22 -0
  26. data/lib/head_music/style/annotations/diatonic.rb +1 -3
  27. data/lib/head_music/style/annotations/direction_changes.rb +1 -3
  28. data/lib/head_music/style/annotations/end_on_perfect_consonance.rb +2 -12
  29. data/lib/head_music/style/annotations/end_on_tonic.rb +6 -8
  30. data/lib/head_music/style/annotations/limit_octave_leaps.rb +20 -0
  31. data/lib/head_music/style/annotations/mostly_conjunct.rb +1 -3
  32. data/lib/head_music/style/annotations/no_rests.rb +2 -4
  33. data/lib/head_music/style/annotations/no_unisons_in_middle.rb +39 -0
  34. data/lib/head_music/style/annotations/notes_same_length.rb +1 -3
  35. data/lib/head_music/style/annotations/one_to_one.rb +3 -13
  36. data/lib/head_music/style/annotations/prefer_contrary_motion.rb +23 -0
  37. data/lib/head_music/style/annotations/prefer_imperfect.rb +23 -0
  38. data/lib/head_music/style/annotations/recover_large_leaps.rb +2 -4
  39. data/lib/head_music/style/annotations/singable_intervals.rb +1 -3
  40. data/lib/head_music/style/annotations/{limit_range.rb → singable_range.rb} +2 -4
  41. data/lib/head_music/style/annotations/start_on_perfect_consonance.rb +3 -13
  42. data/lib/head_music/style/annotations/start_on_tonic.rb +1 -11
  43. data/lib/head_music/style/annotations/step_down_to_final_note.rb +1 -3
  44. data/lib/head_music/style/annotations/step_out_of_unison.rb +38 -0
  45. data/lib/head_music/style/annotations/step_up_to_final_note.rb +1 -3
  46. data/lib/head_music/style/annotations/up_to_thirteen_notes.rb +2 -4
  47. data/lib/head_music/style/rulesets/cantus_firmus.rb +2 -2
  48. data/lib/head_music/style/rulesets/first_species_harmony.rb +8 -1
  49. data/lib/head_music/style/rulesets/first_species_melody.rb +10 -2
  50. data/lib/head_music/version.rb +1 -1
  51. data/lib/head_music/voice.rb +31 -1
  52. data/lib/head_music.rb +14 -1
  53. metadata +15 -3
@@ -1,5 +1,5 @@
1
1
  class HeadMusic::Spelling
2
- MATCHER = /^\s*([A-G])([b#]*)(\-?\d+)?\s*$/
2
+ MATCHER = /^\s*([A-G])([b#]*)(\-?\d+)?\s*$/i
3
3
 
4
4
  attr_reader :pitch_class
5
5
  attr_reader :letter_name
@@ -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.map(&:fitness).reduce(1, :*)
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
- raise NotImplementedError
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
- def message
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
- def message
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
- return HeadMusic::Style::Mark.new(
16
+ HeadMusic::Style::Mark.new(
19
17
  HeadMusic::Position.new(composition, "1:1"),
20
18
  HeadMusic::Position.new(composition, "2:1"),
21
- fitness: HeadMusic::PENALTY_FACTOR**MINIMUM_NOTES
19
+ fitness: 0
22
20
  )
23
21
  end
24
22
 
25
23
  def deficiency_mark
26
- deficiency = [MINIMUM_NOTES - notes.length, 0].max
27
- if deficiency > 0
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
- def message
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(composition.key_signature.tonic_spelling)
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
- def message
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
- def message
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
- def message
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
- functional_interval.perfect_consonance?(:two_part_harmony)
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
- def message
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
- notes &&
19
- notes.last &&
20
- composition &&
21
- composition.key_signature &&
22
- composition.key_signature.tonic_spelling == notes.last.spelling
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
- def message
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
- def message
6
- "Use only notes."
7
- end
5
+ MESSAGE = "Use only notes."
8
6
 
9
7
  def marks
10
- rests.map { |rest| HeadMusic::Style::Mark.for(rest) }
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
- def message
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
- def message
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 other_voice && other_voice.notes.length > 0
8
+ if cantus_firmus && cantus_firmus.notes.length > 0
11
9
  HeadMusic::Style::Mark.for_each(
12
- notes_without_match(voice, other_voice) + notes_without_match(other_voice, 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
- def message
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.leap? &&
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
- def message
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::LimitRange < HeadMusic::Style::Annotation
4
+ class HeadMusic::Style::Annotations::SingableRange < HeadMusic::Style::Annotation
5
5
  MAXIMUM_RANGE = 10
6
6
 
7
- def message
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
- def message
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
- functional_interval.perfect_consonance?(:two_part_harmony)
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