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

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

Overview

Renders a flow as an ABC tune string. Whole-flow problems raise before any string assembly, so callers never receive a truncated tune. Repeat barlines and voltas are deliberately not rendered; bars carrying repeat flags degrade to plain bar lines.

Defined Under Namespace

Classes: Segment

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(flow, reference_number: 1, transposed: false) ⇒ Writer

Returns a new instance of Writer.



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

def initialize(flow, reference_number: 1, transposed: false)
  @flow = flow
  @reference_number = reference_number
  @transposed = transposed
end

Instance Attribute Details

#flow ⇒ Object (readonly)

Returns the value of attribute flow.



16
17
18
# File 'lib/head_music/notation/abc/writer.rb', line 16

def flow
  @flow
end

#reference_number ⇒ Object (readonly)

Returns the value of attribute reference_number.



16
17
18
# File 'lib/head_music/notation/abc/writer.rb', line 16

def reference_number
  @reference_number
end

#transposed ⇒ Object (readonly)

Returns the value of attribute transposed.



16
17
18
# File 'lib/head_music/notation/abc/writer.rb', line 16

def transposed
  @transposed
end

Instance Method Details

#bar_strings ⇒ Object (private)



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

def bar_strings
  @bar_strings ||= build_bar_strings
end

#body_lines ⇒ Object (private)



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

def body_lines
  lines = bar_strings.each_slice(BARS_PER_LINE).map { |line_bars| "#{line_bars.join("|")}|" }
  lines.last&.concat("]")
  lines
end

#build_bar_strings ⇒ Object (private)



115
116
117
118
119
120
121
122
123
# File 'lib/head_music/notation/abc/writer.rb', line 115

def build_bar_strings
  pitch_writer = PitchWriter.new(written_key_signature)
  duration_writer = DurationWriter.new(UNIT_NOTE_LENGTH)
  segments_by_bar.map do |bar_segments|
    # Accidental state must mirror what a re-parse accumulates bar by bar.
    pitch_writer.start_new_bar
    render_bar(bar_segments, pitch_writer, duration_writer)
  end
end

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



191
192
193
194
195
196
# File 'lib/head_music/notation/abc/writer.rb', line 191

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

#ensure_contiguous_voices(flow) ⇒ Object (private) Originally defined in module PreflightChecks

#ensure_no_instrument_change ⇒ Object (private)

A tune has one K: field, so a part that picks up an instrument reading in another key cannot be written -- the same limit as a mid-piece key change.



51
52
53
54
55
# File 'lib/head_music/notation/abc/writer.rb', line 51

def ensure_no_instrument_change
  return unless transposed

  refuse_change("instrument", flow.parts.flat_map { |part| part.instrument_changes.keys })
end

#ensure_no_mid_piece_changes ⇒ Object (private)



44
45
46
47
# File 'lib/head_music/notation/abc/writer.rb', line 44

def ensure_no_mid_piece_changes
  refuse_change("meter", flow.meter_changes.keys)
  refuse_change("key signature", flow.key_signature_changes.keys)
end

#ensure_notes_within_barlines(flow) ⇒ Object (private) Originally defined in module PreflightChecks

#ensure_pitched_sounds(placement) ⇒ Object (private) Originally defined in module PlacementValidation

Raises:

  • (render_error_class)

#ensure_single_voice ⇒ Object (private)

Raises:



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

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

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

#fraction_to_bar_end(position) ⇒ Object (private)



148
149
150
151
# File 'lib/head_music/notation/abc/writer.rb', line 148

def fraction_to_bar_end(position)
  meter = position.meter
  Rational(meter.top_number, meter.bottom_number) - offset_in_bar(position)
end

#fraction_within_bar(from, to) ⇒ Object (private)



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

def fraction_within_bar(from, to)
  offset_in_bar(to) - offset_in_bar(from)
end

#header_lines ⇒ Object (private)



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

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

#key_field ⇒ Object (private)



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

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

#multiplier_for(segment, duration_writer) ⇒ Object (private)



185
186
187
188
189
# File 'lib/head_music/notation/abc/writer.rb', line 185

def multiplier_for(segment, duration_writer)
  return duration_writer.multiplier_string(segment.placement.rhythmic_value) unless segment.fraction

  duration_writer.multiplier_string_for_fraction(segment.fraction, segment.placement.rhythmic_value)
end

#offset_in_bar(position) ⇒ Object (private)



157
158
159
160
# File 'lib/head_music/notation/abc/writer.rb', line 157

def offset_in_bar(position)
  meter = position.meter
  (position.count - 1 + Rational(position.tick, meter.ticks_per_count)) / meter.bottom_number
end

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



92
93
94
# File 'lib/head_music/notation/abc/writer.rb', line 92

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

#placements ⇒ Object (private)



64
65
66
67
# File 'lib/head_music/notation/abc/writer.rb', line 64

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

#raise_gap_error(voice, expected_position, found_placement) ⇒ Object (private) Originally defined in module PreflightChecks

Raises:

  • (render_error_class)

#refuse_change(subject, bar_numbers) ⇒ Object (private)

Raises:



57
58
59
60
61
62
# File 'lib/head_music/notation/abc/writer.rb', line 57

def refuse_change(subject, bar_numbers)
  bar_number = bar_numbers.min
  return unless bar_number

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

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

The inter-token space is dropped only where the placement was authored as beamed to its predecessor; a true or nil beam_break_before keeps it, so programmatic flows render with every-token spacing. Every bar token re-lexes unambiguously with no separator, so dropping it is safe.



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

def render_bar(bar_segments, pitch_writer, duration_writer)
  bar_segments.map do |segment|
    token = token(segment, pitch_writer, duration_writer)
    (segment.placement.beam_break_before == false) ? token : " #{token}"
  end.join.lstrip
end

#render_error_class ⇒ Object (private)



198
199
200
# File 'lib/head_music/notation/abc/writer.rb', line 198

def render_error_class
  RenderError
end

#segments_by_bar ⇒ Object (private)



131
132
133
134
# File 'lib/head_music/notation/abc/writer.rb', line 131

def segments_by_bar
  placements.flat_map { |placement| segments_of(placement) }
    .chunk_while { |previous, current| previous.bar_number == current.bar_number }
end

#segments_of(placement) ⇒ Object (private)



136
137
138
139
140
141
142
143
144
145
146
# File 'lib/head_music/notation/abc/writer.rb', line 136

def segments_of(placement)
  start = placement.position
  finish = placement.next_position
  segments = []
  while finish > start.start_of_next_bar
    segments << Segment.new(placement, start.bar_number, fraction_to_bar_end(start), true)
    start = start.start_of_next_bar
  end
  fraction = segments.empty? ? nil : fraction_within_bar(start, finish)
  segments << Segment.new(placement, start.bar_number, fraction, false)
end

#to_s ⇒ Object



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

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

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



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/head_music/notation/abc/writer.rb', line 173

def token(segment, pitch_writer, duration_writer)
  placement = segment.placement
  ensure_pitched_sounds(placement)

  multiplier = multiplier_for(segment, duration_writer)
  tie = segment.continues ? "-" : ""
  return "z#{multiplier}" if placement.rest?
  return chord_token(placement, pitch_writer, multiplier) + tie if placement.chord?

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

#transposition ⇒ Object (private)



75
76
77
78
# File 'lib/head_music/notation/abc/writer.rb', line 75

def transposition
  instrument = transposed ? (flow.voices.first&.part || flow.parts.first)&.instrument : nil
  HeadMusic::Content::Layout::Transposition.for(instrument)
end

#unit_note_length_field ⇒ Object (private)



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

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

#validate! ⇒ Object (private)



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

def validate!
  ensure_single_voice
  ensure_no_mid_piece_changes
  ensure_no_instrument_change
  ensure_contiguous_voices(flow)
end

#written_key_signature ⇒ Object (private)

No %%transpose directive is emitted: the pitches are already written, and abcm2ps would move them a second time.



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

def written_key_signature
  @written_key_signature ||= transposition.key_signature(flow.key_signature)
end