head_music 0.11.9 → 0.13.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/head_music/bar.rb +9 -7
  4. data/lib/head_music/clef.rb +4 -6
  5. data/lib/head_music/composition.rb +40 -9
  6. data/lib/head_music/functional_interval.rb +34 -40
  7. data/lib/head_music/harmonic_interval.rb +49 -0
  8. data/lib/head_music/instrument.rb +3 -10
  9. data/lib/head_music/melodic_interval.rb +36 -1
  10. data/lib/head_music/meter.rb +33 -17
  11. data/lib/head_music/motion.rb +70 -0
  12. data/lib/head_music/named_rudiment.rb +27 -0
  13. data/lib/head_music/placement.rb +7 -1
  14. data/lib/head_music/position.rb +10 -5
  15. data/lib/head_music/rhythmic_unit.rb +21 -10
  16. data/lib/head_music/spelling.rb +1 -1
  17. data/lib/head_music/style/analysis.rb +12 -1
  18. data/lib/head_music/style/annotation.rb +72 -1
  19. data/lib/head_music/style/annotations/always_move.rb +1 -3
  20. data/lib/head_music/style/annotations/approach_perfection_contrarily.rb +24 -0
  21. data/lib/head_music/style/annotations/at_least_eight_notes.rb +5 -8
  22. data/lib/head_music/style/annotations/avoid_crossing_voices.rb +40 -0
  23. data/lib/head_music/style/annotations/avoid_overlapping_voices.rb +42 -0
  24. data/lib/head_music/style/annotations/consonant_climax.rb +3 -5
  25. data/lib/head_music/style/annotations/consonant_downbeats.rb +22 -0
  26. data/lib/head_music/style/annotations/diatonic.rb +1 -3
  27. data/lib/head_music/style/annotations/direction_changes.rb +1 -3
  28. data/lib/head_music/style/annotations/end_on_perfect_consonance.rb +2 -12
  29. data/lib/head_music/style/annotations/end_on_tonic.rb +6 -8
  30. data/lib/head_music/style/annotations/limit_octave_leaps.rb +20 -0
  31. data/lib/head_music/style/annotations/mostly_conjunct.rb +1 -3
  32. data/lib/head_music/style/annotations/no_rests.rb +2 -4
  33. data/lib/head_music/style/annotations/no_unisons_in_middle.rb +39 -0
  34. data/lib/head_music/style/annotations/notes_same_length.rb +1 -3
  35. data/lib/head_music/style/annotations/one_to_one.rb +3 -13
  36. data/lib/head_music/style/annotations/prefer_contrary_motion.rb +23 -0
  37. data/lib/head_music/style/annotations/prefer_imperfect.rb +23 -0
  38. data/lib/head_music/style/annotations/recover_large_leaps.rb +2 -4
  39. data/lib/head_music/style/annotations/singable_intervals.rb +1 -3
  40. data/lib/head_music/style/annotations/{limit_range.rb → singable_range.rb} +2 -4
  41. data/lib/head_music/style/annotations/start_on_perfect_consonance.rb +3 -13
  42. data/lib/head_music/style/annotations/start_on_tonic.rb +1 -11
  43. data/lib/head_music/style/annotations/step_down_to_final_note.rb +1 -3
  44. data/lib/head_music/style/annotations/step_out_of_unison.rb +38 -0
  45. data/lib/head_music/style/annotations/step_up_to_final_note.rb +1 -3
  46. data/lib/head_music/style/annotations/up_to_thirteen_notes.rb +2 -4
  47. data/lib/head_music/style/rulesets/cantus_firmus.rb +2 -2
  48. data/lib/head_music/style/rulesets/first_species_harmony.rb +8 -1
  49. data/lib/head_music/style/rulesets/first_species_melody.rb +10 -2
  50. data/lib/head_music/version.rb +1 -1
  51. data/lib/head_music/voice.rb +31 -1
  52. data/lib/head_music.rb +14 -1
  53. metadata +15 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3a0baef52f11f8aab0c4becbe31e70928b358499
4
- data.tar.gz: 47ae1ef06c1e25a2abd0559eb40a20494c47eed3
3
+ metadata.gz: f1c8d179b57fdcf25212aa045104c235981b7618
4
+ data.tar.gz: 324f02d328e7f94a35bcce3edf754d186f6f127b
5
5
  SHA512:
6
- metadata.gz: 07e0d608df0d6e5901e652c8796b84ef99c2987c94a346347efff6c73d0b159352d0813eaa2a932530fe7f5e1613548c96ca9c2dbcc2b486ef4978d4e6509235
7
- data.tar.gz: 1dd90a0197c26138b56b109de9dae548c5b9d4b6f4f382b13da80b2fa1093d006c197e26523ff4f2bacb5863bee878d36037049857e8c2655bb3fd61125ec7ae
6
+ metadata.gz: 6d59e0eee2a58b5126bed466801d27bb2f64b93b736057a2802b9b17f133e67b9b60fdbc19aefbd974e5ea6872a9a50095f760b4677e5f31c5631734c47dd977
7
+ data.tar.gz: 92548ec40646df28b54230aba80dba41bd9470024397b627658e8d27c2026ea6909992effc98d8959fd49e715f1b1750c532e8950edc06b363f4d692f1cb2339
data/README.md CHANGED
@@ -24,7 +24,7 @@ Or install it yourself as:
24
24
 
25
25
  ## Usage
26
26
 
27
- TODO: Write usage instructions here
27
+ HeadMusic can be used as a foundation to build larger applications that notate, analyze, play, or generate music in the language of music theory.
28
28
 
29
29
  ## Development
30
30
 
@@ -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
- delegate :key_signature, :meter, to: :composition
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
- # TODO: encapsulate key changes and meter changes
11
- # Assume the key and meter of the previous bar
12
- # all the way back to the first bar,
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
@@ -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 = name.to_s
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 :name, :pitch, :line
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, :bars, :voices
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 add_bar
10
- add_bars(1)
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 add_bars(number)
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
- number.times do
16
- @bars << HeadMusic::Bar.new(self)
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 add_voice(role: nil)
21
- @voices << HeadMusic::Voice.new(composition: self, role: role)
22
- @voices.last
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, :direction
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
- if perfect?
165
- if fourth? && style == :two_part_harmony
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 == 3
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 ==(other)
211
- self.to_s.gsub(/\W/, '_') == other.to_s.gsub(/\W/, '_')
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 || simple_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
- @instruments ||= {}
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
- notes.map(&:pitch)
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
@@ -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 == 1 && tick == 0
63
- return 80 if strong_counts.include?(count) && tick == 0
64
- return 60 if tick == 0
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 == 1 ||
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
@@ -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)
@@ -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
- bar, count, tick = code_or_bar.split(/\D+/)
11
- ensure_state(composition, bar, count, tick)
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 :name, :numerator, :denominator
19
- delegate :to_s, to: :name
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 ||= MULTIPLES.include?(name) ? 2**MULTIPLES.index(name) : 1
24
- @denominator ||= FRACTIONS.include?(name) ? 2**FRACTIONS.index(name) : 1
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