module Music
module Transcription

# Abstraction of a musical pitch. Contains values for octave and semitone.
#
# Octaves represent the largest means of differing two pitches. Each 
# octave added will double the ratio. At zero octaves, the ratio is 
# 1.0. At one octave, the ratio will be 2.0. Each semitone is an increment
# of less-than-power-of-two.
#
# Semitones are the primary steps between octaves. The number of
# semitones per octave is 12.

# @author James Tunnell
# 
# @!attribute [r] octave
#   @return [Fixnum] The pitch octave.
# @!attribute [r] semitone
#   @return [Fixnum] The pitch semitone.
#
class Pitch
  include Comparable
  attr_reader :octave, :semitone

  #The default number of semitones per octave is 12, corresponding to 
  # the twelve-tone equal temperment tuning system.
  SEMITONES_PER_OCTAVE = 12

  # The base ferquency is C0
  BASE_FREQ = 16.351597831287414
  
  def initialize octave:0, semitone:0
    @octave = octave
    @semitone = semitone
    normalize!
  end

  # Return the pitch's frequency, which is determined by multiplying the base 
  # frequency and the pitch ratio. Base frequency defaults to DEFAULT_BASE_FREQ,
  # but can be set during initialization to something else by specifying the 
  # :base_freq key.
  def freq
    return self.ratio() * BASE_FREQ
  end
  
  # Set the pitch according to the given frequency. Uses the current base_freq 
  # to determine what the pitch ratio should be, and sets it accordingly.
  def freq= freq
    self.ratio = freq / BASE_FREQ
  end

  # Calculate the total semitone count. Converts octave to semitone count
  # before adding to existing semitone count.
  # @return [Fixnum] total semitone count
  def total_semitone
    return (@octave * SEMITONES_PER_OCTAVE) + @semitone
  end
  
  # Set the Pitch ratio according to a total number of semitones.
  # @param [Fixnum] semitone The total number of semitones to use.
  # @raise [NonIntegerError] if semitone is not an Integer
  def total_semitone= semitone
    unless semitone.is_a?(Integer)
      raise NonIntegerError, "semitone #{semitone} is not a Integer"
    end
    @octave, @semitone = 0, semitone
    normalize!
  end

  # Calculate the pitch ratio. Raises 2 to the power of the total semitone
  # count divided by semitones-per-octave.
  # @return [Float] ratio
  def ratio
    2.0**(self.total_semitone.to_f / SEMITONES_PER_OCTAVE)
  end

  # Represent the Pitch ratio according to a ratio.
  # @param [Numeric] ratio The ratio to represent.
  # @raise [NonPositiveError] unless ratio is > 0
  def ratio= ratio
    raise NonPositiveError, "ratio #{ratio} is not > 0" unless ratio > 0
    
    x = Math.log2 ratio
    self.total_semitone = (x * SEMITONES_PER_OCTAVE).round
  end

  # Round to the nearest semitone.
  def round
    self.clone.round!
  end

  # Calculates the number of semitones which would represent the pitch's
  # octave and semitone count
  def total_semitone
    return (@octave * SEMITONES_PER_OCTAVE) + @semitone
  end
  
  # Override default hash method.
  def hash
    return self.total_semitone
  end
  
  # Compare pitch equality using total semitone
  def ==(other)
    return (self.class == other.class &&
      self.total_semitone == other.total_semitone)
  end
  
  def eql?(other)
    self == other
  end
  
  # Compare pitches. A higher ratio or total semitone is considered larger.
  # @param [Pitch] other The pitch object to compare.
  def <=> (other)
    self.total_semitone <=> other.total_semitone
  end

  # Add pitches by adding the total semitone count of each.
  # @param [Pitch] other The pitch object to add. 
  def + (other)
    self.class.new(
      octave: (@octave + other.octave),
      semitone: (@semitone + other.semitone)
    )
  end

  # Add pitches by subtracting the total semitone count.
  # @param [Pitch] other The pitch object to subtract.
  def - (other)
    self.class.new(
      octave: (@octave - other.octave),
      semitone: (@semitone - other.semitone),
    )
  end
  
  # Produce an identical Pitch object.
  def clone
    Marshal.load(Marshal.dump(self))  # is this cheating?
  end
  
  # Balance out the octave and semitone count. 
  def normalize!
    semitoneTotal = (@octave * SEMITONES_PER_OCTAVE) + @semitone
    
    @octave = semitoneTotal / SEMITONES_PER_OCTAVE
    semitoneTotal -= @octave * SEMITONES_PER_OCTAVE
    
    @semitone = semitoneTotal
    return self
  end
  
  def self.make_from_freq(freq)
    pitch = Pitch.new()
    pitch.ratio = freq / BASE_FREQ
    return pitch
  end
  
  def self.make_from_semitone semitones
    pitch = Pitch.new()
    pitch.total_semitone = semitones
    return pitch
  end
end

end
end