head_music 0.11.9 → 0.13.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/head_music/bar.rb +9 -7
- data/lib/head_music/clef.rb +4 -6
- data/lib/head_music/composition.rb +40 -9
- data/lib/head_music/functional_interval.rb +34 -40
- data/lib/head_music/harmonic_interval.rb +49 -0
- data/lib/head_music/instrument.rb +3 -10
- data/lib/head_music/melodic_interval.rb +36 -1
- data/lib/head_music/meter.rb +33 -17
- data/lib/head_music/motion.rb +70 -0
- data/lib/head_music/named_rudiment.rb +27 -0
- data/lib/head_music/placement.rb +7 -1
- data/lib/head_music/position.rb +10 -5
- data/lib/head_music/rhythmic_unit.rb +21 -10
- data/lib/head_music/spelling.rb +1 -1
- data/lib/head_music/style/analysis.rb +12 -1
- data/lib/head_music/style/annotation.rb +72 -1
- data/lib/head_music/style/annotations/always_move.rb +1 -3
- data/lib/head_music/style/annotations/approach_perfection_contrarily.rb +24 -0
- data/lib/head_music/style/annotations/at_least_eight_notes.rb +5 -8
- data/lib/head_music/style/annotations/avoid_crossing_voices.rb +40 -0
- data/lib/head_music/style/annotations/avoid_overlapping_voices.rb +42 -0
- data/lib/head_music/style/annotations/consonant_climax.rb +3 -5
- data/lib/head_music/style/annotations/consonant_downbeats.rb +22 -0
- data/lib/head_music/style/annotations/diatonic.rb +1 -3
- data/lib/head_music/style/annotations/direction_changes.rb +1 -3
- data/lib/head_music/style/annotations/end_on_perfect_consonance.rb +2 -12
- data/lib/head_music/style/annotations/end_on_tonic.rb +6 -8
- data/lib/head_music/style/annotations/limit_octave_leaps.rb +20 -0
- data/lib/head_music/style/annotations/mostly_conjunct.rb +1 -3
- data/lib/head_music/style/annotations/no_rests.rb +2 -4
- data/lib/head_music/style/annotations/no_unisons_in_middle.rb +39 -0
- data/lib/head_music/style/annotations/notes_same_length.rb +1 -3
- data/lib/head_music/style/annotations/one_to_one.rb +3 -13
- data/lib/head_music/style/annotations/prefer_contrary_motion.rb +23 -0
- data/lib/head_music/style/annotations/prefer_imperfect.rb +23 -0
- data/lib/head_music/style/annotations/recover_large_leaps.rb +2 -4
- data/lib/head_music/style/annotations/singable_intervals.rb +1 -3
- data/lib/head_music/style/annotations/{limit_range.rb → singable_range.rb} +2 -4
- data/lib/head_music/style/annotations/start_on_perfect_consonance.rb +3 -13
- data/lib/head_music/style/annotations/start_on_tonic.rb +1 -11
- data/lib/head_music/style/annotations/step_down_to_final_note.rb +1 -3
- data/lib/head_music/style/annotations/step_out_of_unison.rb +38 -0
- data/lib/head_music/style/annotations/step_up_to_final_note.rb +1 -3
- data/lib/head_music/style/annotations/up_to_thirteen_notes.rb +2 -4
- data/lib/head_music/style/rulesets/cantus_firmus.rb +2 -2
- data/lib/head_music/style/rulesets/first_species_harmony.rb +8 -1
- data/lib/head_music/style/rulesets/first_species_melody.rb +10 -2
- data/lib/head_music/version.rb +1 -1
- data/lib/head_music/voice.rb +31 -1
- data/lib/head_music.rb +14 -1
- metadata +15 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1c8d179b57fdcf25212aa045104c235981b7618
|
4
|
+
data.tar.gz: 324f02d328e7f94a35bcce3edf754d186f6f127b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d59e0eee2a58b5126bed466801d27bb2f64b93b736057a2802b9b17f133e67b9b60fdbc19aefbd974e5ea6872a9a50095f760b4677e5f31c5631734c47dd977
|
7
|
+
data.tar.gz: 92548ec40646df28b54230aba80dba41bd9470024397b627658e8d27c2026ea6909992effc98d8959fd49e715f1b1750c532e8950edc06b363f4d692f1cb2339
|
data/README.md
CHANGED
data/lib/head_music/bar.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
+
# Representation of a bar in a composition
|
2
|
+
# Encapsulates meter and key signature changes
|
1
3
|
class HeadMusic::Bar
|
2
4
|
attr_reader :composition
|
5
|
+
attr_accessor :key_signature, :meter
|
3
6
|
|
4
|
-
|
5
|
-
|
6
|
-
def initialize(composition)
|
7
|
+
def initialize(composition, key_signature: nil, meter: nil)
|
7
8
|
@composition = composition
|
9
|
+
@key_signature = HeadMusic::KeySignature.get(key_signature) if key_signature
|
10
|
+
@meter = HeadMusic::Meter.get(meter) if meter
|
8
11
|
end
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# which defaults to the key and meter of the composition
|
13
|
+
def to_s
|
14
|
+
['Bar', key_signature, meter].reject(&:nil?).join(' ')
|
15
|
+
end
|
14
16
|
end
|
data/lib/head_music/clef.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
class HeadMusic::Clef
|
2
|
+
include HeadMusic::NamedRudiment
|
3
|
+
|
2
4
|
CLEFS = [
|
3
5
|
{ pitch: 'G4', line: 2, names: ['treble', 'G-clef'], modern: true },
|
4
6
|
{ pitch: 'G4', line: 1, names: ['French', 'French violin'] },
|
@@ -18,14 +20,10 @@ class HeadMusic::Clef
|
|
18
20
|
]
|
19
21
|
|
20
22
|
def self.get(name)
|
21
|
-
name
|
22
|
-
@clefs ||= {}
|
23
|
-
key = HeadMusic::Utilities::HashKey.for(name)
|
24
|
-
@clefs[key] ||= new(name)
|
23
|
+
get_by_name(name)
|
25
24
|
end
|
26
25
|
|
27
|
-
attr_reader :
|
28
|
-
delegate :to_s, to: :name
|
26
|
+
attr_reader :pitch, :line
|
29
27
|
|
30
28
|
def initialize(name)
|
31
29
|
@name = name.to_s
|
@@ -1,25 +1,48 @@
|
|
1
1
|
class HeadMusic::Composition
|
2
|
-
attr_reader :name, :key_signature, :meter, :
|
2
|
+
attr_reader :name, :key_signature, :meter, :voices
|
3
3
|
|
4
4
|
def initialize(name: nil, key_signature: nil, meter: nil)
|
5
5
|
ensure_attributes(name, key_signature, meter)
|
6
6
|
@voices = []
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
10
|
-
|
9
|
+
def add_voice(role: nil)
|
10
|
+
@voices << HeadMusic::Voice.new(composition: self, role: role)
|
11
|
+
@voices.last
|
12
|
+
end
|
13
|
+
|
14
|
+
def meter_at(bar_number)
|
15
|
+
meter_change = last_meter_change(bar_number)
|
16
|
+
meter_change ? meter_change.meter : meter
|
11
17
|
end
|
12
18
|
|
13
|
-
def
|
19
|
+
def key_signature_at(bar_number)
|
20
|
+
key_signature_change = last_key_signature_change(bar_number)
|
21
|
+
key_signature_change ? key_signature_change.key_signature : key_signature
|
22
|
+
end
|
23
|
+
|
24
|
+
def bars(last = latest_bar_number)
|
14
25
|
@bars ||= []
|
15
|
-
|
16
|
-
@bars
|
26
|
+
(earliest_bar_number..last).each do |bar_number|
|
27
|
+
@bars[bar_number] ||= Bar.new(self)
|
17
28
|
end
|
29
|
+
@bars[earliest_bar_number..last]
|
18
30
|
end
|
19
31
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
32
|
+
def change_key_signature(bar_number, key_signature)
|
33
|
+
bars(bar_number).last.key_signature = key_signature
|
34
|
+
end
|
35
|
+
|
36
|
+
def change_meter(bar_number, meter)
|
37
|
+
bars(bar_number).last.meter = meter
|
38
|
+
end
|
39
|
+
|
40
|
+
def earliest_bar_number
|
41
|
+
[voices.map(&:earliest_bar_number), 1].flatten.min
|
42
|
+
end
|
43
|
+
|
44
|
+
def latest_bar_number
|
45
|
+
[voices.map(&:earliest_bar_number), 1].flatten.max
|
23
46
|
end
|
24
47
|
|
25
48
|
private
|
@@ -31,4 +54,12 @@ class HeadMusic::Composition
|
|
31
54
|
@meter = HeadMusic::Meter.get(meter) if meter
|
32
55
|
@meter ||= HeadMusic::Meter.default
|
33
56
|
end
|
57
|
+
|
58
|
+
def last_meter_change(bar_number)
|
59
|
+
bars(bar_number)[earliest_bar_number..bar_number].reverse.detect(&:meter)
|
60
|
+
end
|
61
|
+
|
62
|
+
def last_key_signature_change(bar_number)
|
63
|
+
bars(bar_number)[earliest_bar_number..bar_number].reverse.detect(&:key_signature)
|
64
|
+
end
|
34
65
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
class HeadMusic::FunctionalInterval
|
2
|
+
include Comparable
|
3
|
+
|
2
4
|
NUMBER_NAMES = %w[unison second third fourth fifth sixth seventh octave ninth tenth eleventh twelfth thirteenth fourteenth fifteenth sixteenth seventeenth]
|
3
5
|
NAME_SUFFIXES = Hash.new('th').merge({ 1 => 'st', 2 => 'nd', 3 => 'rd' })
|
4
6
|
|
@@ -22,7 +24,7 @@ class HeadMusic::FunctionalInterval
|
|
22
24
|
seventeenth: { major: 28 }
|
23
25
|
}
|
24
26
|
|
25
|
-
attr_reader :lower_pitch, :higher_pitch
|
27
|
+
attr_reader :lower_pitch, :higher_pitch
|
26
28
|
|
27
29
|
delegate :to_s, to: :name
|
28
30
|
delegate :perfect?, :major?, :minor?, :diminished?, :augmented?, :doubly_diminished?, :doubly_augmented?, to: :quality
|
@@ -62,33 +64,17 @@ class HeadMusic::FunctionalInterval
|
|
62
64
|
def initialize(pitch1, pitch2)
|
63
65
|
pitch1 = HeadMusic::Pitch.get(pitch1)
|
64
66
|
pitch2 = HeadMusic::Pitch.get(pitch2)
|
65
|
-
set_direction(pitch1, pitch2)
|
66
67
|
@lower_pitch, @higher_pitch = [pitch1, pitch2].sort
|
67
68
|
end
|
68
69
|
|
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
|
86
|
-
end
|
87
|
-
|
88
70
|
def number
|
89
71
|
simple_number + octaves * 7
|
90
72
|
end
|
91
73
|
|
74
|
+
def steps
|
75
|
+
number - 1
|
76
|
+
end
|
77
|
+
|
92
78
|
def simple_number
|
93
79
|
@simple_number ||= @lower_pitch.letter_name.steps_to(@higher_pitch.letter_name) + 1
|
94
80
|
end
|
@@ -161,21 +147,9 @@ class HeadMusic::FunctionalInterval
|
|
161
147
|
end
|
162
148
|
|
163
149
|
def consonance(style = :standard_practice)
|
164
|
-
|
165
|
-
|
166
|
-
HeadMusic::Consonance.get(:dissonant)
|
167
|
-
else
|
168
|
-
HeadMusic::Consonance.get(:perfect)
|
169
|
-
end
|
170
|
-
elsif major? || minor?
|
171
|
-
if third? || sixth?
|
172
|
-
HeadMusic::Consonance.get(:imperfect)
|
173
|
-
else
|
174
|
-
HeadMusic::Consonance.get(:dissonant)
|
175
|
-
end
|
176
|
-
else
|
150
|
+
consonance_for_perfect(style) ||
|
151
|
+
consonance_for_major_and_minor ||
|
177
152
|
HeadMusic::Consonance.get(:dissonant)
|
178
|
-
end
|
179
153
|
end
|
180
154
|
|
181
155
|
def consonance?(style = :standard_practice)
|
@@ -199,19 +173,39 @@ class HeadMusic::FunctionalInterval
|
|
199
173
|
end
|
200
174
|
|
201
175
|
def skip?
|
202
|
-
number
|
176
|
+
number >= 3
|
203
177
|
end
|
204
178
|
|
205
179
|
def leap?
|
180
|
+
number >= 3
|
181
|
+
end
|
182
|
+
|
183
|
+
def large_leap?
|
206
184
|
number > 3
|
207
185
|
end
|
208
|
-
alias_method :large_leap?, :leap?
|
209
186
|
|
210
|
-
def
|
211
|
-
|
187
|
+
def <=>(other)
|
188
|
+
if !other.is_a?(FunctionalInterval)
|
189
|
+
other = self.class.get(other)
|
190
|
+
end
|
191
|
+
self.semitones <=> other.semitones
|
212
192
|
end
|
213
193
|
|
214
194
|
NUMBER_NAMES.each do |method_name|
|
215
|
-
define_method(:"#{method_name}?") { number_name == method_name
|
195
|
+
define_method(:"#{method_name}?") { number_name == method_name }
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def consonance_for_perfect(style = :standard_practice)
|
201
|
+
HeadMusic::Consonance.get(dissonant_fourth?(style) ? :dissonant : :perfect) if perfect?
|
202
|
+
end
|
203
|
+
|
204
|
+
def consonance_for_major_and_minor
|
205
|
+
HeadMusic::Consonance.get((third? || sixth?) ? :imperfect : :dissonant) if (major? || minor?)
|
206
|
+
end
|
207
|
+
|
208
|
+
def dissonant_fourth?(style = :standard_practice)
|
209
|
+
fourth? && style == :two_part_harmony
|
216
210
|
end
|
217
211
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class HeadMusic::HarmonicInterval
|
2
|
+
attr_reader :voice1, :voice2, :position
|
3
|
+
|
4
|
+
def initialize(voice1, voice2, position)
|
5
|
+
@voice1 = voice1
|
6
|
+
@voice2 = voice2
|
7
|
+
@position = position.is_a?(String) ? Position.new(voice1.composition, position) : position
|
8
|
+
end
|
9
|
+
|
10
|
+
def functional_interval
|
11
|
+
@functional_interval ||= HeadMusic::FunctionalInterval.new(lower_pitch, upper_pitch)
|
12
|
+
end
|
13
|
+
|
14
|
+
def voices
|
15
|
+
[voice1, voice2].reject(&:nil?)
|
16
|
+
end
|
17
|
+
|
18
|
+
def notes
|
19
|
+
@notes ||= voices.map { |voice| voice.note_at(position) }.reject(&:nil?).sort_by(&:pitch)
|
20
|
+
end
|
21
|
+
|
22
|
+
def lower_note
|
23
|
+
notes.first
|
24
|
+
end
|
25
|
+
|
26
|
+
def upper_note
|
27
|
+
notes.last
|
28
|
+
end
|
29
|
+
|
30
|
+
def pitches
|
31
|
+
@pitches ||= notes.map(&:pitch).sort_by(&:to_i)
|
32
|
+
end
|
33
|
+
|
34
|
+
def lower_pitch
|
35
|
+
pitches.first
|
36
|
+
end
|
37
|
+
|
38
|
+
def upper_pitch
|
39
|
+
pitches.last
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
"#{functional_interval} at #{position}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing(method_name, *args, &block)
|
47
|
+
functional_interval.send(method_name, *args, &block)
|
48
|
+
end
|
49
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
class HeadMusic::Instrument
|
2
|
+
include HeadMusic::NamedRudiment
|
3
|
+
|
2
4
|
INSTRUMENTS = {
|
3
5
|
violin: {
|
4
6
|
name: "violin",
|
@@ -13,14 +15,9 @@ class HeadMusic::Instrument
|
|
13
15
|
}
|
14
16
|
|
15
17
|
def self.get(name)
|
16
|
-
|
17
|
-
key = HeadMusic::Utilities::HashKey.for(name)
|
18
|
-
@instruments[key] ||= new(name.to_s)
|
18
|
+
get_by_name(name)
|
19
19
|
end
|
20
20
|
|
21
|
-
attr_reader :name
|
22
|
-
delegate :to_s, to: :name
|
23
|
-
|
24
21
|
def initialize(name)
|
25
22
|
@name = name.to_s
|
26
23
|
end
|
@@ -29,10 +26,6 @@ class HeadMusic::Instrument
|
|
29
26
|
@data ||= INSTRUMENTS[hash_key]
|
30
27
|
end
|
31
28
|
|
32
|
-
def hash_key
|
33
|
-
HeadMusic::Utilities::HashKey.for(name)
|
34
|
-
end
|
35
|
-
|
36
29
|
def family
|
37
30
|
data[:family]
|
38
31
|
end
|
@@ -24,13 +24,48 @@ class HeadMusic::MelodicInterval
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def pitches
|
27
|
-
|
27
|
+
[first_pitch, second_pitch]
|
28
|
+
end
|
29
|
+
|
30
|
+
def first_pitch
|
31
|
+
@first_pitch ||= first_note.pitch
|
32
|
+
end
|
33
|
+
|
34
|
+
def second_pitch
|
35
|
+
@second_pitch ||= second_note.pitch
|
28
36
|
end
|
29
37
|
|
30
38
|
def to_s
|
31
39
|
[direction, functional_interval].join(' ')
|
32
40
|
end
|
33
41
|
|
42
|
+
def ascending?
|
43
|
+
direction == :ascending
|
44
|
+
end
|
45
|
+
|
46
|
+
def descending?
|
47
|
+
direction == :descending
|
48
|
+
end
|
49
|
+
|
50
|
+
def moving?
|
51
|
+
ascending? || descending?
|
52
|
+
end
|
53
|
+
|
54
|
+
def repetition?
|
55
|
+
!moving?
|
56
|
+
end
|
57
|
+
|
58
|
+
def direction
|
59
|
+
@direction ||=
|
60
|
+
if first_pitch < second_pitch
|
61
|
+
:ascending
|
62
|
+
elsif first_pitch > second_pitch
|
63
|
+
:descending
|
64
|
+
else
|
65
|
+
:none
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
34
69
|
def method_missing(method_name, *args, &block)
|
35
70
|
functional_interval.send(method_name, *args, &block)
|
36
71
|
end
|
data/lib/head_music/meter.rb
CHANGED
@@ -59,9 +59,9 @@ class HeadMusic::Meter
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def beat_strength(count, tick: 0)
|
62
|
-
return 100 if count
|
63
|
-
return 80 if
|
64
|
-
return 60 if tick
|
62
|
+
return 100 if downbeat?(count, tick)
|
63
|
+
return 80 if strong_beat?(count, tick)
|
64
|
+
return 60 if beat?(tick)
|
65
65
|
return 40 if strong_ticks.include?(tick)
|
66
66
|
20
|
67
67
|
end
|
@@ -70,13 +70,6 @@ class HeadMusic::Meter
|
|
70
70
|
@ticks_per_count ||= count_unit.ticks
|
71
71
|
end
|
72
72
|
|
73
|
-
def strong_ticks
|
74
|
-
@strong_ticks ||=
|
75
|
-
[2,3,4].map do |sixths|
|
76
|
-
ticks_per_count * (sixths / 6.0)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
73
|
def count_unit
|
81
74
|
HeadMusic::RhythmicUnit.for_denominator_value(bottom_number)
|
82
75
|
end
|
@@ -102,14 +95,37 @@ class HeadMusic::Meter
|
|
102
95
|
def strong_counts
|
103
96
|
@strong_counts ||= begin
|
104
97
|
(1..counts_per_bar).select do |count|
|
105
|
-
count
|
106
|
-
count == counts_per_bar / 2.0 + 1 ||
|
107
|
-
(
|
108
|
-
counts_per_bar % 3 == 0 &&
|
109
|
-
counts_per_bar > 6 &&
|
110
|
-
count % 3 == 1
|
111
|
-
)
|
98
|
+
downbeat?(count) || strong_beat_in_duple?(count) || strong_beat_in_triple?(count)
|
112
99
|
end
|
113
100
|
end
|
114
101
|
end
|
102
|
+
|
103
|
+
def strong_ticks
|
104
|
+
@strong_ticks ||=
|
105
|
+
[2,3,4].map do |sixths|
|
106
|
+
ticks_per_count * (sixths / 6.0)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def downbeat?(count, tick = 0)
|
113
|
+
beat?(tick) && count == 1
|
114
|
+
end
|
115
|
+
|
116
|
+
def strong_beat?(count, tick = 0)
|
117
|
+
beat?(tick) && (strong_beat_in_duple?(count, tick) || strong_beat_in_triple?(count, tick))
|
118
|
+
end
|
119
|
+
|
120
|
+
def strong_beat_in_duple?(count, tick = 0)
|
121
|
+
beat?(tick) && (count == counts_per_bar / 2.0 + 1)
|
122
|
+
end
|
123
|
+
|
124
|
+
def strong_beat_in_triple?(count, tick = 0)
|
125
|
+
beat?(tick) && counts_per_bar % 3 == 0 && counts_per_bar > 6 && count % 3 == 1
|
126
|
+
end
|
127
|
+
|
128
|
+
def beat?(tick)
|
129
|
+
tick == 0
|
130
|
+
end
|
115
131
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class HeadMusic::Motion
|
2
|
+
attr_reader :first_harmonic_interval, :second_harmonic_interval
|
3
|
+
|
4
|
+
def initialize(first_harmonic_interval, second_harmonic_interval)
|
5
|
+
@first_harmonic_interval = first_harmonic_interval
|
6
|
+
@second_harmonic_interval = second_harmonic_interval
|
7
|
+
end
|
8
|
+
|
9
|
+
def repetition?
|
10
|
+
upper_melodic_interval.repetition? && lower_melodic_interval.repetition?
|
11
|
+
end
|
12
|
+
|
13
|
+
def oblique?
|
14
|
+
upper_melodic_interval.repetition? && lower_melodic_interval.moving? ||
|
15
|
+
lower_melodic_interval.repetition? && upper_melodic_interval.moving?
|
16
|
+
end
|
17
|
+
|
18
|
+
def direct?
|
19
|
+
parallel? || similar?
|
20
|
+
end
|
21
|
+
|
22
|
+
def parallel?
|
23
|
+
upper_melodic_interval.moving? &&
|
24
|
+
upper_melodic_interval.direction == lower_melodic_interval.direction &&
|
25
|
+
upper_melodic_interval.steps == lower_melodic_interval.steps
|
26
|
+
end
|
27
|
+
|
28
|
+
def similar?
|
29
|
+
upper_melodic_interval.direction == lower_melodic_interval.direction &&
|
30
|
+
upper_melodic_interval.steps != lower_melodic_interval.steps
|
31
|
+
end
|
32
|
+
|
33
|
+
def contrary?
|
34
|
+
upper_melodic_interval.moving? &&
|
35
|
+
lower_melodic_interval.moving? &&
|
36
|
+
upper_melodic_interval.direction != lower_melodic_interval.direction
|
37
|
+
end
|
38
|
+
|
39
|
+
def notes
|
40
|
+
upper_notes + lower_notes
|
41
|
+
end
|
42
|
+
|
43
|
+
def contrapuntal_motion
|
44
|
+
[:parallel, :similar, :oblique, :contrary, :repetition].detect do |motion_type|
|
45
|
+
send("#{motion_type}?")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"#{contrapuntal_motion} motion from #{first_harmonic_interval} to #{second_harmonic_interval}"
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def upper_melodic_interval
|
56
|
+
HeadMusic::MelodicInterval.new(upper_notes.first.voice, upper_notes.first, upper_notes.last)
|
57
|
+
end
|
58
|
+
|
59
|
+
def lower_melodic_interval
|
60
|
+
HeadMusic::MelodicInterval.new(lower_notes.first.voice, lower_notes.first, lower_notes.last)
|
61
|
+
end
|
62
|
+
|
63
|
+
def upper_notes
|
64
|
+
[first_harmonic_interval, second_harmonic_interval].map(&:upper_note)
|
65
|
+
end
|
66
|
+
|
67
|
+
def lower_notes
|
68
|
+
[first_harmonic_interval, second_harmonic_interval].map(&:lower_note)
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module HeadMusic
|
2
|
+
module NamedRudiment
|
3
|
+
attr_reader :name
|
4
|
+
delegate :to_s, to: :name
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name.to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
def hash_key
|
11
|
+
HeadMusic::Utilities::HashKey.for(name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
base.extend(ClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def get_by_name(name)
|
20
|
+
name = name.to_s
|
21
|
+
@instances_by_name ||= {}
|
22
|
+
key = HeadMusic::Utilities::HashKey.for(name)
|
23
|
+
@instances_by_name[key] ||= new(name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/head_music/placement.rb
CHANGED
@@ -18,13 +18,19 @@ class HeadMusic::Placement
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def next_position
|
21
|
-
position + rhythmic_value
|
21
|
+
@next_position ||= position + rhythmic_value
|
22
22
|
end
|
23
23
|
|
24
24
|
def <=>(other)
|
25
25
|
self.position <=> other.position
|
26
26
|
end
|
27
27
|
|
28
|
+
def during?(other_placement)
|
29
|
+
(other_placement.position >= position && other_placement.position < next_position) ||
|
30
|
+
(other_placement.next_position > position && other_placement.next_position <= next_position) ||
|
31
|
+
(other_placement.position <= position && other_placement.next_position >= next_position)
|
32
|
+
end
|
33
|
+
|
28
34
|
private
|
29
35
|
|
30
36
|
def ensure_attributes(voice, position, rhythmic_value, pitch)
|
data/lib/head_music/position.rb
CHANGED
@@ -3,17 +3,20 @@ class HeadMusic::Position
|
|
3
3
|
|
4
4
|
attr_reader :composition, :bar_number, :count, :tick
|
5
5
|
delegate :to_s, to: :code
|
6
|
-
delegate :meter, to: :composition
|
7
6
|
|
8
7
|
def initialize(composition, code_or_bar, count = nil, tick = nil)
|
9
8
|
if code_or_bar.is_a?(String) && code_or_bar =~ /\D/
|
10
|
-
|
11
|
-
ensure_state(composition,
|
9
|
+
bar_number, count, tick = code_or_bar.split(/\D+/)
|
10
|
+
ensure_state(composition, bar_number, count, tick)
|
12
11
|
else
|
13
12
|
ensure_state(composition, code_or_bar, count, tick)
|
14
13
|
end
|
15
14
|
end
|
16
15
|
|
16
|
+
def meter
|
17
|
+
composition.meter_at(bar_number)
|
18
|
+
end
|
19
|
+
|
17
20
|
def code
|
18
21
|
tick_string = tick.to_s.rjust(3, '0')
|
19
22
|
[bar_number, count, tick_string].join(':')
|
@@ -27,6 +30,10 @@ class HeadMusic::Position
|
|
27
30
|
[bar_number, count, tick]
|
28
31
|
end
|
29
32
|
|
33
|
+
def within_placement?(placement)
|
34
|
+
placement.position <= self && placement.next_position > self
|
35
|
+
end
|
36
|
+
|
30
37
|
def <=>(other)
|
31
38
|
if other.is_a?(String) && other =~ /\D/
|
32
39
|
other = self.class.new(composition, other)
|
@@ -73,7 +80,6 @@ class HeadMusic::Position
|
|
73
80
|
end
|
74
81
|
|
75
82
|
def roll_over_ticks
|
76
|
-
# TODO account for meter changes in bars
|
77
83
|
while @tick >= meter.ticks_per_count
|
78
84
|
@tick -= meter.ticks_per_count.to_i
|
79
85
|
@count += 1
|
@@ -81,7 +87,6 @@ class HeadMusic::Position
|
|
81
87
|
end
|
82
88
|
|
83
89
|
def roll_over_counts
|
84
|
-
# TODO account for meter changes in bars
|
85
90
|
while @count > meter.counts_per_bar
|
86
91
|
@count -= meter.counts_per_bar
|
87
92
|
@bar_number += 1
|
@@ -1,27 +1,26 @@
|
|
1
1
|
class HeadMusic::RhythmicUnit
|
2
|
+
include HeadMusic::NamedRudiment
|
3
|
+
|
2
4
|
MULTIPLES = ['whole', 'double whole', 'longa', 'maxima']
|
3
5
|
FRACTIONS = ['whole', 'half', 'quarter', 'eighth', 'sixteenth', 'thirty-second', 'sixty-fourth', 'hundred twenty-eighth', 'two hundred fifty-sixth']
|
4
6
|
|
5
7
|
BRITISH_MULTIPLE_NAMES = %w[semibreve breve longa maxima]
|
6
8
|
BRITISH_DIVISION_NAMES = %w[semibreve minim crotchet quaver semiquaver demisemiquaver hemidemisemiquaver semihemidemisemiquaver demisemihemidemisemiquaver]
|
7
9
|
|
8
|
-
def self.get(name)
|
9
|
-
@rhythmic_units ||= {}
|
10
|
-
hash_key = HeadMusic::Utilities::HashKey.for(name)
|
11
|
-
@rhythmic_units[hash_key] ||= new(name.to_s)
|
12
|
-
end
|
13
|
-
|
14
10
|
def self.for_denominator_value(denominator)
|
15
11
|
get(FRACTIONS[Math.log2(denominator).to_i])
|
16
12
|
end
|
17
13
|
|
18
|
-
attr_reader :
|
19
|
-
|
14
|
+
attr_reader :numerator, :denominator
|
15
|
+
|
16
|
+
def self.get(name)
|
17
|
+
get_by_name(name)
|
18
|
+
end
|
20
19
|
|
21
20
|
def initialize(canonical_name)
|
22
21
|
@name ||= canonical_name
|
23
|
-
@numerator
|
24
|
-
@denominator
|
22
|
+
@numerator = 2**numerator_exponent
|
23
|
+
@denominator = 2**denominator_exponent
|
25
24
|
end
|
26
25
|
|
27
26
|
def relative_value
|
@@ -60,8 +59,20 @@ class HeadMusic::RhythmicUnit
|
|
60
59
|
BRITISH_MULTIPLE_NAMES[MULTIPLES.index(name)]
|
61
60
|
elsif FRACTIONS.include?(name)
|
62
61
|
BRITISH_DIVISION_NAMES[FRACTIONS.index(name)]
|
62
|
+
elsif BRITISH_MULTIPLE_NAMES.include?(name) || BRITISH_DIVISION_NAMES.include?(name)
|
63
|
+
name
|
63
64
|
end
|
64
65
|
end
|
65
66
|
|
66
67
|
private_class_method :new
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def numerator_exponent
|
72
|
+
MULTIPLES.index(name) || BRITISH_MULTIPLE_NAMES.index(name) || 0
|
73
|
+
end
|
74
|
+
|
75
|
+
def denominator_exponent
|
76
|
+
FRACTIONS.index(name) || BRITISH_DIVISION_NAMES.index(name) || 0
|
77
|
+
end
|
67
78
|
end
|