Class: HeadMusic::Notation::ABC::Writer
- Inherits:
-
Object
- Object
- HeadMusic::Notation::ABC::Writer
- Includes:
- PlacementValidation, PreflightChecks
- 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_voices(composition) ⇒ Object
included
from PreflightChecks
private
-
#ensure_no_mid_piece_changes ⇒ Object
private
-
#ensure_notes_within_barlines(composition) ⇒ Object
included
from PreflightChecks
private
-
#ensure_pitched_sounds(placement) ⇒ Object
included
from PlacementValidation
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
-
#raise_gap_error(voice, expected_position, found_placement) ⇒ Object
included
from PreflightChecks
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.
23 24 25 26 |
# File 'lib/head_music/notation/abc/writer.rb', line 23 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.
21 22 23 |
# File 'lib/head_music/notation/abc/writer.rb', line 21 def composition @composition end |
#reference_number ⇒ Object (readonly)
Returns the value of attribute reference_number.
21 22 23 |
# File 'lib/head_music/notation/abc/writer.rb', line 21 def reference_number @reference_number end |
Instance Method Details
#bar_strings ⇒ Object (private)
99 100 101 |
# File 'lib/head_music/notation/abc/writer.rb', line 99 def @bar_strings ||= end |
#body_lines ⇒ Object (private)
89 90 91 92 93 94 95 96 97 |
# File 'lib/head_music/notation/abc/writer.rb', line 89 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)
103 104 105 106 107 108 109 110 111 |
# File 'lib/head_music/notation/abc/writer.rb', line 103 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)
147 148 149 150 151 152 153 |
# File 'lib/head_music/notation/abc/writer.rb', line 147 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_voices(composition) ⇒ Object (private) Originally defined in module PreflightChecks
#ensure_no_mid_piece_changes ⇒ Object (private)
47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/head_music/notation/abc/writer.rb', line 47 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_notes_within_barlines(composition) ⇒ Object (private) Originally defined in module PreflightChecks
#ensure_pitched_sounds(placement) ⇒ Object (private) Originally defined in module PlacementValidation
#ensure_single_voice ⇒ Object (private)
41 42 43 44 45 |
# File 'lib/head_music/notation/abc/writer.rb', line 41 def ensure_single_voice return if composition.voices.length <= 1 raise RenderError, "multi-voice ABC output is not supported" end |
#header_lines ⇒ Object (private)
64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/head_music/notation/abc/writer.rb', line 64 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.
130 131 132 133 134 135 |
# File 'lib/head_music/notation/abc/writer.rb', line 130 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)
84 85 86 87 |
# File 'lib/head_music/notation/abc/writer.rb', line 84 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)
76 77 78 |
# File 'lib/head_music/notation/abc/writer.rb', line 76 def optional_field(letter, value) "#{letter}:#{value}" if value end |
#placements ⇒ Object (private)
59 60 61 62 |
# File 'lib/head_music/notation/abc/writer.rb', line 59 def placements voice = composition.voices.first voice ? voice.placements : [] end |
#placements_by_bar ⇒ Object (private)
113 114 115 116 117 |
# File 'lib/head_music/notation/abc/writer.rb', line 113 def placements.chunk_while do |previous, current| previous.position. == current.position. end end |
#raise_gap_error(voice, expected_position, found_placement) ⇒ Object (private) Originally defined in module PreflightChecks
#render_bar(bar_placements, pitch_writer, duration_writer) ⇒ Object (private)
119 120 121 122 |
# File 'lib/head_music/notation/abc/writer.rb', line 119 def (, pitch_writer, duration_writer) tokens = .map { |placement| token(placement, pitch_writer, duration_writer) } (, tokens) end |
#render_error_class ⇒ Object (private)
155 156 157 |
# File 'lib/head_music/notation/abc/writer.rb', line 155 def render_error_class RenderError end |
#to_s ⇒ Object
28 29 30 31 |
# File 'lib/head_music/notation/abc/writer.rb', line 28 def to_s validate! (header_lines + body_lines).join("\n") + "\n" end |
#token(placement, pitch_writer, duration_writer) ⇒ Object (private)
137 138 139 140 141 142 143 144 145 |
# File 'lib/head_music/notation/abc/writer.rb', line 137 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)
80 81 82 |
# File 'lib/head_music/notation/abc/writer.rb', line 80 def unit_note_length_field "L:#{UNIT_NOTE_LENGTH.numerator}/#{UNIT_NOTE_LENGTH.denominator}" end |
#validate! ⇒ Object (private)
35 36 37 38 39 |
# File 'lib/head_music/notation/abc/writer.rb', line 35 def validate! ensure_single_voice ensure_no_mid_piece_changes ensure_contiguous_voices(composition) end |