head_music 0.8.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/head_music/accidental.rb +0 -1
  3. data/lib/head_music/{measure.rb → bar.rb} +3 -3
  4. data/lib/head_music/circle.rb +0 -1
  5. data/lib/head_music/composition.rb +8 -8
  6. data/lib/head_music/consonance.rb +0 -1
  7. data/lib/head_music/functional_interval.rb +37 -8
  8. data/lib/head_music/interval.rb +0 -1
  9. data/lib/head_music/letter_name.rb +0 -1
  10. data/lib/head_music/melodic_interval.rb +29 -0
  11. data/lib/head_music/meter.rb +6 -6
  12. data/lib/head_music/note.rb +11 -7
  13. data/lib/head_music/octave.rb +0 -1
  14. data/lib/head_music/pitch.rb +0 -1
  15. data/lib/head_music/pitch_class.rb +0 -1
  16. data/lib/head_music/placement.rb +6 -1
  17. data/lib/head_music/position.rb +31 -13
  18. data/lib/head_music/quality.rb +0 -1
  19. data/lib/head_music/rhythmic_unit.rb +0 -1
  20. data/lib/head_music/rhythmic_value.rb +33 -0
  21. data/lib/head_music/scale_type.rb +0 -1
  22. data/lib/head_music/spelling.rb +0 -1
  23. data/lib/head_music/style/analysis.rb +17 -0
  24. data/lib/head_music/style/annotation.rb +28 -0
  25. data/lib/head_music/style/mark.rb +29 -0
  26. data/lib/head_music/style/rule.rb +13 -0
  27. data/lib/head_music/style/rules/always_move.rb +19 -0
  28. data/lib/head_music/style/rules/at_least_eight_notes.rb +26 -0
  29. data/lib/head_music/style/rules/diatonic.rb +15 -0
  30. data/lib/head_music/style/rules/end_on_tonic.rb +27 -0
  31. data/lib/head_music/style/rules/limit_range.rb +31 -0
  32. data/lib/head_music/style/rules/mostly_conjunct.rb +30 -0
  33. data/lib/head_music/style/rules/no_rests.rb +14 -0
  34. data/lib/head_music/style/rules/notes_same_length.rb +49 -0
  35. data/lib/head_music/style/rules/start_on_tonic.rb +27 -0
  36. data/lib/head_music/style/rules/step_down_to_final_note.rb +34 -0
  37. data/lib/head_music/style/rules/up_to_thirteen_notes.rb +24 -0
  38. data/lib/head_music/style/rulesets/cantus_firmus.rb +22 -0
  39. data/lib/head_music/utilities/hash_key.rb +2 -2
  40. data/lib/head_music/version.rb +1 -1
  41. data/lib/head_music/voice.rb +48 -3
  42. data/lib/head_music.rb +27 -4
  43. metadata +20 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 56a211e11bb35239af47cdf2e79e2032b6b97f7b
4
- data.tar.gz: 293be36552913da8cd64f946b4b1ece210114991
3
+ metadata.gz: f3c4a7d9f36e3a577a3b3e15dde8e3b9b8719937
4
+ data.tar.gz: dc196e99e9f17077d7135fb17047a07a8fee13be
5
5
  SHA512:
6
- metadata.gz: d809adbad72a56eca2910a2c234d6497028d3c0a9723bfd02a214cf62f22e44777c38ff08a20707beff1d65e59cd9b5e8ddacacce82d2d64b9b826c48a5c3329
7
- data.tar.gz: a2aa89d2b462c9a8dd938ddef98376a78a4a1a2ba7dd721e2f4e39913f5074c53d201465ae1ba0e59e11d6706e4905cb74434bb596b892b903dc298604e2ba06
6
+ metadata.gz: 1fa70d04a30f7f652420d88224192f4a2dd89a8436a363a3306129336d3a61a390ecd0d4c8baf2a7ca9cd05798dc4bbd09e0969bf0107335a7adaa3a3ae7f195
7
+ data.tar.gz: 95a836da90aa931b77935d7558c2fb6710b9b559ec69578b39451655551e058bfa19ef68fdea8541dbb24912011d34490d61e34f9baf4eff65f22fd8fec48f85
@@ -11,7 +11,6 @@ class HeadMusic::Accidental
11
11
  def self.get(identifier)
12
12
  for_symbol(identifier) || for_interval(identifier)
13
13
  end
14
- singleton_class.send(:alias_method, :[], :get)
15
14
 
16
15
  def self.for_symbol(identifier)
17
16
  @accidentals ||= {}
@@ -1,4 +1,4 @@
1
- class HeadMusic::Measure
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 measure
12
- # all the way back to the first measure,
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
@@ -11,7 +11,6 @@ class HeadMusic::Circle
11
11
  @circles ||= {}
12
12
  @circles[interval.to_i] ||= new(interval)
13
13
  end
14
- singleton_class.send(:alias_method, :[], :get)
15
14
 
16
15
  attr_reader :interval, :pitch_classes
17
16
 
@@ -1,26 +1,26 @@
1
1
  class HeadMusic::Composition
2
- attr_reader :name, :key_signature, :meter, :measures, :voices
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
- add_measure
6
+ add_bar
7
7
  add_voice
8
8
  end
9
9
 
10
- def add_measure
11
- add_measures(1)
10
+ def add_bar
11
+ add_bars(1)
12
12
  end
13
13
 
14
- def add_measures(number)
15
- @measures ||= []
14
+ def add_bars(number)
15
+ @bars ||= []
16
16
  number.times do
17
- @measures << HeadMusic::Measure.new(self)
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
@@ -5,7 +5,6 @@ class HeadMusic::Consonance
5
5
  @consonances ||= {}
6
6
  @consonances[name.to_sym] ||= new(name) if LEVELS.include?(name.to_s)
7
7
  end
8
- singleton_class.send(:alias_method, :[], :get)
9
8
 
10
9
  attr_reader :name
11
10
 
@@ -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
- @lower_pitch, @higher_pitch = [HeadMusic::Pitch.get(pitch1), HeadMusic::Pitch.get(pitch2)].sort
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
- [quality.shorthand, number].join
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 > 2
186
+ number == 3
162
187
  end
163
188
 
164
- def step?
165
- number <= 2
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|
@@ -11,7 +11,6 @@ class HeadMusic::Interval
11
11
  @intervals ||= {}
12
12
  @intervals[semitones.to_i] ||= new(semitones.to_i)
13
13
  end
14
- singleton_class.send(:alias_method, :[], :get)
15
14
 
16
15
  def self.named(name)
17
16
  name = name.to_s
@@ -18,7 +18,6 @@ class HeadMusic::LetterName
18
18
  def self.get(identifier)
19
19
  from_name(identifier) || from_pitch_class(identifier)
20
20
  end
21
- singleton_class.send(:alias_method, :[], :get)
22
21
 
23
22
  def self.from_name(name)
24
23
  @letter_names ||= {}
@@ -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
@@ -50,11 +50,11 @@ class HeadMusic::Meter
50
50
  top_number == 4
51
51
  end
52
52
 
53
- def beats_per_measure
53
+ def beats_per_bar
54
54
  compound? ? top_number / 3 : top_number
55
55
  end
56
56
 
57
- def counts_per_measure
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..counts_per_measure).select do |count|
104
+ (1..counts_per_bar).select do |count|
105
105
  count == 1 ||
106
- count == counts_per_measure / 2.0 + 1 ||
106
+ count == counts_per_bar / 2.0 + 1 ||
107
107
  (
108
- counts_per_measure % 3 == 0 &&
109
- counts_per_measure > 6 &&
108
+ counts_per_bar % 3 == 0 &&
109
+ counts_per_bar > 6 &&
110
110
  count % 3 == 1
111
111
  )
112
112
  end
@@ -1,14 +1,18 @@
1
1
  class HeadMusic::Note
2
- attr_reader :pitch, :rhythmic_value
2
+ attr_accessor :pitch, :rhythmic_value, :voice, :position
3
3
 
4
- delegate :ticks, to: :rhythmic_value
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.new(rhythmic_unit, rhythmic_value_modifiers)
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 duration
12
- rhythmic_value.total_value
15
+ def method_missing(method_name, *args, &block)
16
+ placement.send(method_name, *args, &block)
13
17
  end
14
18
  end
@@ -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
@@ -14,7 +14,6 @@ class HeadMusic::Pitch
14
14
  def self.get(value)
15
15
  from_name(value) || from_number(value)
16
16
  end
17
- singleton_class.send(:alias_method, :[], :get)
18
17
 
19
18
  def self.from_name(name)
20
19
  return nil unless name == name.to_s
@@ -10,7 +10,6 @@ class HeadMusic::PitchClass
10
10
  number ||= identifier.to_i % 12
11
11
  @pitch_classes[number] ||= new(number)
12
12
  end
13
- singleton_class.send(:alias_method, :[], :get)
14
13
 
15
14
  class << self
16
15
  alias_method :[], :get
@@ -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
@@ -1,20 +1,22 @@
1
1
  class HeadMusic::Position
2
2
  include Comparable
3
3
 
4
- attr_reader :composition, :measure_number, :count, :tick
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, code_or_measure, count = nil, tick = nil)
9
- if code_or_measure.is_a?(String) && code_or_measure =~ /\D/
10
- ensure_state(composition, *code_or_measure.split(/\D+/))
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, code_or_measure, count, tick)
13
+ ensure_state(composition, code_or_bar, count, tick)
13
14
  end
14
15
  end
15
16
 
16
17
  def code
17
- values.join(':')
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
- [measure_number, count, tick]
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, measure_number, count, tick)
62
+ def ensure_state(composition, bar_number, count, tick = nil)
47
63
  @composition = composition
48
- @measure_number = measure_number.to_i
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
- while @tick > meter.ticks_per_count
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
- while @count > meter.counts_per_measure
68
- @count -= meter.counts_per_measure
69
- @measure_number += 1
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
@@ -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
@@ -64,7 +64,6 @@ class HeadMusic::ScaleType
64
64
  attributes = SCALE_TYPES[name]
65
65
  @scale_types[name] ||= new(name, attributes)
66
66
  end
67
- singleton_class.send(:alias_method, :[], :get)
68
67
 
69
68
  def self.default
70
69
  get(:major)
@@ -13,7 +13,6 @@ class HeadMusic::Spelling
13
13
  def self.get(identifier)
14
14
  from_name(identifier) || from_number(identifier)
15
15
  end
16
- singleton_class.send(:alias_method, :[], :get)
17
16
 
18
17
  def self.match(string)
19
18
  string.to_s.match(MATCHER)
@@ -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
@@ -1,8 +1,8 @@
1
1
  module HeadMusic
2
2
  module Utilities
3
3
  class HashKey
4
- def self.for(name)
5
- name.to_s.downcase.gsub(/\W+/, '_').to_sym
4
+ def self.for(identifier)
5
+ identifier.to_s.downcase.gsub(/\W+/, '_').to_sym
6
6
  end
7
7
  end
8
8
  end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "0.8.0"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -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 = nil)
7
- @composition = composition || Composition.new(name: "Unnamed")
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/composition'
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
- require 'head_music/voice'
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.8.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-04 00:00:00.000000000 Z
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/measure.rb
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