Class: HeadMusic::Notation::ABC::Header

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

Overview

Splits an ABC tune string into header fields and tune body.

The K: (key) field is required and must be the last header field; everything after that line is the tune body. Requiring K: avoids silently defaulting the composition to C major.

Constant Summary collapse

FIELD_PATTERN =
/\A([A-Za-z]):(.*)\z/
FRACTION_PATTERN =
%r{\A\d+/\d+\z}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(abc_string, start_line: 1) ⇒ Header

start_line offsets reported line numbers, so a tune parsed out of a larger book raises errors with book-relative line numbers.



16
17
18
19
20
21
# File 'lib/head_music/notation/abc/header.rb', line 16

def initialize(abc_string, start_line: 1)
  @annotations = []
  @voice_ids = []
  @start_line = start_line
  parse(abc_string)
end

Instance Attribute Details

#annotationsObject (readonly)

Returns the value of attribute annotations.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def annotations
  @annotations
end

#bodyObject (readonly)

Returns the value of attribute body.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def body
  @body
end

#body_start_lineObject (readonly)

Returns the value of attribute body_start_line.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def body_start_line
  @body_start_line
end

#composerObject (readonly)

Returns the value of attribute composer.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def composer
  @composer
end

#key_signatureObject (readonly)

Returns the value of attribute key_signature.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def key_signature
  @key_signature
end

#meterObject (readonly)

Returns the value of attribute meter.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def meter
  @meter
end

#originObject (readonly)

Returns the value of attribute origin.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def origin
  @origin
end

#reference_numberObject (readonly)

Returns the value of attribute reference_number.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def reference_number
  @reference_number
end

#titleObject (readonly)

Returns the value of attribute title.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def title
  @title
end

#voice_idsObject (readonly)

Returns the value of attribute voice_ids.



10
11
12
# File 'lib/head_music/notation/abc/header.rb', line 10

def voice_ids
  @voice_ids
end

Instance Method Details

#apply_defaultsObject (private)



115
116
117
# File 'lib/head_music/notation/abc/header.rb', line 115

def apply_defaults
  @meter ||= HeadMusic::Rudiment::Meter.common_time
end

#assign_field(stripped_line, line_number) ⇒ Object (private)

Returns true when the field is K:, which terminates the header.



46
47
48
49
50
51
52
# File 'lib/head_music/notation/abc/header.rb', line 46

def assign_field(stripped_line, line_number)
  letter, value = parse_field(stripped_line, line_number)
  return assign_key(value, line_number) if letter == "K"

  store_field(letter, value, line_number, stripped_line)
  false
end

#assign_key(value, line_number) ⇒ Object (private)



65
66
67
68
# File 'lib/head_music/notation/abc/header.rb', line 65

def assign_key(value, line_number)
  @key_signature = HeadMusic::Notation::ABC::KeyMapper.new(value, line_number: line_number).key_signature
  true
end

#capture_body(lines, key_line_index) ⇒ Object (private)



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

def capture_body(lines, key_line_index)
  @body = lines[(key_line_index + 1)..].join
  @body_start_line = key_line_index + 1 + @start_line
end

#default_unit_note_lengthObject (private)

ABC 2.1 default: when L: is absent, meters smaller than 3/4 imply sixteenth notes; 3/4 and larger imply eighth notes.



121
122
123
124
# File 'lib/head_music/notation/abc/header.rb', line 121

def default_unit_note_length
  meter_fraction = Rational(meter.top_number, meter.bottom_number)
  (meter_fraction < Rational(3, 4)) ? Rational(1, 16) : Rational(1, 8)
end

#ensure_fraction!(value, description, line_number) ⇒ Object (private)



107
108
109
110
111
112
113
# File 'lib/head_music/notation/abc/header.rb', line 107

def ensure_fraction!(value, description, line_number)
  return if FRACTION_PATTERN.match?(value)

  raise HeadMusic::Notation::ABC::ParseError.new(
    "Invalid #{description} #{value.inspect}", line_number: line_number, snippet: value
  )
end

#parse(abc_string) ⇒ Object (private)



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/head_music/notation/abc/header.rb', line 29

def parse(abc_string)
  lines = abc_string.to_s.lines
  lines.each_with_index do |raw_line, index|
    line_number = index + @start_line
    stripped = raw_line.strip
    next if stripped.empty? || stripped.start_with?("%")

    if assign_field(stripped, line_number)
      capture_body(lines, index)
      break
    end
  end
  raise_missing_key_error unless @key_signature
  apply_defaults
end

#parse_field(stripped_line, line_number) ⇒ Object (private)



54
55
56
57
58
59
60
61
62
63
# File 'lib/head_music/notation/abc/header.rb', line 54

def parse_field(stripped_line, line_number)
  match = FIELD_PATTERN.match(stripped_line)
  unless match
    raise HeadMusic::Notation::ABC::ParseError.new(
      "Expected a header field; the tune body may not begin before the K: (key) field",
      line_number: line_number, snippet: stripped_line
    )
  end
  [match[1], match[2].strip]
end

#raise_missing_key_errorObject (private)



126
127
128
129
# File 'lib/head_music/notation/abc/header.rb', line 126

def raise_missing_key_error
  raise HeadMusic::Notation::ABC::ParseError,
    "Missing required K: (key) field"
end

#resolve_meter(value, line_number) ⇒ Object (private)



92
93
94
95
96
97
98
99
100
# File 'lib/head_music/notation/abc/header.rb', line 92

def resolve_meter(value, line_number)
  # "C" and "C|" must be translated before calling Meter.get, which
  # memoizes by identifier and would cache a meaningless "C" meter.
  return HeadMusic::Rudiment::Meter.common_time if value == "C"
  return HeadMusic::Rudiment::Meter.cut_time if value == "C|"

  ensure_fraction!(value, "meter", line_number)
  HeadMusic::Rudiment::Meter.get(value)
end

#resolve_unit_note_length(value, line_number) ⇒ Object (private)



102
103
104
105
# File 'lib/head_music/notation/abc/header.rb', line 102

def resolve_unit_note_length(value, line_number)
  ensure_fraction!(value, "unit note length", line_number)
  Rational(value)
end

#store_field(letter, value, line_number, stripped_line) ⇒ Object (private)



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/head_music/notation/abc/header.rb', line 70

def store_field(letter, value, line_number, stripped_line)
  case letter
  when "X" then @reference_number = value
  when "T" then @title = value
  when "C" then @composer = value
  when "O" then @origin = value
  when "N" then @annotations << value
  when "M" then @meter = resolve_meter(value, line_number)
  when "L" then @unit_note_length = resolve_unit_note_length(value, line_number)
  when "V" then @voice_ids << value.split.first
  else
    raise HeadMusic::Notation::ABC::UnsupportedFeatureError.new(
      "Unsupported header field #{letter.inspect}", line_number: line_number, snippet: stripped_line
    )
  end
end

#unit_note_lengthObject



23
24
25
# File 'lib/head_music/notation/abc/header.rb', line 23

def unit_note_length
  @unit_note_length || default_unit_note_length
end