Module: OutroRails::Theory::Transposer

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

Overview

Transposes freeform chord-chart text between chord symbols and Nashville numbers, or between keys. Tokens that don't parse as a known chord (or number) pass through untouched, so lyrics and other notation survive:

Transposer.transpose_chords("C Am7 F G7", from: "C", to: "Eb")
# => "Eb Cm7 Ab Bb7"
Transposer.chords_to_nashville("C Am7 F G7", key: "C")
# => "1 6m7 4 57"
Transposer.nashville_to_chords("1 6m7 4 5", key: "G")
# => "G Em7 C D"

Constant Summary collapse

NOTE_PATTERN =

A note name with its accidental, shared with NoteName so the two can't drift.

NoteName::NAME_PATTERN
CHORD_PATTERN =

A whole chord symbol: root, everything up to a slash as the quality/extension suffix, then an optional slash bass.

%r{
  \A
  (?<root>#{NOTE_PATTERN})
  (?<suffix>[^/]*)
  (?:/(?<bass>#{NOTE_PATTERN}))?
  \z
}x
WRAPPER_PATTERN =

Chart punctuation that can wrap a chord: "|C|", "(Am)", "F,".

%r{
  \A
  (?<open>[(\[|]*)
  (?<core>.*?)
  (?<close>[)\]|,.;:!?]*)
  \z
}mx
KNOWN_SUFFIXES =

Every quality/extension suffix the vocabulary can symbolize.

ChordVocabulary::SYMBOLS.values
.flat_map(&:values)
.uniq
.freeze
TAIL_PATTERN =
/(?:#{tails})\z/

Class Method Summary collapse

Class Method Details

.annotate_chords(text, from: nil, to: nil) ⇒ Object

Wraps every recognized chord token in text with whatever the block returns, given the symbol as it should be displayed - lyrics and punctuation pass through untouched. Pass from/to to transpose the wrapped symbol at the same time; omitted (or equal) leaves chords spelled as written. Used to render a chart body with its chords highlighted, optionally previewed in another key, without mutating anything in storage:

Transposer.annotate_chords(body, from: "C", to: "D")
{ |symbol| "<mark>#{symbol}</mark>" }


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/outro_rails/theory/transposer.rb', line 174

def annotate_chords(text, from: nil, to: nil)
  from_key = from && Key.parse(from)
  to_key = to && Key.parse(to)

  transform(text) do |core|
    chord = parse_chord(core)
    next nil unless chord

    root = chord[:root]
    bass = chord[:bass]

    if from_key && to_key
      root = from_key.transpose_note(root, to: to_key)
      bass = bass && from_key.transpose_note(bass, to: to_key)
    end

    yield("#{root}#{chord[:suffix]}#{"/#{bass}" if bass}")
  end
end

.chords_to_nashville(text, key:) ⇒ Object

Chord symbols => Nashville numbers relative to key; the quality suffix carries over verbatim (Am7 in C => 6m7, Bb in C => b7).



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/outro_rails/theory/transposer.rb', line 80

def chords_to_nashville(text, key:)
  key = Key.parse(key)

  transform(text) do |core|
    chord = parse_chord(core)
    next nil unless chord

    number = nashville_root(key, chord[:root])
    next nil unless number

    if chord[:bass]
      bass = nashville_root(key, chord[:bass])
      next nil unless bass
    end

    "#{number}#{chord[:suffix]}#{"/#{bass}" if bass}"
  end
end

.nashville_to_chords(text, key:) ⇒ Object

Nashville numbers (or roman numerals) => chord symbols in key, via Key#nashville. Slash basses are degrees too: "1/3" in C => C/E.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/outro_rails/theory/transposer.rb', line 101

def nashville_to_chords(text, key:)
  key = Key.parse(key)

  transform(text) do |core|
    number, bass = core.split("/", 2)

    begin
      symbol = key.nashville(number).fetch(:symbol)

      if bass
        parsed = NashvilleNumber.parse(bass)
        note = key.note_for_degree(
          parsed.degree,
          parsed.accidental_offset
        )
        symbol = "#{symbol}/#{note}"
      end

      symbol
    rescue ArgumentError, KeyError
      nil
    end
  end
end

.scan_chords(text) ⇒ Object

Every distinct chord symbol recognized in freeform chart text, in the order each first appears; lyrics and anything else pass unrecognized ("Amazing Grace, how sweet the C sound" only yields "C"). Used to find which chords a chart references, e.g. to look up their diagrams:

Transposer.scan_chords("C  Am7  F  G7")  # => ["C", "Am7", "F"]


133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/outro_rails/theory/transposer.rb', line 133

def scan_chords(text)
  symbols = []

  transform(text) do |core|
    chord = parse_chord(core)
    next nil unless chord

    symbol =
      "#{chord[:root]}#{chord[:suffix]}" \
      "#{"/#{chord[:bass]}" if chord[:bass]}"
    symbols << symbol unless symbols.include?(symbol)
    nil # never replace - this only scans, it doesn't rewrite.
  end

  symbols
end

.split_bass(symbol) ⇒ Object

Splits a chord symbol into its base symbol and slash bass:

Transposer.split_bass("G/B")  # => ["G", "B"]
Transposer.split_bass("Am7")  # => ["Am7", nil]

Unrecognized input comes back unchanged with a nil bass, so callers can feed it anything scan_chords produced.



157
158
159
160
161
162
# File 'lib/outro_rails/theory/transposer.rb', line 157

def split_bass(symbol)
  chord = parse_chord(symbol.to_s)
  return [ symbol.to_s, nil ] unless chord

  [ "#{chord[:root]}#{chord[:suffix]}", chord[:bass] ]
end

.transpose_chords(text, from:, to:) ⇒ Object

Chord symbols respelled into a new key; each root (and slash bass) keeps its relationship to the key, so G major's F# lands on Bb major's A.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/outro_rails/theory/transposer.rb', line 63

def transpose_chords(text, from:, to:)
  from = Key.parse(from)
  to = Key.parse(to)

  transform(text) do |core|
    chord = parse_chord(core)
    next nil unless chord

    root = from.transpose_note(chord[:root], to: to)
    bass = chord[:bass] && from.transpose_note(chord[:bass], to: to)

    "#{root}#{chord[:suffix]}#{"/#{bass}" if bass}"
  end
end