Class: HeadMusic::Notation::MusicXML::RenderPlan
- Inherits:
-
RenderPlan
- Object
- RenderPlan
- 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 flow: the divisions resolution, and the duration components and beams of every notehead, on top of the measure signatures the base plan tracks. 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 Method Summary collapse
- #annotate_bar(voice, bar_number, annotations) ⇒ Object private
-
#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
-
#group_unit_divisions(bar_number) ⇒ Object
private
The beam-group span in integer divisions for a bar's effective meter.
-
#key_value(event) ⇒ Object
private
is required and is optional, which is exactly the shape of the event: the signature always, the interpretation when there is one. - #precompute_eager_data ⇒ Object private
-
#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.
-
#written_duration(voice, bar_number) ⇒ Object
The divisions a voice's notes occupy in a bar: the sum of its components there, or the whole-measure rest that stands in for an empty bar.
Constructor Details
This class inherits a constructor from HeadMusic::Notation::RenderPlan
Instance Method Details
#annotate_bar(voice, bar_number, annotations) ⇒ Object (private)
82 83 84 85 86 87 88 89 90 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 82 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 |
#beam_annotations ⇒ Object
A Hash keyed by [placement, component_index] holding the Array
39 40 41 42 43 44 45 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 39 def beam_annotations @beam_annotations ||= {}.tap do |annotations| flow.voices.each do |voice| .each { || (voice, , annotations) } end end end |
#beam_levels(placement, component) ⇒ Object (private)
108 109 110 111 112 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 108 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)
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 92 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
30 31 32 33 34 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 30 def components_by_placement @components_by_placement ||= flow.voices.flat_map(&:placements).to_h do |placement| [placement, duration_writer.components(placement.rhythmic_value)] end end |
#divisions ⇒ Object
26 27 28 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 26 def divisions @divisions ||= Divisions.for(flow) end |
#duration_writer ⇒ Object (private)
78 79 80 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 78 def duration_writer @duration_writer ||= DurationWriter.new(divisions) 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.
117 118 119 120 121 122 123 124 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 117 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_value(event) ⇒ Object (private)
74 75 76 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 74 def key_value(event) {fifths: event.signature, mode: KeyMapper.mode(event.tonal_context)} end |
#precompute_eager_data ⇒ Object (private)
66 67 68 69 70 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 66 def precompute_eager_data key_value(flow.timeline.opening_key_signature_event) super 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.
49 50 51 52 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 49 def whole_measure_duration() meter = effective_meter() (Rational(4 * meter.top_number, meter.bottom_number) * divisions).numerator end |
#written_duration(voice, bar_number) ⇒ Object
The divisions a voice's notes occupy in a bar: the sum of its components there, or the whole-measure rest that stands in for an empty bar. Less than a measure where the voice ended mid-bar.
57 58 59 60 61 62 |
# File 'lib/head_music/notation/music_xml/render_plan.rb', line 57 def written_duration(voice, ) placements = (voice)[] return whole_measure_duration() unless placements placements.sum { |placement| components_by_placement[placement].sum(&:duration) } end |