Class: HeadMusic::Notation::MusicXML::Writer
- Inherits:
-
Object
- Object
- HeadMusic::Notation::MusicXML::Writer
- Includes:
- PlacementValidation
- Defined in:
- lib/head_music/notation/music_xml/writer.rb
Overview
Renders a HeadMusic::Content::Composition as a score-partwise MusicXML 4.0 document string.
Whole-composition problems (no voices, positional gaps, barline-crossing notes, unmappable keys or durations, forbidden control characters) raise before any assembly, so #to_s only ever returns a complete document.
Constant Summary collapse
- INDENT =
" "- XML_ESCAPES =
{ "&" => "&", "<" => "<", ">" => ">", '"' => """, "'" => "'" }.freeze
Instance Attribute Summary collapse
-
#composition ⇒ Object
readonly
Returns the value of attribute composition.
Instance Method Summary collapse
-
#attribute_lines(voice, bar_number) ⇒ Object
private
-
#beam_lines(beams) ⇒ Object
private
-
#clef_lines(voice) ⇒ Object
private
-
#document_lines ⇒ Object
private
-
#ensure_pitched_sounds(placement) ⇒ Object
included
from PlacementValidation
private
-
#escape(text) ⇒ Object
private
-
#first_measure_attribute_lines(voice) ⇒ Object
private
-
#identification_lines ⇒ Object
private
-
#initialize(composition) ⇒ Writer
constructor
A new instance of Writer.
-
#key_lines(key) ⇒ Object
private
-
#measure_content_lines(voice, bar_number) ⇒ Object
private
-
#measure_lines(voice, bar_number) ⇒ Object
private
-
#measure_open_tag(bar_number) ⇒ Object
private
A bar before bar 1 — a pickup written out in full with leading rests — is marked implicit by convention.
-
#notation_lines(placement, component) ⇒ Object
private
-
#note_element_lines(placement, component, pitch: nil, chord: false, beams: []) ⇒ Object
private
A chord note carries
as its first child, before , marking it as sounding with the preceding note; the lead note (and every single note and rest) omits it, so this path stays byte-identical for those. -
#note_lines(placement) ⇒ Object
private
-
#note_slots(placement) ⇒ Object
private
A rest emits one empty slot; a sounded placement emits its pitches low to high, so the lowest note leads and the rest carry
. -
#part_lines ⇒ Object
private
-
#part_list_lines ⇒ Object
private
-
#part_name(voice, index) ⇒ Object
private
-
#pitch_lines(pitch) ⇒ Object
private
-
#plan ⇒ Object
private
The computed rendering facts.
-
#render_error_class ⇒ Object
private
-
#tie_lines(placement, component) ⇒ Object
private
Rests take no tie elements; the links of a rest’s tied chain render as consecutive independent rests.
-
#time_lines(meter) ⇒ Object
private
-
#to_s ⇒ Object
-
#whole_measure_rest_lines(bar_number) ⇒ Object
private
-
#work_lines ⇒ Object
private
Constructor Details
#initialize(composition) ⇒ Writer
Returns a new instance of Writer.
32 33 34 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 32 def initialize(composition) @composition = composition end |
Instance Attribute Details
#composition ⇒ Object (readonly)
Returns the value of attribute composition.
20 21 22 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 20 def composition @composition end |
Instance Method Details
#attribute_lines(voice, bar_number) ⇒ Object (private)
124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 124 def attribute_lines(voice, ) return first_measure_attribute_lines(voice) if == .first key = measure_key_changes[] meter = measure_time_changes[] return [] unless key || meter [ "#{INDENT * 3}<attributes>", *(key ? key_lines(key) : []), *(meter ? time_lines(meter) : []), "#{INDENT * 3}</attributes>" ] end |
#beam_lines(beams) ⇒ Object (private)
236 237 238 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 236 def beam_lines(beams) beams.map { |beam| %(#{INDENT * 4}<beam number="#{beam.number}">#{beam.type}</beam>) } end |
#clef_lines(voice) ⇒ Object (private)
168 169 170 171 172 173 174 175 176 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 168 def clef_lines(voice) clef = ClefSelector.for(voice) [ "#{INDENT * 4}<clef>", "#{INDENT * 5}<sign>#{clef.pitch.letter_name}</sign>", "#{INDENT * 5}<line>#{clef.line}</line>", "#{INDENT * 4}</clef>" ] end |
#document_lines ⇒ Object (private)
50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 50 def document_lines [ %(<?xml version="1.0" encoding="UTF-8"?>), %(<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">), %(<score-partwise version="4.0">), *work_lines, *identification_lines, *part_list_lines, *part_lines, "</score-partwise>" ] end |
#ensure_pitched_sounds(placement) ⇒ Object (private) Originally defined in module PlacementValidation
#escape(text) ⇒ Object (private)
273 274 275 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 273 def escape(text) text.to_s.gsub(/[&<>"']/) { |character| XML_ESCAPES[character] } end |
#first_measure_attribute_lines(voice) ⇒ Object (private)
139 140 141 142 143 144 145 146 147 148 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 139 def first_measure_attribute_lines(voice) [ "#{INDENT * 3}<attributes>", "#{INDENT * 4}<divisions>#{divisions}</divisions>", *key_lines(first_measure_key), *time_lines(first_measure_meter), *clef_lines(voice), "#{INDENT * 3}</attributes>" ] end |
#identification_lines ⇒ Object (private)
71 72 73 74 75 76 77 78 79 80 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 71 def identification_lines [ "#{INDENT}<identification>", composition.composer && %(#{INDENT * 2}<creator type="composer">#{escape(composition.composer)}</creator>), "#{INDENT * 2}<encoding>", "#{INDENT * 3}<software>head_music #{HeadMusic::VERSION}</software>", "#{INDENT * 2}</encoding>", "#{INDENT}</identification>" ].compact end |
#key_lines(key) ⇒ Object (private)
150 151 152 153 154 155 156 157 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 150 def key_lines(key) [ "#{INDENT * 4}<key>", "#{INDENT * 5}<fifths>#{key[:fifths]}</fifths>", "#{INDENT * 5}<mode>#{key[:mode]}</mode>", "#{INDENT * 4}</key>" ] end |
#measure_content_lines(voice, bar_number) ⇒ Object (private)
178 179 180 181 182 183 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 178 def measure_content_lines(voice, ) placements = (voice)[] return whole_measure_rest_lines() unless placements placements.flat_map { |placement| note_lines(placement) } end |
#measure_lines(voice, bar_number) ⇒ Object (private)
107 108 109 110 111 112 113 114 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 107 def measure_lines(voice, ) [ measure_open_tag(), *attribute_lines(voice, ), *measure_content_lines(voice, ), "#{INDENT * 2}</measure>" ] end |
#measure_open_tag(bar_number) ⇒ Object (private)
A bar before bar 1 — a pickup written out in full with leading rests — is marked implicit by convention. A partially filled first bar is rejected as a gap in Preflight, so only complete pickup bars reach here.
119 120 121 122 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 119 def measure_open_tag() implicit = ( < 1) ? %( implicit="yes") : "" %(#{INDENT * 2}<measure number="#{}"#{implicit}>) end |
#notation_lines(placement, component) ⇒ Object (private)
262 263 264 265 266 267 268 269 270 271 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 262 def notation_lines(placement, component) return [] if placement.rest? || (!component.tie_start && !component.tie_stop) [ "#{INDENT * 4}<notations>", component.tie_stop ? %(#{INDENT * 5}<tied type="stop"/>) : nil, component.tie_start ? %(#{INDENT * 5}<tied type="start"/>) : nil, "#{INDENT * 4}</notations>" ].compact end |
#note_element_lines(placement, component, pitch: nil, chord: false, beams: []) ⇒ Object (private)
A chord note carries
221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 221 def note_element_lines(placement, component, pitch: nil, chord: false, beams: []) [ "#{INDENT * 3}<note>", *(chord ? ["#{INDENT * 4}<chord/>"] : []), *(pitch ? pitch_lines(pitch) : ["#{INDENT * 4}<rest/>"]), "#{INDENT * 4}<duration>#{component.duration}</duration>", *tie_lines(placement, component), "#{INDENT * 4}<type>#{component.type}</type>", *Array.new(component.dots) { "#{INDENT * 4}<dot/>" }, *beam_lines(beams), *notation_lines(placement, component), "#{INDENT * 3}</note>" ] end |
#note_lines(placement) ⇒ Object (private)
194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 194 def note_lines(placement) ensure_pitched_sounds(placement) components_by_placement[placement].each_with_index.flat_map do |component, component_index| beams = beam_annotations[[placement, component_index]] || [] note_slots(placement).each_with_index.flat_map do |pitch, index| note_element_lines( placement, component, pitch: pitch, chord: index.positive?, beams: index.zero? ? beams : [] ) end end end |
#note_slots(placement) ⇒ Object (private)
A rest emits one empty slot; a sounded placement emits its pitches low to
high, so the lowest note leads and the rest carry
210 211 212 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 210 def note_slots(placement) placement.rest? ? [nil] : placement.pitches.sort end |
#part_lines ⇒ Object (private)
97 98 99 100 101 102 103 104 105 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 97 def part_lines composition.voices.each_with_index.flat_map do |voice, index| [ %(#{INDENT}<part id="P#{index + 1}">), *.flat_map { || measure_lines(voice, ) }, "#{INDENT}</part>" ] end end |
#part_list_lines ⇒ Object (private)
82 83 84 85 86 87 88 89 90 91 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 82 def part_list_lines score_part_lines = composition.voices.each_with_index.flat_map do |voice, index| [ %(#{INDENT * 2}<score-part id="P#{index + 1}">), "#{INDENT * 3}<part-name>#{escape(part_name(voice, index))}</part-name>", "#{INDENT * 2}</score-part>" ] end ["#{INDENT}<part-list>", *score_part_lines, "#{INDENT}</part-list>"] end |
#part_name(voice, index) ⇒ Object (private)
93 94 95 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 93 def part_name(voice, index) voice.role || "Voice #{index + 1}" end |
#pitch_lines(pitch) ⇒ Object (private)
240 241 242 243 244 245 246 247 248 249 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 240 def pitch_lines(pitch) attributes = PitchWriter.attributes(pitch) [ "#{INDENT * 4}<pitch>", "#{INDENT * 5}<step>#{attributes[:step]}</step>", attributes[:alter] && "#{INDENT * 5}<alter>#{attributes[:alter]}</alter>", "#{INDENT * 5}<octave>#{attributes[:octave]}</octave>", "#{INDENT * 4}</pitch>" ].compact end |
#plan ⇒ Object (private)
The computed rendering facts. Built here — before assembly — so an unmappable key or duration raises before any output is produced.
46 47 48 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 46 def plan @plan ||= RenderPlan.new(composition) end |
#render_error_class ⇒ Object (private)
214 215 216 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 214 def render_error_class RenderError end |
#tie_lines(placement, component) ⇒ Object (private)
Rests take no tie elements; the links of a rest’s tied chain render as consecutive independent rests.
253 254 255 256 257 258 259 260 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 253 def tie_lines(placement, component) return [] if placement.rest? [ component.tie_stop ? %(#{INDENT * 4}<tie type="stop"/>) : nil, component.tie_start ? %(#{INDENT * 4}<tie type="start"/>) : nil ].compact end |
#time_lines(meter) ⇒ Object (private)
159 160 161 162 163 164 165 166 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 159 def time_lines(meter) [ "#{INDENT * 4}<time>", "#{INDENT * 5}<beats>#{meter.top_number}</beats>", "#{INDENT * 5}<beat-type>#{meter.bottom_number}</beat-type>", "#{INDENT * 4}</time>" ] end |
#to_s ⇒ Object
36 37 38 39 40 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 36 def to_s Preflight.check!(composition) plan document_lines.join("\n") + "\n" end |
#whole_measure_rest_lines(bar_number) ⇒ Object (private)
185 186 187 188 189 190 191 192 |
# File 'lib/head_music/notation/music_xml/writer.rb', line 185 def whole_measure_rest_lines() [ "#{INDENT * 3}<note>", %(#{INDENT * 4}<rest measure="yes"/>), "#{INDENT * 4}<duration>#{whole_measure_duration()}</duration>", "#{INDENT * 3}</note>" ] end |