Class: HeadMusic::Notation::MusicXML::RenderPlan

Inherits:
RenderPlan
  • Object
show all
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

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 annotate_bar(voice, bar_number, annotations)
  placements = placements_by_bar(voice)[bar_number]
  return unless placements

  keys = []
  events = build_bar_events(placements, keys)
  beams = BeamGrouper.annotate(events, group_unit_divisions(bar_number))
  keys.each_with_index { |key, index| annotations[key] = beams[index] }
end

#beam_annotations ⇒ Object

A Hash keyed by [placement, component_index] holding the Array that BeamGrouper computed for that notehead. Built one bar at a time so a notehead's onset is its exact integer offset from the bar start.



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|
      bar_numbers.each { |bar_number| annotate_bar(voice, bar_number, 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 build_bar_events(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(bar_number)
  fraction = DurationWriter.single_quarter_fraction(effective_meter(bar_number).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)

is required and is optional, which is exactly the shape of the event: the signature always, the interpretation when there is one.



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(bar_number)
  meter = effective_meter(bar_number)
  (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, bar_number)
  placements = placements_by_bar(voice)[bar_number]
  return whole_measure_duration(bar_number) unless placements

  placements.sum { |placement| components_by_placement[placement].sum(&:duration) }
end