Class: HeadMusic::Notation::ABC::Writer
- Inherits:
-
Object
- Object
- HeadMusic::Notation::ABC::Writer
- 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
-
#composition ⇒ Object
readonly
Returns the value of attribute composition.
-
#reference_number ⇒ Object
readonly
Returns the value of attribute reference_number.
Instance Method Summary collapse
-
#bar_strings ⇒ Object
private
-
#body_lines ⇒ Object
private
-
#build_bar_strings ⇒ Object
private
-
#chord_token(placement, pitch_writer, multiplier) ⇒ Object
private
-
#ensure_contiguous_placements ⇒ Object
private
-
#ensure_no_mid_piece_changes ⇒ Object
private
-
#ensure_pitched_sounds(placement) ⇒ Object
included
from PlacementValidation
private
-
#ensure_placement_starts_bar(placement) ⇒ Object
private
-
#ensure_single_voice ⇒ Object
private
-
#header_lines ⇒ Object
private
-
#initialize(composition, reference_number: 1) ⇒ Writer
constructor
A new instance of Writer.
-
#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).
-
#key_field ⇒ Object
private
-
#optional_field(letter, value) ⇒ Object
private
-
#placements ⇒ Object
private
-
#placements_by_bar ⇒ Object
private
-
#render_bar(bar_placements, pitch_writer, duration_writer) ⇒ Object
private
-
#render_error_class ⇒ Object
private
-
#to_s ⇒ Object
-
#token(placement, pitch_writer, duration_writer) ⇒ Object
private
-
#unit_note_length_field ⇒ Object
private
-
#validate! ⇒ Object
private
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
#composition ⇒ Object (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_number ⇒ Object (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_strings ⇒ Object (private)
119 120 121 |
# File 'lib/head_music/notation/abc/writer.rb', line 119 def @bar_strings ||= end |
#body_lines ⇒ Object (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 .empty? lines = .each_slice(BARS_PER_LINE).map do || .join("|") + "|" end lines[-1] = lines[-1].sub(/\|\z/, "|]") lines end |
#build_bar_strings ⇒ Object (private)
123 124 125 126 127 128 129 130 131 |
# File 'lib/head_music/notation/abc/writer.rb', line 123 def pitch_writer = PitchWriter.new(composition.key_signature) duration_writer = DurationWriter.new(UNIT_NOTE_LENGTH) .each_with_index.map do |, index| # Accidental state must mirror what a re-parse accumulates bar by bar. pitch_writer. if index.positive? (, 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_placements ⇒ Object (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 (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_changes ⇒ Object (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..each_with_index do |, index| = composition. + index if .meter raise RenderError, "cannot render the meter change at bar #{} in ABC output" end next unless .key_signature raise RenderError, "cannot render the key signature change at bar #{} in ABC output" end end |
#ensure_pitched_sounds(placement) ⇒ Object (private) Originally defined in module PlacementValidation
#ensure_placement_starts_bar(placement) ⇒ Object (private)
71 72 73 74 75 76 77 |
# File 'lib/head_music/notation/abc/writer.rb', line 71 def (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_voice ⇒ Object (private)
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_lines ⇒ Object (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 (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_field ⇒ Object (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 |
#placements ⇒ Object (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_bar ⇒ Object (private)
133 134 135 136 137 |
# File 'lib/head_music/notation/abc/writer.rb', line 133 def placements.chunk_while do |previous, current| previous.position. == current.position. 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 (, pitch_writer, duration_writer) tokens = .map { |placement| token(placement, pitch_writer, duration_writer) } (, tokens) end |
#render_error_class ⇒ Object (private)
175 176 177 |
# File 'lib/head_music/notation/abc/writer.rb', line 175 def render_error_class RenderError end |
#to_s ⇒ Object
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_field ⇒ Object (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 |