Class: OutroRails::Theory::Scale
- Inherits:
-
Object
- Object
- OutroRails::Theory::Scale
- Defined in:
- lib/outro_rails/theory/scale.rb
Overview
A scale: a root NoteName plus a named pattern. Not persisted - scales are generated on demand from the pattern table.
Theory::Scale.new('A', :natural_minor).note_names.map(&:to_s)
# => ['A', 'B', 'C', 'D', 'E', 'F', 'G']
Defined Under Namespace
Classes: DiatonicChord
Constant Summary collapse
- PATTERNS =
Each degree is [semitones above root, letter steps above root]. Carrying the letter step makes spelling unambiguous (F# major gets E#, not F; G# major gets F#).
{ major: [ [ 0, 0 ], [ 2, 1 ], [ 4, 2 ], [ 5, 3 ], [ 7, 4 ], [ 9, 5 ], [ 11, 6 ] ], natural_minor: [ [ 0, 0 ], [ 2, 1 ], [ 3, 2 ], [ 5, 3 ], [ 7, 4 ], [ 8, 5 ], [ 10, 6 ] ], harmonic_minor: [ [ 0, 0 ], [ 2, 1 ], [ 3, 2 ], [ 5, 3 ], [ 7, 4 ], [ 8, 5 ], [ 11, 6 ] ], melodic_minor: [ [ 0, 0 ], [ 2, 1 ], [ 3, 2 ], [ 5, 3 ], [ 7, 4 ], [ 9, 5 ], [ 11, 6 ] ], major_pentatonic: [ [ 0, 0 ], [ 2, 1 ], [ 4, 2 ], [ 7, 4 ], [ 9, 5 ] ], minor_pentatonic: [ [ 0, 0 ], [ 3, 2 ], [ 5, 3 ], [ 7, 4 ], [ 10, 6 ] ], major_blues: [ [ 0, 0 ], [ 2, 1 ], [ 3, 2 ], [ 4, 2 ], [ 7, 4 ], [ 9, 5 ] ], minor_blues: [ [ 0, 0 ], [ 3, 2 ], [ 5, 3 ], [ 6, 4 ], [ 7, 4 ], [ 10, 6 ] ], # Modes of the major scale (ionian == major, aeolian == natural_minor). dorian: [ [ 0, 0 ], [ 2, 1 ], [ 3, 2 ], [ 5, 3 ], [ 7, 4 ], [ 9, 5 ], [ 10, 6 ] ], phrygian: [ [ 0, 0 ], [ 1, 1 ], [ 3, 2 ], [ 5, 3 ], [ 7, 4 ], [ 8, 5 ], [ 10, 6 ] ], lydian: [ [ 0, 0 ], [ 2, 1 ], [ 4, 2 ], [ 6, 3 ], [ 7, 4 ], [ 9, 5 ], [ 11, 6 ] ], mixolydian: [ [ 0, 0 ], [ 2, 1 ], [ 4, 2 ], [ 5, 3 ], [ 7, 4 ], [ 9, 5 ], [ 10, 6 ] ], locrian: [ [ 0, 0 ], [ 1, 1 ], [ 3, 2 ], [ 5, 3 ], [ 6, 4 ], [ 8, 5 ], [ 10, 6 ] ] }.freeze
- MINOR_PATTERNS =
The minor-family patterns; all share the same relative major.
%i[natural_minor harmonic_minor melodic_minor].freeze
- MODE_PARENT_DEGREES =
Degree of the parent major scale each mode starts on (dorian is built on the 2nd degree, and so on).
{ dorian: 2, phrygian: 3, lydian: 4, mixolydian: 5, locrian: 7 }.freeze
Instance Attribute Summary collapse
-
#pattern_name ⇒ Object
readonly
The tonic NoteName and the pattern key it was built with.
-
#root ⇒ Object
readonly
The tonic NoteName and the pattern key it was built with.
Class Method Summary collapse
-
.pattern?(name) ⇒ Boolean
Whether a pattern by this name exists, for validating user input.
Instance Method Summary collapse
-
#church_mode? ⇒ Boolean
(also: #mode?)
True for the named modes of the major scale (excluding major itself and natural minor, which the app treats as keys in their own right).
-
#degree(number) ⇒ Object
1-based scale degree => NoteName ("what's the 5th of D major?").
-
#diatonic_chords ⇒ Object
The triad on each degree, built by stacking alternate scale notes (1-3-5 within the scale).
-
#diatonic_chords? ⇒ Boolean
True when the scale supports stacked-third diatonic triads (any seven-note pattern; pentatonic and blues scales don't).
-
#diatonic_quality(number) ⇒ Object
Diatonic triad quality for a 1-based degree, derived from the stacked-third triads (any seven-note pattern).
-
#include?(note) ⇒ Boolean
Whether a note belongs to the scale, compared by sound so enharmonics match (Gb counts in a scale spelling F#).
-
#initialize(root, pattern_name = :major) ⇒ Scale
constructor
Parses the root and validates the pattern name up front, so an unknown scale raises here rather than on first use.
-
#intervals ⇒ Object
Semitones above the root, one per degree.
-
#major? ⇒ Boolean
True only for the major pattern itself, not its modes.
-
#minor? ⇒ Boolean
True for the minor-family patterns (natural, harmonic, melodic).
-
#modes ⇒ Object
The named modes built on this major scale's degrees, in degree order (dorian on the 2nd, and so on) - the inverse of #parent_major: Scale.new('C').modes.map(&:to_s) # => ['D dorian', 'E phrygian', 'F lydian', 'G mixolydian', # 'B locrian'].
-
#net_accidentals ⇒ Object
(also: #signature)
Net accidentals across the spelled scale; negative leans flat.
-
#note_names ⇒ Object
Spelled notes, one per degree.
-
#parent_major ⇒ Object
The major scale a mode is drawn from, correctly spelled: Scale.new('C', :dorian).parent_major # => Bb major Scale.new('F#', :lydian).parent_major # => C# major.
-
#pattern ⇒ Object
The [semitones, letter steps] pairs for this scale.
-
#pitch_classes ⇒ Object
Sounding pitch classes, 0-11, ignoring spelling.
-
#pitched_notes(start_octave: 4, octaves: 1) ⇒ Object
Pitches for rendering on a keyboard: the scale laid out ascending from the root, spanning
octavesoctaves plus the top root. -
#preferred_accidentals ⇒ Object
:flats or :sharps - the spelling that matches this scale, for naming pitches the scale itself doesn't spell (display padding, fretboard labels).
-
#relative_major ⇒ Object
The major key sharing this minor's tonality - a minor third above the root, correctly spelled: Scale.new('A', :natural_minor).relative_major # => C major Scale.new('Eb', :harmonic_minor).relative_major # => Gb major.
-
#relative_minor ⇒ Object
The natural minor sharing this major's notes - built on the 6th degree, so the spelling is inherited from the scale itself: Scale.new('C').relative_minor # => A natural minor Scale.new('Gb').relative_minor # => Eb natural minor.
-
#relative_minors ⇒ Object
All three minor forms on the relative tonic (natural, harmonic, melodic) - the inverse of #relative_major.
-
#size ⇒ Object
Number of degrees - 7 for the diatonic patterns, fewer otherwise.
-
#slug ⇒ Object
URL segment for the pattern, e.g.
-
#to_s ⇒ Object
Display name, e.g.
Constructor Details
#initialize(root, pattern_name = :major) ⇒ Scale
Parses the root and validates the pattern name up front, so an unknown scale raises here rather than on first use.
88 89 90 91 92 93 94 |
# File 'lib/outro_rails/theory/scale.rb', line 88 def initialize(root, pattern_name = :major) @root = NoteName.parse(root) @pattern_name = pattern_name.to_sym PATTERNS.fetch(@pattern_name) do raise ArgumentError, "unknown scale #{pattern_name.inspect}" end end |
Instance Attribute Details
#pattern_name ⇒ Object (readonly)
The tonic NoteName and the pattern key it was built with
79 80 81 |
# File 'lib/outro_rails/theory/scale.rb', line 79 def pattern_name @pattern_name end |
#root ⇒ Object (readonly)
The tonic NoteName and the pattern key it was built with
79 80 81 |
# File 'lib/outro_rails/theory/scale.rb', line 79 def root @root end |
Class Method Details
.pattern?(name) ⇒ Boolean
Whether a pattern by this name exists, for validating user input
82 83 84 |
# File 'lib/outro_rails/theory/scale.rb', line 82 def self.pattern?(name) PATTERNS.key?(name.to_s.to_sym) end |
Instance Method Details
#church_mode? ⇒ Boolean Also known as: mode?
True for the named modes of the major scale (excluding major itself and natural minor, which the app treats as keys in their own right). Named to avoid colliding with Key#mode, which means major/minor tonality, not a church mode.
175 176 177 |
# File 'lib/outro_rails/theory/scale.rb', line 175 def church_mode? MODE_PARENT_DEGREES.key?(pattern_name) end |
#degree(number) ⇒ Object
1-based scale degree => NoteName ("what's the 5th of D major?").
135 136 137 138 139 140 141 142 143 |
# File 'lib/outro_rails/theory/scale.rb', line 135 def degree(number) note_names.fetch(number - 1) do = "degree #{number} out of range for " \ "#{pattern_name} (1..#{size})" raise ArgumentError, end end |
#diatonic_chords ⇒ Object
The triad on each degree, built by stacking alternate scale notes (1-3-5 within the scale). Works for any seven-note pattern, so modes and the altered minors get correct qualities (harmonic minor yields a major V and an augmented III):
Scale.new('C').diatonic_chords.map(&:symbol)
# => ['C', 'Dm', 'Em', 'F', 'G', 'Am', 'Bdim']
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/outro_rails/theory/scale.rb', line 265 def diatonic_chords unless diatonic_chords? raise ArgumentError, "no diatonic chords for #{pattern_name} " \ "(needs a seven-note scale)" end names = note_names (1..size).map do |degree| chord_root = names[degree - 1] third = names[(degree + 1) % size] fifth = names[(degree + 3) % size] third_interval = (third.pitch_class - chord_root.pitch_class) % Interval::SEMITONES_PER_OCTAVE fifth_interval = (fifth.pitch_class - chord_root.pitch_class) % Interval::SEMITONES_PER_OCTAVE intervals = [ 0, third_interval, fifth_interval ] quality = ChordVocabulary::QUALITIES.key(intervals) unless quality raise ArgumentError, "no triad quality for intervals #{intervals.inspect} on " \ "degree #{degree} of #{self}" end DiatonicChord.new(degree: degree, root: chord_root, quality: quality) end end |
#diatonic_chords? ⇒ Boolean
True when the scale supports stacked-third diatonic triads (any seven-note pattern; pentatonic and blues scales don't).
182 183 184 |
# File 'lib/outro_rails/theory/scale.rb', line 182 def diatonic_chords? size == 7 end |
#diatonic_quality(number) ⇒ Object
Diatonic triad quality for a 1-based degree, derived from the stacked-third triads (any seven-note pattern).
306 307 308 309 310 311 |
# File 'lib/outro_rails/theory/scale.rb', line 306 def diatonic_quality(number) diatonic_chords.fetch(number - 1) do raise ArgumentError, "degree #{number} out of range for #{pattern_name} (1..#{size})" end.quality end |
#include?(note) ⇒ Boolean
Whether a note belongs to the scale, compared by sound so enharmonics match (Gb counts in a scale spelling F#).
130 131 132 |
# File 'lib/outro_rails/theory/scale.rb', line 130 def include?(note) pitch_classes.include?(NoteName.parse(note).pitch_class) end |
#intervals ⇒ Object
Semitones above the root, one per degree
107 108 109 |
# File 'lib/outro_rails/theory/scale.rb', line 107 def intervals pattern.map(&:first) end |
#major? ⇒ Boolean
True only for the major pattern itself, not its modes.
162 163 164 |
# File 'lib/outro_rails/theory/scale.rb', line 162 def major? pattern_name == :major end |
#minor? ⇒ Boolean
True for the minor-family patterns (natural, harmonic, melodic).
167 168 169 |
# File 'lib/outro_rails/theory/scale.rb', line 167 def minor? MINOR_PATTERNS.include?(pattern_name) end |
#modes ⇒ Object
249 250 251 252 253 254 255 256 257 |
# File 'lib/outro_rails/theory/scale.rb', line 249 def modes unless major? raise ArgumentError, "modes are derived from a major scale" end MODE_PARENT_DEGREES.map do |pattern, degree_number| self.class.new(degree(degree_number), pattern) end end |
#net_accidentals ⇒ Object Also known as: signature
Net accidentals across the spelled scale; negative leans flat. (Not a key signature: A harmonic minor scores +1 for its G#, and pentatonic/blues scales have no key signature at all - Key#signature is the key-signature reading of this number.)
149 150 151 |
# File 'lib/outro_rails/theory/scale.rb', line 149 def net_accidentals note_names.sum(&:accidental_offset) end |
#note_names ⇒ Object
Spelled notes, one per degree.
117 118 119 |
# File 'lib/outro_rails/theory/scale.rb', line 117 def note_names pattern.map { |semitones, steps| root.at_degree(semitones, steps) } end |
#parent_major ⇒ Object
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/outro_rails/theory/scale.rb', line 220 def parent_major degree = MODE_PARENT_DEGREES.fetch(pattern_name) do raise ArgumentError, "#{pattern_name} is not a mode of the major scale" end # The mode root sits `semitones`/`steps` above its parent tonic, so # the tonic is the complementary degree *up* from the root. semitones, steps = PATTERNS.fetch(:major)[degree - 1] semitone_offset = (Interval::SEMITONES_PER_OCTAVE - semitones) % Interval::SEMITONES_PER_OCTAVE step_offset = (NoteName::LETTERS_PER_OCTAVE - steps) % NoteName::LETTERS_PER_OCTAVE tonic = root.at_degree(semitone_offset, step_offset) self.class.new(tonic, :major) end |
#pattern ⇒ Object
The [semitones, letter steps] pairs for this scale
97 98 99 |
# File 'lib/outro_rails/theory/scale.rb', line 97 def pattern PATTERNS.fetch(pattern_name) end |
#pitch_classes ⇒ Object
Sounding pitch classes, 0-11, ignoring spelling
122 123 124 125 126 |
# File 'lib/outro_rails/theory/scale.rb', line 122 def pitch_classes intervals.map do |i| (root.pitch_class + i) % Interval::SEMITONES_PER_OCTAVE end end |
#pitched_notes(start_octave: 4, octaves: 1) ⇒ Object
Pitches for rendering on a keyboard: the scale laid out
ascending from the root, spanning octaves octaves plus the top
root. Written octaves follow the letter (a Cb is written in the
octave above the B it sounds as).
317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/outro_rails/theory/scale.rb', line 317 def pitched_notes(start_octave: 4, octaves: 1) root_midi = Pitch.new(root, start_octave).midi names = note_names pitches = octaves.times.flat_map do |octave| pattern.each_with_index.map do |(semitones, _), index| Pitch.sounding(names[index], root_midi + (octave * Interval::OCTAVE) + semitones) end end pitches << Pitch.sounding(root, root_midi + (octaves * Interval::OCTAVE)) end |
#preferred_accidentals ⇒ Object
:flats or :sharps - the spelling that matches this scale, for naming pitches the scale itself doesn't spell (display padding, fretboard labels).
157 158 159 |
# File 'lib/outro_rails/theory/scale.rb', line 157 def preferred_accidentals net_accidentals.negative? ? :flats : :sharps end |
#relative_major ⇒ Object
209 210 211 212 213 214 215 |
# File 'lib/outro_rails/theory/scale.rb', line 209 def relative_major unless minor? raise ArgumentError, "#{pattern_name} has no relative major" end self.class.new(root.at_degree(Interval::MINOR_THIRD, 2), :major) end |
#relative_minor ⇒ Object
190 191 192 193 194 195 196 |
# File 'lib/outro_rails/theory/scale.rb', line 190 def relative_minor unless major? raise ArgumentError, "#{pattern_name} has no relative minor" end self.class.new(degree(6), :natural_minor) end |
#relative_minors ⇒ Object
All three minor forms on the relative tonic (natural, harmonic, melodic) - the inverse of #relative_major.
200 201 202 203 |
# File 'lib/outro_rails/theory/scale.rb', line 200 def relative_minors tonic = relative_minor.root MINOR_PATTERNS.map { |pattern| self.class.new(tonic, pattern) } end |
#size ⇒ Object
Number of degrees - 7 for the diatonic patterns, fewer otherwise
112 113 114 |
# File 'lib/outro_rails/theory/scale.rb', line 112 def size pattern.size end |
#slug ⇒ Object
URL segment for the pattern, e.g. :major_blues => 'major-blues'
102 103 104 |
# File 'lib/outro_rails/theory/scale.rb', line 102 def slug pattern_name.to_s.tr("_", "-") end |
#to_s ⇒ Object
Display name, e.g. "A natural minor"
333 334 335 |
# File 'lib/outro_rails/theory/scale.rb', line 333 def to_s "#{root} #{pattern_name.to_s.tr('_', ' ')}" end |