Class: HeadMusic::Notation::ABC::VoiceState

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/notation/abc/voice_state.rb

Overview

Per-voice interpretation state and the note-assembly it drives. Accidentals, the deferred note, ties, and volta tracking are all independent between voices, so each voice owns one of these. Beam adjacency and tie bookkeeping are exposed as transitions rather than raw setters, and the parser hands notes and chords here to be buffered, tied, and flushed onto the voice — the behavior that reads and mutates this state lives with the state itself.

Defined Under Namespace

Classes: PendingNote

Constant Summary collapse

ONE =

The identity length scale, used wherever no stretching applies.

Rational(1)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(voice, pitch_builder, duration_resolver = nil) ⇒ VoiceState

Returns a new instance of VoiceState.



28
29
30
31
32
33
34
35
# File 'lib/head_music/notation/abc/voice_state.rb', line 28

def initialize(voice, pitch_builder, duration_resolver = nil)
  @voice = voice
  @pitch_builder = pitch_builder
  @duration_resolver = duration_resolver
  @beam_break_pending = false
  @beam_last_was_note = false
  @tie_open = false
end

Instance Attribute Details

#active_passes ⇒ Object

Returns the value of attribute active_passes.



25
26
27
# File 'lib/head_music/notation/abc/voice_state.rb', line 25

def active_passes
  @active_passes
end

#awaiting_scale ⇒ Object

Returns the value of attribute awaiting_scale.



25
26
27
# File 'lib/head_music/notation/abc/voice_state.rb', line 25

def awaiting_scale
  @awaiting_scale
end

#broken_line ⇒ Object

Returns the value of attribute broken_line.



25
26
27
# File 'lib/head_music/notation/abc/voice_state.rb', line 25

def broken_line
  @broken_line
end

#pending_note ⇒ Object

Returns the value of attribute pending_note.



25
26
27
# File 'lib/head_music/notation/abc/voice_state.rb', line 25

def pending_note
  @pending_note
end

#pitch_builder ⇒ Object (readonly)

Returns the value of attribute pitch_builder.



24
25
26
# File 'lib/head_music/notation/abc/voice_state.rb', line 24

def pitch_builder
  @pitch_builder
end

#tie_line ⇒ Object (readonly)

Returns the value of attribute tie_line.



24
25
26
# File 'lib/head_music/notation/abc/voice_state.rb', line 24

def tie_line
  @tie_line
end

#voice ⇒ Object (readonly)

Returns the value of attribute voice.



24
25
26
# File 'lib/head_music/notation/abc/voice_state.rb', line 24

def voice
  @voice
end

#volta_start_bar ⇒ Object

Returns the value of attribute volta_start_bar.



25
26
27
# File 'lib/head_music/notation/abc/voice_state.rb', line 25

def volta_start_bar
  @volta_start_bar
end

Instance Method Details

#bar_start?(position) ⇒ Boolean (private)

Returns:

  • (Boolean)


166
167
168
# File 'lib/head_music/notation/abc/voice_state.rb', line 166

def bar_start?(position)
  position.to_a.drop(1) == [1, 0, 0]
end

#close_tie ⇒ Object



87
88
89
90
# File 'lib/head_music/notation/abc/voice_state.rb', line 87

def close_tie
  @tie_open = false
  @tie_line = nil
end

#completed_bar_number ⇒ Object

Counted from where the last note ends rather than where it starts: a note tied across a bar line is still pending when the repeat tagger asks, and a note longer than its bar has crossed one already.



40
41
42
43
44
45
# File 'lib/head_music/notation/abc/voice_state.rb', line 40

def completed_bar_number
  finish = pending_note ? pending_end_position : voice.last_placement&.next_position
  return unless finish

  bar_start?(finish) ? finish.bar_number - 1 : finish.bar_number
end

#defer_placement(pitches, length, inner_scale = ONE) ⇒ Object

Buffers a note or chord as the pending note, flushing whatever was pending first. A broken-rhythm scale awaiting its right note, and any per-chord inner scale, fold into the buffered length. When a tie is open the arriving note instead extends the pending tie chain.



96
97
98
99
100
101
102
103
104
105
# File 'lib/head_music/notation/abc/voice_state.rb', line 96

def defer_placement(pitches, length, inner_scale = ONE)
  scale = (awaiting_scale || ONE) * inner_scale
  self.awaiting_scale = nil
  return tie_onto_pending(pitches, length, scale) if tie_open?

  flush_pending_note
  self.pending_note = PendingNote.new(
    pitches: pitches, length: length, scale: scale, beam_break: next_beam_break
  )
end

#entered_bar_number ⇒ Object



47
48
49
# File 'lib/head_music/notation/abc/voice_state.rb', line 47

def entered_bar_number
  (pending_note ? pending_end_position : voice.next_position).bar_number
end

#flush_pending_note ⇒ Object

Places the pending note onto the voice, if any, carrying its authored beam break onto the placement.



109
110
111
112
113
114
115
# File 'lib/head_music/notation/abc/voice_state.rb', line 109

def flush_pending_note
  pending = pending_note
  return unless pending

  self.pending_note = nil
  place_next(pending_rhythmic_value(pending), pending.pitches).beam_break_before = pending.beam_break
end

#letters_and_registers(pitches) ⇒ Object (private)



158
159
160
# File 'lib/head_music/notation/abc/voice_state.rb', line 158

def letters_and_registers(pitches)
  pitches.map { |pitch| [pitch.letter_name.to_s, pitch.register] }.sort
end

#mark_beam_break ⇒ Object

Records an explicit beam break; the next note consumes it.



52
53
54
# File 'lib/head_music/notation/abc/voice_state.rb', line 52

def mark_beam_break
  @beam_break_pending = true
end

#next_beam_break ⇒ Object

The beam-break flag for the note now beginning: true if a break was marked, false if the previous token was also a note (so they beam together), or nil when there is no prior note to beam against. Consumes the pending break and records that a note is now current.



60
61
62
63
64
65
66
67
68
69
# File 'lib/head_music/notation/abc/voice_state.rb', line 60

def next_beam_break
  flag = if @beam_break_pending
    true
  elsif @beam_last_was_note
    false
  end
  @beam_break_pending = false
  @beam_last_was_note = true
  flag
end

#open_tie(line) ⇒ Object



82
83
84
85
# File 'lib/head_music/notation/abc/voice_state.rb', line 82

def open_tie(line)
  @tie_open = true
  @tie_line = line
end

#pending_end_position ⇒ Object (private)



162
163
164
# File 'lib/head_music/notation/abc/voice_state.rb', line 162

def pending_end_position
  voice.next_position + pending_rhythmic_value(pending_note)
end

#pending_rhythmic_value(pending) ⇒ Object (private)

A pending note's own value, with any tied prefix appended ahead of it so the whole tie chain renders as one sounding note.



172
173
174
175
176
# File 'lib/head_music/notation/abc/voice_state.rb', line 172

def pending_rhythmic_value(pending)
  own = @duration_resolver.rhythmic_value(pending.length, scale: pending.scale)
  prefix = pending.tied_prefix
  prefix ? prefix.append_tied(own) : own
end

#place(length, pitches, scale: ONE) ⇒ Object

Places a note, chord, or rest (nil pitches) directly onto the voice, bypassing the pending-note buffer.



119
120
121
# File 'lib/head_music/notation/abc/voice_state.rb', line 119

def place(length, pitches, scale: ONE)
  place_next(@duration_resolver.rhythmic_value(length, scale: scale), pitches)
end

#place_next(rhythmic_value, pitches) ⇒ Object (private)



125
126
127
# File 'lib/head_music/notation/abc/voice_state.rb', line 125

def place_next(rhythmic_value, pitches)
  voice.place(voice.next_position, rhythmic_value, pitches)
end

#reset_beam_adjacency ⇒ Object

After a rest, bar line, volta, or voice change, the next note must not beam to whatever preceded the boundary.



73
74
75
76
# File 'lib/head_music/notation/abc/voice_state.rb', line 73

def reset_beam_adjacency
  @beam_last_was_note = false
  @beam_break_pending = false
end

#same_letters?(pitches, other_pitches) ⇒ Boolean (private)

Returns:

  • (Boolean)


154
155
156
# File 'lib/head_music/notation/abc/voice_state.rb', line 154

def same_letters?(pitches, other_pitches)
  letters_and_registers(pitches) == letters_and_registers(other_pitches)
end

#tie_onto_pending(pitches, length, scale) ⇒ Object (private)

Closes an open tie: the pending note becomes the new note's tied prefix, so the pair (and any longer chain) resolves to a single placement whose rhythmic value carries the author's chosen split.



132
133
134
135
136
137
138
139
140
# File 'lib/head_music/notation/abc/voice_state.rb', line 132

def tie_onto_pending(pitches, length, scale)
  pending = pending_note
  prefix = pending_rhythmic_value(pending)
  close_tie
  self.pending_note = PendingNote.new(
    pitches: tied_pitches(pending, pitches), length: length, scale: scale, tied_prefix: prefix,
    beam_break: pending.beam_break
  )
end

#tie_open? ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/head_music/notation/abc/voice_state.rb', line 78

def tie_open?
  @tie_open
end

#tied_pitches(pending, pitches) ⇒ Object (private)

A tie carries its accidental across the bar line, where the key signature would otherwise respell the note, so the same letters in the same octaves are the same pitches.

Raises:



145
146
147
148
149
150
151
152
# File 'lib/head_music/notation/abc/voice_state.rb', line 145

def tied_pitches(pending, pitches)
  return pending.pitches if same_letters?(pending.pitches, pitches)

  raise ParseError.new(
    "A tie must connect two notes of the same pitch",
    line_number: tie_line, snippet: "-"
  )
end