lib/coltrane/theory/note.rb
# frozen_string_literal: true
module Coltrane
module Theory
# Notes are different ways of calling pitch classes. In the context of equal
# tempered scales, they're more like a conceptual subject for
# matters of convention than an actual thing.
#
# Take for example A# and Bb. Those are different notes. Nevertheless, in the
# context of equal tempered scales they represent pretty much the same
# frequency.
#
# The theory of notes have changed too much in the course of time, which
# lead us with a lot of conventions and strategies when dealing with music.
# That's what this class is for.
class Note < PitchClass
attr_reader :alteration
ALTERATIONS = {
'b' => -1,
'#' => +1
}.freeze
def initialize(arg)
note_name = case arg
when String then arg
when PitchClass then arg.true_notation
when Numeric, Frequency then PitchClass.new(arg).true_notation
else raise(WrongArgumentsError, arg)
end
chars = note_name.chars
letter = chars.shift
raise InvalidNoteLetterError, arg unless ('A'..'G').cover?(letter)
@alteration = chars.reduce(0) do |alt, symbol|
raise InvalidNoteLetterError, arg unless ALTERATIONS.include?(symbol)
alt + ALTERATIONS[symbol]
end
super((PitchClass[letter].integer + @alteration) % PitchClass.size)
end
def self.[](arg)
new(arg)
end
def name
"#{base_pitch_class}#{accidents}".gsub(/#b|b#/, '')
end
def base_pitch_class
PitchClass[integer - alteration]
end
def pitch_class
PitchClass.new(self)
end
def alteration=(a)
@alteration = a unless PitchClass[integer - a].accidental?
end
def alter(x)
Note.new(name).tap { |n| n.alteration = x }
end
def sharp?
alteration == 1
end
def double_sharp?
alteration == 2
end
def flat?
alteration == -1
end
def double_flat?
alteration == -2
end
def natural?
alteration == 0
end
def accidents
(@alteration > 0 ? '#' : 'b') * alteration.abs
end
def -(other)
super(other).yield_self { |r| r.is_a?(Note) ? r.alter(alteration) : r }
end
def +(other)
super(other).yield_self { |r| r.is_a?(Note) ? r.alter(alteration) : r }
end
def as(letter)
a = (Note[letter] - self)
alter([a.semitones, -(-a).semitones].min_by(&:abs))
end
end
end
end