lib/head_music/scale.rb in head_music-0.17.0 vs lib/head_music/scale.rb in head_music-0.18.0
- old
+ new
@@ -1,20 +1,23 @@
+# frozen_string_literal: true
+
+# A scale contains ordered pitches starting at a tonal center.
class HeadMusic::Scale
SCALE_REGEX = /^[A-G][#b]?\s+\w+$/
def self.get(root_pitch, scale_type = nil)
- if root_pitch.is_a?(String) && scale_type =~ SCALE_REGEX
- root_pitch, scale_type = root_pitch.split(/\s+/)
- end
+ root_pitch, scale_type = root_pitch.split(/\s+/) if root_pitch.is_a?(String) && scale_type =~ SCALE_REGEX
root_pitch = HeadMusic::Pitch.get(root_pitch)
scale_type = HeadMusic::ScaleType.get(scale_type || :major)
@scales ||= {}
name = [root_pitch, scale_type].join(' ')
hash_key = HeadMusic::Utilities::HashKey.for(name)
@scales[hash_key] ||= new(root_pitch, scale_type)
end
+ delegate :letter_name_cycle, to: :root_pitch
+
attr_reader :root_pitch, :scale_type
def initialize(root_pitch, scale_type)
@root_pitch = HeadMusic::Pitch.get(root_pitch)
@scale_type = HeadMusic::ScaleType.get(scale_type)
@@ -24,80 +27,97 @@
@pitches ||= {}
@pitches[direction] ||= {}
@pitches[direction][octaves] ||= determine_scale_pitches(direction, octaves)
end
- def determine_scale_pitches(direction, octaves)
- semitones_from_root = 0
- [root_pitch].tap do |pitches|
- [:ascending, :descending].each do |single_direction|
- if [single_direction, :both].include?(direction)
- (1..octaves).each do
- direction_intervals(single_direction).each_with_index do |semitones, i|
- semitones_from_root += semitones * direction_sign(single_direction)
- pitches << pitch_for_step(i+1, semitones_from_root, single_direction)
- end
- end
- end
- end
- end
- end
-
- def direction_sign(direction)
- direction == :descending ? -1 : 1
- end
-
- def direction_intervals(direction)
- scale_type.send("#{direction}_intervals")
- end
-
def spellings(direction: :ascending, octaves: 1)
pitches(direction: direction, octaves: octaves).map(&:spelling).map(&:to_s)
end
def pitch_names(direction: :ascending, octaves: 1)
pitches(direction: direction, octaves: octaves).map(&:name)
end
- def letter_name_cycle
- @letter_name_cycle ||= root_pitch.letter_name_cycle
- end
-
def root_pitch_number
@root_pitch_number ||= root_pitch.number
end
def degree(degree_number)
pitches[degree_number - 1]
end
private
+ def determine_scale_pitches(direction, octaves)
+ semitones_from_root = 0
+ pitches = [root_pitch]
+ %i[ascending descending].each do |single_direction|
+ next unless [single_direction, :both].include?(direction)
+ (1..octaves).each do
+ pitches += octave_scale_pitches(single_direction, semitones_from_root)
+ semitones_from_root += 12 * direction_sign(single_direction)
+ end
+ end
+ pitches
+ end
+
+ def octave_scale_pitches(direction, semitones_from_root)
+ direction_intervals(direction).map.with_index do |semitones, i|
+ semitones_from_root += semitones * direction_sign(direction)
+ pitch_for_step(i + 1, semitones_from_root, direction)
+ end
+ end
+
+ def direction_sign(direction)
+ direction == :descending ? -1 : 1
+ end
+
+ def direction_intervals(direction)
+ scale_type.send("#{direction}_intervals")
+ end
+
def parent_scale_pitches
HeadMusic::Scale.get(root_pitch, scale_type.parent_name).pitches if scale_type.parent
end
def parent_scale_pitch_for(semitones_from_root)
- parent_scale_pitches.detect { |parent_scale_pitch|
+ parent_scale_pitches.detect do |parent_scale_pitch|
parent_scale_pitch.pitch_class == (root_pitch + semitones_from_root).to_i % 12
- }
- end
-
- def letter_for_step(step, semitones_from_root, direction)
- pitch_class_number = (root_pitch.pitch_class.to_i + semitones_from_root) % 12
- if scale_type.intervals.length == 7
- direction == :ascending ? letter_name_cycle[step % 7] : letter_name_cycle[-step % 7]
- elsif scale_type.intervals.length < 7 && parent_scale_pitches
- parent_scale_pitch_for(semitones_from_root).letter_name
- elsif root_pitch.flat?
- HeadMusic::PitchClass::FLAT_SPELLINGS[pitch_class_number]
- else
- HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_class_number]
end
end
def pitch_for_step(step, semitones_from_root, direction)
pitch_number = root_pitch_number + semitones_from_root
letter_name = letter_for_step(step, semitones_from_root, direction)
HeadMusic::Pitch.from_number_and_letter(pitch_number, letter_name)
+ end
+
+ def letter_for_step(step, semitones_from_root, direction)
+ diatonic_letter_for_step(direction, step) ||
+ child_scale_letter_for_step(semitones_from_root) ||
+ flat_letter_for_step(semitones_from_root) ||
+ sharp_letter_for_step(semitones_from_root)
+ end
+
+ def diatonic_letter_for_step(direction, step)
+ return unless scale_type.diatonic?
+ direction == :ascending ? letter_name_cycle[step % 7] : letter_name_cycle[-step % 7]
+ end
+
+ def child_scale_letter_for_step(semitones_from_root)
+ return unless scale_type.parent
+ parent_scale_pitch_for(semitones_from_root).letter_name
+ end
+
+ def flat_letter_for_step(semitones_from_root)
+ return unless root_pitch.flat?
+ HeadMusic::PitchClass::FLAT_SPELLINGS[pitch_class_number(semitones_from_root)]
+ end
+
+ def sharp_letter_for_step(semitones_from_root)
+ HeadMusic::PitchClass::SHARP_SPELLINGS[pitch_class_number(semitones_from_root)]
+ end
+
+ def pitch_class_number(semitones_from_root)
+ (root_pitch.pitch_class.to_i + semitones_from_root) % 12
end
end