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

Inherits:
Object
  • Object
show all
Includes:
XmlText
Defined in:
lib/head_music/notation/music_xml/writer.rb

Overview

Renders a flow as a score-partwise MusicXML 4.0 document, assembled down to the measure; AttributesWriter serializes a measure's attributes and NoteWriter its notes. Whole-flow problems raise before any assembly, so #to_s only ever returns a complete document.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(flow, work_title: nil, movement_number: nil, transposed: false, arranger: nil) ⇒ Writer

Returns a new instance of Writer.



16
17
18
19
20
21
22
# File 'lib/head_music/notation/music_xml/writer.rb', line 16

def initialize(flow, work_title: nil, movement_number: nil, transposed: false, arranger: nil)
  @flow = flow
  @work_title = work_title
  @movement_number = movement_number
  @transposed = transposed
  @arranger = arranger
end

Instance Attribute Details

#arranger ⇒ Object (readonly)

Returns the value of attribute arranger.



12
13
14
# File 'lib/head_music/notation/music_xml/writer.rb', line 12

def arranger
  @arranger
end

#flow ⇒ Object (readonly)

Returns the value of attribute flow.



12
13
14
# File 'lib/head_music/notation/music_xml/writer.rb', line 12

def flow
  @flow
end

#movement_number ⇒ Object (readonly)

Returns the value of attribute movement_number.



12
13
14
# File 'lib/head_music/notation/music_xml/writer.rb', line 12

def movement_number
  @movement_number
end

#transposed ⇒ Object (readonly)

Returns the value of attribute transposed.



12
13
14
# File 'lib/head_music/notation/music_xml/writer.rb', line 12

def transposed
  @transposed
end

#work_title ⇒ Object (readonly)

Returns the value of attribute work_title.



12
13
14
# File 'lib/head_music/notation/music_xml/writer.rb', line 12

def work_title
  @work_title
end

Instance Method Details

#attributes_writer ⇒ Object (private)



42
43
44
# File 'lib/head_music/notation/music_xml/writer.rb', line 42

def attributes_writer
  @attributes_writer ||= AttributesWriter.new(plan)
end

#backup_lines(duration) ⇒ Object (private)



144
145
146
147
148
149
150
# File 'lib/head_music/notation/music_xml/writer.rb', line 144

def backup_lines(duration)
  [
    "#{INDENT * 3}<backup>",
    "#{INDENT * 4}<duration>#{duration}</duration>",
    "#{INDENT * 3}</backup>"
  ]
end

#document_lines ⇒ Object (private)



46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/head_music/notation/music_xml/writer.rb', line 46

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,
    *movement_lines,
    *identification_lines,
    *part_list_lines,
    *part_lines,
    "</score-partwise>"
  ]
end

#escape(text) ⇒ Object (private) Originally defined in module XmlText

#identification_lines ⇒ Object (private)



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/head_music/notation/music_xml/writer.rb', line 77

def identification_lines
  [
    "#{INDENT}<identification>",
    flow.composer && %(#{INDENT * 2}<creator type="composer">#{escape(flow.composer)}</creator>),
    arranger && %(#{INDENT * 2}<creator type="arranger">#{escape(arranger)}</creator>),
    "#{INDENT * 2}<encoding>",
    "#{INDENT * 3}<software>head_music #{HeadMusic::VERSION}</software>",
    "#{INDENT * 2}</encoding>",
    "#{INDENT}</identification>"
  ].compact
end

#measure_content_lines(part, voice, bar_number) ⇒ Object (private)

A voice of nil is a part nobody plays in, which renders as whole-measure rests on its first staff so the chair keeps its line in the score.



161
162
163
164
165
166
167
168
169
170
# File 'lib/head_music/notation/music_xml/writer.rb', line 161

def measure_content_lines(part, voice, bar_number)
  voice_number = (part.voices.length > 1) ? part.voices.index(voice) + 1 : nil
  staff_number = staff_number(part, voice, bar_number)
  placements = voice && placements_by_bar(voice)[bar_number]
  return note_writer.whole_measure_rest_lines(bar_number, voice_number: voice_number, staff_number: staff_number) unless placements

  placements.flat_map do |placement|
    note_writer.lines(placement, voice_number: voice_number, staff_number: staff_number)
  end
end

#measure_lines(part, bar_number) ⇒ Object (private)



121
122
123
124
125
126
127
128
# File 'lib/head_music/notation/music_xml/writer.rb', line 121

def measure_lines(part, bar_number)
  [
    measure_open_tag(bar_number),
    *attributes_writer.lines(part, bar_number),
    *part_content_lines(part, bar_number),
    "#{INDENT * 2}</measure>"
  ]
end

#measure_open_tag(bar_number) ⇒ Object (private)

A bar before bar 1 is marked implicit by convention. A partially filled first bar is rejected as a gap in Preflight.



154
155
156
157
# File 'lib/head_music/notation/music_xml/writer.rb', line 154

def measure_open_tag(bar_number)
  implicit = (bar_number < 1) ? %( implicit="yes") : ""
  %(#{INDENT * 2}<measure number="#{bar_number}"#{implicit}>)
end

#movement_lines ⇒ Object (private)

A movement titles itself only where a work titles the whole; a document standing alone would otherwise say the same name twice.



70
71
72
73
74
75
# File 'lib/head_music/notation/music_xml/writer.rb', line 70

def movement_lines
  [
    movement_number && "#{INDENT}<movement-number>#{escape(movement_number.to_s)}</movement-number>",
    work_title && "#{INDENT}<movement-title>#{escape(flow.name)}</movement-title>"
  ].compact
end

#note_writer ⇒ Object (private)



38
39
40
# File 'lib/head_music/notation/music_xml/writer.rb', line 38

def note_writer
  @note_writer ||= NoteWriter.new(plan)
end

#part_content_lines(part, bar_number) ⇒ Object (private)

A before each voice after the first is how MusicXML writes simultaneous voices in one part. It rewinds by what the previous voice actually wrote, which is less than a measure when it ended mid-bar.



133
134
135
136
137
138
139
140
141
142
# File 'lib/head_music/notation/music_xml/writer.rb', line 133

def part_content_lines(part, bar_number)
  return measure_content_lines(part, part.voices.first, bar_number) if part.voices.length <= 1

  part.voices.each_with_index.flat_map do |voice, index|
    [
      *(index.positive? ? backup_lines(written_duration(part.voices[index - 1], bar_number)) : []),
      *measure_content_lines(part, voice, bar_number)
    ]
  end
end

#part_lines ⇒ Object (private)



111
112
113
114
115
116
117
118
119
# File 'lib/head_music/notation/music_xml/writer.rb', line 111

def part_lines
  flow.parts.each_with_index.flat_map do |part, index|
    [
      %(#{INDENT}<part id="P#{index + 1}">),
      *bar_numbers.flat_map { |bar_number| measure_lines(part, bar_number) },
      "#{INDENT}</part>"
    ]
  end
end

#part_list_lines ⇒ Object (private)

One per part, not per voice. A part holding one voice renders exactly as it always did, which keeps existing documents unchanged.



91
92
93
94
95
96
97
98
99
100
# File 'lib/head_music/notation/music_xml/writer.rb', line 91

def part_list_lines
  score_part_lines = flow.parts.each_with_index.flat_map do |part, index|
    [
      %(#{INDENT * 2}<score-part id="P#{index + 1}">),
      "#{INDENT * 3}<part-name>#{escape(part_name(part, index))}</part-name>",
      "#{INDENT * 2}</score-part>"
    ]
  end
  ["#{INDENT}<part-list>", *score_part_lines, "#{INDENT}</part-list>"]
end

#part_name(part, index) ⇒ Object (private)

A voice's role names the part only where the part holds a single voice, so that one voice's role does not stand for several.



104
105
106
107
108
109
# File 'lib/head_music/notation/music_xml/writer.rb', line 104

def part_name(part, index)
  part.player&.name ||
    part.instrument&.name ||
    (part.voices.one? ? part.voices.first.role : nil) ||
    "Voice #{index + 1}"
end

#plan ⇒ Object (private)

The computed rendering facts. Built here — before assembly — so an unmappable key or duration raises before any output is produced.



34
35
36
# File 'lib/head_music/notation/music_xml/writer.rb', line 34

def plan
  @plan ||= RenderPlan.new(flow, transposed: transposed)
end

#staff_number(part, voice, bar_number) ⇒ Object (private)

Where a crossing shows up: the same voice reports a different staff on either side of it.



174
175
176
177
178
179
180
# File 'lib/head_music/notation/music_xml/writer.rb', line 174

def staff_number(part, voice, bar_number)
  staves = part.staff_system_at(bar_number).staves
  return nil if staves.length <= 1 || voice.nil?

  index = staves.index { |staff| staff.equal?(voice.staff_at(bar_number)) }
  index && index + 1
end

#to_s ⇒ Object



24
25
26
27
28
# File 'lib/head_music/notation/music_xml/writer.rb', line 24

def to_s
  Preflight.check!(flow)
  plan
  document_lines.join("\n") + "\n"
end

#work_lines ⇒ Object (private)



60
61
62
63
64
65
66
# File 'lib/head_music/notation/music_xml/writer.rb', line 60

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