Class: HeadMusic::Notation::MusicXML::RenderPlan
- Inherits:
-
Object
- Object
- HeadMusic::Notation::MusicXML::RenderPlan
- Defined in:
- lib/head_music/notation/music_xml/render_plan.rb
Overview
The computed musical facts a Writer needs to serialize a composition: the divisions resolution, the duration components and beams of every notehead, and the key/time signatures in force at each measure. Separating this model from the Writer lets the beam and meter-tracking logic — the intricate part of MusicXML rendering — be reasoned about and tested without generating XML.
Construction eagerly computes everything that can raise on unmappable keys or durations, so a RenderPlan that builds successfully cannot fail assembly on those grounds; beam annotations stay lazy because their integer-duration check raises only when a bar is actually laid out.
Constant Summary collapse
- BEAM_LEVELS_BY_TYPE =
The number of beams a notehead of each MusicXML
carries alone. Every other type (quarter and longer) and every rest carries none. { "eighth" => 1, "16th" => 2, "32nd" => 3, "64th" => 4, "128th" => 5, "256th" => 6 }.freeze
Instance Attribute Summary collapse
-
#composition ⇒ Object
readonly
Returns the value of attribute composition.
Instance Method Summary collapse
-
#annotate_bar(voice, bar_number, annotations) ⇒ Object
private
-
#bar_numbers ⇒ Object
-
#beam_annotations ⇒ Object
A Hash keyed by [placement, component_index] holding the Array
that BeamGrouper computed for that notehead. -
#beam_levels(placement, component) ⇒ Object
private
-
#build_bar_events(placements, keys) ⇒ Object
private
-
#components_by_placement ⇒ Object
-
#divisions ⇒ Object
-
#duration_writer ⇒ Object
private
-
#effective_meter(bar_number) ⇒ Object
-
#first_measure_key ⇒ Object
-
#first_measure_meter ⇒ Object
-
#group_unit_divisions(bar_number) ⇒ Object
private
The beam-group span in integer divisions for a bar’s effective meter.
-
#initialize(composition) ⇒ RenderPlan
constructor
A new instance of RenderPlan.
-
#key_element_values(key_signature) ⇒ Object
private
-
#measure_key_changes ⇒ Object
-
#measure_time_changes ⇒ Object
-
#placements_by_bar(voice) ⇒ Object
-
#precompute_eager_data ⇒ Object
private
Everything that can raise on unmappable keys or durations is computed here so it raises at construction, before the Writer assembles output.
-
#whole_measure_duration(bar_number) ⇒ Object
Divisions.for guarantees a whole measure of any effective meter is an integer number of divisions, so the Rational’s numerator is the value.
Constructor Details
#initialize(composition) ⇒ RenderPlan
Returns a new instance of RenderPlan.
27 28 29 30 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 27 def initialize(composition) @composition = composition precompute_eager_data end |
Instance Attribute Details
#composition ⇒ Object (readonly)
Returns the value of attribute composition.
25 26 27 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 25 def composition @composition end |
Instance Method Details
#annotate_bar(voice, bar_number, annotations) ⇒ Object (private)
112 113 114 115 116 117 118 119 120 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 112 def (voice, , annotations) placements = (voice)[] return unless placements keys = [] events = (placements, keys) beams = BeamGrouper.annotate(events, group_unit_divisions()) keys.each_with_index { |key, index| annotations[key] = beams[index] } end |
#bar_numbers ⇒ Object
53 54 55 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 53 def composition...composition. end |
#beam_annotations ⇒ Object
A Hash keyed by [placement, component_index] holding the Array
45 46 47 48 49 50 51 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 45 def beam_annotations @beam_annotations ||= {}.tap do |annotations| composition.voices.each do |voice| .each { || (voice, , annotations) } end end end |
#beam_levels(placement, component) ⇒ Object (private)
138 139 140 141 142 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 138 def beam_levels(placement, component) return 0 if placement.rest? BEAM_LEVELS_BY_TYPE.fetch(component.type, 0) end |
#build_bar_events(placements, keys) ⇒ Object (private)
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 122 def (placements, keys) onset = 0 placements.flat_map do |placement| components_by_placement[placement].each_with_index.map do |component, component_index| event = BeamGrouper::Event.new( levels: beam_levels(placement, component), onset: onset, beam_break_before: component_index.zero? ? placement.beam_break_before : nil ) keys << [placement, component_index] onset += component.duration event end end end |
#components_by_placement ⇒ Object
36 37 38 39 40 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 36 def components_by_placement @components_by_placement ||= composition.voices.flat_map(&:placements).to_h do |placement| [placement, duration_writer.components(placement.rhythmic_value)] end end |
#divisions ⇒ Object
32 33 34 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 32 def divisions @divisions ||= Divisions.for(composition) end |
#duration_writer ⇒ Object (private)
108 109 110 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 108 def duration_writer @duration_writer ||= DurationWriter.new(divisions) end |
#effective_meter(bar_number) ⇒ Object
78 79 80 81 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 78 def effective_meter() = measure_time_changes.keys.select { |number| number <= }.max ? measure_time_changes[] : composition.meter end |
#first_measure_key ⇒ Object
69 70 71 72 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 69 def first_measure_key @first_measure_key ||= measure_key_changes[.first] || key_element_values(composition.key_signature) end |
#first_measure_meter ⇒ Object
74 75 76 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 74 def first_measure_meter @first_measure_meter ||= effective_meter(.first) end |
#group_unit_divisions(bar_number) ⇒ Object (private)
The beam-group span in integer divisions for a bar’s effective meter. DurationWriter.single_quarter_fraction returns an exact Rational, so the product with the integer divisions is integral for every supported meter.
147 148 149 150 151 152 153 154 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 147 def group_unit_divisions() fraction = DurationWriter.single_quarter_fraction(effective_meter().beam_group_unit) * divisions unless fraction.denominator == 1 raise RenderError, "cannot express the beam group unit as an integer duration at #{divisions} divisions per quarter note" end fraction.to_i end |
#key_element_values(key_signature) ⇒ Object (private)
156 157 158 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 156 def key_element_values(key_signature) {fifths: KeyMapper.fifths(key_signature), mode: KeyMapper.mode(key_signature)} end |
#measure_key_changes ⇒ Object
57 58 59 60 61 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 57 def measure_key_changes @measure_key_changes ||= .zip(composition.).filter_map { |, | [, key_element_values(.key_signature)] if .key_signature }.to_h end |
#measure_time_changes ⇒ Object
63 64 65 66 67 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 63 def measure_time_changes @measure_time_changes ||= .zip(composition.).filter_map { |, | [, .meter] if .meter }.to_h end |
#placements_by_bar(voice) ⇒ Object
83 84 85 86 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 83 def (voice) @placements_by_bar ||= {} @placements_by_bar[voice] ||= voice.placements.group_by { |placement| placement.position. } end |
#precompute_eager_data ⇒ Object (private)
Everything that can raise on unmappable keys or durations is computed here so it raises at construction, before the Writer assembles output.
99 100 101 102 103 104 105 106 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 99 def precompute_eager_data key_element_values(composition.key_signature) first_measure_key first_measure_meter measure_key_changes measure_time_changes components_by_placement end |
#whole_measure_duration(bar_number) ⇒ Object
Divisions.for guarantees a whole measure of any effective meter is an integer number of divisions, so the Rational’s numerator is the value.
90 91 92 93 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 90 def whole_measure_duration() meter = effective_meter() (Rational(4 * meter.top_number, meter.bottom_number) * divisions).numerator end |