Class: HeadMusic::Notation::LilyPond::FlowBuilder

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

TICKS_PER_WHOLE_NOTE =
HeadMusic::Rudiment::Rhythm::PPQN * 4

Instance Attribute Summary collapse

Instance Method Summary collapse

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 place_whole_bar_rest(event, voice)
  when :bar_check then check_bar(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)
  bar_number = change_bar_number(event, voice, command)
  return if flow.public_send(at_reader, bar_number) == value
  if bar_number == 1 || flow.public_send(:"#{attribute}_change_at", bar_number)
    raise ParseError.new("Conflicting #{command} at bar #{bar_number}", line_number: event.line)
  end

  flow.public_send(changer, bar_number, value)
end

#bar_start?(position) ⇒ Boolean (private)

Returns:

  • (Boolean)


142
143
144
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 142

def bar_start?(position)
  position.count == 1 && position.tick.zero?
end

#build ⇒ Object (private)

Raises:



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 change_bar_number(event, voice, command)
  position = voice.next_position
  return position.bar_number if bar_start?(position)

  raise unsupported("#{command} in the middle of a bar is not supported", event)
end

#check_bar(event, voice) ⇒ Object (private)

Raises:



89
90
91
92
93
94
95
96
97
# File 'lib/head_music/notation/lily_pond/flow_builder.rb', line 89

def check_bar(event, voice)
  position = voice.next_position
  return if bar_start?(position)

  raise ParseError.new(
    "Bar check failed at: #{elapsed_fraction(position)} in bar #{position.bar_number}",
    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 place_whole_bar_rest(event, voice)
  position = voice.next_position
  unless bar_start?(position)
    raise unsupported("A whole-bar rest must start a bar", event)
  end

  meter = voice.flow.meter_at(position.bar_number)
  bar_fraction = Rational(meter.top_number, meter.bottom_number)
  rhythmic_value = HeadMusic::Notation::DottedDuration.rhythmic_value_for(event.fraction)
  unless event.fraction == bar_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(message, event)
  UnsupportedFeatureError.new(message, 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