head_music 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/head_music/accidental.rb +0 -1
- data/lib/head_music/{measure.rb → bar.rb} +3 -3
- data/lib/head_music/circle.rb +0 -1
- data/lib/head_music/composition.rb +8 -8
- data/lib/head_music/consonance.rb +0 -1
- data/lib/head_music/functional_interval.rb +37 -8
- data/lib/head_music/interval.rb +0 -1
- data/lib/head_music/letter_name.rb +0 -1
- data/lib/head_music/melodic_interval.rb +29 -0
- data/lib/head_music/meter.rb +6 -6
- data/lib/head_music/note.rb +11 -7
- data/lib/head_music/octave.rb +0 -1
- data/lib/head_music/pitch.rb +0 -1
- data/lib/head_music/pitch_class.rb +0 -1
- data/lib/head_music/placement.rb +6 -1
- data/lib/head_music/position.rb +31 -13
- data/lib/head_music/quality.rb +0 -1
- data/lib/head_music/rhythmic_unit.rb +0 -1
- data/lib/head_music/rhythmic_value.rb +33 -0
- data/lib/head_music/scale_type.rb +0 -1
- data/lib/head_music/spelling.rb +0 -1
- data/lib/head_music/style/analysis.rb +17 -0
- data/lib/head_music/style/annotation.rb +28 -0
- data/lib/head_music/style/mark.rb +29 -0
- data/lib/head_music/style/rule.rb +13 -0
- data/lib/head_music/style/rules/always_move.rb +19 -0
- data/lib/head_music/style/rules/at_least_eight_notes.rb +26 -0
- data/lib/head_music/style/rules/diatonic.rb +15 -0
- data/lib/head_music/style/rules/end_on_tonic.rb +27 -0
- data/lib/head_music/style/rules/limit_range.rb +31 -0
- data/lib/head_music/style/rules/mostly_conjunct.rb +30 -0
- data/lib/head_music/style/rules/no_rests.rb +14 -0
- data/lib/head_music/style/rules/notes_same_length.rb +49 -0
- data/lib/head_music/style/rules/start_on_tonic.rb +27 -0
- data/lib/head_music/style/rules/step_down_to_final_note.rb +34 -0
- data/lib/head_music/style/rules/up_to_thirteen_notes.rb +24 -0
- data/lib/head_music/style/rulesets/cantus_firmus.rb +22 -0
- data/lib/head_music/utilities/hash_key.rb +2 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music/voice.rb +48 -3
- data/lib/head_music.rb +27 -4
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3c4a7d9f36e3a577a3b3e15dde8e3b9b8719937
|
4
|
+
data.tar.gz: dc196e99e9f17077d7135fb17047a07a8fee13be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fa70d04a30f7f652420d88224192f4a2dd89a8436a363a3306129336d3a61a390ecd0d4c8baf2a7ca9cd05798dc4bbd09e0969bf0107335a7adaa3a3ae7f195
|
7
|
+
data.tar.gz: 95a836da90aa931b77935d7558c2fb6710b9b559ec69578b39451655551e058bfa19ef68fdea8541dbb24912011d34490d61e34f9baf4eff65f22fd8fec48f85
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class HeadMusic::
|
1
|
+
class HeadMusic::Bar
|
2
2
|
attr_reader :composition
|
3
3
|
|
4
4
|
delegate :key_signature, :meter, to: :composition
|
@@ -8,7 +8,7 @@ class HeadMusic::Measure
|
|
8
8
|
end
|
9
9
|
|
10
10
|
# TODO: encapsulate key changes and meter changes
|
11
|
-
# Assume the key and meter of the previous
|
12
|
-
# all the way back to the first
|
11
|
+
# Assume the key and meter of the previous bar
|
12
|
+
# all the way back to the first bar,
|
13
13
|
# which defaults to the key and meter of the composition
|
14
14
|
end
|
data/lib/head_music/circle.rb
CHANGED
@@ -1,26 +1,26 @@
|
|
1
1
|
class HeadMusic::Composition
|
2
|
-
attr_reader :name, :key_signature, :meter, :
|
2
|
+
attr_reader :name, :key_signature, :meter, :bars, :voices
|
3
3
|
|
4
4
|
def initialize(name:, key_signature: nil, meter: nil)
|
5
5
|
ensure_attributes(name, key_signature, meter)
|
6
|
-
|
6
|
+
add_bar
|
7
7
|
add_voice
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
10
|
+
def add_bar
|
11
|
+
add_bars(1)
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
@
|
14
|
+
def add_bars(number)
|
15
|
+
@bars ||= []
|
16
16
|
number.times do
|
17
|
-
@
|
17
|
+
@bars << HeadMusic::Bar.new(self)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
def add_voice
|
22
22
|
@voices ||= []
|
23
|
-
@voices << HeadMusic::Voice.new(self)
|
23
|
+
@voices << HeadMusic::Voice.new(composition: self)
|
24
24
|
end
|
25
25
|
|
26
26
|
private
|
@@ -22,10 +22,9 @@ class HeadMusic::FunctionalInterval
|
|
22
22
|
seventeenth: { major: 28 }
|
23
23
|
}
|
24
24
|
|
25
|
-
attr_reader :lower_pitch, :higher_pitch
|
25
|
+
attr_reader :lower_pitch, :higher_pitch, :direction
|
26
26
|
|
27
27
|
delegate :to_s, to: :name
|
28
|
-
delegate :==, to: :to_s
|
29
28
|
delegate :perfect?, :major?, :minor?, :diminished?, :augmented?, :doubly_diminished?, :doubly_augmented?, to: :quality
|
30
29
|
|
31
30
|
def self.get(name)
|
@@ -38,7 +37,6 @@ class HeadMusic::FunctionalInterval
|
|
38
37
|
higher_pitch = HeadMusic::Pitch.from_number_and_letter(lower_pitch + semitones, higher_letter)
|
39
38
|
new(lower_pitch, higher_pitch)
|
40
39
|
end
|
41
|
-
singleton_class.send(:alias_method, :[], :get)
|
42
40
|
|
43
41
|
def self.degree_quality_semitones
|
44
42
|
@degree_quality_semitones ||= begin
|
@@ -62,7 +60,29 @@ class HeadMusic::FunctionalInterval
|
|
62
60
|
end
|
63
61
|
|
64
62
|
def initialize(pitch1, pitch2)
|
65
|
-
|
63
|
+
pitch1 = HeadMusic::Pitch.get(pitch1)
|
64
|
+
pitch2 = HeadMusic::Pitch.get(pitch2)
|
65
|
+
set_direction(pitch1, pitch2)
|
66
|
+
@lower_pitch, @higher_pitch = [pitch1, pitch2].sort
|
67
|
+
end
|
68
|
+
|
69
|
+
def set_direction(pitch1, pitch2)
|
70
|
+
@direction =
|
71
|
+
if pitch1 == pitch2
|
72
|
+
:none
|
73
|
+
elsif pitch1 < pitch2
|
74
|
+
:ascending
|
75
|
+
else
|
76
|
+
:descending
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def descending?
|
81
|
+
direction == :descending
|
82
|
+
end
|
83
|
+
|
84
|
+
def ascending?
|
85
|
+
direction == :ascending
|
66
86
|
end
|
67
87
|
|
68
88
|
def number
|
@@ -108,7 +128,8 @@ class HeadMusic::FunctionalInterval
|
|
108
128
|
end
|
109
129
|
|
110
130
|
def shorthand
|
111
|
-
|
131
|
+
step_shorthand = number == 1 ? 'U' : number
|
132
|
+
[quality.shorthand, step_shorthand].join
|
112
133
|
end
|
113
134
|
|
114
135
|
def quality
|
@@ -157,12 +178,20 @@ class HeadMusic::FunctionalInterval
|
|
157
178
|
end
|
158
179
|
end
|
159
180
|
|
181
|
+
def step?
|
182
|
+
number <= 2
|
183
|
+
end
|
184
|
+
|
160
185
|
def skip?
|
161
|
-
number
|
186
|
+
number == 3
|
162
187
|
end
|
163
188
|
|
164
|
-
def
|
165
|
-
number
|
189
|
+
def leap?
|
190
|
+
number > 3
|
191
|
+
end
|
192
|
+
|
193
|
+
def ==(other)
|
194
|
+
self.to_s.gsub(/\W/, '_') == other.to_s.gsub(/\W/, '_')
|
166
195
|
end
|
167
196
|
|
168
197
|
NUMBER_NAMES.each do |method_name|
|
data/lib/head_music/interval.rb
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
class HeadMusic::MelodicInterval
|
2
|
+
attr_reader :voice, :first_note, :second_note
|
3
|
+
|
4
|
+
def initialize(voice, note1, note2)
|
5
|
+
@voice = voice
|
6
|
+
@first_note = note1
|
7
|
+
@second_note = note2
|
8
|
+
end
|
9
|
+
|
10
|
+
def functional_interval
|
11
|
+
@functional_interval ||= HeadMusic::FunctionalInterval.new(first_note.pitch, second_note.pitch)
|
12
|
+
end
|
13
|
+
|
14
|
+
def position_start
|
15
|
+
first_note.position
|
16
|
+
end
|
17
|
+
|
18
|
+
def position_end
|
19
|
+
second_note.next_position
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
[direction, functional_interval].join(' ')
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(method_name, *args, &block)
|
27
|
+
functional_interval.send(method_name, *args, &block)
|
28
|
+
end
|
29
|
+
end
|
data/lib/head_music/meter.rb
CHANGED
@@ -50,11 +50,11 @@ class HeadMusic::Meter
|
|
50
50
|
top_number == 4
|
51
51
|
end
|
52
52
|
|
53
|
-
def
|
53
|
+
def beats_per_bar
|
54
54
|
compound? ? top_number / 3 : top_number
|
55
55
|
end
|
56
56
|
|
57
|
-
def
|
57
|
+
def counts_per_bar
|
58
58
|
top_number
|
59
59
|
end
|
60
60
|
|
@@ -101,12 +101,12 @@ class HeadMusic::Meter
|
|
101
101
|
|
102
102
|
def strong_counts
|
103
103
|
@strong_counts ||= begin
|
104
|
-
(1..
|
104
|
+
(1..counts_per_bar).select do |count|
|
105
105
|
count == 1 ||
|
106
|
-
count ==
|
106
|
+
count == counts_per_bar / 2.0 + 1 ||
|
107
107
|
(
|
108
|
-
|
109
|
-
|
108
|
+
counts_per_bar % 3 == 0 &&
|
109
|
+
counts_per_bar > 6 &&
|
110
110
|
count % 3 == 1
|
111
111
|
)
|
112
112
|
end
|
data/lib/head_music/note.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
class HeadMusic::Note
|
2
|
-
|
2
|
+
attr_accessor :pitch, :rhythmic_value, :voice, :position
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
def initialize(pitch, rhythmic_unit, rhythmic_value_modifiers = {})
|
4
|
+
def initialize(pitch, rhythmic_value, voice = nil, position = nil)
|
7
5
|
@pitch = HeadMusic::Pitch.get(pitch)
|
8
|
-
@rhythmic_value = HeadMusic::RhythmicValue.
|
6
|
+
@rhythmic_value = HeadMusic::RhythmicValue.get(rhythmic_value)
|
7
|
+
@voice = voice || Voice.new
|
8
|
+
@position = position || HeadMusic::Position.new(@voice.composition, '1:1')
|
9
|
+
end
|
10
|
+
|
11
|
+
def placement
|
12
|
+
@placement ||= HeadMusic::Placement.new(voice, position, rhythmic_value, pitch)
|
9
13
|
end
|
10
14
|
|
11
|
-
def
|
12
|
-
|
15
|
+
def method_missing(method_name, *args, &block)
|
16
|
+
placement.send(method_name, *args, &block)
|
13
17
|
end
|
14
18
|
end
|
data/lib/head_music/octave.rb
CHANGED
@@ -8,7 +8,6 @@ class HeadMusic::Octave
|
|
8
8
|
def self.get(identifier)
|
9
9
|
from_number(identifier) || from_name(identifier) || default
|
10
10
|
end
|
11
|
-
singleton_class.send(:alias_method, :[], :get)
|
12
11
|
|
13
12
|
def self.from_number(identifier)
|
14
13
|
return nil unless identifier.to_s == identifier.to_i.to_s
|
data/lib/head_music/pitch.rb
CHANGED
data/lib/head_music/placement.rb
CHANGED
@@ -3,8 +3,9 @@ class HeadMusic::Placement
|
|
3
3
|
|
4
4
|
attr_reader :voice, :position, :rhythmic_value, :pitch
|
5
5
|
delegate :composition, to: :voice
|
6
|
+
delegate :spelling, to: :pitch, allow_nil: true
|
6
7
|
|
7
|
-
def initialize(voice, position, rhythmic_value, pitch)
|
8
|
+
def initialize(voice, position, rhythmic_value, pitch = nil)
|
8
9
|
ensure_attributes(voice, position, rhythmic_value, pitch)
|
9
10
|
end
|
10
11
|
|
@@ -16,6 +17,10 @@ class HeadMusic::Placement
|
|
16
17
|
!note?
|
17
18
|
end
|
18
19
|
|
20
|
+
def next_position
|
21
|
+
position + rhythmic_value
|
22
|
+
end
|
23
|
+
|
19
24
|
def <=>(other)
|
20
25
|
self.position <=> other.position
|
21
26
|
end
|
data/lib/head_music/position.rb
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
class HeadMusic::Position
|
2
2
|
include Comparable
|
3
3
|
|
4
|
-
attr_reader :composition, :
|
4
|
+
attr_reader :composition, :bar_number, :count, :tick
|
5
5
|
delegate :to_s, to: :code
|
6
6
|
delegate :meter, to: :composition
|
7
7
|
|
8
|
-
def initialize(composition,
|
9
|
-
if
|
10
|
-
|
8
|
+
def initialize(composition, code_or_bar, count = nil, tick = nil)
|
9
|
+
if code_or_bar.is_a?(String) && code_or_bar =~ /\D/
|
10
|
+
bar, count, tick = code_or_bar.split(/\D+/)
|
11
|
+
ensure_state(composition, bar, count, tick)
|
11
12
|
else
|
12
|
-
ensure_state(composition,
|
13
|
+
ensure_state(composition, code_or_bar, count, tick)
|
13
14
|
end
|
14
15
|
end
|
15
16
|
|
16
17
|
def code
|
17
|
-
|
18
|
+
tick_string = tick.to_s.rjust(3, '0')
|
19
|
+
[bar_number, count, tick_string].join(':')
|
18
20
|
end
|
19
21
|
|
20
22
|
def state
|
@@ -22,10 +24,13 @@ class HeadMusic::Position
|
|
22
24
|
end
|
23
25
|
|
24
26
|
def values
|
25
|
-
[
|
27
|
+
[bar_number, count, tick]
|
26
28
|
end
|
27
29
|
|
28
30
|
def <=>(other)
|
31
|
+
if other.is_a?(String) && other =~ /\D/
|
32
|
+
other = self.class.new(composition, other)
|
33
|
+
end
|
29
34
|
self.values <=> other.values
|
30
35
|
end
|
31
36
|
|
@@ -41,11 +46,22 @@ class HeadMusic::Position
|
|
41
46
|
!strong?
|
42
47
|
end
|
43
48
|
|
49
|
+
def +(rhythmic_value)
|
50
|
+
if [HeadMusic::RhythmicUnit, Symbol, String].include?(rhythmic_value.class)
|
51
|
+
rhythmic_value = HeadMusic::RhythmicValue.new(rhythmic_value)
|
52
|
+
end
|
53
|
+
self.class.new(composition, bar_number, count, tick + rhythmic_value.ticks)
|
54
|
+
end
|
55
|
+
|
56
|
+
def start_of_next_bar
|
57
|
+
self.class.new(composition, bar_number + 1, 1, 0)
|
58
|
+
end
|
59
|
+
|
44
60
|
private
|
45
61
|
|
46
|
-
def ensure_state(composition,
|
62
|
+
def ensure_state(composition, bar_number, count, tick = nil)
|
47
63
|
@composition = composition
|
48
|
-
@
|
64
|
+
@bar_number = bar_number.to_i
|
49
65
|
@count = (count || 1).to_i
|
50
66
|
@tick = (tick || 0).to_i
|
51
67
|
roll_over_units
|
@@ -57,16 +73,18 @@ class HeadMusic::Position
|
|
57
73
|
end
|
58
74
|
|
59
75
|
def roll_over_ticks
|
60
|
-
|
76
|
+
# TODO account for meter changes in bars
|
77
|
+
while @tick >= meter.ticks_per_count
|
61
78
|
@tick -= meter.ticks_per_count.to_i
|
62
79
|
@count += 1
|
63
80
|
end
|
64
81
|
end
|
65
82
|
|
66
83
|
def roll_over_counts
|
67
|
-
|
68
|
-
|
69
|
-
@
|
84
|
+
# TODO account for meter changes in bars
|
85
|
+
while @count > meter.counts_per_bar
|
86
|
+
@count -= meter.counts_per_bar
|
87
|
+
@bar_number += 1
|
70
88
|
end
|
71
89
|
end
|
72
90
|
end
|
data/lib/head_music/quality.rb
CHANGED
@@ -31,7 +31,6 @@ class HeadMusic::Quality
|
|
31
31
|
identifier = identifier.to_s.to_sym
|
32
32
|
@qualities[identifier] ||= new(identifier) if NAMES.include?(identifier)
|
33
33
|
end
|
34
|
-
singleton_class.send(:alias_method, :[], :get)
|
35
34
|
|
36
35
|
def self.from(starting_quality, delta)
|
37
36
|
if starting_quality == :perfect
|
@@ -10,7 +10,6 @@ class HeadMusic::RhythmicUnit
|
|
10
10
|
hash_key = HeadMusic::Utilities::HashKey.for(name)
|
11
11
|
@rhythmic_units[hash_key] ||= new(name.to_s)
|
12
12
|
end
|
13
|
-
singleton_class.send(:alias_method, :[], :get)
|
14
13
|
|
15
14
|
def self.for_denominator_value(denominator)
|
16
15
|
get(FRACTIONS[Math.log2(denominator).to_i])
|
@@ -4,6 +4,39 @@ class HeadMusic::RhythmicValue
|
|
4
4
|
delegate :name, to: :unit, prefix: true
|
5
5
|
delegate :to_s, to: :name
|
6
6
|
|
7
|
+
def self.get(identifier)
|
8
|
+
case identifier
|
9
|
+
when RhythmicValue
|
10
|
+
identifier
|
11
|
+
when RhythmicUnit
|
12
|
+
new(identifier)
|
13
|
+
when Symbol, String
|
14
|
+
identifier = identifier.to_s.downcase.strip.gsub(/\W+/, '_')
|
15
|
+
from_words(identifier)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_words(identifier)
|
20
|
+
new(unit_from_words(identifier), dots: dots_from_words(identifier))
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.unit_from_words(identifier)
|
24
|
+
identifier.gsub(/^\w*dotted_/, '')
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.dots_from_words(identifier)
|
28
|
+
return 0 unless identifier =~ /dotted/
|
29
|
+
modifier, _ = identifier.split(/_*dotted_*/)
|
30
|
+
case modifier
|
31
|
+
when /tripl\w/
|
32
|
+
3
|
33
|
+
when /doubl\w/
|
34
|
+
2
|
35
|
+
else
|
36
|
+
1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
7
40
|
def initialize(unit, dots: nil, tied_value: nil)
|
8
41
|
@unit = HeadMusic::RhythmicUnit.get(unit)
|
9
42
|
@dots = [0, 1, 2, 3].include?(dots) ? dots : 0
|
data/lib/head_music/spelling.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
module HeadMusic
|
2
|
+
module Style
|
3
|
+
class Analysis
|
4
|
+
attr_reader :ruleset, :subject, :annotations
|
5
|
+
|
6
|
+
def initialize(ruleset, subject)
|
7
|
+
@ruleset = ruleset
|
8
|
+
@subject = subject
|
9
|
+
@annotations = @ruleset.analyze(subject)
|
10
|
+
end
|
11
|
+
|
12
|
+
def fitness
|
13
|
+
annotations.map(&:fitness).reduce(1, :*)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class HeadMusic::Style::Annotation
|
2
|
+
attr_reader :fitness, :message, :marks, :subject
|
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
|
10
|
+
|
11
|
+
def voice
|
12
|
+
subject if subject.is_a?(HeadMusic::Voice)
|
13
|
+
end
|
14
|
+
|
15
|
+
def composition
|
16
|
+
voice ? voice.composition : subject
|
17
|
+
end
|
18
|
+
|
19
|
+
def marks_count
|
20
|
+
marks ? marks.length : 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def first_mark_code
|
24
|
+
marks.first.code if marks.first
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :to_s, :message
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class HeadMusic::Style::Mark
|
2
|
+
attr_reader :start_position, :end_position, :placements
|
3
|
+
|
4
|
+
def self.for(placement)
|
5
|
+
new(placement.position, placement.next_position, placement)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.for_all(placements)
|
9
|
+
placements = [placements].flatten
|
10
|
+
start_position = placements.map { |placement| placement.position }.sort.first
|
11
|
+
end_position = placements.map { |placement| placement.next_position }.sort.last
|
12
|
+
new(start_position, end_position, placements)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.for_each(placements)
|
16
|
+
placements = [placements].flatten
|
17
|
+
placements.map { |placement| new(placement.position, placement.next_position, placement) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(start_position, end_position, placements = [])
|
21
|
+
@start_position = start_position
|
22
|
+
@end_position = end_position
|
23
|
+
@placements = [placements].flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
def code
|
27
|
+
[start_position, end_position].join(' to ')
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
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
|
@@ -0,0 +1,19 @@
|
|
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
|
@@ -0,0 +1,26 @@
|
|
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
|
@@ -0,0 +1,15 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,31 @@
|
|
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
|
@@ -0,0 +1,30 @@
|
|
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
|
@@ -0,0 +1,14 @@
|
|
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
|
@@ -0,0 +1,49 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,34 @@
|
|
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
|
@@ -0,0 +1,24 @@
|
|
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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module HeadMusic::Style::Rulesets
|
2
|
+
end
|
3
|
+
|
4
|
+
class HeadMusic::Style::Rulesets::CantusFirmus
|
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,
|
17
|
+
]
|
18
|
+
|
19
|
+
def self.analyze(voice)
|
20
|
+
RULESET.map { |rule| rule.analyze(voice) }
|
21
|
+
end
|
22
|
+
end
|
data/lib/head_music/version.rb
CHANGED
data/lib/head_music/voice.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
class HeadMusic::Voice
|
2
2
|
include Comparable
|
3
3
|
|
4
|
-
attr_reader :composition, :placements
|
4
|
+
attr_reader :composition, :placements, :role
|
5
|
+
delegate :key_signature, to: :composition
|
5
6
|
|
6
|
-
def initialize(composition
|
7
|
-
@composition = composition || Composition.new(name: "
|
7
|
+
def initialize(composition: nil, role: nil)
|
8
|
+
@composition = composition || Composition.new(name: "Composition")
|
9
|
+
@role = role
|
8
10
|
@placements = []
|
9
11
|
end
|
10
12
|
|
@@ -14,6 +16,49 @@ class HeadMusic::Voice
|
|
14
16
|
}
|
15
17
|
end
|
16
18
|
|
19
|
+
def notes
|
20
|
+
@placements.select(&:note?)
|
21
|
+
end
|
22
|
+
|
23
|
+
def notes_not_in_key
|
24
|
+
key_spellings = key_signature.pitches.map(&:spelling).uniq
|
25
|
+
notes.reject { |note| key_spellings.include? note.pitch.spelling }
|
26
|
+
end
|
27
|
+
|
28
|
+
def pitches
|
29
|
+
notes.map(&:pitch)
|
30
|
+
end
|
31
|
+
|
32
|
+
def rests
|
33
|
+
@placements.select(&:rest?)
|
34
|
+
end
|
35
|
+
|
36
|
+
def highest_pitch
|
37
|
+
pitches.sort.last
|
38
|
+
end
|
39
|
+
|
40
|
+
def lowest_pitch
|
41
|
+
pitches.sort.first
|
42
|
+
end
|
43
|
+
|
44
|
+
def highest_notes
|
45
|
+
notes.select { |note| note.pitch == highest_pitch }
|
46
|
+
end
|
47
|
+
|
48
|
+
def lowest_notes
|
49
|
+
notes.select { |note| note.pitch == lowest_pitch }
|
50
|
+
end
|
51
|
+
|
52
|
+
def range
|
53
|
+
HeadMusic::FunctionalInterval.new(lowest_pitch, highest_pitch)
|
54
|
+
end
|
55
|
+
|
56
|
+
def melodic_intervals
|
57
|
+
notes.map.with_index do |note, i|
|
58
|
+
HeadMusic::MelodicInterval.new(self, notes[i-1], note) if i > 0
|
59
|
+
end.reject(&:nil?)
|
60
|
+
end
|
61
|
+
|
17
62
|
private
|
18
63
|
|
19
64
|
def insert_into_placements(placement)
|
data/lib/head_music.rb
CHANGED
@@ -5,8 +5,10 @@ require 'active_support/core_ext/string/access'
|
|
5
5
|
require 'humanize'
|
6
6
|
|
7
7
|
require 'head_music/accidental'
|
8
|
+
require 'head_music/bar'
|
8
9
|
require 'head_music/circle'
|
9
10
|
require 'head_music/clef'
|
11
|
+
require 'head_music/composition'
|
10
12
|
require 'head_music/consonance'
|
11
13
|
require 'head_music/functional_interval'
|
12
14
|
require 'head_music/grand_staff'
|
@@ -14,13 +16,12 @@ require 'head_music/instrument'
|
|
14
16
|
require 'head_music/interval'
|
15
17
|
require 'head_music/key_signature'
|
16
18
|
require 'head_music/letter_name'
|
17
|
-
require 'head_music/
|
18
|
-
require 'head_music/measure'
|
19
|
+
require 'head_music/melodic_interval'
|
19
20
|
require 'head_music/meter'
|
20
21
|
require 'head_music/note'
|
21
22
|
require 'head_music/octave'
|
22
|
-
require 'head_music/pitch_class'
|
23
23
|
require 'head_music/pitch'
|
24
|
+
require 'head_music/pitch_class'
|
24
25
|
require 'head_music/placement'
|
25
26
|
require 'head_music/position'
|
26
27
|
require 'head_music/quality'
|
@@ -31,8 +32,30 @@ require 'head_music/scale'
|
|
31
32
|
require 'head_music/scale_type'
|
32
33
|
require 'head_music/spelling'
|
33
34
|
require 'head_music/staff'
|
34
|
-
|
35
|
+
|
36
|
+
require 'head_music/style/analysis'
|
37
|
+
require 'head_music/style/annotation'
|
38
|
+
require 'head_music/style/mark'
|
39
|
+
require 'head_music/style/rule'
|
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'
|
52
|
+
|
53
|
+
require 'head_music/style/rulesets/cantus_firmus'
|
54
|
+
|
35
55
|
require 'head_music/utilities/hash_key'
|
56
|
+
require 'head_music/voice'
|
36
57
|
|
37
58
|
module HeadMusic
|
59
|
+
GOLDEN_RATIO = (1 + 5**0.5) / 2.0
|
60
|
+
GOLDEN_RATIO_INVERSE = 1 / GOLDEN_RATIO
|
38
61
|
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.10.0
|
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-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -117,6 +117,7 @@ files:
|
|
117
117
|
- head_music.gemspec
|
118
118
|
- lib/head_music.rb
|
119
119
|
- lib/head_music/accidental.rb
|
120
|
+
- lib/head_music/bar.rb
|
120
121
|
- lib/head_music/circle.rb
|
121
122
|
- lib/head_music/clef.rb
|
122
123
|
- lib/head_music/composition.rb
|
@@ -127,7 +128,7 @@ files:
|
|
127
128
|
- lib/head_music/interval.rb
|
128
129
|
- lib/head_music/key_signature.rb
|
129
130
|
- lib/head_music/letter_name.rb
|
130
|
-
- lib/head_music/
|
131
|
+
- lib/head_music/melodic_interval.rb
|
131
132
|
- lib/head_music/meter.rb
|
132
133
|
- lib/head_music/note.rb
|
133
134
|
- lib/head_music/octave.rb
|
@@ -143,6 +144,22 @@ files:
|
|
143
144
|
- lib/head_music/scale_type.rb
|
144
145
|
- lib/head_music/spelling.rb
|
145
146
|
- lib/head_music/staff.rb
|
147
|
+
- lib/head_music/style/analysis.rb
|
148
|
+
- lib/head_music/style/annotation.rb
|
149
|
+
- 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
|
+
- lib/head_music/style/rulesets/cantus_firmus.rb
|
146
163
|
- lib/head_music/utilities/hash_key.rb
|
147
164
|
- lib/head_music/version.rb
|
148
165
|
- lib/head_music/voice.rb
|