lib/head_music/pitch.rb in head_music-0.19.1 vs lib/head_music/pitch.rb in head_music-0.19.2

- old
+ new

@@ -12,18 +12,30 @@ delegate :pitch_class, to: :spelling delegate :semitones, to: :sign, prefix: true, allow_nil: true delegate :smallest_interval_to, to: :pitch_class + delegate :enharmonic_equivalent?, :enharmonic?, to: :enharmonic_equivalence + delegate :octave_equivalent?, to: :octave_equivalence + def self.get(value) - from_name(value) || from_number(value) + from_pitch_class(value) || from_name(value) || from_number(value) end def self.middle_c get('C4') end + def self.concert_a + 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 @@ -47,11 +59,12 @@ natural_letter_pitch = get(HeadMusic::LetterName.get(letter_name).pitch_class) natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i).to_i >= 11 get(natural_letter_pitch) end - def self.fetch_or_create(spelling, octave) + 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 @@ -82,14 +95,10 @@ def natural HeadMusic::Pitch.get(to_s.gsub(/[#b]/, '')) end - def enharmonic?(other) - midi_note_number == other.midi_note_number - end - def +(other) HeadMusic::Pitch.get(to_i + other.to_i) end def -(other) @@ -117,14 +126,30 @@ def natural_steps(num_steps) HeadMusic::Pitch.get([target_letter_name(num_steps), octave + octaves_delta(num_steps)].join) end + def frequency + tuning.frequency_for(self) + end + private_class_method :new private + def enharmonic_equivalence + @enharmonic_equivalence ||= EnharmonicEquivalence.get(self) + end + + def octave_equivalence + @octave_equivalence ||= OctaveEquivalence.get(self) + end + + def tuning + @tuning ||= HeadMusic::Tuning.new + end + def octaves_delta(num_steps) octaves_delta = (num_steps.abs / 7) * (num_steps >= 0 ? 1 : -1) if wrapped_down?(num_steps) octaves_delta -= 1 elsif wrapped_up?(num_steps) @@ -142,7 +167,57 @@ end def target_letter_name(num_steps) @target_letter_name ||= {} @target_letter_name[num_steps] ||= letter_name.steps(num_steps) + end + + # Enharmonic equivalence occurs when two pitches are spelled differently but refer to the same frequency, such as D♯ and E♭. + 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 end end