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.
Preflight must have run first (it normalizes bar markers). 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.
28 29 30 31 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 28 def initialize(composition) @composition = composition precompute_eager_data end |
Instance Attribute Details
#composition ⇒ Object (readonly)
Returns the value of attribute composition.
26 27 28 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 26 def composition @composition end |
Instance Method Details
#annotate_bar(voice, bar_number, annotations) ⇒ Object (private)
113 114 115 116 117 118 119 120 121 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 113 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
54 55 56 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 54 def composition...composition. end |
#beam_annotations ⇒ Object
A Hash keyed by [placement, component_index] holding the Array
46 47 48 49 50 51 52 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 46 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)
139 140 141 142 143 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 139 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)
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 123 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
37 38 39 40 41 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 37 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
33 34 35 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 33 def divisions @divisions ||= Divisions.for(composition) end |
#duration_writer ⇒ Object (private)
109 110 111 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 109 def duration_writer @duration_writer ||= DurationWriter.new(divisions) end |
#effective_meter(bar_number) ⇒ Object
79 80 81 82 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 79 def effective_meter() = measure_time_changes.keys.select { |number| number <= }.max ? measure_time_changes[] : composition.meter end |
#first_measure_key ⇒ Object
70 71 72 73 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 70 def first_measure_key @first_measure_key ||= measure_key_changes[.first] || key_element_values(composition.key_signature) end |
#first_measure_meter ⇒ Object
75 76 77 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 75 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.
148 149 150 151 152 153 154 155 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 148 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)
157 158 159 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 157 def key_element_values(key_signature) {fifths: KeyMapper.fifths(key_signature), mode: KeyMapper.mode(key_signature)} end |
#measure_key_changes ⇒ Object
58 59 60 61 62 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 58 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
64 65 66 67 68 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 64 def measure_time_changes @measure_time_changes ||= .zip(composition.).filter_map { |, | [, .meter] if .meter }.to_h end |
#placements_by_bar(voice) ⇒ Object
84 85 86 87 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 84 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.
100 101 102 103 104 105 106 107 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 100 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.
91 92 93 94 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 91 def whole_measure_duration() meter = effective_meter() (Rational(4 * meter.top_number, meter.bottom_number) * divisions).numerator end |