lib/music-transcription/model/pitch.rb in music-transcription-0.15.0 vs lib/music-transcription/model/pitch.rb in music-transcription-0.16.0
- old
+ new
@@ -1,92 +1,108 @@
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
+# 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, :total_semitone
+ attr_reader :octave, :semitone, :cent, :total_cents
- #The default number of semitones per octave is 12, corresponding to
+ #The default number of semitones per octave is 12, corresponding to
# the twelve-tone equal temperment tuning system.
SEMITONES_PER_OCTAVE = 12
+ CENTS_PER_SEMITONE = 100
+ CENTS_PER_OCTAVE = SEMITONES_PER_OCTAVE * CENTS_PER_SEMITONE
# The base ferquency is C0
BASE_FREQ = 16.351597831287414
-
- def initialize octave:0, semitone:0
+
+ def initialize octave:0, semitone:0, cent: 0
+ raise NonIntegerError, "octave #{octave} is not an integer" unless octave.is_a?(Integer)
+ raise NonIntegerError, "semitone #{semitone} is not an integer" unless semitone.is_a?(Integer)
+ raise NonIntegerError, "cent #{cent} is not an integer" unless cent.is_a?(Integer)
+
@octave = octave
@semitone = semitone
+ @cent = cent
+ @total_cents = (@octave*SEMITONES_PER_OCTAVE + @semitone)*CENTS_PER_SEMITONE + @cent
normalize!
- @total_semitone = @octave*SEMITONES_PER_OCTAVE + @semitone
end
- # Return the pitch's frequency, which is determined by multiplying the base
+ # 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
+ # but can be set during initialization to something else by specifying the
# :base_freq key.
def freq
return self.ratio() * BASE_FREQ
end
-
- # Calculate the pitch ratio. Raises 2 to the power of the total semitone
- # count divided by semitones-per-octave.
+
+ # Calculate the pitch ratio. Raises 2 to the power of the total cent
+ # count divided by cents-per-octave.
# @return [Float] ratio
def ratio
- 2.0**(@total_semitone.to_f / SEMITONES_PER_OCTAVE)
+ 2.0**(@total_cents.to_f / CENTS_PER_OCTAVE)
end
-
+
# Override default hash method.
def hash
- return @total_semitone
+ return @total_cents
end
-
+
# Compare pitch equality using total semitone
def ==(other)
return (self.class == other.class &&
- @total_semitone == other.total_semitone)
+ @total_cents == other.total_cents)
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)
- @total_semitone <=> other.total_semitone
+ @total_cents <=> other.total_cents
end
-
+
+ # rounds to the nearest semitone
+ def round
+ if @cent == 0
+ self.clone
+ else
+ Pitch.new(semitone: (@total_cents / CENTS_PER_SEMITONE.to_f).round)
+ end
+ end
+
+ # diff in (rounded) semitones
def diff other
- @total_semitone - other.total_semitone
+ Rational(@total_cents - other.total_cents, CENTS_PER_SEMITONE)
end
-
- def transpose interval
- Pitch.from_semitones @total_semitone + interval
+
+ def transpose semitones
+ Pitch.new(cent: (@total_cents + semitones * CENTS_PER_SEMITONE).round)
end
-
- # Produce an identical Pitch object.
+
def clone
- Marshal.load(Marshal.dump(self)) # is this cheating?
+ Pitch.new(cent: @total_cents)
end
-
+
def to_s(sharpit = false)
letter = case semitone
when 0 then "C"
when 1 then sharpit ? "C#" : "Db"
when 2 then "D"
@@ -99,37 +115,41 @@
when 9 then "A"
when 10 then sharpit ? "A#" : "Bb"
when 11 then "B"
end
- return letter + octave.to_s
+ if @cent == 0
+ return letter + octave.to_s
+ elsif @cent > 0
+ return letter + octave.to_s + "+" + @cent.to_s
+ else
+ return letter + octave.to_s + @cent.to_s
+ end
end
-
+
def self.from_ratio ratio
raise NonPositiveError, "ratio #{ratio} is not > 0" unless ratio > 0
x = Math.log2 ratio
- semitones = (x * SEMITONES_PER_OCTAVE).round
- from_semitones(semitones)
+ new(cent: (x * CENTS_PER_OCTAVE).round)
end
-
+
def self.from_freq freq
from_ratio(freq / BASE_FREQ)
end
-
- def self.from_semitones semitones
- Pitch.new(semitone: semitones)
- end
-
+
private
-
- # Balance out the octave and semitone count.
+
+ # Balance out the octave and semitone count.
def normalize!
- semitoneTotal = (@octave * SEMITONES_PER_OCTAVE) + @semitone
+ centsTotal = @total_cents
- @octave = semitoneTotal / SEMITONES_PER_OCTAVE
- semitoneTotal -= @octave * SEMITONES_PER_OCTAVE
+ @octave = centsTotal / CENTS_PER_OCTAVE
+ centsTotal -= @octave * CENTS_PER_OCTAVE
- @semitone = semitoneTotal
+ @semitone = centsTotal / CENTS_PER_SEMITONE
+ centsTotal -= @semitone * CENTS_PER_SEMITONE
+
+ @cent = centsTotal
return self
end
end
end