head_music 0.17.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -1145
  3. data/Gemfile +8 -2
  4. data/Rakefile +7 -5
  5. data/bin/console +4 -3
  6. data/circle.yml +1 -1
  7. data/head_music.gemspec +19 -17
  8. data/lib/head_music/bar.rb +2 -0
  9. data/lib/head_music/chord.rb +24 -6
  10. data/lib/head_music/circle.rb +5 -0
  11. data/lib/head_music/clef.rb +5 -2
  12. data/lib/head_music/composition.rb +3 -0
  13. data/lib/head_music/consonance.rb +5 -2
  14. data/lib/head_music/functional_interval.rb +84 -47
  15. data/lib/head_music/grand_staff.rb +11 -11
  16. data/lib/head_music/harmonic_interval.rb +13 -7
  17. data/lib/head_music/instrument.rb +9 -6
  18. data/lib/head_music/interval.rb +13 -7
  19. data/lib/head_music/key_signature.rb +8 -5
  20. data/lib/head_music/language.rb +21 -14
  21. data/lib/head_music/letter_name.rb +13 -10
  22. data/lib/head_music/melodic_interval.rb +10 -4
  23. data/lib/head_music/meter.rb +12 -12
  24. data/lib/head_music/motion.rb +12 -9
  25. data/lib/head_music/named_rudiment.rb +22 -20
  26. data/lib/head_music/note.rb +7 -1
  27. data/lib/head_music/octave.rb +9 -7
  28. data/lib/head_music/pitch.rb +54 -27
  29. data/lib/head_music/pitch_class.rb +17 -12
  30. data/lib/head_music/placement.rb +22 -9
  31. data/lib/head_music/position.rb +8 -9
  32. data/lib/head_music/quality.rb +9 -6
  33. data/lib/head_music/rhythm.rb +2 -0
  34. data/lib/head_music/rhythmic_unit.rb +29 -19
  35. data/lib/head_music/rhythmic_value.rb +5 -2
  36. data/lib/head_music/scale.rb +65 -45
  37. data/lib/head_music/scale_degree.rb +9 -6
  38. data/lib/head_music/scale_type.rb +70 -30
  39. data/lib/head_music/sign.rb +18 -13
  40. data/lib/head_music/spelling.rb +14 -10
  41. data/lib/head_music/staff.rb +4 -1
  42. data/lib/head_music/style/analysis.rb +36 -34
  43. data/lib/head_music/style/annotation.rb +14 -13
  44. data/lib/head_music/style/annotations/always_move.rb +7 -6
  45. data/lib/head_music/style/annotations/approach_perfection_contrarily.rb +5 -2
  46. data/lib/head_music/style/annotations/at_least_eight_notes.rb +10 -8
  47. data/lib/head_music/style/annotations/avoid_crossing_voices.rb +11 -8
  48. data/lib/head_music/style/annotations/avoid_overlapping_voices.rb +17 -10
  49. data/lib/head_music/style/annotations/consonant_climax.rb +18 -15
  50. data/lib/head_music/style/annotations/consonant_downbeats.rb +6 -3
  51. data/lib/head_music/style/annotations/diatonic.rb +8 -5
  52. data/lib/head_music/style/annotations/direction_changes.rb +8 -6
  53. data/lib/head_music/style/annotations/end_on_perfect_consonance.rb +5 -5
  54. data/lib/head_music/style/annotations/end_on_tonic.rb +7 -6
  55. data/lib/head_music/style/annotations/frequent_direction_changes.rb +6 -3
  56. data/lib/head_music/style/annotations/limit_octave_leaps.rb +9 -7
  57. data/lib/head_music/style/annotations/moderate_direction_changes.rb +6 -3
  58. data/lib/head_music/style/annotations/mostly_conjunct.rb +8 -5
  59. data/lib/head_music/style/annotations/no_rests.rb +6 -3
  60. data/lib/head_music/style/annotations/no_unisons_in_middle.rb +6 -3
  61. data/lib/head_music/style/annotations/notes_same_length.rb +9 -6
  62. data/lib/head_music/style/annotations/one_to_one.rb +10 -7
  63. data/lib/head_music/style/annotations/prefer_contrary_motion.rb +6 -3
  64. data/lib/head_music/style/annotations/prefer_imperfect.rb +7 -6
  65. data/lib/head_music/style/annotations/prepare_octave_leaps.rb +21 -12
  66. data/lib/head_music/style/annotations/recover_large_leaps.rb +17 -12
  67. data/lib/head_music/style/annotations/singable_intervals.rb +8 -5
  68. data/lib/head_music/style/annotations/singable_range.rb +7 -6
  69. data/lib/head_music/style/annotations/single_large_leaps.rb +6 -3
  70. data/lib/head_music/style/annotations/start_on_perfect_consonance.rb +6 -5
  71. data/lib/head_music/style/annotations/start_on_tonic.rb +6 -5
  72. data/lib/head_music/style/annotations/step_down_to_final_note.rb +12 -12
  73. data/lib/head_music/style/annotations/step_out_of_unison.rb +10 -7
  74. data/lib/head_music/style/annotations/step_to_final_note.rb +6 -3
  75. data/lib/head_music/style/annotations/step_up_to_final_note.rb +12 -12
  76. data/lib/head_music/style/annotations/up_to_fourteen_notes.rb +6 -5
  77. data/lib/head_music/style/mark.rb +10 -4
  78. data/lib/head_music/style/rulesets/first_species_harmony.rb +6 -3
  79. data/lib/head_music/style/rulesets/first_species_melody.rb +6 -3
  80. data/lib/head_music/style/rulesets/fux_cantus_firmus.rb +6 -3
  81. data/lib/head_music/style/rulesets/modern_cantus_firmus.rb +6 -3
  82. data/lib/head_music/utilities/hash_key.rb +9 -7
  83. data/lib/head_music/version.rb +3 -1
  84. data/lib/head_music/voice.rb +7 -3
  85. data/lib/head_music.rb +2 -0
  86. metadata +4 -4
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A language.
1
4
  class HeadMusic::Language
2
5
  include Comparable
3
6
  include HeadMusic::NamedRudiment
@@ -10,22 +13,24 @@ class HeadMusic::Language
10
13
  { name: 'Italian', native_name: 'Italiano', abbreviation: 'it' },
11
14
  { name: 'Spanish', native_name: 'Español', abbreviation: 'es' },
12
15
  { name: 'Russian', native_name: 'русский', abbreviation: 'ru' },
13
- ]
16
+ ].freeze
14
17
 
15
- LANGUAGES.map { |language| ::HeadMusic::Utilities::HashKey.for(language[:name]) }.each do |language_key|
16
- define_singleton_method(language_key) { HeadMusic::Language.get(language_key) }
17
- end
18
+ LANGUAGES.
19
+ map { |language| ::HeadMusic::Utilities::HashKey.for(language[:name]) }.
20
+ each { |language_key| define_singleton_method(language_key) { HeadMusic::Language.get(language_key) } }
18
21
 
19
- LANGUAGES.map { |language| ::HeadMusic::Utilities::HashKey.for(language[:native_name]) }.reject(&:nil?).each do |language_key|
20
- define_singleton_method(language_key) { HeadMusic::Language.get(language_key) }
21
- end
22
+ LANGUAGES.
23
+ map { |language| ::HeadMusic::Utilities::HashKey.for(language[:native_name]) }.
24
+ reject(&:nil?).
25
+ each { |language_key| define_singleton_method(language_key) { HeadMusic::Language.get(language_key) } }
22
26
 
23
- LANGUAGES.map { |language| ::HeadMusic::Utilities::HashKey.for(language[:short_name]) }.reject(&:nil?).each do |language_key|
24
- define_singleton_method(language_key) { HeadMusic::Language.get(language_key) }
25
- end
27
+ LANGUAGES.
28
+ map { |language| ::HeadMusic::Utilities::HashKey.for(language[:short_name]) }.
29
+ reject(&:nil?).
30
+ each { |language_key| define_singleton_method(language_key) { HeadMusic::Language.get(language_key) } }
26
31
 
27
32
  def self.default
28
- self.english
33
+ english
29
34
  end
30
35
 
31
36
  def self.get(name)
@@ -36,9 +41,11 @@ class HeadMusic::Language
36
41
 
37
42
  def initialize(identifier)
38
43
  identifier_key = HeadMusic::Utilities::HashKey.for(identifier)
39
- language_data = LANGUAGES.detect { |data|
40
- [:name, :native_name, :short_name].map { |key| HeadMusic::Utilities::HashKey.for(data[key]) }.include?(identifier_key)
41
- }
44
+ language_data = LANGUAGES.detect do |data|
45
+ %i[name native_name short_name].
46
+ map { |key| HeadMusic::Utilities::HashKey.for(data[key]) }.
47
+ include?(identifier_key)
48
+ end
42
49
  @name = language_data[:name]
43
50
  @native_name = language_data[:native_name]
44
51
  @short_name = language_data[:short_name]
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Music has seven lette names that are used to identify pitches and pitch classes.
1
4
  class HeadMusic::LetterName
2
- NAMES = %w[C D E F G A B]
5
+ NAMES = %w[C D E F G A B].freeze
3
6
 
4
7
  NATURAL_PITCH_CLASS_NUMBERS = {
5
8
  'C' => 0,
@@ -9,10 +12,10 @@ class HeadMusic::LetterName
9
12
  'G' => 7,
10
13
  'A' => 9,
11
14
  'B' => 11,
12
- }
15
+ }.freeze
13
16
 
14
17
  def self.all
15
- NAMES.map { |letter_name| get(letter_name)}
18
+ NAMES.map { |letter_name| get(letter_name) }
16
19
  end
17
20
 
18
21
  def self.get(identifier)
@@ -48,12 +51,12 @@ class HeadMusic::LetterName
48
51
  HeadMusic::PitchClass.get(NATURAL_PITCH_CLASS_NUMBERS[name])
49
52
  end
50
53
 
51
- def ==(value)
52
- to_s == value.to_s
54
+ def ==(other)
55
+ to_s == other.to_s
53
56
  end
54
57
 
55
58
  def position
56
- NAMES.index(self.to_s) + 1
59
+ NAMES.index(to_s) + 1
57
60
  end
58
61
 
59
62
  def steps(num)
@@ -73,11 +76,11 @@ class HeadMusic::LetterName
73
76
  end
74
77
 
75
78
  def cycle
76
- cycle = NAMES
77
- while cycle.first != self.to_s
78
- cycle = cycle.rotate
79
+ @cycle ||= begin
80
+ cycle = NAMES
81
+ cycle = cycle.rotate while cycle.first != to_s
82
+ cycle
79
83
  end
80
- cycle
81
84
  end
82
85
 
83
86
  private_class_method :new
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A melodic interval is the distance between one note and the next.
1
4
  class HeadMusic::MelodicInterval
2
- attr_reader :voice, :first_note, :second_note
5
+ attr_reader :first_note, :second_note
3
6
 
4
- def initialize(voice, note1, note2)
5
- @voice = voice
7
+ def initialize(note1, note2)
6
8
  @first_note = note1
7
9
  @second_note = note2
8
10
  end
@@ -86,6 +88,10 @@ class HeadMusic::MelodicInterval
86
88
  end
87
89
 
88
90
  def method_missing(method_name, *args, &block)
89
- functional_interval.send(method_name, *args, &block)
91
+ respond_to_missing?(method_name) ? functional_interval.send(method_name, *args, &block) : super
92
+ end
93
+
94
+ def respond_to_missing?(method_name, *_args)
95
+ functional_interval.respond_to?(method_name)
90
96
  end
91
97
  end
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Meter is the rhythmic size of a measure, such as 4/4 or 6/8
1
4
  class HeadMusic::Meter
2
5
  attr_reader :top_number, :bottom_number
3
6
 
4
7
  NAMED = {
5
8
  common_time: '4/4',
6
- cut_time: '2/2'
7
- }
9
+ cut_time: '2/2',
10
+ }.freeze
8
11
 
9
12
  def self.get(identifier)
10
13
  identifier = identifier.to_s
@@ -27,7 +30,8 @@ class HeadMusic::Meter
27
30
  end
28
31
 
29
32
  def initialize(top_number, bottom_number)
30
- @top_number, @bottom_number = top_number, bottom_number
33
+ @top_number = top_number
34
+ @bottom_number = bottom_number
31
35
  end
32
36
 
33
37
  def simple?
@@ -43,7 +47,7 @@ class HeadMusic::Meter
43
47
  end
44
48
 
45
49
  def triple?
46
- top_number % 3 == 0
50
+ (top_number % 3).zero?
47
51
  end
48
52
 
49
53
  def quadruple?
@@ -77,8 +81,7 @@ class HeadMusic::Meter
77
81
  def beat_unit
78
82
  @beat_unit ||=
79
83
  if compound?
80
- unit = HeadMusic::RhythmicUnit.for_denominator_value(bottom_number / 2)
81
- HeadMusic::RhythmicValue.new(unit, dots: 1)
84
+ HeadMusic::RhythmicValue.new(HeadMusic::RhythmicUnit.for_denominator_value(bottom_number / 2), dots: 1)
82
85
  else
83
86
  HeadMusic::RhythmicValue.new(count_unit)
84
87
  end
@@ -101,10 +104,7 @@ class HeadMusic::Meter
101
104
  end
102
105
 
103
106
  def strong_ticks
104
- @strong_ticks ||=
105
- [2,3,4].map do |sixths|
106
- ticks_per_count * (sixths / 6.0)
107
- end
107
+ @strong_ticks ||= [2, 3, 4].map { |sixths| ticks_per_count * (sixths / 6.0) }
108
108
  end
109
109
 
110
110
  private
@@ -122,10 +122,10 @@ class HeadMusic::Meter
122
122
  end
123
123
 
124
124
  def strong_beat_in_triple?(count, tick = 0)
125
- beat?(tick) && counts_per_bar % 3 == 0 && counts_per_bar > 6 && count % 3 == 1
125
+ beat?(tick) && (counts_per_bar % 3).zero? && counts_per_bar > 6 && count % 3 == 1
126
126
  end
127
127
 
128
128
  def beat?(tick)
129
- tick == 0
129
+ tick.zero?
130
130
  end
131
131
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Motion defines the relative pitch direction of the upper and lower voices of subsequence intervals.
1
4
  class HeadMusic::Motion
2
5
  attr_reader :first_harmonic_interval, :second_harmonic_interval
3
6
 
@@ -12,7 +15,7 @@ class HeadMusic::Motion
12
15
 
13
16
  def oblique?
14
17
  upper_melodic_interval.repetition? && lower_melodic_interval.moving? ||
15
- lower_melodic_interval.repetition? && upper_melodic_interval.moving?
18
+ lower_melodic_interval.repetition? && upper_melodic_interval.moving?
16
19
  end
17
20
 
18
21
  def direct?
@@ -21,19 +24,19 @@ class HeadMusic::Motion
21
24
 
22
25
  def parallel?
23
26
  upper_melodic_interval.moving? &&
24
- upper_melodic_interval.direction == lower_melodic_interval.direction &&
25
- upper_melodic_interval.steps == lower_melodic_interval.steps
27
+ upper_melodic_interval.direction == lower_melodic_interval.direction &&
28
+ upper_melodic_interval.steps == lower_melodic_interval.steps
26
29
  end
27
30
 
28
31
  def similar?
29
32
  upper_melodic_interval.direction == lower_melodic_interval.direction &&
30
- upper_melodic_interval.steps != lower_melodic_interval.steps
33
+ upper_melodic_interval.steps != lower_melodic_interval.steps
31
34
  end
32
35
 
33
36
  def contrary?
34
37
  upper_melodic_interval.moving? &&
35
- lower_melodic_interval.moving? &&
36
- upper_melodic_interval.direction != lower_melodic_interval.direction
38
+ lower_melodic_interval.moving? &&
39
+ upper_melodic_interval.direction != lower_melodic_interval.direction
37
40
  end
38
41
 
39
42
  def notes
@@ -41,7 +44,7 @@ class HeadMusic::Motion
41
44
  end
42
45
 
43
46
  def contrapuntal_motion
44
- [:parallel, :similar, :oblique, :contrary, :repetition].detect do |motion_type|
47
+ %i[parallel similar oblique contrary repetition].detect do |motion_type|
45
48
  send("#{motion_type}?")
46
49
  end
47
50
  end
@@ -53,11 +56,11 @@ class HeadMusic::Motion
53
56
  private
54
57
 
55
58
  def upper_melodic_interval
56
- HeadMusic::MelodicInterval.new(upper_notes.first.voice, upper_notes.first, upper_notes.last)
59
+ HeadMusic::MelodicInterval.new(upper_notes.first, upper_notes.last)
57
60
  end
58
61
 
59
62
  def lower_melodic_interval
60
- HeadMusic::MelodicInterval.new(lower_notes.first.voice, lower_notes.first, lower_notes.last)
63
+ HeadMusic::MelodicInterval.new(lower_notes.first, lower_notes.last)
61
64
  end
62
65
 
63
66
  def upper_notes
@@ -1,27 +1,29 @@
1
- module HeadMusic
2
- module NamedRudiment
3
- attr_reader :name
4
- delegate :to_s, to: :name
1
+ # frozen_string_literal: true
5
2
 
6
- def initialize(name)
7
- @name = name.to_s
8
- end
3
+ # NameRudiment is a module to be included in classes whose instances may be identified by name.
4
+ module HeadMusic::NamedRudiment
5
+ attr_reader :name
6
+ delegate :to_s, to: :name
9
7
 
10
- def hash_key
11
- HeadMusic::Utilities::HashKey.for(name)
12
- end
8
+ def initialize(name)
9
+ @name = name.to_s
10
+ end
13
11
 
14
- def self.included(base)
15
- base.extend(ClassMethods)
16
- end
12
+ def hash_key
13
+ HeadMusic::Utilities::HashKey.for(name)
14
+ end
15
+
16
+ def self.included(base)
17
+ base.extend(ClassMethods)
18
+ end
17
19
 
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
20
+ # Adds .get_by_name to the including class.
21
+ module ClassMethods
22
+ def get_by_name(name)
23
+ name = name.to_s
24
+ @instances_by_name ||= {}
25
+ key = HeadMusic::Utilities::HashKey.for(name)
26
+ @instances_by_name[key] ||= new(name)
25
27
  end
26
28
  end
27
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Note is like a placement, except:
2
4
  # - always has a pitch
3
5
  # - doesn't require voice and position
@@ -20,6 +22,10 @@ class HeadMusic::Note
20
22
  end
21
23
 
22
24
  def method_missing(method_name, *args, &block)
23
- placement.send(method_name, *args, &block)
25
+ respond_to_missing?(method_name) ? placement.send(method_name, *args, &block) : super
26
+ end
27
+
28
+ def respond_to_missing?(method_name, *_args)
29
+ placement.respond_to?(method_name)
24
30
  end
25
31
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Octave identifier is a number used in scientific pitch notation.
1
4
  class HeadMusic::Octave
2
5
  include Comparable
3
6
 
@@ -9,17 +12,16 @@ class HeadMusic::Octave
9
12
 
10
13
  def self.from_number(identifier)
11
14
  return nil unless identifier.to_s == identifier.to_i.to_s
12
- return nil unless (-2..12).include?(identifier.to_i)
15
+ return nil unless (-2..12).cover?(identifier.to_i)
13
16
  @octaves ||= {}
14
17
  @octaves[identifier.to_i] ||= new(identifier.to_i)
15
18
  end
16
19
 
17
20
  def self.from_name(string)
18
- if string.to_s.match(HeadMusic::Spelling::MATCHER)
19
- _letter, _sign, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
20
- @octaves ||= {}
21
- @octaves[octave_string.to_i] ||= new(octave_string.to_i) if octave_string
22
- end
21
+ return unless string.to_s.match?(HeadMusic::Spelling::MATCHER)
22
+ _letter, _sign, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
23
+ @octaves ||= {}
24
+ @octaves[octave_string.to_i] ||= new(octave_string.to_i) if octave_string
23
25
  end
24
26
 
25
27
  def self.default
@@ -34,7 +36,7 @@ class HeadMusic::Octave
34
36
  end
35
37
 
36
38
  def <=>(other)
37
- self.to_i <=> other.to_i
39
+ to_i <=> other.to_i
38
40
  end
39
41
 
40
42
  private_class_method :new
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A pitch is a named frequency represented by a spelling and an octive.
1
4
  class HeadMusic::Pitch
2
5
  include Comparable
3
6
 
@@ -15,6 +18,10 @@ class HeadMusic::Pitch
15
18
  from_name(value) || from_number(value)
16
19
  end
17
20
 
21
+ def self.middle_c
22
+ get('C4')
23
+ end
24
+
18
25
  def self.from_name(name)
19
26
  return nil unless name == name.to_s
20
27
  fetch_or_create(HeadMusic::Spelling.get(name), HeadMusic::Octave.get(name).to_i)
@@ -29,21 +36,24 @@ class HeadMusic::Pitch
29
36
 
30
37
  def self.from_number_and_letter(number, letter_name)
31
38
  letter_name = HeadMusic::LetterName.get(letter_name)
32
- natural_letter_pitch = get(HeadMusic::LetterName.get(letter_name).pitch_class)
33
- natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i).to_i >= 11
34
- natural_letter_pitch = get(natural_letter_pitch)
39
+ natural_letter_pitch = natural_letter_pitch(number, letter_name)
35
40
  sign_interval = natural_letter_pitch.smallest_interval_to(HeadMusic::PitchClass.get(number))
36
41
  sign = HeadMusic::Sign.by(:semitones, sign_interval) if sign_interval != 0
37
42
  spelling = HeadMusic::Spelling.fetch_or_create(letter_name, sign)
38
43
  fetch_or_create(spelling, natural_letter_pitch.octave)
39
44
  end
40
45
 
46
+ def self.natural_letter_pitch(number, letter_name)
47
+ natural_letter_pitch = get(HeadMusic::LetterName.get(letter_name).pitch_class)
48
+ natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i).to_i >= 11
49
+ get(natural_letter_pitch)
50
+ end
51
+
41
52
  def self.fetch_or_create(spelling, octave)
53
+ return unless spelling && (-1..9).cover?(octave)
42
54
  @pitches ||= {}
43
- if spelling && (-1..9).include?(octave)
44
- hash_key = [spelling, octave].join
45
- @pitches[hash_key] ||= new(spelling, octave)
46
- end
55
+ hash_key = [spelling, octave].join
56
+ @pitches[hash_key] ||= new(spelling, octave)
47
57
  end
48
58
 
49
59
  def initialize(spelling, octave)
@@ -59,8 +69,8 @@ class HeadMusic::Pitch
59
69
  (octave + 1) * 12 + letter_name.pitch_class.to_i + sign_semitones.to_i
60
70
  end
61
71
 
62
- alias_method :midi, :midi_note_number
63
- alias_method :number, :midi_note_number
72
+ alias midi midi_note_number
73
+ alias number midi_note_number
64
74
 
65
75
  def to_s
66
76
  name
@@ -71,34 +81,34 @@ class HeadMusic::Pitch
71
81
  end
72
82
 
73
83
  def natural
74
- HeadMusic::Pitch.get(self.to_s.gsub(/[#b]/, ''))
84
+ HeadMusic::Pitch.get(to_s.gsub(/[#b]/, ''))
75
85
  end
76
86
 
77
87
  def enharmonic?(other)
78
- self.midi_note_number == other.midi_note_number
88
+ midi_note_number == other.midi_note_number
79
89
  end
80
90
 
81
- def +(value)
82
- HeadMusic::Pitch.get(self.to_i + value.to_i)
91
+ def +(other)
92
+ HeadMusic::Pitch.get(to_i + other.to_i)
83
93
  end
84
94
 
85
- def -(value)
86
- if value.is_a?(HeadMusic::Pitch)
95
+ def -(other)
96
+ if other.is_a?(HeadMusic::Pitch)
87
97
  # return an interval
88
- HeadMusic::Interval.get(self.to_i - value.to_i)
98
+ HeadMusic::Interval.get(to_i - other.to_i)
89
99
  else
90
100
  # assume value represents an interval in semitones and return another pitch
91
- HeadMusic::Pitch.get(self.to_i - value.to_i)
101
+ HeadMusic::Pitch.get(to_i - other.to_i)
92
102
  end
93
103
  end
94
104
 
95
- def ==(value)
96
- other = HeadMusic::Pitch.get(value)
105
+ def ==(other)
106
+ other = HeadMusic::Pitch.get(other)
97
107
  to_s == other.to_s
98
108
  end
99
109
 
100
110
  def <=>(other)
101
- self.midi_note_number <=> other.midi_note_number
111
+ midi_note_number <=> other.midi_note_number
102
112
  end
103
113
 
104
114
  def scale(scale_type_name = nil)
@@ -106,16 +116,33 @@ class HeadMusic::Pitch
106
116
  end
107
117
 
108
118
  def natural_steps(num_steps)
109
- target_letter_name = self.letter_name.steps(num_steps)
110
- direction = num_steps >= 0 ? 1 : -1
111
- octaves_delta = (num_steps.abs / 7) * direction
112
- if num_steps < 0 && target_letter_name.position > letter_name.position
119
+ HeadMusic::Pitch.get([target_letter_name(num_steps), octave + octaves_delta(num_steps)].join)
120
+ end
121
+
122
+ private_class_method :new
123
+
124
+ private
125
+
126
+ def octaves_delta(num_steps)
127
+ octaves_delta = (num_steps.abs / 7) * (num_steps >= 0 ? 1 : -1)
128
+ if wrapped_down?(num_steps)
113
129
  octaves_delta -= 1
114
- elsif num_steps > 0 && target_letter_name.position < letter_name.position
130
+ elsif wrapped_up?(num_steps)
115
131
  octaves_delta += 1
116
132
  end
117
- HeadMusic::Pitch.get([target_letter_name, octave + octaves_delta].join)
133
+ octaves_delta
118
134
  end
119
135
 
120
- private_class_method :new
136
+ def wrapped_down?(num_steps)
137
+ num_steps.negative? && target_letter_name(num_steps).position > letter_name.position
138
+ end
139
+
140
+ def wrapped_up?(num_steps)
141
+ num_steps.positive? && target_letter_name(num_steps).position < letter_name.position
142
+ end
143
+
144
+ def target_letter_name(num_steps)
145
+ @target_letter_name ||= {}
146
+ @target_letter_name[num_steps] ||= letter_name.steps(num_steps)
147
+ end
121
148
  end
@@ -1,18 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A pitch class is a set of notes separated by octaves.
1
4
  class HeadMusic::PitchClass
2
5
  attr_reader :number
3
6
 
4
- SHARP_SPELLINGS = %w[C C# D D# E F F# G G# A A# B]
5
- FLAT_SPELLINGS = %w[C Db D Eb E F Gb G Ab A Bb B]
7
+ SHARP_SPELLINGS = %w[C C# D D# E F F# G G# A A# B].freeze
8
+ FLAT_SPELLINGS = %w[C Db D Eb E F Gb G Ab A Bb B].freeze
6
9
 
7
10
  def self.get(identifier)
8
11
  @pitch_classes ||= {}
9
- number = HeadMusic::Spelling.get(identifier).pitch_class.to_i if HeadMusic::Spelling.match(identifier)
12
+ number = HeadMusic::Spelling.get(identifier).pitch_class.to_i if HeadMusic::Spelling.matching_string(identifier)
10
13
  number ||= identifier.to_i % 12
11
14
  @pitch_classes[number] ||= new(number)
12
15
  end
13
16
 
14
17
  class << self
15
- alias_method :[], :get
18
+ alias [] get
16
19
  end
17
20
 
18
21
  def initialize(pitch_class_or_midi_number)
@@ -23,22 +26,24 @@ class HeadMusic::PitchClass
23
26
  number
24
27
  end
25
28
 
26
- def +(semitones)
27
- HeadMusic::PitchClass.get(to_i + semitones.to_i)
29
+ # Pass in the number of semitones
30
+ def +(other)
31
+ HeadMusic::PitchClass.get(to_i + other.to_i)
28
32
  end
29
33
 
30
- def -(semitones)
31
- HeadMusic::PitchClass.get(to_i - semitones.to_i)
34
+ # Pass in the number of semitones
35
+ def -(other)
36
+ HeadMusic::PitchClass.get(to_i - other.to_i)
32
37
  end
33
38
 
34
- def ==(value)
35
- to_i == value.to_i
39
+ def ==(other)
40
+ to_i == other.to_i
36
41
  end
37
- alias_method :enharmonic?, :==
42
+ alias enharmonic? ==
38
43
 
39
44
  def intervals_to(other)
40
45
  delta = other.to_i - to_i
41
- inverse = delta > 0 ? delta - 12 : delta + 12
46
+ inverse = delta.positive? ? delta - 12 : delta + 12
42
47
  [delta, inverse].sort_by(&:abs).map { |interval| HeadMusic::Interval.get(interval) }
43
48
  end
44
49
 
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A placement is a note or rest at a position within a voice in a composition
1
4
  class HeadMusic::Placement
2
5
  include Comparable
3
6
 
@@ -22,13 +25,11 @@ class HeadMusic::Placement
22
25
  end
23
26
 
24
27
  def <=>(other)
25
- self.position <=> other.position
28
+ position <=> other.position
26
29
  end
27
30
 
28
31
  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
+ starts_during?(other_placement) || ends_during?(other_placement) || within?(other_placement)
32
33
  end
33
34
 
34
35
  def to_s
@@ -37,6 +38,18 @@ class HeadMusic::Placement
37
38
 
38
39
  private
39
40
 
41
+ def starts_during?(other_placement)
42
+ (other_placement.next_position > position && other_placement.next_position <= next_position)
43
+ end
44
+
45
+ def ends_during?(other_placement)
46
+ (other_placement.position >= position && other_placement.position < next_position)
47
+ end
48
+
49
+ def within?(other_placement)
50
+ (other_placement.position <= position && other_placement.next_position >= next_position)
51
+ end
52
+
40
53
  def ensure_attributes(voice, position, rhythmic_value, pitch)
41
54
  @voice = voice
42
55
  ensure_position(position)
@@ -45,10 +58,10 @@ class HeadMusic::Placement
45
58
  end
46
59
 
47
60
  def ensure_position(position)
48
- if position.is_a?(HeadMusic::Position)
49
- @position = position
50
- else
51
- @position = HeadMusic::Position.new(composition, position)
52
- end
61
+ @position = if position.is_a?(HeadMusic::Position)
62
+ position
63
+ else
64
+ HeadMusic::Position.new(composition, position)
65
+ end
53
66
  end
54
67
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A position is a moment in time within the rhythmic framework of a composition.
1
4
  class HeadMusic::Position
2
5
  include Comparable
3
6
 
@@ -35,10 +38,8 @@ class HeadMusic::Position
35
38
  end
36
39
 
37
40
  def <=>(other)
38
- if other.is_a?(String) && other =~ /\D/
39
- other = self.class.new(composition, other)
40
- end
41
- self.values <=> other.values
41
+ other = self.class.new(composition, other) if other.is_a?(String) && other =~ /\D/
42
+ values <=> other.values
42
43
  end
43
44
 
44
45
  def strength
@@ -53,11 +54,9 @@ class HeadMusic::Position
53
54
  !strong?
54
55
  end
55
56
 
56
- def +(rhythmic_value)
57
- if [HeadMusic::RhythmicUnit, Symbol, String].include?(rhythmic_value.class)
58
- rhythmic_value = HeadMusic::RhythmicValue.new(rhythmic_value)
59
- end
60
- self.class.new(composition, bar_number, count, tick + rhythmic_value.ticks)
57
+ def +(other)
58
+ other = HeadMusic::RhythmicValue.new(other) if [HeadMusic::RhythmicUnit, Symbol, String].include?(other.class)
59
+ self.class.new(composition, bar_number, count, tick + other.ticks)
61
60
  end
62
61
 
63
62
  def start_of_next_bar