pedrozath/coltrane

View on GitHub
lib/coltrane/theory/note.rb

Summary

Maintainability
A
25 mins
Test Coverage
A
91%
# 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