Class: HeadMusic::Notation::MusicXML::Writer

Inherits:
Object
  • Object
show all
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 =
{
  "&" => "&",
  "<" => "&lt;",
  ">" => "&gt;",
  '"' => "&quot;",
  "'" => "&apos;"
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

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

#compositionObject (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, bar_number)
  return first_measure_attribute_lines(voice) if bar_number == bar_numbers.first

  key = measure_key_changes[bar_number]
  meter = measure_time_changes[bar_number]
  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_linesObject (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

Raises:

  • (render_error_class)

#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_linesObject (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, bar_number)
  placements = placements_by_bar(voice)[bar_number]
  return whole_measure_rest_lines(bar_number) 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, bar_number)
  [
    measure_open_tag(bar_number),
    *attribute_lines(voice, bar_number),
    *measure_content_lines(voice, bar_number),
    "#{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(bar_number)
  implicit = (bar_number < 1) ? %( implicit="yes") : ""
  %(#{INDENT * 2}<measure number="#{bar_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 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.



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 . ensure_pitched_sounds has already rejected any unpitched sound, so pitches covers every sound here.



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_linesObject (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}">),
      *bar_numbers.flat_map { |bar_number| measure_lines(voice, bar_number) },
      "#{INDENT}</part>"
    ]
  end
end

#part_list_linesObject (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

#planObject (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_classObject (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_sObject



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(bar_number)
  [
    "#{INDENT * 3}<note>",
    %(#{INDENT * 4}<rest measure="yes"/>),
    "#{INDENT * 4}<duration>#{whole_measure_duration(bar_number)}</duration>",
    "#{INDENT * 3}</note>"
  ]
end

#work_linesObject (private)



63
64
65
66
67
68
69
# File 'lib/head_music/notation/music_xml/writer.rb', line 63

def work_lines
  [
    "#{INDENT}<work>",
    "#{INDENT * 2}<work-title>#{escape(composition.name)}</work-title>",
    "#{INDENT}</work>"
  ]
end