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)



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

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/head_music/notation/abc/header.rb', line 46

def assign_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
  letter = match[1]
  value = match[2].strip
  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
  when "K"
    @key_signature = HeadMusic::Notation::ABC::KeyMapper.new(value, line_number: line_number).key_signature
    return true
  else
    raise HeadMusic::Notation::ABC::UnsupportedFeatureError.new(
      "Unsupported header field #{letter.inspect}", line_number: line_number, snippet: stripped_line
    )
  end
  false
end

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



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

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.



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

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

#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

#raise_missing_key_errorObject (private)



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

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

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



81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/head_music/notation/abc/header.rb', line 81

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|"

  unless FRACTION_PATTERN.match?(value)
    raise HeadMusic::Notation::ABC::ParseError.new(
      "Invalid meter #{value.inspect}", line_number: line_number, snippet: value
    )
  end
  HeadMusic::Rudiment::Meter.get(value)
end

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



95
96
97
98
99
100
101
102
# File 'lib/head_music/notation/abc/header.rb', line 95

def resolve_unit_note_length(value, line_number)
  unless FRACTION_PATTERN.match?(value)
    raise HeadMusic::Notation::ABC::ParseError.new(
      "Invalid unit note length #{value.inspect}", line_number: line_number, snippet: value
    )
  end
  Rational(value)
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