Class: HeadMusic::Notation::ABC::KeyMapper

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/notation/abc/key_mapper.rb

Overview

Converts an ABC K: field value into a key signature.

ABC key values like “Ador” or “F#m” cannot be passed directly to KeySignature.get, which splits tonic and scale type on whitespace, so the mode word is normalized into a “tonic mode” string first.

Constant Summary collapse

KEY_PATTERN =

The alteration group wraps the pattern in (?:…) before making it optional. Writing (…)? instead leaves match[2] nil for every unaltered key (“C”, “Ador”), which raises downstream where the capture is interpolated into a string.

/\A([A-G])((?:#{HeadMusic::Rudiment::Alteration::PATTERN.source})?)\s*([A-Za-z]*)/
MODE_NAMES_BY_PREFIX =

Mode words are matched case-insensitively on their first three letters, so abbreviations (“dor”) and full names (“Dorian”) both resolve.

{
  "maj" => "major",
  "ion" => "major",
  "min" => "minor",
  "aeo" => "minor",
  "dor" => "dorian",
  "phr" => "phrygian",
  "lyd" => "lydian",
  "mix" => "mixolydian",
  "loc" => "locrian"
}.freeze
ABC_SUFFIXES_BY_MODE =

MODE_NAMES_BY_PREFIX is many-to-one, so rendering uses this explicit inverse map. Every suffix here parses back to an equal key signature (ionian/aeolian differ from major/minor only in name, not alterations).

{
  "major" => "",
  "ionian" => "",
  "minor" => "m",
  "aeolian" => "m",
  "dorian" => "dor",
  "phrygian" => "phr",
  "lydian" => "lyd",
  "mixolydian" => "mix",
  "locrian" => "loc"
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(value, line_number: nil) ⇒ KeyMapper

Returns a new instance of KeyMapper.



79
80
81
82
# File 'lib/head_music/notation/abc/key_mapper.rb', line 79

def initialize(value, line_number: nil)
  @value = value.to_s.strip
  @line_number = line_number
end

Instance Attribute Details

#line_numberObject (readonly)

Returns the value of attribute line_number.



77
78
79
# File 'lib/head_music/notation/abc/key_mapper.rb', line 77

def line_number
  @line_number
end

#valueObject (readonly)

Returns the value of attribute value.



77
78
79
# File 'lib/head_music/notation/abc/key_mapper.rb', line 77

def value
  @value
end

Class Method Details

.abc_value(key_signature) ⇒ Object

Returns the ABC K: field value for a key signature.



42
43
44
45
# File 'lib/head_music/notation/abc/key_mapper.rb', line 42

def self.abc_value(key_signature)
  key_signature = HeadMusic::Rudiment::KeySignature.get(key_signature)
  "#{tonic_string(key_signature)}#{mode_suffix(key_signature)}"
end

.mode_suffix(key_signature) ⇒ Object (private)



66
67
68
69
# File 'lib/head_music/notation/abc/key_mapper.rb', line 66

def self.mode_suffix(key_signature)
  ABC_SUFFIXES_BY_MODE[key_signature.scale_type.name.to_s] ||
    raise_render_error("Cannot render scale type #{key_signature.scale_type} in an ABC K: field")
end

.raise_render_error(message) ⇒ Object (private)



72
73
74
# File 'lib/head_music/notation/abc/key_mapper.rb', line 72

def self.raise_render_error(message)
  raise HeadMusic::Notation::ABC::RenderError, message
end

.tonic_string(key_signature) ⇒ Object (private)



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/head_music/notation/abc/key_mapper.rb', line 47

def self.tonic_string(key_signature)
  spelling = key_signature.tonic_spelling
  alteration = spelling.alteration
  if alteration && (alteration.double_sharp? || alteration.double_flat?)
    raise_render_error("Cannot render double-altered tonic #{spelling} in an ABC K: field")
  end

  # ABC convention uses ASCII "#"/"b" rather than the unicode signs that
  # Spelling#to_s produces. The double-altered guard above runs first, since this
  # would otherwise happily emit "Cx".
  #
  # A natural sign is dropped rather than converted. Accidentals.to_ascii preserves
  # "♮" on purpose — mapping it away would turn the spelling "C♮" into the different
  # spelling "C" — but an ABC K: field has no way to express a natural tonic, and
  # "C♮ major" names the same key signature as "C major".
  HeadMusic::Utilities::Accidentals.to_ascii(spelling.to_s).delete("")
end

Instance Method Details

#key_signatureObject



88
89
90
# File 'lib/head_music/notation/abc/key_mapper.rb', line 88

def key_signature
  HeadMusic::Rudiment::KeySignature.get(key_signature_name)
end

#key_signature_nameObject



84
85
86
# File 'lib/head_music/notation/abc/key_mapper.rb', line 84

def key_signature_name
  "#{tonic} #{mode_name}"
end

#matchObject (private)



94
95
96
97
# File 'lib/head_music/notation/abc/key_mapper.rb', line 94

def match
  @match ||= KEY_PATTERN.match(value) ||
    raise_parse_error("Unrecognized key")
end

#mode_nameObject (private)



114
115
116
117
118
119
120
121
# File 'lib/head_music/notation/abc/key_mapper.rb', line 114

def mode_name
  word = match[3].downcase
  return "major" if word.empty?
  return "minor" if word == "m"

  MODE_NAMES_BY_PREFIX[word[0, 3]] ||
    raise_parse_error("Unrecognized mode")
end

#raise_parse_error(message) ⇒ Object (private)



123
124
125
126
127
128
129
# File 'lib/head_music/notation/abc/key_mapper.rb', line 123

def raise_parse_error(message)
  raise HeadMusic::Notation::ABC::ParseError.new(
    "#{message} in K: field value #{value.inspect}",
    line_number: line_number,
    snippet: value
  )
end

#tonicObject (private)

An ABC K: field cannot express a double-altered tonic, and .abc_value raises rather than rendering one. Reject it on the way in too: KEY_PATTERN interpolates the whole Alteration::PATTERN so that “bb” and “##” bind correctly, which also makes them matchable here. Without this guard “K:Cx” would parse to C𝄪 major and report “no sharps or flats” — a plausible-looking wrong answer where the narrower pattern used to give a clear parse error.



105
106
107
108
109
110
111
112
# File 'lib/head_music/notation/abc/key_mapper.rb', line 105

def tonic
  alteration = HeadMusic::Rudiment::Alteration.get(match[2])
  if alteration && (alteration.double_sharp? || alteration.double_flat?)
    raise_parse_error("Cannot express double-altered tonic #{match[1]}#{match[2]} in an ABC K: field")
  end

  match[1] + match[2]
end