head_music 0.10.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/head_music/chord.rb +29 -0
  3. data/lib/head_music/composition.rb +2 -2
  4. data/lib/head_music/consonance.rb +4 -0
  5. data/lib/head_music/functional_interval.rb +10 -9
  6. data/lib/head_music/key_signature.rb +6 -10
  7. data/lib/head_music/letter_name.rb +1 -1
  8. data/lib/head_music/melodic_interval.rb +8 -0
  9. data/lib/head_music/meter.rb +1 -1
  10. data/lib/head_music/rhythmic_value.rb +1 -1
  11. data/lib/head_music/scale.rb +1 -2
  12. data/lib/head_music/scale_type.rb +3 -3
  13. data/lib/head_music/spelling.rb +2 -2
  14. data/lib/head_music/style/analysis.rb +4 -1
  15. data/lib/head_music/style/annotation.rb +21 -17
  16. data/lib/head_music/style/annotations/always_move.rb +16 -0
  17. data/lib/head_music/style/annotations/at_least_eight_notes.rb +31 -0
  18. data/lib/head_music/style/annotations/consonant_climax.rb +19 -0
  19. data/lib/head_music/style/annotations/diatonic.rb +12 -0
  20. data/lib/head_music/style/annotations/direction_changes.rb +35 -0
  21. data/lib/head_music/style/annotations/end_on_tonic.rb +24 -0
  22. data/lib/head_music/style/annotations/limit_range.rb +26 -0
  23. data/lib/head_music/style/annotations/mostly_conjunct.rb +26 -0
  24. data/lib/head_music/style/annotations/no_rests.rb +12 -0
  25. data/lib/head_music/style/annotations/notes_same_length.rb +45 -0
  26. data/lib/head_music/style/annotations/permitted_intervals.rb +27 -0
  27. data/lib/head_music/style/annotations/recover_large_leaps.rb +44 -0
  28. data/lib/head_music/style/annotations/start_on_tonic.rb +22 -0
  29. data/lib/head_music/style/annotations/step_down_to_final_note.rb +33 -0
  30. data/lib/head_music/style/annotations/up_to_thirteen_notes.rb +22 -0
  31. data/lib/head_music/style/mark.rb +10 -9
  32. data/lib/head_music/style/rulesets/cantus_firmus.rb +16 -12
  33. data/lib/head_music/version.rb +1 -1
  34. data/lib/head_music/voice.rb +5 -1
  35. data/lib/head_music.rb +18 -12
  36. metadata +18 -14
  37. data/lib/head_music/style/rule.rb +0 -13
  38. data/lib/head_music/style/rules/always_move.rb +0 -19
  39. data/lib/head_music/style/rules/at_least_eight_notes.rb +0 -26
  40. data/lib/head_music/style/rules/diatonic.rb +0 -15
  41. data/lib/head_music/style/rules/end_on_tonic.rb +0 -27
  42. data/lib/head_music/style/rules/limit_range.rb +0 -31
  43. data/lib/head_music/style/rules/mostly_conjunct.rb +0 -30
  44. data/lib/head_music/style/rules/no_rests.rb +0 -14
  45. data/lib/head_music/style/rules/notes_same_length.rb +0 -49
  46. data/lib/head_music/style/rules/start_on_tonic.rb +0 -27
  47. data/lib/head_music/style/rules/step_down_to_final_note.rb +0 -34
  48. data/lib/head_music/style/rules/up_to_thirteen_notes.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f3c4a7d9f36e3a577a3b3e15dde8e3b9b8719937
4
- data.tar.gz: dc196e99e9f17077d7135fb17047a07a8fee13be
3
+ metadata.gz: 349d91b0d02df9a4722f7634e1f8d99a9849a2c0
4
+ data.tar.gz: 3ec53a83af4f774862d3613a4984c313b5f3ea68
5
5
  SHA512:
6
- metadata.gz: 1fa70d04a30f7f652420d88224192f4a2dd89a8436a363a3306129336d3a61a390ecd0d4c8baf2a7ca9cd05798dc4bbd09e0969bf0107335a7adaa3a3ae7f195
7
- data.tar.gz: 95a836da90aa931b77935d7558c2fb6710b9b559ec69578b39451655551e058bfa19ef68fdea8541dbb24912011d34490d61e34f9baf4eff65f22fd8fec48f85
6
+ metadata.gz: 69b19f4ba6c53650588b961fd05dda708050a79134442ea0530e7608e6e6eebe877fce5f4dd72b4e002fc8db4ae3280d5a9220b7ca65b511cefbcb4b25c4f539
7
+ data.tar.gz: bcd8464c13c4cfcaddc8a91f1a11e657f3cebe34cba000b067a8c7554774f921fdd822deead34a01479495201c8db9f5d3dc0d75d335d2f8cadec12f37c2f19a
@@ -0,0 +1,29 @@
1
+ class HeadMusic::Chord
2
+ attr_reader :pitches
3
+
4
+ def initialize(pitches)
5
+ raise ArgumentError if pitches.length < 3
6
+ @pitches = pitches.map { |pitch| Pitch.get(pitch) }.sort
7
+ end
8
+
9
+ def consonant_triad?
10
+ pitches.length == 3 &&
11
+ (
12
+ intervals.map(&:shorthand).sort == %w[M3 m3] ||
13
+ invert.intervals.map(&:shorthand).sort == %w[M3 m3] ||
14
+ invert.invert.intervals.map(&:shorthand).sort == %w[M3 m3]
15
+ )
16
+ end
17
+
18
+ def intervals
19
+ pitches[1..-1].map.with_index do |pitch, i|
20
+ FunctionalInterval.new(pitches[i], pitch)
21
+ end
22
+ end
23
+
24
+ def invert
25
+ inverted_pitch = pitches[0] + HeadMusic::Interval.get(12)
26
+ new_pitches = pitches[1..-1] + [inverted_pitch]
27
+ HeadMusic::Chord.new(new_pitches)
28
+ end
29
+ end
@@ -1,7 +1,7 @@
1
1
  class HeadMusic::Composition
2
2
  attr_reader :name, :key_signature, :meter, :bars, :voices
3
3
 
4
- def initialize(name:, key_signature: nil, meter: nil)
4
+ def initialize(name: nil, key_signature: nil, meter: nil)
5
5
  ensure_attributes(name, key_signature, meter)
6
6
  add_bar
7
7
  add_voice
@@ -26,7 +26,7 @@ class HeadMusic::Composition
26
26
  private
27
27
 
28
28
  def ensure_attributes(name, key_signature, meter)
29
- @name = name
29
+ @name = name || 'Composition'
30
30
  @key_signature = HeadMusic::KeySignature.get(key_signature) if key_signature
31
31
  @key_signature ||= HeadMusic::KeySignature.default
32
32
  @meter = HeadMusic::Meter.get(meter) if meter
@@ -14,6 +14,10 @@ class HeadMusic::Consonance
14
14
  @name = name.to_s.to_sym
15
15
  end
16
16
 
17
+ def ==(other)
18
+ self.to_s == other.to_s
19
+ end
20
+
17
21
  LEVELS.each do |method_name|
18
22
  define_method(:"#{method_name}?") { to_s == method_name }
19
23
  end
@@ -121,7 +121,7 @@ class HeadMusic::FunctionalInterval
121
121
  if number < NUMBER_NAMES.length
122
122
  [quality_name, number_name].join(' ')
123
123
  elsif simple_name == 'perfect unison'
124
- string = "#{octaves.humanize} octaves"
124
+ "#{octaves.humanize} octaves"
125
125
  else
126
126
  "#{octaves.humanize} octaves and #{quality.article} #{simple_name}"
127
127
  end
@@ -160,18 +160,18 @@ class HeadMusic::FunctionalInterval
160
160
  HeadMusic::FunctionalInterval.new(higher_pitch, inverted_low_pitch)
161
161
  end
162
162
 
163
- def consonance
164
- if quality.perfect?
165
- if [number, simple_number].include?(4)
163
+ def consonance(style = :standard_practice)
164
+ if perfect?
165
+ if fourth? && style == :two_part_harmony
166
166
  HeadMusic::Consonance.get(:dissonant)
167
167
  else
168
168
  HeadMusic::Consonance.get(:perfect)
169
169
  end
170
- elsif quality.major? || quality.minor?
171
- if [number, simple_number] & [3, 6].empty?
172
- HeadMusic::Consonance.get(:dissonant)
173
- else
170
+ elsif major? || minor?
171
+ if third? || sixth?
174
172
  HeadMusic::Consonance.get(:imperfect)
173
+ else
174
+ HeadMusic::Consonance.get(:dissonant)
175
175
  end
176
176
  else
177
177
  HeadMusic::Consonance.get(:dissonant)
@@ -189,12 +189,13 @@ class HeadMusic::FunctionalInterval
189
189
  def leap?
190
190
  number > 3
191
191
  end
192
+ alias_method :large_leap?, :leap?
192
193
 
193
194
  def ==(other)
194
195
  self.to_s.gsub(/\W/, '_') == other.to_s.gsub(/\W/, '_')
195
196
  end
196
197
 
197
198
  NUMBER_NAMES.each do |method_name|
198
- define_method(:"#{method_name}?") { number_name == method_name }
199
+ define_method(:"#{method_name}?") { number_name == method_name || simple_number_name == method_name }
199
200
  end
200
201
  end
@@ -30,20 +30,16 @@ class HeadMusic::KeySignature
30
30
  @scale = HeadMusic::Scale.get(@tonic_spelling, @scale_type)
31
31
  end
32
32
 
33
+ def spellings
34
+ pitches.map(&:spelling).uniq
35
+ end
36
+
33
37
  def sharps
34
- pitches.map(&:spelling).uniq.select { |spelling|
35
- spelling.sharp?
36
- }.map(&:to_s).sort_by { |sharp|
37
- SHARPS.index(sharp)
38
- }
38
+ spellings.select(&:sharp?).sort_by { |sharp| SHARPS.index(sharp.to_s) }
39
39
  end
40
40
 
41
41
  def flats
42
- pitches.map(&:spelling).uniq.select { |spelling|
43
- spelling.flat?
44
- }.map(&:to_s).sort_by { |flat|
45
- FLATS.index(flat)
46
- }
42
+ spellings.select(&:flat?).sort_by { |flat| FLATS.index(flat.to_s) }
47
43
  end
48
44
 
49
45
  def num_sharps
@@ -29,7 +29,7 @@ class HeadMusic::LetterName
29
29
  @letter_names ||= {}
30
30
  return nil if pitch_class.to_s == pitch_class
31
31
  pitch_class = pitch_class.to_i % 12
32
- name = NAMES.detect { |name| pitch_class == NATURAL_PITCH_CLASS_NUMBERS[name] }
32
+ name = NAMES.detect { |candidate| pitch_class == NATURAL_PITCH_CLASS_NUMBERS[candidate] }
33
33
  name ||= HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_class].first
34
34
  @letter_names[name] ||= new(name) if NAMES.include?(name)
35
35
  end
@@ -19,6 +19,14 @@ class HeadMusic::MelodicInterval
19
19
  second_note.next_position
20
20
  end
21
21
 
22
+ def notes
23
+ [first_note, second_note]
24
+ end
25
+
26
+ def pitches
27
+ notes.map(&:pitch)
28
+ end
29
+
22
30
  def to_s
23
31
  [direction, functional_interval].join(' ')
24
32
  end
@@ -7,7 +7,7 @@ class HeadMusic::Meter
7
7
  }
8
8
 
9
9
  def self.get(identifier)
10
- identifer = identifer.to_s
10
+ identifier = identifier.to_s
11
11
  hash_key = HeadMusic::Utilities::HashKey.for(identifier)
12
12
  time_signature_string = NAMED[hash_key] || identifier
13
13
  @meters ||= {}
@@ -56,7 +56,7 @@ class HeadMusic::RhythmicValue
56
56
  end
57
57
 
58
58
  def multiplier
59
- (0..dots).reduce(0) { |sum, i| sum += (1.0/2)**i }
59
+ (0..dots).reduce(0) { |sum, i| sum + (1.0/2)**i }
60
60
  end
61
61
 
62
62
  def ticks
@@ -27,12 +27,11 @@ class HeadMusic::Scale
27
27
  end
28
28
 
29
29
  def determine_scale_pitches(direction, octaves)
30
- letter_name_cycle = root_pitch.letter_name_cycle
31
30
  semitones_from_root = 0
32
31
  [root_pitch].tap do |pitches|
33
32
  [:ascending, :descending].each do |single_direction|
34
33
  if [single_direction, :both].include?(direction)
35
- (1..octaves).each do |i|
34
+ (1..octaves).each do
36
35
  direction_intervals(single_direction).each_with_index do |semitones, i|
37
36
  semitones_from_root += semitones * direction_sign(single_direction)
38
37
  pitches << pitch_for_step(i+1, semitones_from_root, single_direction)
@@ -60,9 +60,9 @@ class HeadMusic::ScaleType
60
60
 
61
61
  def self.get(name)
62
62
  @scale_types ||= {}
63
- name = name.to_s.to_sym
64
- attributes = SCALE_TYPES[name]
65
- @scale_types[name] ||= new(name, attributes)
63
+ identifier = HeadMusic::Utilities::HashKey.for(name)
64
+ attributes = SCALE_TYPES[identifier]
65
+ @scale_types[identifier] ||= new(identifier, attributes)
66
66
  end
67
67
 
68
68
  def self.default
@@ -37,8 +37,8 @@ class HeadMusic::Spelling
37
37
 
38
38
  def self.from_number_and_letter(number, letter_name)
39
39
  letter_name = HeadMusic::LetterName.get(letter_name)
40
- natural_letter_pitch_class = HeadMusic::LetterName.get(letter_name).pitch_class
41
- accidental_interval = letter_name.pitch_class.smallest_interval_to(HeadMusic::PitchClass.get(number))
40
+ natural_letter_pitch_class = letter_name.pitch_class
41
+ accidental_interval = natural_letter_pitch_class.smallest_interval_to(HeadMusic::PitchClass.get(number))
42
42
  accidental = HeadMusic::Accidental.for_interval(accidental_interval)
43
43
  fetch_or_create(letter_name, accidental)
44
44
  end
@@ -6,7 +6,10 @@ module HeadMusic
6
6
  def initialize(ruleset, subject)
7
7
  @ruleset = ruleset
8
8
  @subject = subject
9
- @annotations = @ruleset.analyze(subject)
9
+ end
10
+
11
+ def annotations
12
+ @annotations ||= @ruleset.analyze(subject)
10
13
  end
11
14
 
12
15
  def fitness
@@ -1,28 +1,32 @@
1
1
  class HeadMusic::Style::Annotation
2
- attr_reader :fitness, :message, :marks, :subject
2
+ attr_reader :voice
3
3
 
4
- def initialize(subject:, fitness:, message: nil, marks: nil)
5
- @subject = subject
6
- @fitness = fitness
7
- @message = message
8
- @marks = [marks].flatten.compact
9
- end
4
+ delegate(
5
+ :composition,
6
+ :highest_notes,
7
+ :lowest_notes,
8
+ :melodic_intervals,
9
+ :notes,
10
+ :notes_not_in_key,
11
+ :placements,
12
+ :range,
13
+ :rests,
14
+ to: :voice
15
+ )
10
16
 
11
- def voice
12
- subject if subject.is_a?(HeadMusic::Voice)
17
+ def initialize(voice)
18
+ @voice = voice
13
19
  end
14
20
 
15
- def composition
16
- voice ? voice.composition : subject
21
+ def fitness
22
+ [marks].flatten.compact.map(&:fitness).reduce(1, :*)
17
23
  end
18
24
 
19
- def marks_count
20
- marks ? marks.length : 0
25
+ def marks
26
+ raise NotImplementedError
21
27
  end
22
28
 
23
- def first_mark_code
24
- marks.first.code if marks.first
29
+ def message
30
+ raise NotImplementedError
25
31
  end
26
-
27
- alias_method :to_s, :message
28
32
  end
@@ -0,0 +1,16 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::AlwaysMove < HeadMusic::Style::Annotation
5
+ def message
6
+ "Always move to a different note."
7
+ end
8
+
9
+ def marks
10
+ melodic_intervals.map.with_index do |interval, i|
11
+ if interval.shorthand == 'PU'
12
+ HeadMusic::Style::Mark.for_all(notes[i..i+1])
13
+ end
14
+ end.reject(&:nil?)
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::AtLeastEightNotes < HeadMusic::Style::Annotation
5
+ MINIMUM_NOTES = 8
6
+
7
+ def message
8
+ "Write at least eight notes."
9
+ end
10
+
11
+ def marks
12
+ placements.empty? ? no_placements_mark : deficiency_mark
13
+ end
14
+
15
+ private
16
+
17
+ def no_placements_mark
18
+ return Style::Mark.new(
19
+ Position.new(composition, "1:1"),
20
+ Position.new(composition, "2:1"),
21
+ fitness: HeadMusic::PENALTY_FACTOR**MINIMUM_NOTES
22
+ )
23
+ end
24
+
25
+ def deficiency_mark
26
+ deficiency = [MINIMUM_NOTES - notes.length, 0].max
27
+ if deficiency > 0
28
+ Style::Mark.for_all(placements, fitness: HeadMusic::PENALTY_FACTOR**deficiency)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::ConsonantClimax < HeadMusic::Style::Annotation
5
+ def message
6
+ "Peak on a consonant high note one time."
7
+ end
8
+
9
+ def marks
10
+ if notes
11
+ improper_climaxes = highest_notes.select.with_index do |note, i|
12
+ tonic_pitch = Pitch.get(composition.key_signature.tonic_spelling)
13
+ interval = FunctionalInterval.new(tonic_pitch, note.pitch)
14
+ interval.consonance.dissonant? || i > 0
15
+ end
16
+ HeadMusic::Style::Mark.for_each(improper_climaxes)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::Diatonic < HeadMusic::Style::Annotation
5
+ def message
6
+ "Use only notes in the key signature."
7
+ end
8
+
9
+ def marks
10
+ HeadMusic::Style::Mark.for_each(notes_not_in_key)
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::DirectionChanges < HeadMusic::Style::Annotation
5
+ MAXIMUM_NOTES_PER_DIRECTION = 3
6
+
7
+ def message
8
+ "Balance ascending and descending motion."
9
+ end
10
+
11
+ def marks
12
+ if overage > 0
13
+ penalty_exponent = overage**0.5
14
+ HeadMusic::Style::Mark.for_all(notes, fitness: PENALTY_FACTOR**penalty_exponent)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def overage
21
+ return 0 if notes.length < 2
22
+ [notes_per_direction - MAXIMUM_NOTES_PER_DIRECTION, 0].max
23
+ end
24
+
25
+ def notes_per_direction
26
+ notes.length.to_f / (melodic_intervals_changing_direction.length + 1)
27
+ end
28
+
29
+ def melodic_intervals_changing_direction
30
+ melodic_intervals[1..-1].select.with_index do |interval, i|
31
+ previous_direction = melodic_intervals[i].direction
32
+ interval.direction != previous_direction
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::EndOnTonic < HeadMusic::Style::Annotation
5
+ def message
6
+ 'End on the tonic.'
7
+ end
8
+
9
+ def marks
10
+ if !notes.empty? && !ends_on_tonic?
11
+ HeadMusic::Style::Mark.for(notes.last)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def ends_on_tonic?
18
+ notes &&
19
+ notes.last &&
20
+ composition &&
21
+ composition.key_signature &&
22
+ composition.key_signature.tonic_spelling == notes.last.spelling
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::LimitRange < HeadMusic::Style::Annotation
5
+ MAXIMUM_RANGE = 10
6
+
7
+ def message
8
+ 'Limit melodic range to a 10th.'
9
+ end
10
+
11
+ def marks
12
+ if overage > 0
13
+ HeadMusic::Style::Mark.for_each(extremes, fitness: HeadMusic::PENALTY_FACTOR**overage)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def overage
20
+ notes.length > 0 ? [range.number - MAXIMUM_RANGE, 0].max : 0
21
+ end
22
+
23
+ def extremes
24
+ (highest_notes + lowest_notes).sort
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::MostlyConjunct < HeadMusic::Style::Annotation
5
+ def message
6
+ "Use mostly conjunct motion."
7
+ end
8
+
9
+ def marks
10
+ marks_for_skips_and_leaps if conjunct_ratio <= 0.5
11
+ end
12
+
13
+ private
14
+
15
+ def marks_for_skips_and_leaps
16
+ melodic_intervals.map.with_index do |interval, i|
17
+ HeadMusic::Style::Mark.for_all(notes[i..i+1], fitness: HeadMusic::SMALL_PENALTY_FACTOR) unless interval.step?
18
+ end.reject(&:nil?)
19
+ end
20
+
21
+ def conjunct_ratio
22
+ return 1 if melodic_intervals.empty?
23
+ steps = melodic_intervals.count { |interval| interval.step? }
24
+ steps.to_f / melodic_intervals.length
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::NoRests < HeadMusic::Style::Annotation
5
+ def message
6
+ "Use only notes."
7
+ end
8
+
9
+ def marks
10
+ rests.map { |rest| HeadMusic::Style::Mark.for(rest) }
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::NotesSameLength < HeadMusic::Style::Annotation
5
+ def message
6
+ 'Use consistent rhythmic unit.'
7
+ end
8
+
9
+ def marks
10
+ preferred_value = first_most_common_rhythmic_value
11
+ wrong_length_notes = all_but_last_note.select { |note| note.rhythmic_value != preferred_value }
12
+ HeadMusic::Style::Mark.for_each(wrong_length_notes)
13
+ end
14
+
15
+ private
16
+
17
+ def all_but_last_note
18
+ notes[0..-2]
19
+ end
20
+
21
+ def distinct_values
22
+ all_but_last_note.map(&:rhythmic_value).uniq.length
23
+ end
24
+
25
+ def first_most_common_rhythmic_value
26
+ candidates = most_common_rhythmic_values
27
+ first_match = notes.detect { |note| candidates.include?(note.rhythmic_value) }
28
+ first_match ? first_match.rhythmic_value : nil
29
+ end
30
+
31
+ def most_common_rhythmic_values
32
+ return [] if notes.empty?
33
+ occurrences = occurrences_by_rhythmic_value
34
+ highest_count = occurrences.values.sort.last
35
+ occurrences.select { |_rhythmic_value, count| count == highest_count }.keys
36
+ end
37
+
38
+ def occurrences_by_rhythmic_value
39
+ rhythmic_values.inject(Hash.new(0)) { |hash, value| hash[value] += 1; hash }
40
+ end
41
+
42
+ def rhythmic_values
43
+ notes.map(&:rhythmic_value)
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::PermittedIntervals < HeadMusic::Style::Annotation
5
+ PERMITTED_ASCENDING = %w[m2 M2 m3 M3 P4 P5 m6 P8]
6
+ PERMITTED_DESCENDING = %w[m2 M2 m3 M3 P4 P5 P8]
7
+
8
+ def message
9
+ "Use only m2, M2, m3, M3, P4, P5, m6 (ascending), P8."
10
+ end
11
+
12
+ def marks
13
+ melodic_intervals.reject { |interval| permitted?(interval) }.map do |unpermitted_interval|
14
+ HeadMusic::Style::Mark.for_all([unpermitted_interval.first_note, unpermitted_interval.second_note])
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def permitted?(melodic_interval)
21
+ whitelist_for_interval(melodic_interval).include?(melodic_interval.shorthand)
22
+ end
23
+
24
+ def whitelist_for_interval(melodic_interval)
25
+ melodic_interval.ascending? ? PERMITTED_ASCENDING : PERMITTED_DESCENDING
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ # Ok, so a rule might be that after the first leap (after previous steps)
5
+ # one should normally move by step in the opposite direction
6
+ # unless another leap (in either direction) creates a consonant triad.
7
+ # - Brian
8
+ class HeadMusic::Style::Annotations::RecoverLargeLeaps < HeadMusic::Style::Annotation
9
+ def message
10
+ "Recover leaps by step in the opposite direction."
11
+ end
12
+
13
+ def marks
14
+ melodic_intervals[1..-1].to_a.map.with_index do |interval, i|
15
+ previous_interval = melodic_intervals[i]
16
+ if unrecovered_leap?(previous_interval, interval)
17
+ HeadMusic::Style::Mark.for_all((previous_interval.notes + interval.notes).uniq)
18
+ end
19
+ end.reject(&:nil?)
20
+ end
21
+
22
+ private
23
+
24
+ def unrecovered_leap?(first_interval, second_interval)
25
+ first_interval.leap? &&
26
+ !spelling_consonant_triad?(first_interval, second_interval) &&
27
+ (
28
+ !direction_changed?(first_interval, second_interval) ||
29
+ !second_interval.step?
30
+ )
31
+ end
32
+
33
+ def spelling_consonant_triad?(first_interval, second_interval)
34
+ return false if first_interval.step? || second_interval.step?
35
+ pitches = (first_interval.pitches + second_interval.pitches).uniq
36
+ return false if pitches.length < 3
37
+ HeadMusic::Chord.new(pitches).consonant_triad?
38
+ end
39
+
40
+ def direction_changed?(first_interval, second_interval)
41
+ first_interval.ascending? && second_interval.descending? ||
42
+ first_interval.descending? && second_interval.ascending?
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::StartOnTonic < HeadMusic::Style::Annotation
5
+ def message
6
+ 'Start on the tonic.'
7
+ end
8
+
9
+ def marks
10
+ if first_note && !starts_on_tonic?
11
+ HeadMusic::Style::Mark.for(first_note)
12
+ end
13
+ end
14
+
15
+ def starts_on_tonic?
16
+ composition.key_signature.tonic_spelling == first_note.spelling
17
+ end
18
+
19
+ def first_note
20
+ notes.first
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::StepDownToFinalNote < HeadMusic::Style::Annotation
5
+ def message
6
+ 'Step down to final note.'
7
+ end
8
+
9
+ def marks
10
+ if !last_melodic_interval.nil?
11
+ fitness = 1
12
+ fitness *= HeadMusic::PENALTY_FACTOR unless step?
13
+ fitness *= HeadMusic::PENALTY_FACTOR unless descending?
14
+ if fitness < 1
15
+ HeadMusic::Style::Mark.for_all(notes[-2..-1], fitness: fitness)
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def descending?
23
+ last_melodic_interval && last_melodic_interval.descending?
24
+ end
25
+
26
+ def step?
27
+ last_melodic_interval && last_melodic_interval.step?
28
+ end
29
+
30
+ def last_melodic_interval
31
+ @last_melodic_interval ||= melodic_intervals.last
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module HeadMusic::Style::Annotations
2
+ end
3
+
4
+ class HeadMusic::Style::Annotations::UpToThirteenNotes < HeadMusic::Style::Annotation
5
+ MAXIMUM_NOTES = 13
6
+
7
+ def message
8
+ 'Write up to thirteen notes.'
9
+ end
10
+
11
+ def marks
12
+ if overage > 0
13
+ Style::Mark.for_all(notes[13..-1])
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def overage
20
+ [notes.length - MAXIMUM_NOTES, 0].max
21
+ end
22
+ end
@@ -1,26 +1,27 @@
1
1
  class HeadMusic::Style::Mark
2
- attr_reader :start_position, :end_position, :placements
2
+ attr_reader :start_position, :end_position, :placements, :fitness
3
3
 
4
- def self.for(placement)
5
- new(placement.position, placement.next_position, placement)
4
+ def self.for(placement, fitness: nil)
5
+ new(placement.position, placement.next_position, placements: [placement], fitness: fitness)
6
6
  end
7
7
 
8
- def self.for_all(placements)
8
+ def self.for_all(placements, fitness: nil)
9
9
  placements = [placements].flatten
10
10
  start_position = placements.map { |placement| placement.position }.sort.first
11
11
  end_position = placements.map { |placement| placement.next_position }.sort.last
12
- new(start_position, end_position, placements)
12
+ new(start_position, end_position, placements: placements, fitness: fitness)
13
13
  end
14
14
 
15
- def self.for_each(placements)
15
+ def self.for_each(placements, fitness: nil)
16
16
  placements = [placements].flatten
17
- placements.map { |placement| new(placement.position, placement.next_position, placement) }
17
+ placements.map { |placement| new(placement.position, placement.next_position, placements: placement, fitness: fitness) }
18
18
  end
19
19
 
20
- def initialize(start_position, end_position, placements = [])
20
+ def initialize(start_position, end_position, placements: [], fitness: nil)
21
21
  @start_position = start_position
22
22
  @end_position = end_position
23
- @placements = [placements].flatten
23
+ @placements = [placements].flatten.compact
24
+ @fitness = fitness || HeadMusic::PENALTY_FACTOR
24
25
  end
25
26
 
26
27
  def code
@@ -3,20 +3,24 @@ end
3
3
 
4
4
  class HeadMusic::Style::Rulesets::CantusFirmus
5
5
  RULESET = [
6
- HeadMusic::Style::Rules::AlwaysMove,
7
- HeadMusic::Style::Rules::AtLeastEightNotes,
8
- HeadMusic::Style::Rules::Diatonic,
9
- HeadMusic::Style::Rules::EndOnTonic,
10
- HeadMusic::Style::Rules::LimitRange,
11
- HeadMusic::Style::Rules::MostlyConjunct,
12
- HeadMusic::Style::Rules::NoRests,
13
- HeadMusic::Style::Rules::NotesSameLength,
14
- HeadMusic::Style::Rules::StartOnTonic,
15
- HeadMusic::Style::Rules::StepDownToFinalNote,
16
- HeadMusic::Style::Rules::UpToThirteenNotes,
6
+ HeadMusic::Style::Annotations::AlwaysMove,
7
+ HeadMusic::Style::Annotations::AtLeastEightNotes,
8
+ HeadMusic::Style::Annotations::ConsonantClimax,
9
+ HeadMusic::Style::Annotations::Diatonic,
10
+ HeadMusic::Style::Annotations::DirectionChanges,
11
+ HeadMusic::Style::Annotations::EndOnTonic,
12
+ HeadMusic::Style::Annotations::LimitRange,
13
+ HeadMusic::Style::Annotations::MostlyConjunct,
14
+ HeadMusic::Style::Annotations::NoRests,
15
+ HeadMusic::Style::Annotations::NotesSameLength,
16
+ HeadMusic::Style::Annotations::PermittedIntervals,
17
+ HeadMusic::Style::Annotations::RecoverLargeLeaps,
18
+ HeadMusic::Style::Annotations::StartOnTonic,
19
+ HeadMusic::Style::Annotations::StepDownToFinalNote,
20
+ HeadMusic::Style::Annotations::UpToThirteenNotes,
17
21
  ]
18
22
 
19
23
  def self.analyze(voice)
20
- RULESET.map { |rule| rule.analyze(voice) }
24
+ RULESET.map { |rule| rule.new(voice) }
21
25
  end
22
26
  end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.1"
3
3
  end
@@ -5,7 +5,7 @@ class HeadMusic::Voice
5
5
  delegate :key_signature, to: :composition
6
6
 
7
7
  def initialize(composition: nil, role: nil)
8
- @composition = composition || Composition.new(name: "Composition")
8
+ @composition = composition || Composition.new
9
9
  @role = role
10
10
  @placements = []
11
11
  end
@@ -59,6 +59,10 @@ class HeadMusic::Voice
59
59
  end.reject(&:nil?)
60
60
  end
61
61
 
62
+ def leaps
63
+ melodic_intervals.select(&:leap?)
64
+ end
65
+
62
66
  private
63
67
 
64
68
  def insert_into_placements(placement)
data/lib/head_music.rb CHANGED
@@ -6,6 +6,7 @@ require 'humanize'
6
6
 
7
7
  require 'head_music/accidental'
8
8
  require 'head_music/bar'
9
+ require 'head_music/chord'
9
10
  require 'head_music/circle'
10
11
  require 'head_music/clef'
11
12
  require 'head_music/composition'
@@ -36,19 +37,22 @@ require 'head_music/staff'
36
37
  require 'head_music/style/analysis'
37
38
  require 'head_music/style/annotation'
38
39
  require 'head_music/style/mark'
39
- require 'head_music/style/rule'
40
40
 
41
- require 'head_music/style/rules/always_move'
42
- require 'head_music/style/rules/at_least_eight_notes'
43
- require 'head_music/style/rules/diatonic'
44
- require 'head_music/style/rules/end_on_tonic'
45
- require 'head_music/style/rules/limit_range'
46
- require 'head_music/style/rules/mostly_conjunct'
47
- require 'head_music/style/rules/no_rests'
48
- require 'head_music/style/rules/notes_same_length'
49
- require 'head_music/style/rules/start_on_tonic'
50
- require 'head_music/style/rules/step_down_to_final_note'
51
- require 'head_music/style/rules/up_to_thirteen_notes'
41
+ require 'head_music/style/annotations/always_move'
42
+ require 'head_music/style/annotations/at_least_eight_notes'
43
+ require 'head_music/style/annotations/consonant_climax'
44
+ require 'head_music/style/annotations/diatonic'
45
+ require 'head_music/style/annotations/direction_changes'
46
+ require 'head_music/style/annotations/end_on_tonic'
47
+ require 'head_music/style/annotations/limit_range'
48
+ require 'head_music/style/annotations/mostly_conjunct'
49
+ require 'head_music/style/annotations/no_rests'
50
+ require 'head_music/style/annotations/notes_same_length'
51
+ require 'head_music/style/annotations/permitted_intervals'
52
+ require 'head_music/style/annotations/recover_large_leaps'
53
+ require 'head_music/style/annotations/start_on_tonic'
54
+ require 'head_music/style/annotations/step_down_to_final_note'
55
+ require 'head_music/style/annotations/up_to_thirteen_notes'
52
56
 
53
57
  require 'head_music/style/rulesets/cantus_firmus'
54
58
 
@@ -58,4 +62,6 @@ require 'head_music/voice'
58
62
  module HeadMusic
59
63
  GOLDEN_RATIO = (1 + 5**0.5) / 2.0
60
64
  GOLDEN_RATIO_INVERSE = 1 / GOLDEN_RATIO
65
+ PENALTY_FACTOR = GOLDEN_RATIO_INVERSE
66
+ SMALL_PENALTY_FACTOR = GOLDEN_RATIO_INVERSE**0.5
61
67
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: head_music
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Head
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-10 00:00:00.000000000 Z
11
+ date: 2017-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -118,6 +118,7 @@ files:
118
118
  - lib/head_music.rb
119
119
  - lib/head_music/accidental.rb
120
120
  - lib/head_music/bar.rb
121
+ - lib/head_music/chord.rb
121
122
  - lib/head_music/circle.rb
122
123
  - lib/head_music/clef.rb
123
124
  - lib/head_music/composition.rb
@@ -146,19 +147,22 @@ files:
146
147
  - lib/head_music/staff.rb
147
148
  - lib/head_music/style/analysis.rb
148
149
  - lib/head_music/style/annotation.rb
150
+ - lib/head_music/style/annotations/always_move.rb
151
+ - lib/head_music/style/annotations/at_least_eight_notes.rb
152
+ - lib/head_music/style/annotations/consonant_climax.rb
153
+ - lib/head_music/style/annotations/diatonic.rb
154
+ - lib/head_music/style/annotations/direction_changes.rb
155
+ - lib/head_music/style/annotations/end_on_tonic.rb
156
+ - lib/head_music/style/annotations/limit_range.rb
157
+ - lib/head_music/style/annotations/mostly_conjunct.rb
158
+ - lib/head_music/style/annotations/no_rests.rb
159
+ - lib/head_music/style/annotations/notes_same_length.rb
160
+ - lib/head_music/style/annotations/permitted_intervals.rb
161
+ - lib/head_music/style/annotations/recover_large_leaps.rb
162
+ - lib/head_music/style/annotations/start_on_tonic.rb
163
+ - lib/head_music/style/annotations/step_down_to_final_note.rb
164
+ - lib/head_music/style/annotations/up_to_thirteen_notes.rb
149
165
  - lib/head_music/style/mark.rb
150
- - lib/head_music/style/rule.rb
151
- - lib/head_music/style/rules/always_move.rb
152
- - lib/head_music/style/rules/at_least_eight_notes.rb
153
- - lib/head_music/style/rules/diatonic.rb
154
- - lib/head_music/style/rules/end_on_tonic.rb
155
- - lib/head_music/style/rules/limit_range.rb
156
- - lib/head_music/style/rules/mostly_conjunct.rb
157
- - lib/head_music/style/rules/no_rests.rb
158
- - lib/head_music/style/rules/notes_same_length.rb
159
- - lib/head_music/style/rules/start_on_tonic.rb
160
- - lib/head_music/style/rules/step_down_to_final_note.rb
161
- - lib/head_music/style/rules/up_to_thirteen_notes.rb
162
166
  - lib/head_music/style/rulesets/cantus_firmus.rb
163
167
  - lib/head_music/utilities/hash_key.rb
164
168
  - lib/head_music/version.rb
@@ -1,13 +0,0 @@
1
- class HeadMusic::Style::Rule
2
- # returns a score between 0 and 1
3
- # Note: absence of a problem or 'not applicable' should score as a 1.
4
- # for example, if the rule is to end on the tonic,
5
- # a composition with no notes should count as a 1.
6
- def fitness(object)
7
- raise NotImplementedError, 'A fitness method is required for all style rules.'
8
- end
9
-
10
- def self.annotations(object)
11
- raise NotImplementedError, 'An annotations method is required for all style rules.'
12
- end
13
- end
@@ -1,19 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::AlwaysMove < HeadMusic::Style::Rule
5
- def self.analyze(voice)
6
- marks = marks(voice)
7
- fitness = HeadMusic::GOLDEN_RATIO_INVERSE**marks.length
8
- message = "Always move to another note." if fitness < 1
9
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: marks, message: message)
10
- end
11
-
12
- def self.marks(voice)
13
- voice.melodic_intervals.map.with_index do |interval, i|
14
- if interval.shorthand == 'PU'
15
- HeadMusic::Style::Mark.for_all(voice.notes[i..i+1])
16
- end
17
- end.reject(&:nil?)
18
- end
19
- end
@@ -1,26 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::AtLeastEightNotes < HeadMusic::Style::Rule
5
- MINIMUM_NOTES = 8
6
-
7
- def self.analyze(voice)
8
- fitness = fitness(voice)
9
- mark = mark(voice)
10
- message = "Add notes until you have at least eight notes." if fitness < 1
11
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: mark, message: message)
12
- end
13
-
14
- def self.fitness(voice)
15
- deficiency = MINIMUM_NOTES - voice.notes.length
16
- deficiency > 0 ? HeadMusic::GOLDEN_RATIO_INVERSE**deficiency : 1
17
- end
18
-
19
- def self.mark(voice)
20
- if voice.placements.empty?
21
- Style::Mark.new(Position.new(voice.composition, "1:1"), Position.new(voice.composition, "2:1"))
22
- else
23
- Style::Mark.for_all(voice.placements)
24
- end
25
- end
26
- end
@@ -1,15 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::Diatonic < HeadMusic::Style::Rule
5
- def self.analyze(voice)
6
- marks = marks(voice)
7
- fitness = HeadMusic::GOLDEN_RATIO_INVERSE**marks.length
8
- message = "Use only notes in the key signature." if fitness < 1
9
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: marks, message: message)
10
- end
11
-
12
- def self.marks(voice)
13
- HeadMusic::Style::Mark.for_each(voice.notes_not_in_key)
14
- end
15
- end
@@ -1,27 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::EndOnTonic < HeadMusic::Style::Rule
5
- def self.analyze(voice)
6
- fitness = fitness(voice)
7
- if fitness < 1
8
- message = 'End on the tonic'
9
- mark = HeadMusic::Style::Mark.for(voice.notes.last)
10
- end
11
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: mark, message: message)
12
- end
13
-
14
- def self.fitness(voice)
15
- return 1 if voice.notes.empty?
16
- return 1 if ends_on_tonic?(voice)
17
- HeadMusic::GOLDEN_RATIO_INVERSE
18
- end
19
-
20
- def self.ends_on_tonic?(voice)
21
- voice.notes &&
22
- voice.notes.last &&
23
- voice.composition &&
24
- voice.composition.key_signature &&
25
- voice.composition.key_signature.tonic_spelling == voice.notes.last.spelling
26
- end
27
- end
@@ -1,31 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::LimitRange < HeadMusic::Style::Rule
5
- MAXIMUM_RANGE = 10
6
-
7
- def self.analyze(voice)
8
- fitness = fitness(voice)
9
- if fitness < 1
10
- message = 'Limit melodic range to a 10th.'
11
- marks = marks(voice)
12
- end
13
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: marks, message: message)
14
- end
15
-
16
- def self.fitness(voice)
17
- return 1 unless voice.notes.length > 0
18
- HeadMusic::GOLDEN_RATIO_INVERSE**overage(voice)
19
- end
20
-
21
- def self.overage(voice)
22
- voice.notes.length > 0 ? [voice.range.number - MAXIMUM_RANGE, 0].max : 0
23
- end
24
-
25
- def self.marks(voice)
26
- if voice.notes
27
- extremes = (voice.highest_notes + voice.lowest_notes).sort
28
- HeadMusic::Style::Mark.for_each(extremes)
29
- end
30
- end
31
- end
@@ -1,30 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::MostlyConjunct < HeadMusic::Style::Rule
5
- def self.analyze(voice)
6
- fitness = fitness(voice)
7
- if fitness < 1
8
- marks = marks(voice)
9
- message = "Use only notes in the key signature."
10
- end
11
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: marks, message: message)
12
- end
13
-
14
- def self.fitness(voice)
15
- intervals = voice.melodic_intervals.length
16
- steps = voice.melodic_intervals.count { |interval| interval.step? }
17
- fitness = 1
18
- fitness *= HeadMusic::GOLDEN_RATIO_INVERSE if steps.to_f / intervals < 0.5
19
- fitness *= HeadMusic::GOLDEN_RATIO_INVERSE if steps.to_f / intervals < 0.25
20
- fitness
21
- end
22
-
23
- def self.marks(voice)
24
- voice.melodic_intervals.map.with_index do |interval, i|
25
- if !interval.step?
26
- HeadMusic::Style::Mark.for_all(voice.notes[i..i+1])
27
- end
28
- end
29
- end
30
- end
@@ -1,14 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::NoRests < HeadMusic::Style::Rule
5
- def self.analyze(voice)
6
- rests = voice.rests
7
- fitness = HeadMusic::GOLDEN_RATIO_INVERSE**rests.length
8
- if rests.length > 0
9
- message = "Change rests to notes."
10
- marks = rests.map { |rest| HeadMusic::Style::Mark.for_all(rest) }
11
- end
12
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: marks, message: message)
13
- end
14
- end
@@ -1,49 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::NotesSameLength < HeadMusic::Style::Rule
5
- def self.analyze(voice)
6
- fitness = fitness(voice)
7
- if fitness < 1
8
- message = "Use consistent rhythmic unit."
9
- end
10
- marks = marks(voice)
11
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: marks, message: message)
12
- end
13
-
14
- def self.fitness(voice)
15
- distinct_values = [distinct_values(voice), 1].max
16
- HeadMusic::GOLDEN_RATIO_INVERSE**(distinct_values-1)
17
- end
18
-
19
- def self.distinct_values(voice)
20
- voice.notes[0..-2].map(&:rhythmic_value).uniq.length
21
- end
22
-
23
- def self.marks(voice)
24
- preferred_value = first_most_common_rhythmic_value(voice)
25
- wrong_length_notes = voice.notes.select { |note| note.rhythmic_value != preferred_value }
26
- HeadMusic::Style::Mark.for_each(wrong_length_notes)
27
- end
28
-
29
- def self.first_most_common_rhythmic_value(voice)
30
- candidates = most_common_rhythmic_values(voice)
31
- first_match = voice.notes.detect { |note| candidates.include?(note.rhythmic_value) }
32
- first_match ? first_match.rhythmic_value : nil
33
- end
34
-
35
- def self.most_common_rhythmic_values(voice)
36
- return [] if voice.notes.empty?
37
- occurrences = occurrences_by_rhythmic_value(voice)
38
- highest_count = occurrences.values.sort.last
39
- occurrences.select { |rhythmic_value, count| count == highest_count }.keys
40
- end
41
-
42
- def self.occurrences_by_rhythmic_value(voice)
43
- rhythmic_values(voice).inject(Hash.new(0)) { |hash, value| hash[value] += 1; hash }
44
- end
45
-
46
- def self.rhythmic_values(voice)
47
- voice.notes.map(&:rhythmic_value)
48
- end
49
- end
@@ -1,27 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::StartOnTonic < HeadMusic::Style::Rule
5
- def self.analyze(voice)
6
- fitness = fitness(voice)
7
- if fitness < 1
8
- message = 'Start on the tonic.'
9
- mark = HeadMusic::Style::Mark.for(voice.notes.last)
10
- end
11
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: mark, message: message)
12
- end
13
-
14
- def self.fitness(voice)
15
- return 1 if voice.notes.empty?
16
- return 1 if starts_on_tonic?(voice)
17
- HeadMusic::GOLDEN_RATIO_INVERSE
18
- end
19
-
20
- def self.starts_on_tonic?(voice)
21
- voice.notes &&
22
- voice.notes.first &&
23
- voice.composition &&
24
- voice.composition.key_signature &&
25
- voice.composition.key_signature.tonic_spelling == voice.notes.first.spelling
26
- end
27
- end
@@ -1,34 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::StepDownToFinalNote < HeadMusic::Style::Rule
5
- def self.analyze(voice)
6
- fitness = fitness(voice)
7
- if fitness < 1
8
- message = 'Step down to final note.'
9
- mark = HeadMusic::Style::Mark.for_all(voice.notes[-2..-1])
10
- end
11
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: mark, message: message)
12
- end
13
-
14
- def self.fitness(voice)
15
- return 1 unless voice.notes.length >= 2
16
- fitness = 1
17
- fitness *= HeadMusic::GOLDEN_RATIO_INVERSE unless step?(voice)
18
- fitness *= HeadMusic::GOLDEN_RATIO_INVERSE unless descending?(voice)
19
- fitness
20
- end
21
-
22
- def self.descending?(voice)
23
- last_melodic_interval(voice).descending?
24
- end
25
-
26
- def self.step?(voice)
27
- last_melodic_interval(voice).step?
28
- end
29
-
30
- def self.last_melodic_interval(voice)
31
- @last_melodic_interval ||= {}
32
- @last_melodic_interval[voice] ||= voice.melodic_intervals.last
33
- end
34
- end
@@ -1,24 +0,0 @@
1
- module HeadMusic::Style::Rules
2
- end
3
-
4
- class HeadMusic::Style::Rules::UpToThirteenNotes < HeadMusic::Style::Rule
5
- MAXIMUM_NOTES = 13
6
-
7
- def self.analyze(voice)
8
- fitness = fitness(voice)
9
- if fitness < 1
10
- mark = mark(voice)
11
- end
12
- message = "Remove notes until you have at most thirteen notes." if fitness < 1
13
- HeadMusic::Style::Annotation.new(subject: voice, fitness: fitness, marks: mark, message: message)
14
- end
15
-
16
- def self.fitness(voice)
17
- overage = voice.notes.length - MAXIMUM_NOTES
18
- overage > 0 ? HeadMusic::GOLDEN_RATIO_INVERSE**overage : 1
19
- end
20
-
21
- def self.mark(voice)
22
- Style::Mark.for_all(voice.notes[13..-1])
23
- end
24
- end