head_music 0.20.0 → 0.22.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 (59) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +22 -0
  3. data/.circleci/setup-rubygems.sh +3 -0
  4. data/.gitignore +1 -0
  5. data/.pairs +8 -0
  6. data/.rubocop.yml +11 -2
  7. data/.ruby-version +1 -0
  8. data/CODE_OF_CONDUCT.md +3 -6
  9. data/Gemfile +2 -2
  10. data/README.md +0 -1
  11. data/TODO.md +18 -0
  12. data/head_music.gemspec +1 -2
  13. data/lib/head_music/chord.rb +9 -2
  14. data/lib/head_music/circle.rb +1 -0
  15. data/lib/head_music/clef.rb +10 -5
  16. data/lib/head_music/consonance.rb +1 -1
  17. data/lib/head_music/content/composition.rb +1 -2
  18. data/lib/head_music/content/note.rb +2 -0
  19. data/lib/head_music/content/placement.rb +1 -1
  20. data/lib/head_music/content/rhythmic_value.rb +1 -0
  21. data/lib/head_music/content/voice.rb +4 -2
  22. data/lib/head_music/functional_interval.rb +43 -17
  23. data/lib/head_music/grand_staff.rb +2 -1
  24. data/lib/head_music/harmonic_interval.rb +1 -0
  25. data/lib/head_music/key_signature.rb +1 -0
  26. data/lib/head_music/letter_name.rb +20 -7
  27. data/lib/head_music/melodic_interval.rb +4 -2
  28. data/lib/head_music/meter.rb +1 -0
  29. data/lib/head_music/octave.rb +2 -0
  30. data/lib/head_music/pitch/enharmonic_equivalence.rb +28 -0
  31. data/lib/head_music/pitch/octave_equivalence.rb +24 -0
  32. data/lib/head_music/pitch.rb +41 -57
  33. data/lib/head_music/pitch_class.rb +5 -0
  34. data/lib/head_music/rhythmic_unit.rb +1 -0
  35. data/lib/head_music/scale.rb +7 -3
  36. data/lib/head_music/scale_degree.rb +2 -1
  37. data/lib/head_music/sign.rb +2 -1
  38. data/lib/head_music/spelling.rb +6 -2
  39. data/lib/head_music/style/analysis.rb +1 -0
  40. data/lib/head_music/style/annotation.rb +2 -2
  41. data/lib/head_music/style/guidelines/at_least_eight_notes.rb +1 -0
  42. data/lib/head_music/style/guidelines/direction_changes.rb +2 -0
  43. data/lib/head_music/style/guidelines/limit_octave_leaps.rb +1 -0
  44. data/lib/head_music/style/guidelines/mostly_conjunct.rb +1 -0
  45. data/lib/head_music/style/guidelines/notes_same_length.rb +2 -1
  46. data/lib/head_music/style/guidelines/one_to_one.rb +1 -0
  47. data/lib/head_music/style/guidelines/prefer_contrary_motion.rb +2 -0
  48. data/lib/head_music/style/guidelines/prefer_imperfect.rb +1 -0
  49. data/lib/head_music/style/guidelines/single_large_leaps.rb +1 -0
  50. data/lib/head_music/style/guidelines/start_on_perfect_consonance.rb +1 -0
  51. data/lib/head_music/style/guidelines/step_down_to_final_note.rb +1 -0
  52. data/lib/head_music/style/guidelines/step_out_of_unison.rb +1 -1
  53. data/lib/head_music/style/guidelines/step_up_to_final_note.rb +1 -0
  54. data/lib/head_music/style/mark.rb +3 -2
  55. data/lib/head_music/tuning.rb +6 -1
  56. data/lib/head_music/version.rb +1 -1
  57. data/lib/head_music.rb +2 -0
  58. metadata +10 -4
  59. data/circle.yml +0 -9
@@ -7,10 +7,14 @@ class HeadMusic::Pitch
7
7
  attr_reader :spelling
8
8
  attr_reader :octave
9
9
 
10
- delegate :letter_name, :letter_name_cycle, to: :spelling
10
+ delegate :letter_name, to: :spelling
11
+ delegate :series_ascending, :series_descending, to: :letter_name, prefix: true
11
12
  delegate :sign, :sharp?, :flat?, to: :spelling
12
13
  delegate :pitch_class, to: :spelling
14
+ delegate :number, to: :pitch_class, prefix: true
15
+ delegate :pitch_class_number, to: :natural, prefix: true
13
16
  delegate :semitones, to: :sign, prefix: true, allow_nil: true
17
+ delegate :steps_to, to: :letter_name, prefix: true
14
18
 
15
19
  delegate :smallest_interval_to, to: :pitch_class
16
20
 
@@ -31,16 +35,19 @@ class HeadMusic::Pitch
31
35
 
32
36
  def self.from_pitch_class(pitch_class)
33
37
  return nil unless pitch_class.is_a?(HeadMusic::PitchClass)
38
+
34
39
  fetch_or_create(pitch_class.sharp_spelling)
35
40
  end
36
41
 
37
42
  def self.from_name(name)
38
43
  return nil unless name == name.to_s
44
+
39
45
  fetch_or_create(HeadMusic::Spelling.get(name), HeadMusic::Octave.get(name).to_i)
40
46
  end
41
47
 
42
48
  def self.from_number(number)
43
49
  return nil unless number == number.to_i
50
+
44
51
  spelling = HeadMusic::Spelling.from_number(number)
45
52
  octave = (number.to_i / 12) - 1
46
53
  fetch_or_create(spelling, octave)
@@ -64,6 +71,7 @@ class HeadMusic::Pitch
64
71
  def self.fetch_or_create(spelling, octave = nil)
65
72
  octave ||= HeadMusic::Octave::DEFAULT
66
73
  return unless spelling && (-1..9).cover?(octave)
74
+
67
75
  @pitches ||= {}
68
76
  hash_key = [spelling, octave].join
69
77
  @pitches[hash_key] ||= new(spelling, octave)
@@ -94,15 +102,24 @@ class HeadMusic::Pitch
94
102
  end
95
103
 
96
104
  def natural
97
- HeadMusic::Pitch.get(to_s.gsub(/[#b]/, ''))
105
+ HeadMusic::Pitch.get(to_s.gsub(HeadMusic::Sign.matcher, ''))
98
106
  end
99
107
 
100
108
  def +(other)
101
- HeadMusic::Pitch.get(to_i + other.to_i)
109
+ if other.is_a?(HeadMusic::FunctionalInterval)
110
+ # return a pitch
111
+ other.above(self)
112
+ else
113
+ # assume value represents an interval in semitones and return another pitch
114
+ HeadMusic::Pitch.get(to_i + other.to_i)
115
+ end
102
116
  end
103
117
 
104
118
  def -(other)
105
- if other.is_a?(HeadMusic::Pitch)
119
+ if other.is_a?(HeadMusic::FunctionalInterval)
120
+ # return a pitch
121
+ other.below(self)
122
+ elsif other.is_a?(HeadMusic::Pitch)
106
123
  # return an interval
107
124
  HeadMusic::Interval.get(to_i - other.to_i)
108
125
  else
@@ -132,16 +149,33 @@ class HeadMusic::Pitch
132
149
  tuning.frequency_for(self)
133
150
  end
134
151
 
152
+ def steps_to(other)
153
+ other = HeadMusic::Pitch.get(other)
154
+ letter_name_steps_to(other) + 7 * octave_changes_to(other)
155
+ end
156
+
135
157
  private_class_method :new
136
158
 
137
159
  private
138
160
 
161
+ def octave_changes_to(other)
162
+ other.octave - octave - octave_adjustment_to(other)
163
+ end
164
+
165
+ def octave_adjustment_to(other)
166
+ (pitch_class_above?(other) ? 1 : 0)
167
+ end
168
+
169
+ def pitch_class_above?(other)
170
+ natural_pitch_class_number > other.natural_pitch_class_number
171
+ end
172
+
139
173
  def enharmonic_equivalence
140
- @enharmonic_equivalence ||= EnharmonicEquivalence.get(self)
174
+ @enharmonic_equivalence ||= HeadMusic::Pitch::EnharmonicEquivalence.get(self)
141
175
  end
142
176
 
143
177
  def octave_equivalence
144
- @octave_equivalence ||= OctaveEquivalence.get(self)
178
+ @octave_equivalence ||= HeadMusic::Pitch::OctaveEquivalence.get(self)
145
179
  end
146
180
 
147
181
  def tuning
@@ -168,56 +202,6 @@ class HeadMusic::Pitch
168
202
 
169
203
  def target_letter_name(num_steps)
170
204
  @target_letter_name ||= {}
171
- @target_letter_name[num_steps] ||= letter_name.steps(num_steps)
172
- end
173
-
174
- # An enharmonic equivalent pitch is the same frequency spelled differently, such as D# and Eb.
175
- class EnharmonicEquivalence
176
- def self.get(pitch)
177
- pitch = HeadMusic::Pitch.get(pitch)
178
- @enharmonic_equivalences ||= {}
179
- @enharmonic_equivalences[pitch.to_s] ||= new(pitch)
180
- end
181
-
182
- attr_reader :pitch
183
-
184
- delegate :pitch_class, to: :pitch
185
-
186
- def initialize(pitch)
187
- @pitch = HeadMusic::Pitch.get(pitch)
188
- end
189
-
190
- def enharmonic_equivalent?(other)
191
- other = HeadMusic::Pitch.get(other)
192
- pitch.midi_note_number == other.midi_note_number && pitch.spelling != other.spelling
193
- end
194
-
195
- alias enharmonic? enharmonic_equivalent?
196
- alias equivalent? enharmonic_equivalent?
197
-
198
- private_class_method :new
199
- end
200
-
201
- # Octave equivalence is the functional equivalence of pitches with the same spelling separated by one or more octaves.
202
- class OctaveEquivalence
203
- def self.get(pitch)
204
- @octave_equivalences ||= {}
205
- @octave_equivalences[pitch.to_s] ||= new(pitch)
206
- end
207
-
208
- attr_reader :pitch
209
-
210
- def initialize(pitch)
211
- @pitch = pitch
212
- end
213
-
214
- def octave_equivalent?(other)
215
- other = HeadMusic::Pitch.get(other)
216
- pitch.spelling == other.spelling && pitch.octave != other.octave
217
- end
218
-
219
- alias equivalent? octave_equivalent?
220
-
221
- private_class_method :new
205
+ @target_letter_name[num_steps] ||= letter_name.steps_up(num_steps)
222
206
  end
223
207
  end
@@ -7,6 +7,7 @@ class HeadMusic::PitchClass
7
7
 
8
8
  SHARP_SPELLINGS = %w[C C# D D# E F F# G G# A A# B].freeze
9
9
  FLAT_SPELLINGS = %w[C Db D Eb E F Gb G Ab A Bb B].freeze
10
+ INTEGER_NOTATION = %w[0 1 2 3 4 5 6 7 8 9 t e].freeze
10
11
 
11
12
  def self.get(identifier)
12
13
  @pitch_classes ||= {}
@@ -30,6 +31,10 @@ class HeadMusic::PitchClass
30
31
  number
31
32
  end
32
33
 
34
+ def to_integer_notation
35
+ INTEGER_NOTATION[number]
36
+ end
37
+
33
38
  def sharp_spelling
34
39
  SHARP_SPELLINGS[number]
35
40
  end
@@ -45,6 +45,7 @@ class HeadMusic::RhythmicUnit
45
45
  return :longa if relative_value == 4
46
46
  return :breve if relative_value == 2
47
47
  return :open if [0.5, 1].include? relative_value
48
+
48
49
  :closed
49
50
  end
50
51
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # A scale contains ordered pitches starting at a tonal center.
4
4
  class HeadMusic::Scale
5
- SCALE_REGEX = /^[A-G][#b]?\s+\w+$/
5
+ SCALE_REGEX = /^[A-G][#b]?\s+\w+$/.freeze
6
6
 
7
7
  def self.get(root_pitch, scale_type = nil)
8
8
  root_pitch, scale_type = root_pitch.split(/\s+/) if root_pitch.is_a?(String) && scale_type =~ SCALE_REGEX
@@ -15,7 +15,7 @@ class HeadMusic::Scale
15
15
  @scales[hash_key] ||= new(root_pitch, scale_type)
16
16
  end
17
17
 
18
- delegate :letter_name_cycle, to: :root_pitch
18
+ delegate :letter_name_series_ascending, :letter_name_series_descending, to: :root_pitch
19
19
 
20
20
  attr_reader :root_pitch, :scale_type
21
21
 
@@ -53,6 +53,7 @@ class HeadMusic::Scale
53
53
  pitches = [root_pitch]
54
54
  %i[ascending descending].each do |single_direction|
55
55
  next unless [single_direction, :both].include?(direction)
56
+
56
57
  (1..octaves).each do
57
58
  pitches += octave_scale_pitches(single_direction, semitones_from_root)
58
59
  semitones_from_root += 12 * direction_sign(single_direction)
@@ -101,16 +102,19 @@ class HeadMusic::Scale
101
102
 
102
103
  def diatonic_letter_for_step(direction, step)
103
104
  return unless scale_type.diatonic?
104
- direction == :ascending ? letter_name_cycle[step % 7] : letter_name_cycle[-step % 7]
105
+
106
+ direction == :ascending ? letter_name_series_ascending[step % 7] : letter_name_series_descending[step % 7]
105
107
  end
106
108
 
107
109
  def child_scale_letter_for_step(semitones_from_root)
108
110
  return unless scale_type.parent
111
+
109
112
  parent_scale_pitch_for(semitones_from_root).letter_name
110
113
  end
111
114
 
112
115
  def flat_letter_for_step(semitones_from_root)
113
116
  return unless root_pitch.flat?
117
+
114
118
  HeadMusic::PitchClass::FLAT_SPELLINGS[pitch_class_number(semitones_from_root)]
115
119
  end
116
120
 
@@ -17,7 +17,7 @@ class HeadMusic::ScaleDegree
17
17
  end
18
18
 
19
19
  def degree
20
- scale.letter_name_cycle.index(spelling.letter_name.to_s) + 1
20
+ scale.letter_name_series_ascending.index(spelling.letter_name.to_s) + 1
21
21
  end
22
22
 
23
23
  def sign
@@ -41,6 +41,7 @@ class HeadMusic::ScaleDegree
41
41
 
42
42
  def name_for_degree
43
43
  return unless scale_type.diatonic?
44
+
44
45
  NAME_FOR_DIATONIC_DEGREE[degree] ||
45
46
  (scale_type.intervals.last == 1 || sign == '#' ? 'leading tone' : 'subtonic')
46
47
  end
@@ -17,7 +17,7 @@ class HeadMusic::Sign
17
17
  end
18
18
 
19
19
  def self.symbols
20
- @sign_symbols ||= all.map { |sign| [sign.ascii, sign.unicode] }.flatten.reject { |s| s.nil? || s.empty? }
20
+ @symbols ||= all.map { |sign| [sign.ascii, sign.unicode] }.flatten.reject { |s| s.nil? || s.empty? }
21
21
  end
22
22
 
23
23
  def self.matcher
@@ -30,6 +30,7 @@ class HeadMusic::Sign
30
30
 
31
31
  def self.get(identifier)
32
32
  return identifier if identifier.is_a?(HeadMusic::Sign)
33
+
33
34
  all.detect do |sign|
34
35
  sign.representions.include?(identifier)
35
36
  end
@@ -4,7 +4,7 @@
4
4
  # Composite of a LetterName and an optional Sign.
5
5
  # Does not include the octave. See Pitch for that.
6
6
  class HeadMusic::Spelling
7
- MATCHER = /^\s*([A-G])(#{HeadMusic::Sign.matcher}?)(\-?\d+)?\s*$/i
7
+ MATCHER = /^\s*([A-G])(#{HeadMusic::Sign.matcher}?)(\-?\d+)?\s*$/i.freeze
8
8
 
9
9
  attr_reader :pitch_class
10
10
  attr_reader :letter_name
@@ -12,11 +12,12 @@ class HeadMusic::Spelling
12
12
 
13
13
  delegate :number, to: :pitch_class, prefix: true
14
14
  delegate :to_i, to: :pitch_class_number
15
- delegate :cycle, to: :letter_name, prefix: true
15
+ delegate :series_ascending, :series_descending, to: :letter_name, prefix: true
16
16
  delegate :enharmonic?, to: :enharmonic_equivalence
17
17
 
18
18
  def self.get(identifier)
19
19
  return identifier if identifier.is_a?(HeadMusic::Spelling)
20
+
20
21
  from_name(identifier) || from_number(identifier)
21
22
  end
22
23
 
@@ -26,15 +27,18 @@ class HeadMusic::Spelling
26
27
 
27
28
  def self.from_name(name)
28
29
  return nil unless matching_string(name)
30
+
29
31
  letter_name, sign_string, _octave = matching_string(name).captures
30
32
  letter_name = HeadMusic::LetterName.get(letter_name)
31
33
  return nil unless letter_name
34
+
32
35
  sign = HeadMusic::Sign.get(sign_string)
33
36
  fetch_or_create(letter_name, sign)
34
37
  end
35
38
 
36
39
  def self.from_number(number)
37
40
  return nil unless number == number.to_i
41
+
38
42
  pitch_class_number = number.to_i % 12
39
43
  letter_name = HeadMusic::LetterName.from_pitch_class(pitch_class_number)
40
44
  from_number_and_letter(number, letter_name)
@@ -23,6 +23,7 @@ class HeadMusic::Style::Analysis
23
23
 
24
24
  def fitness
25
25
  return 1.0 if annotations.empty?
26
+
26
27
  @fitness ||= fitness_scores.inject(:+).to_f / fitness_scores.length
27
28
  end
28
29
 
@@ -41,11 +41,11 @@ class HeadMusic::Style::Annotation
41
41
  end
42
42
 
43
43
  def start_position
44
- [marks].flatten.compact.map(&:start_position).sort.first
44
+ [marks].flatten.compact.map(&:start_position).min
45
45
  end
46
46
 
47
47
  def end_position
48
- [marks].flatten.compact.map(&:end_position).sort.last
48
+ [marks].flatten.compact.map(&:end_position).max
49
49
  end
50
50
 
51
51
  def marks
@@ -25,6 +25,7 @@ class HeadMusic::Style::Guidelines::AtLeastEightNotes < HeadMusic::Style::Annota
25
25
 
26
26
  def deficiency_mark
27
27
  return unless notes.length < MINIMUM_NOTES
28
+
28
29
  HeadMusic::Style::Mark.for_all(placements, fitness: notes.length.to_f / MINIMUM_NOTES)
29
30
  end
30
31
  end
@@ -7,6 +7,7 @@ module HeadMusic::Style::Guidelines; end
7
7
  class HeadMusic::Style::Guidelines::DirectionChanges < HeadMusic::Style::Annotation
8
8
  def marks
9
9
  return unless overage.positive?
10
+
10
11
  penalty_exponent = overage**0.5
11
12
  HeadMusic::Style::Mark.for_all(notes, fitness: HeadMusic::PENALTY_FACTOR**penalty_exponent)
12
13
  end
@@ -15,6 +16,7 @@ class HeadMusic::Style::Guidelines::DirectionChanges < HeadMusic::Style::Annotat
15
16
 
16
17
  def overage
17
18
  return 0 if notes.length < 2
19
+
18
20
  [notes_per_direction - self.class.maximum_notes_per_direction, 0].max
19
21
  end
20
22
 
@@ -9,6 +9,7 @@ class HeadMusic::Style::Guidelines::LimitOctaveLeaps < HeadMusic::Style::Annotat
9
9
 
10
10
  def marks
11
11
  return if octave_leaps.length <= 1
12
+
12
13
  octave_leaps.map do |leap|
13
14
  HeadMusic::Style::Mark.for_all(leap.notes)
14
15
  end
@@ -25,6 +25,7 @@ class HeadMusic::Style::Guidelines::MostlyConjunct < HeadMusic::Style::Annotatio
25
25
 
26
26
  def conjunct_ratio
27
27
  return 1 if melodic_intervals.empty?
28
+
28
29
  melodic_intervals.count(&:step?).to_f / melodic_intervals.length
29
30
  end
30
31
  end
@@ -53,8 +53,9 @@ class HeadMusic::Style::Guidelines::NotesSameLength < HeadMusic::Style::Annotati
53
53
 
54
54
  def most_common_rhythmic_values
55
55
  return [] if notes.empty?
56
+
56
57
  occurrences = occurrences_by_rhythmic_value
57
- highest_count = occurrences.values.sort.last
58
+ highest_count = occurrences.values.max
58
59
  occurrences.select { |_rhythmic_value, count| count == highest_count }.keys
59
60
  end
60
61
 
@@ -10,6 +10,7 @@ class HeadMusic::Style::Guidelines::OneToOne < HeadMusic::Style::Annotation
10
10
  def marks
11
11
  return unless cantus_firmus&.notes
12
12
  return if cantus_firmus.notes.empty?
13
+
13
14
  HeadMusic::Style::Mark.for_each(
14
15
  notes_without_match(voice, cantus_firmus) + notes_without_match(cantus_firmus, voice)
15
16
  )
@@ -10,6 +10,7 @@ class HeadMusic::Style::Guidelines::PreferContraryMotion < HeadMusic::Style::Ann
10
10
  def marks
11
11
  return nil if notes.length < 2
12
12
  return nil if direct_motion_ratio <= 0.5
13
+
13
14
  direct_motions.map { |motion| HeadMusic::Style::Mark.for_all(motion.notes) }
14
15
  end
15
16
 
@@ -21,6 +22,7 @@ class HeadMusic::Style::Guidelines::PreferContraryMotion < HeadMusic::Style::Ann
21
22
 
22
23
  def direct_motion_ratio
23
24
  return 0 if motions.empty?
25
+
24
26
  direct_motions.count / motions.count.to_f
25
27
  end
26
28
  end
@@ -15,6 +15,7 @@ class HeadMusic::Style::Guidelines::PreferImperfect < HeadMusic::Style::Annotati
15
15
 
16
16
  def ratio_of_perfect_intervals
17
17
  return 0 if downbeat_harmonic_intervals_in_middle.nil?
18
+
18
19
  perfect_intervals.length.to_f / downbeat_harmonic_intervals.length
19
20
  end
20
21
 
@@ -14,6 +14,7 @@ class HeadMusic::Style::Guidelines::SingleLargeLeaps < HeadMusic::Style::Guideli
14
14
  return false if spelling_consonant_triad?(first_interval, second_interval, third_interval)
15
15
  return false if second_interval.step?
16
16
  return false if second_interval.repetition?
17
+
17
18
  !direction_changed?(first_interval, second_interval) && second_interval.leap?
18
19
  end
19
20
  end
@@ -9,6 +9,7 @@ class HeadMusic::Style::Guidelines::StartOnPerfectConsonance < HeadMusic::Style:
9
9
 
10
10
  def marks
11
11
  return unless first_note && ((bass_voice? && !starts_on_tonic?) || !starts_on_perfect_consonance?)
12
+
12
13
  HeadMusic::Style::Mark.for(first_note)
13
14
  end
14
15
 
@@ -9,6 +9,7 @@ class HeadMusic::Style::Guidelines::StepDownToFinalNote < HeadMusic::Style::Anno
9
9
 
10
10
  def marks
11
11
  return if last_melodic_interval.nil?
12
+
12
13
  fitness = 1
13
14
  fitness *= HeadMusic::PENALTY_FACTOR unless step?
14
15
  fitness *= HeadMusic::PENALTY_FACTOR unless descending?
@@ -29,6 +29,6 @@ class HeadMusic::Style::Guidelines::StepOutOfUnison < HeadMusic::Style::Annotati
29
29
  end
30
30
 
31
31
  def perfect_unisons
32
- @unisons ||= harmonic_intervals.select(&:perfect_consonance?).select(&:unison?)
32
+ @perfect_unisons ||= harmonic_intervals.select(&:perfect_consonance?).select(&:unison?)
33
33
  end
34
34
  end
@@ -9,6 +9,7 @@ class HeadMusic::Style::Guidelines::StepUpToFinalNote < HeadMusic::Style::Annota
9
9
 
10
10
  def marks
11
11
  return if last_melodic_interval.nil?
12
+
12
13
  fitness = 1
13
14
  fitness *= HeadMusic::PENALTY_FACTOR unless step?
14
15
  fitness *= HeadMusic::PENALTY_FACTOR unless ascending?
@@ -12,8 +12,9 @@ class HeadMusic::Style::Mark
12
12
  def self.for_all(placements, fitness: nil)
13
13
  placements = [placements].flatten.compact
14
14
  return [] if placements.empty?
15
- start_position = placements.map(&:position).sort.first
16
- end_position = placements.map(&:next_position).sort.last
15
+
16
+ start_position = placements.map(&:position).min
17
+ end_position = placements.map(&:next_position).max
17
18
  new(start_position, end_position, placements: placements, fitness: fitness)
18
19
  end
19
20
 
@@ -3,8 +3,8 @@
3
3
  # A tuning has a reference pitch and frequency and provides frequencies for all pitches
4
4
  # The base class assumes equal temperament tuning. By default, A4 = 440.0 Hz
5
5
  class HeadMusic::Tuning
6
- REFERENCE_FREQUENCY = 440.0
7
6
  REFERENCE_PITCH_NAME = 'A4'
7
+ REFERENCE_FREQUENCY = 440.0
8
8
 
9
9
  attr_reader :reference_pitch, :reference_frequency
10
10
 
@@ -18,3 +18,8 @@ class HeadMusic::Tuning
18
18
  reference_frequency * (2**(1.0 / 12))**(pitch - reference_pitch).semitones
19
19
  end
20
20
  end
21
+
22
+ # TODO: other tunings
23
+ # Create website that hosts videos on theory and history, handy charts, etc.
24
+ # one of those charts can be a frequency table in various tunings
25
+ # maybe show pythagorean commas and such. or cents sharp or flat relative to either equal temperment or just intonation
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HeadMusic
4
- VERSION = '0.20.0'
4
+ VERSION = '0.22.0'
5
5
  end
data/lib/head_music.rb CHANGED
@@ -36,6 +36,8 @@ require 'head_music/meter'
36
36
  require 'head_music/motion'
37
37
  require 'head_music/octave'
38
38
  require 'head_music/pitch'
39
+ require 'head_music/pitch/enharmonic_equivalence'
40
+ require 'head_music/pitch/octave_equivalence'
39
41
  require 'head_music/pitch_class'
40
42
  require 'head_music/quality'
41
43
  require 'head_music/rhythm'
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.20.0
4
+ version: 0.22.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: 2018-05-16 00:00:00.000000000 Z
11
+ date: 2018-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -102,18 +102,22 @@ executables: []
102
102
  extensions: []
103
103
  extra_rdoc_files: []
104
104
  files:
105
+ - ".circleci/config.yml"
106
+ - ".circleci/setup-rubygems.sh"
105
107
  - ".gitignore"
108
+ - ".pairs"
106
109
  - ".rspec"
107
110
  - ".rubocop.yml"
111
+ - ".ruby-version"
108
112
  - ".travis.yml"
109
113
  - CODE_OF_CONDUCT.md
110
114
  - Gemfile
111
115
  - LICENSE.txt
112
116
  - README.md
113
117
  - Rakefile
118
+ - TODO.md
114
119
  - bin/console
115
120
  - bin/setup
116
- - circle.yml
117
121
  - head_music.gemspec
118
122
  - lib/head_music.rb
119
123
  - lib/head_music/chord.rb
@@ -140,6 +144,8 @@ files:
140
144
  - lib/head_music/named_rudiment.rb
141
145
  - lib/head_music/octave.rb
142
146
  - lib/head_music/pitch.rb
147
+ - lib/head_music/pitch/enharmonic_equivalence.rb
148
+ - lib/head_music/pitch/octave_equivalence.rb
143
149
  - lib/head_music/pitch_class.rb
144
150
  - lib/head_music/quality.rb
145
151
  - lib/head_music/rhythm.rb
@@ -213,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
219
  version: '0'
214
220
  requirements: []
215
221
  rubyforge_project:
216
- rubygems_version: 2.6.8
222
+ rubygems_version: 2.7.6
217
223
  signing_key:
218
224
  specification_version: 4
219
225
  summary: The rudiments of western music theory.
data/circle.yml DELETED
@@ -1,9 +0,0 @@
1
- ## Customize the test machine
2
- machine:
3
- timezone:
4
- America/Los_Angeles # Set the timezone
5
-
6
- # Version of ruby to use
7
- ruby:
8
- version:
9
- 2.4.0