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 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
-
#flow ⇒ Object
readonly
Returns the value of attribute flow.
-
#reference_number ⇒ Object
readonly
Returns the value of attribute reference_number.
-
#transposed ⇒ Object
readonly
Returns the value of attribute transposed.
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(flow) ⇒ Object included from PreflightChecks private
-
#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.
- #ensure_no_mid_piece_changes ⇒ Object private
- #ensure_notes_within_barlines(flow) ⇒ Object included from PreflightChecks private
- #ensure_pitched_sounds(placement) ⇒ Object included from PlacementValidation private
- #ensure_single_voice ⇒ Object private
- #fraction_to_bar_end(position) ⇒ Object private
- #fraction_within_bar(from, to) ⇒ Object private
- #header_lines ⇒ Object private
-
#initialize(flow, reference_number: 1, transposed: false) ⇒ Writer
constructor
A new instance of Writer.
- #key_field ⇒ Object private
- #multiplier_for(segment, duration_writer) ⇒ Object private
- #offset_in_bar(position) ⇒ Object private
- #optional_field(letter, value) ⇒ Object private
- #placements ⇒ Object private
- #raise_gap_error(voice, expected_position, found_placement) ⇒ Object included from PreflightChecks private
- #refuse_change(subject, bar_numbers) ⇒ Object private
-
#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.
- #render_error_class ⇒ Object private
- #segments_by_bar ⇒ Object private
- #segments_of(placement) ⇒ Object private
- #to_s ⇒ Object
- #token(segment, pitch_writer, duration_writer) ⇒ Object private
- #transposition ⇒ Object private
- #unit_note_length_field ⇒ Object private
- #validate! ⇒ Object private
-
#written_key_signature ⇒ Object
private
No %%transpose directive is emitted: the pitches are already written, and abcm2ps would move them a second time.
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 ||= end |
#body_lines ⇒ Object (private)
105 106 107 108 109 |
# File 'lib/head_music/notation/abc/writer.rb', line 105 def body_lines lines = .each_slice(BARS_PER_LINE).map { || "#{.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 pitch_writer = PitchWriter.new(written_key_signature) duration_writer = DurationWriter.new(UNIT_NOTE_LENGTH) .map do || # Accidental state must mirror what a re-parse accumulates bar by bar. pitch_writer. (, 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
#ensure_single_voice ⇒ Object (private)
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 (position) meter = position.meter Rational(meter.top_number, meter.bottom_number) - (position) end |
#fraction_within_bar(from, to) ⇒ Object (private)
153 154 155 |
# File 'lib/head_music/notation/abc/writer.rb', line 153 def (from, to) (to) - (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 (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
#refuse_change(subject, bar_numbers) ⇒ Object (private)
57 58 59 60 61 62 |
# File 'lib/head_music/notation/abc/writer.rb', line 57 def refuse_change(subject, ) = .min return unless raise RenderError, "cannot render the #{subject} change at bar #{} 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 (, pitch_writer, duration_writer) .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 placements.flat_map { |placement| segments_of(placement) } .chunk_while { |previous, current| previous. == current. } 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. segments << Segment.new(placement, start., (start), true) start = start. end fraction = segments.empty? ? nil : (start, finish) segments << Segment.new(placement, start., 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 |