Module: OutroRails::Theory::ChordVocabulary

Defined in:
lib/outro_rails/theory/chord_vocabulary.rb

Overview

The chord vocabulary including which qualities exist, which extensions/alterations/added tones apply to them, the intervals they contribute, and how the combinations are symbolized.

Constant Summary collapse

QUALITIES =

Qualities mapped to their intervals.

{
  major:       [ 0, 4, 7 ],
  minor:       [ 0, 3, 7 ],
  diminished:  [ 0, 3, 6 ],
  augmented:   [ 0, 4, 8 ],
  suspended_2: [ 0, 2, 7 ],
  suspended_4: [ 0, 5, 7 ],
  power:       [ 0, 7 ]
}.freeze
EXTENSIONS =

Which extensions are valid on which qualities, and the intervals they add.

"Extension" here means anything appended to a bare triad symbol as one lookup axis - so it deliberately includes '6' (strictly an added tone) and 'dim7' (strictly part of the quality) alongside true extensions like '7'/'9'/'11'/'13'.

{
  '6':    {
            add: [ 9 ],
            qualities: %i[major minor]
          },
  '7':    {
            add: [ 10 ],
            qualities: %i[major minor diminished suspended_2 suspended_4]
          },
  'maj7': {
            add: [ 11 ],
            qualities: %i[major minor augmented]
          },
  'dim7': {
            add: [ 9 ],
            qualities: %i[diminished]
          },
  '9':    {
            add: [ 10, 14 ],
            qualities: %i[major minor suspended_4]
          },
  '11':   {
            add: [ 10, 14, 17 ],
            qualities: %i[major minor]
          },
  '13':   {
            add: [ 10, 14, 21 ],
            qualities: %i[major minor suspended_4]
          }
}.freeze
SYMBOLS =

How each quality + extension pair is symbolized. :base is the bare chord.

{
  major:        {
                  base:   "",
                  '6':    "6",
                  '7':    "7",
                  'maj7': "maj7",
                  '9':    "9",
                  '11':   "11",
                  '13':   "13"
                },
  minor:        {
                  base:   "m",
                  '6':    "m6",
                  '7':    "m7",
                  'maj7': "m(maj7)",
                  '9':    "m9",
                  '11':   "m11",
                  '13':   "m13"
                },
  diminished:   {
                  base:   "dim",
                  '7':    "m7b5",
                  'dim7': "dim7"
                },
  augmented:    {
                  base:   "aug",
                  "maj7": "maj7#5"
                },
  suspended_2:  {
                  base: "sus2",
                  '7':  "7sus2"
                },
  suspended_4:  {
                  base: "sus4",
                  '7':  "7sus4",
                  '9':  "9sus4",
                  '13': "13sus4"
                },
  power:        {
                  base: "5"
                }
}.freeze
ALTERATIONS =

natural: the diatonic interval the alteration replaces (if present) altered: the interval it becomes/adds.

{
  'b5':  { natural: 7,  altered: 6 },
  '#5':  { natural: 7,  altered: 8 },
  'b9':  { natural: 14, altered: 13 },
  '#9':  { natural: 14, altered: 15 },
  '#11': { natural: 17, altered: 18 },
  'b13': { natural: 21, altered: 20 }
}.freeze
ADDED_TONE_INTERVALS =

Mapping of added-tone names to the intervals (in semitones) they contribute above the root.

{
  add2:  2,
  add4:  5,
  add6:  9,
  add9:  14,
  add11: 17,
  add13: 21
}.freeze

Class Method Summary collapse

Class Method Details

.apply_alterations(base, alterations) ⇒ Object

Apply interval alterations by replacing natural intervals with their altered equivalents, then return the intervals in ascending order.



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/outro_rails/theory/chord_vocabulary.rb', line 163

def apply_alterations(base, alterations)
  intervals = base.dup

  alterations.each do |alteration|
    spec = ALTERATIONS.fetch(alteration.to_sym)
    intervals.delete(spec[:natural])
    intervals << spec[:altered]
  end

  intervals.sort
end

.intervals(quality:, extension: nil) ⇒ Object

The intervals for a quality with an extension merged in (drops duplicated pitch classes, e.g. the 11th on a sus4 chord).



154
155
156
157
158
159
# File 'lib/outro_rails/theory/chord_vocabulary.rb', line 154

def intervals(quality:, extension: nil)
  base = QUALITIES.fetch(quality.to_sym)
  return base unless extension

  merge(base, EXTENSIONS.fetch(extension.to_sym).fetch(:add))
end

.merge(base, additions) ⇒ Object

Merge added intervals into a base set; drops any interval whose pitch class is already present.



180
181
182
183
184
185
186
187
188
189
# File 'lib/outro_rails/theory/chord_vocabulary.rb', line 180

def merge(base, additions)
  pcs = pitch_classes(base)

  (
    base +
    additions.reject do |i|
      pcs.include?(i % Interval::SEMITONES_PER_OCTAVE)
    end
  ).sort
end

.pitch_classes(intervals) ⇒ Object

Reduce intervals to pitch classes (modulo 12), discarding octave information.



193
194
195
# File 'lib/outro_rails/theory/chord_vocabulary.rb', line 193

def pitch_classes(intervals)
  intervals.map { |i| i % Interval::SEMITONES_PER_OCTAVE }
end

.slug(symbol) ⇒ Object

URL/file slug for a chord or voicing symbol: 'F#m7b5' => 'fsharpm7flat5', 'A/C#' => 'aslashcsharp'



141
142
143
144
145
146
147
148
149
150
# File 'lib/outro_rails/theory/chord_vocabulary.rb', line 141

def slug(symbol)
  symbol
    .gsub("b", "flat")
    .gsub("#", "sharp")
    .gsub("(", "-")
    .gsub(")", "-")
    .gsub("/", "slash")
    .delete_suffix("-")
    .downcase
end

.symbol(root_name:, quality:, extension: nil, alterations: [], added_tones: []) ⇒ Object

"Cmaj7", "F#m7b5", "Bbsus4" - the canonical symbol for a root/quality/extension/etc. combination.



129
130
131
132
133
134
135
136
137
# File 'lib/outro_rails/theory/chord_vocabulary.rb', line 129

def symbol(root_name:,
           quality:,
           extension: nil,
           alterations: [],
           added_tones: [])
  "#{root_name}" \
    "#{SYMBOLS.fetch(quality.to_sym).fetch(extension&.to_sym || :base)}" \
    "#{alterations.join}#{added_tones.join}"
end