lib/mass/pitch.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'forwardable'

module Mass
  # Represents the pitch of a given note, and calculates its
  # correct MIDI value, leaving the +Note+ class more for
  # calculating the proper duration and actually playing out
  # the note.
  class Pitch
    extend Forwardable

    # ID of the pitch, e.g. 'C4'
    #
    # @attr_reader [String]
    attr_reader :id

    # The name of the note, e.g. 'C'.
    #
    # @attr_reader [String]
    attr_reader :name

    # Numerical octave of the note, e.g. 4.
    #
    # @attr_reader [Integer]
    attr_reader :octave

    # The calculated MIDI value of this pitch, which is the
    # base value as defined in +VALUES+ multiplied by the
    # octave it is placed in.
    #
    # @attr_reader [Integer]
    attr_reader :value

    # A dictionary of MIDI note values that are substituted
    # for a given String note value.
    VALUES = {
      'C' => 36,
      'C#' => 37,
      'Db' => 37,
      'D' => 38,
      'D#' => 39,
      'Eb' => 39,
      'E' => 40,
      'F' => 41,
      'F#' => 42,
      'Gb' => 42,
      'G' => 43,
      'G#' => 44,
      'Ab' => 44,
      'A' => 45,
      'A#' => 46,
      'Bb' => 46,
      'B' => 47
    }

    # A collection of attributes which are required for
    # this object to be considered a valid pitch.
    REQUIRED = %i(name octave value)

    # @param [String] id - Identifier string of this pitch.
    def initialize(id: '')
      @id = id.to_s
      @name = @id.gsub(/\d/, '').to_s
      @octave = @id.gsub(/#{name}/, '').to_i
      @value = begin
                 VALUES[name]
               rescue
                 nil
               end
    end

    # Find a +Pitch+ by its given +id+.
    #
    # @param [String] by_id
    # @return [Pitch] when the id is valid
    def self.find(by_id = nil)
      return if by_id.nil?
      new id: by_id
    end

    # Make sure the +name+ and +octave+ attributes have
    # been correctly parsed out of the given +id+.
    #
    # @return [Boolean] whether the object is valid
    def valid?
      required_params.all? do |param|
        !param.nil? && (param != '') && (param != 0)
      end
    end

    # The MIDI value of this pitch.
    #
    # @return [Integer]
    def to_i
      value + octave_modifier
    end

    # String representation of this pitch for logging purposes.
    #
    # @return [String]
    def to_s
      "#{id} (#{to_i})"
    end

    # Convert the ID of this pitch to a Symbol.
    #
    # @return [Symbol]
    def to_sym
      id.to_sym
    end

    # Use the +id+ parameter to define equivalence.
    #
    # @return [Boolean] whether both pitches have the same ID.
    def ==(other)
      other.to_sym == to_sym
    end

    private

    # Collection of parameters required for this pitch to be
    # valid.
    #
    # @private
    # @return [Array]
    def required_params
      REQUIRED.map do |param_name|
        send param_name
      end
    end

    # The number by which the given scalar pitch gets modified to
    # produce the correct MIDI note value for playback.
    #
    # @private
    # @return [Integer]
    def octave_modifier
      12 * octave
    end
  end
end