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.
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