pedrozath/coltrane

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

Summary

Maintainability
A
3 hrs
Test Coverage
B
83%
# frozen_string_literal: true

module Coltrane
  module Theory
    # Musical scale creation and manipulation
    class Scale
      extend ClassicScales
      extend Forwardable

      def_delegators :notes, :accidentals, :sharps, :flats

      attr_reader :interval_sequence, :tone

      def initialize(*relative_intervals, tone: 'C',
                     mode: 1,
                     name: nil,
                     notes: nil)
        @name = name
        if relative_intervals.any? && tone
          @tone              = Note[tone]
          relative_intervals = relative_intervals.rotate(mode - 1)
          @interval_sequence = IntervalSequence.new(
            relative_intervals: relative_intervals
          )
        elsif notes
          @notes             = NoteSet[*notes]
          @tone              = @notes.first
          ds                 = @notes.interval_sequence.relative_intervals
          @interval_sequence = IntervalSequence.new(relative_intervals: ds)
        else
          raise WrongKeywordsError,
                '[*relative_intervals, tone: "C", mode: 1] || [notes:]'
        end
      end

      def id
        [(name || @interval_sequence), tone.number]
      end

      def ==(other)
        id == other.id
      end

      def name
        @name ||= begin
          is = interval_sequence.relative_intervals
          (0...is.size).each do |i|
            if (scale_name = ClassicScales::SCALES.key(is.rotate(i)))
              return scale_name
            end
          end
          nil
        end
      end

      def full_name
        "#{tone} #{name}"
      end

      alias to_s full_name

      def pretty_name
        "#{tone.pretty_name} #{name}"
      end

      alias full_name pretty_name

      def degree(d)
        raise WrongDegreeError, d if d < 1 || d > size
        tone + interval_sequence[d - 1].semitones
      end

      alias [] degree

      def degrees
        (1..size)
      end

      def degree_of_chord(chord)
        return if chords(chord.size).map(&:name).include?(chord.name)
        degree_of_note(chord.root_note)
      end

      def degree_of_note(note)
        notes.index(note)
      end

      def &(other)
        raise HasNoNotesError unless other.respond_to?(:notes)
        notes & other
      end

      def include_notes?(arg)
        noteset = arg.is_a?(Note) ? NoteSet[arg] : arg
        (self & noteset).size == noteset.size
      end

      alias include? include_notes?

      def notes
        @notes ||= NoteSet[*degrees.map { |d| degree(d) }]
      end

      def interval(i)
        interval_sequence[(i - 1) % size]
      end

      def size
        interval_sequence.size
      end

      def tertians(n = 3)
        degrees.size.times.reduce([]) do |memo, d|
          ns = NoteSet[ *Array.new(n) { |i| notes[(d + (i * 2)) % size] } ]
          begin
            chord = Chord.new(notes: ns)
          rescue ChordNotFoundError
            memo
          else
            memo + [chord]
          end
        end
      end

      def triads
        tertians(3)
      end

      def sevenths
        tertians(4)
      end

      def pentads
        tertians(5)
      end

      def progression(*degrees)
        Progression.new(self, degrees)
      end

      def chords(size = 3..12)
        size = (size..size) if size.is_a?(Integer)
        scale_rotations = interval_sequence.inversions
        ChordQuality.intervals_per_name.reduce([]) do |memo1, (qname, qintervals)|
          next memo1 unless size.include?(qintervals.size)
          memo1 + scale_rotations.each_with_index
                                 .reduce([]) do |memo2, (rot, index)|
                    if (rot & qintervals).size == qintervals.size
                      memo2 + [Chord.new(root_note: degree(index + 1),
                                         quality: ChordQuality.new(name: qname))]
                    else
                      memo2
                    end
                  end
        end
      end

      alias all_chords chords
    end
  end
end