Class: HeadMusic::Notation::ABC::Writer

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

Overview

Renders a HeadMusic::Content::Composition as an ABC tune string.

Whole-composition problems (multiple voices, mid-piece meter or key changes, positional gaps) raise before any string assembly, and #to_s only returns a fully assembled document, so callers never receive a truncated tune.

Repeat barlines and voltas are deliberately not rendered; bars carrying repeat flags degrade to plain bar lines.

Constant Summary collapse

UNIT_NOTE_LENGTH =

A fixed unit note length keeps the L: field and the duration multiplier arithmetic in sync.

Rational(1, 8)
BARS_PER_LINE =
4

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(composition, reference_number: 1) ⇒ Writer

Returns a new instance of Writer.



20
21
22
23
# File 'lib/head_music/notation/abc/writer.rb', line 20

def initialize(composition, reference_number: 1)
  @composition = composition
  @reference_number = reference_number
end

Instance Attribute Details

#compositionObject (readonly)

Returns the value of attribute composition.



18
19
20
# File 'lib/head_music/notation/abc/writer.rb', line 18

def composition
  @composition
end

#reference_numberObject (readonly)

Returns the value of attribute reference_number.



18
19
20
# File 'lib/head_music/notation/abc/writer.rb', line 18

def reference_number
  @reference_number
end

Instance Method Details

#bar_stringsObject (private)



105
106
107
# File 'lib/head_music/notation/abc/writer.rb', line 105

def bar_strings
  @bar_strings ||= build_bar_strings
end

#body_linesObject (private)



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

def body_lines
  return [] if bar_strings.empty?

  lines = bar_strings.each_slice(BARS_PER_LINE).map do |line_bars|
    line_bars.join("|") + "|"
  end
  lines[-1] = lines[-1].sub(/\|\z/, "|]")
  lines
end

#build_bar_stringsObject (private)



109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/head_music/notation/abc/writer.rb', line 109

def build_bar_strings
  pitch_writer = PitchWriter.new(composition.key_signature)
  duration_writer = DurationWriter.new(UNIT_NOTE_LENGTH)
  bar_groups = placements.chunk_while do |previous, current|
    previous.position.bar_number == current.position.bar_number
  end
  bar_groups.each_with_index.map do |bar_placements, index|
    # Accidental state must mirror what a re-parse accumulates bar by bar.
    pitch_writer.start_new_bar if index.positive?
    bar_placements.map { |placement| token(placement, pitch_writer, duration_writer) }.join(" ")
  end
end

#ensure_contiguous_placementsObject (private)



56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/head_music/notation/abc/writer.rb', line 56

def ensure_contiguous_placements
  first = placements.first
  return unless first

  ensure_placement_starts_bar(first)
  placements.each_cons(2) do |previous, current|
    next if current.position == previous.next_position

    raise RenderError, "expected a placement at #{previous.next_position}, " \
      "found one at #{current.position}; insert explicit rests to fill gaps"
  end
end

#ensure_no_mid_piece_changesObject (private)



44
45
46
47
48
49
50
51
52
53
54
# File 'lib/head_music/notation/abc/writer.rb', line 44

def ensure_no_mid_piece_changes
  composition.bars.each_with_index do |bar, index|
    bar_number = composition.earliest_bar_number + index
    if bar.meter
      raise RenderError, "cannot render the meter change at bar #{bar_number} in ABC output"
    end
    next unless bar.key_signature

    raise RenderError, "cannot render the key signature change at bar #{bar_number} in ABC output"
  end
end

#ensure_placement_starts_bar(placement) ⇒ Object (private)

Raises:



69
70
71
72
73
74
75
# File 'lib/head_music/notation/abc/writer.rb', line 69

def ensure_placement_starts_bar(placement)
  position = placement.position
  return if position.count == 1 && position.tick.zero?

  raise RenderError, "the first placement must start its bar " \
    "(found #{position}); insert explicit rests to fill the gap"
end

#ensure_single_voiceObject (private)

Raises:



38
39
40
41
42
# File 'lib/head_music/notation/abc/writer.rb', line 38

def ensure_single_voice
  return if composition.voices.length <= 1

  raise RenderError, "multi-voice ABC output is not supported"
end

#header_linesObject (private)



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

def header_lines
  [
    "X:#{reference_number}",
    "T:#{composition.name}",
    composition.composer && "C:#{composition.composer}",
    composition.origin && "O:#{composition.origin}",
    "M:#{composition.meter}",
    "L:#{UNIT_NOTE_LENGTH.numerator}/#{UNIT_NOTE_LENGTH.denominator}",
    # The parser requires K: to terminate the header.
    "K:#{KeyMapper.abc_value(composition.key_signature)}"
  ].compact
end

#placementsObject (private)



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

def placements
  voice = composition.voices.first
  voice ? voice.placements : []
end

#to_sObject



25
26
27
28
# File 'lib/head_music/notation/abc/writer.rb', line 25

def to_s
  validate!
  (header_lines + body_lines).join("\n") + "\n"
end

#token(placement, pitch_writer, duration_writer) ⇒ Object (private)



122
123
124
125
126
127
# File 'lib/head_music/notation/abc/writer.rb', line 122

def token(placement, pitch_writer, duration_writer)
  multiplier = duration_writer.multiplier_string(placement.rhythmic_value)
  return "z#{multiplier}" if placement.rest?

  "#{pitch_writer.token(placement.pitch)}#{multiplier}"
end

#validate!Object (private)



32
33
34
35
36
# File 'lib/head_music/notation/abc/writer.rb', line 32

def validate!
  ensure_single_voice
  ensure_no_mid_piece_changes
  ensure_contiguous_placements
end