lib/head_music/pitch.rb in head_music-0.20.0 vs lib/head_music/pitch.rb in head_music-0.22.0

- old
+ new

@@ -5,14 +5,18 @@ include Comparable attr_reader :spelling attr_reader :octave - delegate :letter_name, :letter_name_cycle, to: :spelling + delegate :letter_name, to: :spelling + delegate :series_ascending, :series_descending, to: :letter_name, prefix: true delegate :sign, :sharp?, :flat?, to: :spelling delegate :pitch_class, to: :spelling + delegate :number, to: :pitch_class, prefix: true + delegate :pitch_class_number, to: :natural, prefix: true delegate :semitones, to: :sign, prefix: true, allow_nil: true + delegate :steps_to, to: :letter_name, prefix: true delegate :smallest_interval_to, to: :pitch_class delegate :enharmonic_equivalent?, :enharmonic?, to: :enharmonic_equivalence delegate :octave_equivalent?, to: :octave_equivalence @@ -29,20 +33,23 @@ get('A4') end def self.from_pitch_class(pitch_class) return nil unless pitch_class.is_a?(HeadMusic::PitchClass) + fetch_or_create(pitch_class.sharp_spelling) end def self.from_name(name) return nil unless name == name.to_s + fetch_or_create(HeadMusic::Spelling.get(name), HeadMusic::Octave.get(name).to_i) end def self.from_number(number) return nil unless number == number.to_i + spelling = HeadMusic::Spelling.from_number(number) octave = (number.to_i / 12) - 1 fetch_or_create(spelling, octave) end @@ -62,10 +69,11 @@ end def self.fetch_or_create(spelling, octave = nil) octave ||= HeadMusic::Octave::DEFAULT return unless spelling && (-1..9).cover?(octave) + @pitches ||= {} hash_key = [spelling, octave].join @pitches[hash_key] ||= new(spelling, octave) end @@ -92,19 +100,28 @@ def to_i midi_note_number end def natural - HeadMusic::Pitch.get(to_s.gsub(/[#b]/, '')) + HeadMusic::Pitch.get(to_s.gsub(HeadMusic::Sign.matcher, '')) end def +(other) - HeadMusic::Pitch.get(to_i + other.to_i) + if other.is_a?(HeadMusic::FunctionalInterval) + # return a pitch + other.above(self) + else + # assume value represents an interval in semitones and return another pitch + HeadMusic::Pitch.get(to_i + other.to_i) + end end def -(other) - if other.is_a?(HeadMusic::Pitch) + if other.is_a?(HeadMusic::FunctionalInterval) + # return a pitch + other.below(self) + elsif other.is_a?(HeadMusic::Pitch) # return an interval HeadMusic::Interval.get(to_i - other.to_i) else # assume value represents an interval in semitones and return another pitch HeadMusic::Pitch.get(to_i - other.to_i) @@ -130,20 +147,37 @@ def frequency tuning.frequency_for(self) end + def steps_to(other) + other = HeadMusic::Pitch.get(other) + letter_name_steps_to(other) + 7 * octave_changes_to(other) + end + private_class_method :new private + def octave_changes_to(other) + other.octave - octave - octave_adjustment_to(other) + end + + def octave_adjustment_to(other) + (pitch_class_above?(other) ? 1 : 0) + end + + def pitch_class_above?(other) + natural_pitch_class_number > other.natural_pitch_class_number + end + def enharmonic_equivalence - @enharmonic_equivalence ||= EnharmonicEquivalence.get(self) + @enharmonic_equivalence ||= HeadMusic::Pitch::EnharmonicEquivalence.get(self) end def octave_equivalence - @octave_equivalence ||= OctaveEquivalence.get(self) + @octave_equivalence ||= HeadMusic::Pitch::OctaveEquivalence.get(self) end def tuning @tuning ||= HeadMusic::Tuning.new end @@ -166,58 +200,8 @@ num_steps.positive? && target_letter_name(num_steps).position < letter_name.position end def target_letter_name(num_steps) @target_letter_name ||= {} - @target_letter_name[num_steps] ||= letter_name.steps(num_steps) - end - - # An enharmonic equivalent pitch is the same frequency spelled differently, such as D# and Eb. - class EnharmonicEquivalence - def self.get(pitch) - pitch = HeadMusic::Pitch.get(pitch) - @enharmonic_equivalences ||= {} - @enharmonic_equivalences[pitch.to_s] ||= new(pitch) - end - - attr_reader :pitch - - delegate :pitch_class, to: :pitch - - def initialize(pitch) - @pitch = HeadMusic::Pitch.get(pitch) - end - - def enharmonic_equivalent?(other) - other = HeadMusic::Pitch.get(other) - pitch.midi_note_number == other.midi_note_number && pitch.spelling != other.spelling - end - - alias enharmonic? enharmonic_equivalent? - alias equivalent? enharmonic_equivalent? - - private_class_method :new - end - - # Octave equivalence is the functional equivalence of pitches with the same spelling separated by one or more octaves. - class OctaveEquivalence - def self.get(pitch) - @octave_equivalences ||= {} - @octave_equivalences[pitch.to_s] ||= new(pitch) - end - - attr_reader :pitch - - def initialize(pitch) - @pitch = pitch - end - - def octave_equivalent?(other) - other = HeadMusic::Pitch.get(other) - pitch.spelling == other.spelling && pitch.octave != other.octave - end - - alias equivalent? octave_equivalent? - - private_class_method :new + @target_letter_name[num_steps] ||= letter_name.steps_up(num_steps) end end