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

Inherits:
Object
  • Object
show all
Includes:
PlacementValidation
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.



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

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

Instance Attribute Details

#compositionObject (readonly)

Returns the value of attribute composition.



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

def composition
  @composition
end

#reference_numberObject (readonly)

Returns the value of attribute reference_number.



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

def reference_number
  @reference_number
end

Instance Method Details

#bar_stringsObject (private)



119
120
121
# File 'lib/head_music/notation/abc/writer.rb', line 119

def bar_strings
  @bar_strings ||= build_bar_strings
end

#body_linesObject (private)



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

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)



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

def build_bar_strings
  pitch_writer = PitchWriter.new(composition.key_signature)
  duration_writer = DurationWriter.new(UNIT_NOTE_LENGTH)
  placements_by_bar.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?
    render_bar(bar_placements, pitch_writer, duration_writer)
  end
end

#chord_token(placement, pitch_writer, multiplier) ⇒ Object (private)



167
168
169
170
171
172
173
# File 'lib/head_music/notation/abc/writer.rb', line 167

def chord_token(placement, pitch_writer, multiplier)
  # Pitches are emitted low-to-high, and the oracle sees them in that
  # same order, so the writer's bar-accidental state cannot diverge from
  # what a re-parse of the emitted brackets accumulates.
  pitch_tokens = placement.pitches.sort.map { |pitch| pitch_writer.token(pitch) }
  "[#{pitch_tokens.join}]#{multiplier}"
end

#ensure_contiguous_placementsObject (private)



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

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)



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

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_pitched_sounds(placement) ⇒ Object (private) Originally defined in module PlacementValidation

Raises:

  • (render_error_class)

#ensure_placement_starts_bar(placement) ⇒ Object (private)

Raises:



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

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:



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

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

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

#header_linesObject (private)



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

def header_lines
  [
    "X:#{reference_number}",
    "T:#{composition.name}",
    optional_field("C", composition.composer),
    optional_field("O", composition.origin),
    "M:#{composition.meter}",
    unit_note_length_field,
    key_field
  ].compact
end

#join_bar_tokens(placements, tokens) ⇒ Object (private)

Suppresses the inter-token space only where the following placement was authored as beamed to its predecessor (beam_break_before == false). A true or nil flag keeps the space, so programmatic (nil-flag) compositions render with today’s every-token spacing. Every bar token (note, rest, or [..] chord) re-lexes unambiguously with no separator, so dropping the space is safe.



150
151
152
153
154
155
# File 'lib/head_music/notation/abc/writer.rb', line 150

def join_bar_tokens(placements, tokens)
  tokens.each_with_index.reduce(+"") do |line, (token, index)|
    separator = (index.zero? || placements[index].beam_break_before == false) ? "" : " "
    line << separator << token
  end
end

#key_fieldObject (private)



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

def key_field
  # The parser requires K: to terminate the header.
  "K:#{KeyMapper.abc_value(composition.key_signature)}"
end

#optional_field(letter, value) ⇒ Object (private)



96
97
98
# File 'lib/head_music/notation/abc/writer.rb', line 96

def optional_field(letter, value)
  "#{letter}:#{value}" if value
end

#placementsObject (private)



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

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

#placements_by_barObject (private)



133
134
135
136
137
# File 'lib/head_music/notation/abc/writer.rb', line 133

def placements_by_bar
  placements.chunk_while do |previous, current|
    previous.position.bar_number == current.position.bar_number
  end
end

#render_bar(bar_placements, pitch_writer, duration_writer) ⇒ Object (private)



139
140
141
142
# File 'lib/head_music/notation/abc/writer.rb', line 139

def render_bar(bar_placements, pitch_writer, duration_writer)
  tokens = bar_placements.map { |placement| token(placement, pitch_writer, duration_writer) }
  join_bar_tokens(bar_placements, tokens)
end

#render_error_classObject (private)



175
176
177
# File 'lib/head_music/notation/abc/writer.rb', line 175

def render_error_class
  RenderError
end

#to_sObject



27
28
29
30
# File 'lib/head_music/notation/abc/writer.rb', line 27

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

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



157
158
159
160
161
162
163
164
165
# File 'lib/head_music/notation/abc/writer.rb', line 157

def token(placement, pitch_writer, duration_writer)
  ensure_pitched_sounds(placement)

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

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

#unit_note_length_fieldObject (private)



100
101
102
# File 'lib/head_music/notation/abc/writer.rb', line 100

def unit_note_length_field
  "L:#{UNIT_NOTE_LENGTH.numerator}/#{UNIT_NOTE_LENGTH.denominator}"
end

#validate!Object (private)



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

def validate!
  ensure_single_voice
  ensure_no_mid_piece_changes
  ensure_contiguous_placements
end