# frozen_string_literal: true module Coltrane module Theory # It describes a sequence of intervals class IntervalSequence extend Forwardable attr_reader :intervals def_delegators :@intervals, :map, :each, :[], :size, :reduce, :delete, :reject!, :delete_if, :detect, :to_a def initialize(*intervals, notes: nil, relative_intervals: nil) if intervals.any? @intervals = if intervals.first.is_a?(Interval) intervals else intervals.map { |i| Interval[i] } end elsif notes notes = NoteSet[*notes] if notes.is_a?(Array) @intervals = intervals_from_notes(notes) elsif relative_intervals @relative_intervals = relative_intervals @intervals = intervals_from_relative_intervals(relative_intervals) else raise WrongKeywordsError, 'Provide: [notes:] || [intervals:] || [relative_intervals:]' end end Interval.all_including_compound_and_altered.each do |interval| # Creates methods such as major_third, returning it if it finds define_method(interval.full_name.underscore.to_s) { find(interval) } # Creates methods such as has_major_third?, returning a boolean define_method("has_#{interval.full_name.underscore}?") { has?(interval) } end Interval.distances_names.map(&:underscore).each_with_index do |distance, i| # Creates methods such as has_third?, returning a boolean define_method("has_#{distance}?") { !!find_by_distance(i + 1) } # Creates methods such third, returning any third it finds define_method(distance.to_s) { find_by_distance(i + 1) } # Creates methods such third!, returning thirds that arent aug or dim define_method("#{distance}!") { find_by_distance(i + 1, false) } end instance_eval { alias [] new } def relative_intervals intervals_semitones[1..-1].each_with_index.map do |n, i| if i.zero? n elsif i < intervals_semitones.size n - intervals_semitones[i] end end + [12 - intervals_semitones.last] end def names intervals.map(&:name) end def find(interval) interval.clone if detect { |i| interval == i } end def has?(interval) !!find(interval) end def find_by_distance(n, accept_altered = true) strategy = (accept_altered ? :as : :as!) map { |interval| interval.send(strategy, n) } .compact .sort_by { |i| i.alteration.abs } .first end alias interval_names names def all intervals end def [](x) intervals[x] end def shift(ammount) self.class.new(*intervals.map do |i| (i.semitones + ammount) % 12 end) end def zero_it shift(-intervals.first.semitones) end def inversion(index) self.class.new(*intervals.rotate(index)).zero_it end def next_inversion inversion(index + 1) end def previous_inversion inversion(index - 1) end def inversions Array.new(intervals.length) { |i| inversion(i) } end def intervals_semitones map(&:semitones) end def names map(&:name) end def full_names map(&:full_name) end def notes_for(root_note) NoteSet[ *intervals.reduce([]) do |memo, interval| memo + [root_note + interval] end ] end def &(other) case other when Array then intervals & other when IntervalSequence then intervals & other.semitones end end private def intervals_from_relative_intervals(relative_intervals) relative_intervals[0..-2].reduce([Interval[0]]) do |memo, d| memo + [memo.last + d] end end def intervals_from_notes(notes) notes.map { |n| notes.root - n }.sort_by(&:semitones) end end end end