Class: HeadMusic::Notation::LilyPond::FlowBuilder
- Inherits:
-
Object
- Object
- HeadMusic::Notation::LilyPond::FlowBuilder
- Defined in:
- lib/head_music/notation/lily_pond/flow_builder.rb
Overview
Replays a Document's voice streams onto a fresh flow.
Every placement lands at the voice's next position, so bar checks and key or meter commands are verified against where the music actually is, the way LilyPond verifies them at compile time.
Defined Under Namespace
Classes: Cursor
Constant Summary collapse
Instance Attribute Summary collapse
-
#document ⇒ Object
readonly
Returns the value of attribute document.
Instance Method Summary collapse
- #apply(event, voice) ⇒ Object private
-
#apply_change(event, voice, command, attribute, at_reader, changer) ⇒ Object
private
A change already in force at its bar is a no-op (the writer repeats each change in every voice); a different explicit value at that bar, or any disagreement with the seed at bar one, is a conflict.
- #bar_start?(position) ⇒ Boolean private
- #build ⇒ Object private
-
#change_bar_number(event, voice, command) ⇒ Object
private
Key and meter live on bars, so a change is only representable at a bar's start.
- #check_bar(event, voice) ⇒ Object private
- #elapsed_fraction(position) ⇒ Object private
- #flow ⇒ Object
-
#initialize(document) ⇒ FlowBuilder
constructor
A new instance of FlowBuilder.
-
#place_whole_bar_rest(event, voice) ⇒ Object
private
A whole-bar rest is one placement filling the bar it starts; a longer span (R1*2 is two bars in LilyPond) has no single-placement representation yet.
-
#replay(cursors) ⇒ Object
private
The voices advance together rather than one after another, so a \time or \key that one staff carries is in force before another staff places the bar it governs.
- #unsupported(message, event) ⇒ Object private
- #whole_notes(fraction) ⇒ Object private
Constructor Details
#initialize(document) ⇒ FlowBuilder
Returns a new instance of FlowBuilder.
42 43 44 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 42 def initialize(document) @document = document end |
Instance Attribute Details
#document ⇒ Object (readonly)
Returns the value of attribute document.
40 41 42 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 40 def document @document end |
Instance Method Details
#apply(event, voice) ⇒ Object (private)
78 79 80 81 82 83 84 85 86 87 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 78 def apply(event, voice) case event.kind when :note then voice.place(voice.next_position, event.rhythmic_value, event.pitches) when :rest then voice.place(voice.next_position, event.rhythmic_value) when :whole_bar_rest then (event, voice) when :bar_check then (event, voice) when :key then apply_change(event, voice, "\\key", :key_signature, :key_signature_at, :change_key_signature) when :time then apply_change(event, voice, "\\time", :meter, :meter_at, :change_meter) end end |
#apply_change(event, voice, command, attribute, at_reader, changer) ⇒ Object (private)
A change already in force at its bar is a no-op (the writer repeats each change in every voice); a different explicit value at that bar, or any disagreement with the seed at bar one, is a conflict.
121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 121 def apply_change(event, voice, command, attribute, at_reader, changer) flow = voice.flow value = event.public_send(attribute) = (event, voice, command) return if flow.public_send(at_reader, ) == value if == 1 || flow.public_send(:"#{attribute}_change_at", ) raise ParseError.new("Conflicting #{command} at bar #{}", line_number: event.line) end flow.public_send(changer, , value) end |
#bar_start?(position) ⇒ Boolean (private)
142 143 144 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 142 def (position) position.count == 1 && position.tick.zero? end |
#build ⇒ Object (private)
52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 52 def build streams = document.streams raise ParseError, "LilyPond input contains no music" if streams.empty? flow = HeadMusic::Content::Flow.new( name: document.title, composer: document.composer, key_signature: document.first_key_signature, meter: document.first_meter ) replay(streams.map { |stream| Cursor.new(stream, flow.add_voice(role: stream.role)) }) flow end |
#change_bar_number(event, voice, command) ⇒ Object (private)
Key and meter live on bars, so a change is only representable at a bar's start.
135 136 137 138 139 140 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 135 def (event, voice, command) position = voice.next_position return position. if (position) raise unsupported("#{command} in the middle of a bar is not supported", event) end |
#check_bar(event, voice) ⇒ Object (private)
89 90 91 92 93 94 95 96 97 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 89 def (event, voice) position = voice.next_position return if (position) raise ParseError.new( "Bar check failed at: #{elapsed_fraction(position)} in bar #{position.}", line_number: event.line, snippet: "|" ) end |
#elapsed_fraction(position) ⇒ Object (private)
150 151 152 153 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 150 def elapsed_fraction(position) meter = position.meter Rational(position.count - 1, meter.bottom_number) + Rational(position.tick, TICKS_PER_WHOLE_NOTE) end |
#flow ⇒ Object
46 47 48 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 46 def flow @flow ||= build end |
#place_whole_bar_rest(event, voice) ⇒ Object (private)
A whole-bar rest is one placement filling the bar it starts; a longer span (R1*2 is two bars in LilyPond) has no single-placement representation yet.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 102 def (event, voice) position = voice.next_position unless (position) raise unsupported("A whole-bar rest must start a bar", event) end meter = voice.flow.meter_at(position.) = Rational(meter.top_number, meter.bottom_number) rhythmic_value = HeadMusic::Notation::DottedDuration.rhythmic_value_for(event.fraction) unless event.fraction == && rhythmic_value raise unsupported("Multi-bar rests are not yet supported (#{whole_notes(event.fraction)} whole notes in #{meter})", event) end voice.place(position, rhythmic_value) end |
#replay(cursors) ⇒ Object (private)
The voices advance together rather than one after another, so a \time or \key that one staff carries is in force before another staff places the bar it governs. Replaying a whole voice at a time would leave the earlier voices positioned under the old meter.
68 69 70 71 72 73 74 75 76 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 68 def replay(cursors) cursors = cursors.reject(&:done?) until cursors.empty? cursor = cursors.min_by(&:sort_key) apply(cursor.event, cursor.voice) cursor.advance cursors = cursors.reject(&:done?) end end |
#unsupported(message, event) ⇒ Object (private)
155 156 157 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 155 def unsupported(, event) UnsupportedFeatureError.new(, line_number: event.line) end |
#whole_notes(fraction) ⇒ Object (private)
146 147 148 |
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 146 def whole_notes(fraction) (fraction.denominator == 1) ? fraction.numerator : fraction end |