Class: OutroRails::Theory::Key
- Inherits:
-
Object
- Object
- OutroRails::Theory::Key
- Defined in:
- lib/outro_rails/theory/key.rb
Overview
A key: a tonic NoteName plus a mode. Not persisted. This object can transpose and turn Nashville numbers into chords.
key = Theory::Key.new('G')
key.note_names.map(&:to_s) # => ['G', 'A', 'B', 'C', 'D', 'E', 'F#']
key.transpose_to('Bb') # semitone/spelling aware
key.nashville('2m') # => { root: A, quality: :minor, ... }
Defined Under Namespace
Classes: CirclePosition
Constant Summary collapse
- MODES =
{ major: :major, minor: :natural_minor }.freeze
- MAJOR_TONICS =
Tonics conventionally used for each mode (the circle of fifths, flattened). Used to pick spellings when transposing.
%w[C G D A E B F# Gb Db Ab Eb Bb F].freeze
- MINOR_TONICS =
%w[A E B F# C# G# Eb Bb F C G D].freeze
Instance Attribute Summary collapse
-
#mode ⇒ Object
readonly
Returns the value of attribute mode.
-
#tonic ⇒ Object
readonly
Returns the value of attribute tonic.
Class Method Summary collapse
-
.canonical(mode = :major) ⇒ Object
One key of the given mode per canonical root spelling - the keys the UI offers wherever a key is picked (chart forms, transposer).
-
.circle_positions ⇒ Object
The 12 wheel positions in circle order starting at C (clockwise: C G D A E B F#/Gb Db Ab Eb Bb F), pairing each major with its relative minor and merging enharmonic spellings into one position.
-
.parse(value) ⇒ Object
Parses a key from a string or returns the key unchanged if already a Key.
Instance Method Summary collapse
-
#circle_of_fifths ⇒ Object
Every key of this mode in circle-of-fifths order, starting from this key's position.
-
#degree_of(note) ⇒ Object
Inverse of #note_for_degree: [degree, accidental_offset] locating a note relative to this key by letter.
-
#flats? ⇒ Boolean
True if the key signature contains flats.
-
#initialize(tonic, mode = :major) ⇒ Key
constructor
Creates an immutable key from a tonic and mode.
-
#interval_to(other) ⇒ Object
Semitones from this key up to another (0..11).
-
#major? ⇒ Boolean
True if this is a major key.
-
#minor? ⇒ Boolean
True if this is a minor key.
-
#nashville(input) ⇒ Object
Resolves a Nashville number (or roman numeral) to the attributes needed to find or build a chord in this key:.
-
#note_for_degree(number, accidental_offset = 0) ⇒ Object
1-based degree with an optional accidental adjustment, spelled the way the key would spell it (b7 in C major => Bb).
-
#note_names ⇒ Object
The seven note names of the key's diatonic scale.
-
#parallel ⇒ Object
The parallel major or minor key, sharing the same tonic.
-
#relative ⇒ Object
The relative major or minor key.
-
#scale ⇒ Object
The scale corresponding to this key and mode.
-
#sharps? ⇒ Boolean
True if the key signature contains sharps.
-
#signature ⇒ Object
Positive = sharps, negative = flats, 0 = C major / A minor.
-
#to_s ⇒ Object
String representation using conventional notation ("C", "F#", "Am").
-
#transpose_note(note, to:) ⇒ Object
Transposes any note, respelled for the destination key: the note's position relative to this key is preserved.
-
#transpose_to(new_tonic) ⇒ Object
Returns a new key with the same mode and a different tonic.
Constructor Details
#initialize(tonic, mode = :major) ⇒ Key
Creates an immutable key from a tonic and mode.
83 84 85 86 87 88 89 |
# File 'lib/outro_rails/theory/key.rb', line 83 def initialize(tonic, mode = :major) @tonic = NoteName.parse(tonic) @mode = mode.to_sym MODES.fetch(@mode) { raise ArgumentError, "unknown mode #{mode.inspect}" } freeze end |
Instance Attribute Details
#mode ⇒ Object (readonly)
Returns the value of attribute mode.
37 38 39 |
# File 'lib/outro_rails/theory/key.rb', line 37 def mode @mode end |
#tonic ⇒ Object (readonly)
Returns the value of attribute tonic.
37 38 39 |
# File 'lib/outro_rails/theory/key.rb', line 37 def tonic @tonic end |
Class Method Details
.canonical(mode = :major) ⇒ Object
One key of the given mode per canonical root spelling - the keys the UI offers wherever a key is picked (chart forms, transposer).
58 59 60 |
# File 'lib/outro_rails/theory/key.rb', line 58 def self.canonical(mode = :major) NoteName::CANONICAL_ROOTS.map { |root| new(root, mode) } end |
.circle_positions ⇒ Object
The 12 wheel positions in circle order starting at C (clockwise: C G D A E B F#/Gb Db Ab Eb Bb F), pairing each major with its relative minor and merging enharmonic spellings into one position.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/outro_rails/theory/key.rb', line 65 def self.circle_positions MAJOR_TONICS.each_with_object([]) do |tonic, positions| key = new(tonic, :major) if positions.any? && positions.last.pitch_class == key.tonic.pitch_class positions.last.majors << key positions.last.minors << key.relative else positions << CirclePosition.new( majors: [ key ], minors: [ key.relative ] ) end end end |
.parse(value) ⇒ Object
Parses a key from a string or returns the key unchanged if already a Key. Strings ending in "m", "min", or "minor" become minor keys; everything else is treated as major.
42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/outro_rails/theory/key.rb', line 42 def self.parse(value) return value if value.is_a?(self) text = value.to_s.strip pattern = /\A([A-Ga-g](?:#{NoteName::ACCIDENTAL_PATTERN})?)\s*(m|min|minor)\z/i if (match = text.match(pattern)) new(match[1], :minor) else new(text, :major) end end |
Instance Method Details
#circle_of_fifths ⇒ Object
Every key of this mode in circle-of-fifths order, starting from this key's position. Major yields 13 keys, not 12 - F# and Gb both appear at the enharmonic seam (see MAJOR_TONICS).
223 224 225 226 227 228 |
# File 'lib/outro_rails/theory/key.rb', line 223 def circle_of_fifths tonics = major? ? MAJOR_TONICS : MINOR_TONICS keys = tonics.map { |t| self.class.new(t, mode) } start = keys.index { |k| k.tonic.pitch_class == tonic.pitch_class } || 0 keys.rotate(start) end |
#degree_of(note) ⇒ Object
Inverse of #note_for_degree: [degree, accidental_offset] locating a note relative to this key by letter. Bb in C major => [7, -1]; F# in C major => [4, 1].
157 158 159 160 161 162 163 164 165 |
# File 'lib/outro_rails/theory/key.rb', line 157 def degree_of(note) note = NoteName.parse(note) index = note_names.index { |name| name.letter == note.letter } [ index + 1, note.accidental_offset - note_names[index].accidental_offset ] end |
#flats? ⇒ Boolean
True if the key signature contains flats.
122 123 124 |
# File 'lib/outro_rails/theory/key.rb', line 122 def flats? signature.negative? end |
#interval_to(other) ⇒ Object
Semitones from this key up to another (0..11).
168 169 170 171 172 173 |
# File 'lib/outro_rails/theory/key.rb', line 168 def interval_to(other) other = self.class.parse(other) semitones = other.tonic.pitch_class - tonic.pitch_class semitones % Interval::SEMITONES_PER_OCTAVE end |
#major? ⇒ Boolean
True if this is a major key.
92 93 94 |
# File 'lib/outro_rails/theory/key.rb', line 92 def major? mode == :major end |
#minor? ⇒ Boolean
True if this is a minor key.
97 98 99 |
# File 'lib/outro_rails/theory/key.rb', line 97 def minor? mode == :minor end |
#nashville(input) ⇒ Object
Resolves a Nashville number (or roman numeral) to the attributes needed to find or build a chord in this key:
Key.new('G').nashville('4')
=> { root_name: "C", quality: :major, ... }
Key.new('G').nashville('2m7')
=> { root_name: "A", quality: :minor, extension: "7" }
Key.new('C').nashville('bVII')
=> { root_name: "Bb", quality: :major, ... }
The result maps directly onto OutroRails::Chord columns, so the
lookup is Chord.find_by(root_name:, quality:, extension:) - and
"the musician flipped the minor 2 to a major 2" is just a different
quality in the same lookup.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/outro_rails/theory/key.rb', line 204 def nashville(input) number = NashvilleNumber.parse(input) root = note_for_degree(number.degree, number.accidental_offset) quality = number.quality || default_quality_for(number) { root_name: root.to_s, root_pitch_class: root.pitch_class, quality: quality, extension: number.extension, symbol: ChordVocabulary.symbol( root_name: root, quality: quality, extension: number.extension ) } end |
#note_for_degree(number, accidental_offset = 0) ⇒ Object
1-based degree with an optional accidental adjustment, spelled the way the key would spell it (b7 in C major => Bb).
142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/outro_rails/theory/key.rb', line 142 def note_for_degree(number, accidental_offset = 0) note = scale.degree(number) return note if accidental_offset.zero? NoteName.new( note.letter, NoteName::ACCIDENTALS_BY_OFFSET.fetch( note.accidental_offset + accidental_offset ) ) end |
#note_names ⇒ Object
The seven note names of the key's diatonic scale.
107 108 109 |
# File 'lib/outro_rails/theory/key.rb', line 107 def note_names scale.note_names end |
#parallel ⇒ Object
The parallel major or minor key, sharing the same tonic.
136 137 138 |
# File 'lib/outro_rails/theory/key.rb', line 136 def parallel self.class.new(tonic, major? ? :minor : :major) end |
#relative ⇒ Object
The relative major or minor key.
127 128 129 130 131 132 133 |
# File 'lib/outro_rails/theory/key.rb', line 127 def relative if major? self.class.new(scale.degree(6), :minor) else self.class.new(scale.degree(3), :major) end end |
#scale ⇒ Object
The scale corresponding to this key and mode.
102 103 104 |
# File 'lib/outro_rails/theory/key.rb', line 102 def scale Scale.new(tonic, MODES.fetch(mode)) end |
#sharps? ⇒ Boolean
True if the key signature contains sharps.
117 118 119 |
# File 'lib/outro_rails/theory/key.rb', line 117 def sharps? signature.positive? end |
#signature ⇒ Object
Positive = sharps, negative = flats, 0 = C major / A minor.
112 113 114 |
# File 'lib/outro_rails/theory/key.rb', line 112 def signature scale.net_accidentals end |
#to_s ⇒ Object
String representation using conventional notation ("C", "F#", "Am").
231 232 233 |
# File 'lib/outro_rails/theory/key.rb', line 231 def to_s major? ? tonic.to_s : "#{tonic}m" end |
#transpose_note(note, to:) ⇒ Object
Transposes any note, respelled for the destination key: the note's position relative to this key is preserved. Moving F# (the 7th of G major) to the key of Bb yields A, and Bb's b7 yields Ab, not G#.
183 184 185 186 187 188 |
# File 'lib/outro_rails/theory/key.rb', line 183 def transpose_note(note, to:) destination = self.class.parse(to) degree, accidental_offset = degree_of(note) destination.note_for_degree(degree, accidental_offset) end |
#transpose_to(new_tonic) ⇒ Object
Returns a new key with the same mode and a different tonic.
176 177 178 |
# File 'lib/outro_rails/theory/key.rb', line 176 def transpose_to(new_tonic) self.class.new(new_tonic, mode) end |