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.
- checksums.yaml +4 -4
- data/lib/head_music/chord.rb +29 -0
- data/lib/head_music/composition.rb +2 -2
- data/lib/head_music/consonance.rb +4 -0
- data/lib/head_music/functional_interval.rb +10 -9
- data/lib/head_music/key_signature.rb +6 -10
- data/lib/head_music/letter_name.rb +1 -1
- data/lib/head_music/melodic_interval.rb +8 -0
- data/lib/head_music/meter.rb +1 -1
- data/lib/head_music/rhythmic_value.rb +1 -1
- data/lib/head_music/scale.rb +1 -2
- data/lib/head_music/scale_type.rb +3 -3
- data/lib/head_music/spelling.rb +2 -2
- data/lib/head_music/style/analysis.rb +4 -1
- data/lib/head_music/style/annotation.rb +21 -17
- data/lib/head_music/style/annotations/always_move.rb +16 -0
- data/lib/head_music/style/annotations/at_least_eight_notes.rb +31 -0
- data/lib/head_music/style/annotations/consonant_climax.rb +19 -0
- data/lib/head_music/style/annotations/diatonic.rb +12 -0
- data/lib/head_music/style/annotations/direction_changes.rb +35 -0
- data/lib/head_music/style/annotations/end_on_tonic.rb +24 -0
- data/lib/head_music/style/annotations/limit_range.rb +26 -0
- data/lib/head_music/style/annotations/mostly_conjunct.rb +26 -0
- data/lib/head_music/style/annotations/no_rests.rb +12 -0
- data/lib/head_music/style/annotations/notes_same_length.rb +45 -0
- data/lib/head_music/style/annotations/permitted_intervals.rb +27 -0
- data/lib/head_music/style/annotations/recover_large_leaps.rb +44 -0
- data/lib/head_music/style/annotations/start_on_tonic.rb +22 -0
- data/lib/head_music/style/annotations/step_down_to_final_note.rb +33 -0
- data/lib/head_music/style/annotations/up_to_thirteen_notes.rb +22 -0
- data/lib/head_music/style/mark.rb +10 -9
- data/lib/head_music/style/rulesets/cantus_firmus.rb +16 -12
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music/voice.rb +5 -1
- data/lib/head_music.rb +18 -12
- metadata +18 -14
- data/lib/head_music/style/rule.rb +0 -13
- data/lib/head_music/style/rules/always_move.rb +0 -19
- data/lib/head_music/style/rules/at_least_eight_notes.rb +0 -26
- data/lib/head_music/style/rules/diatonic.rb +0 -15
- data/lib/head_music/style/rules/end_on_tonic.rb +0 -27
- data/lib/head_music/style/rules/limit_range.rb +0 -31
- data/lib/head_music/style/rules/mostly_conjunct.rb +0 -30
- data/lib/head_music/style/rules/no_rests.rb +0 -14
- data/lib/head_music/style/rules/notes_same_length.rb +0 -49
- data/lib/head_music/style/rules/start_on_tonic.rb +0 -27
- data/lib/head_music/style/rules/step_down_to_final_note.rb +0 -34
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 349d91b0d02df9a4722f7634e1f8d99a9849a2c0
|
4
|
+
data.tar.gz: 3ec53a83af4f774862d3613a4984c313b5f3ea68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
@@ -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
|
-
|
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
|
165
|
-
if
|
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
|
171
|
-
if
|
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
|
-
|
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
|
-
|
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 { |
|
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
|
data/lib/head_music/meter.rb
CHANGED
data/lib/head_music/scale.rb
CHANGED
@@ -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
|
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
|
-
|
64
|
-
attributes = SCALE_TYPES[
|
65
|
-
@scale_types[
|
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
|
data/lib/head_music/spelling.rb
CHANGED
@@ -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 =
|
41
|
-
accidental_interval =
|
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
|
@@ -1,28 +1,32 @@
|
|
1
1
|
class HeadMusic::Style::Annotation
|
2
|
-
attr_reader :
|
2
|
+
attr_reader :voice
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
17
|
+
def initialize(voice)
|
18
|
+
@voice = voice
|
13
19
|
end
|
14
20
|
|
15
|
-
def
|
16
|
-
|
21
|
+
def fitness
|
22
|
+
[marks].flatten.compact.map(&:fitness).reduce(1, :*)
|
17
23
|
end
|
18
24
|
|
19
|
-
def
|
20
|
-
|
25
|
+
def marks
|
26
|
+
raise NotImplementedError
|
21
27
|
end
|
22
28
|
|
23
|
-
def
|
24
|
-
|
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,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::
|
7
|
-
HeadMusic::Style::
|
8
|
-
HeadMusic::Style::
|
9
|
-
HeadMusic::Style::
|
10
|
-
HeadMusic::Style::
|
11
|
-
HeadMusic::Style::
|
12
|
-
HeadMusic::Style::
|
13
|
-
HeadMusic::Style::
|
14
|
-
HeadMusic::Style::
|
15
|
-
HeadMusic::Style::
|
16
|
-
HeadMusic::Style::
|
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.
|
24
|
+
RULESET.map { |rule| rule.new(voice) }
|
21
25
|
end
|
22
26
|
end
|
data/lib/head_music/version.rb
CHANGED
data/lib/head_music/voice.rb
CHANGED
@@ -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
|
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/
|
42
|
-
require 'head_music/style/
|
43
|
-
require 'head_music/style/
|
44
|
-
require 'head_music/style/
|
45
|
-
require 'head_music/style/
|
46
|
-
require 'head_music/style/
|
47
|
-
require 'head_music/style/
|
48
|
-
require 'head_music/style/
|
49
|
-
require 'head_music/style/
|
50
|
-
require 'head_music/style/
|
51
|
-
require 'head_music/style/
|
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.
|
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-
|
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
|