Class: HeadMusic::Content::Voice

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/content/voice.rb,
lib/head_music/content/voice/continuity.rb,
lib/head_music/content/voice/melodic_line.rb,
lib/head_music/content/voice/melodic_note_pair.rb

Overview

A Voice is a stream of music with some indepedence that is conceptually one part or for one performer. The melodic lines in counterpoint are each a voice.

Defined Under Namespace

Classes: Continuity, MelodicLine, MelodicNotePair

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(part: nil, flow: nil, role: nil) ⇒ Voice

A voice is always in a part, always in a flow. Given neither, it mints the chain rather than raising, so that a voice remains the smallest thing a caller can construct and reason about on its own.



20
21
22
23
24
25
26
27
28
29
# File 'lib/head_music/content/voice.rb', line 20

def initialize(part: nil, flow: nil, role: nil)
  @part = part || detached_part(flow)
  @role = role
  @placements = []
  # No stored event for the opening staff: a voice sits on its part's first
  # staff until it says otherwise, and a single-staff part needs no
  # assignments at all.
  @staff_assignment_map = HeadMusic::Time::EventMap.new
  @part.attach(self)
end

Instance Attribute Details

#partObject (readonly)

Returns the value of attribute part.



7
8
9
# File 'lib/head_music/content/voice.rb', line 7

def part
  @part
end

#placementsObject (readonly)

Returns the value of attribute placements.



7
8
9
# File 'lib/head_music/content/voice.rb', line 7

def placements
  @placements
end

#roleObject (readonly)

Returns the value of attribute role.



7
8
9
# File 'lib/head_music/content/voice.rb', line 7

def role
  @role
end

Instance Method Details

#assign_staff(bar_number, staff) ⇒ Object

Write this voice onto a staff of its own part from a bar onward.

A crossing is one event, not a span: a left hand that rises into the treble staff at bar 5 and comes back down at bar 9 is two crossings, each authored where it happens. A single cross-staff note is a crossing and, a bar later, another. There is no note-level special case, and nothing to overlap.

Parameters:



51
52
53
54
# File 'lib/head_music/content/voice.rb', line 51

def assign_staff(bar_number, staff)
  ensure_staff_in_system!(staff, bar_number)
  @staff_assignment_map.add(downbeat_of(bar_number), staff).value
end

#bar_number_of(placement) ⇒ Object (private)



167
168
169
# File 'lib/head_music/content/voice.rb', line 167

def bar_number_of(placement)
  placement ? placement.position.bar_number : 1
end

#cantus_firmus?Boolean

Returns:

  • (Boolean)


96
97
98
# File 'lib/head_music/content/voice.rb', line 96

def cantus_firmus?
  role.to_s =~ /cantus.?firmus/i
end

#cross_to(staff, from:) ⇒ Object

#assign_staff in the order the sentence is spoken: cross to the treble staff from bar 5.



58
59
60
# File 'lib/head_music/content/voice.rb', line 58

def cross_to(staff, from:)
  assign_staff(from, staff)
end

#detached_part(flow) ⇒ Object (private)

A part in the given flow, or in a flow of its own. Not registered with the flow: a voice constructed directly has never appeared in its flow's voices, and preserving that is what keeps the harmony guides seeing the same companions they saw before.



163
164
165
# File 'lib/head_music/content/voice.rb', line 163

def detached_part(flow)
  HeadMusic::Content::Part.new(flow: flow || HeadMusic::Content::Flow.new)
end

#downbeat_of(bar_number) ⇒ Object (private)



155
156
157
# File 'lib/head_music/content/voice.rb', line 155

def downbeat_of(bar_number)
  HeadMusic::Time::MusicalPosition.new(bar_number, HeadMusic::Time::MusicalPosition::FIRST_COUNT, 0, 0)
end

#earliest_bar_numberObject



100
101
102
# File 'lib/head_music/content/voice.rb', line 100

def earliest_bar_number
  bar_number_of(placements.first)
end

#ensure_staff_in_system!(staff, bar_number) ⇒ Object (private)

A voice may only be written on a staff its part actually has. Crossing to someone else's staff is a cue, which is a layout concern, not this.

Raises:

  • (ArgumentError)


149
150
151
152
153
# File 'lib/head_music/content/voice.rb', line 149

def ensure_staff_in_system!(staff, bar_number)
  return if part.staff_system_at(bar_number).include?(staff)

  raise ArgumentError, "the staff is not in the part's staff system at bar #{bar_number}"
end

#first_gapObject

Returns nil if placements are contiguous, or [expected_position, found_placement] for the first gap.



118
119
120
# File 'lib/head_music/content/voice.rb', line 118

def first_gap
  Continuity.new(flow, placements).first_gap
end

#insert_into_placements(placement) ⇒ Object (private)



185
186
187
# File 'lib/head_music/content/voice.rb', line 185

def insert_into_placements(placement)
  placements.insert(insertion_index(placement), placement)
end

#insertion_index(placement) ⇒ Object (private)

Positions are unique within a voice (place merges same-position placements), so insertion order is simply position order. Both the lookup and the insertion point are binary searches over that order, which keeps placing a long voice linear in its length rather than quadratic.



181
182
183
# File 'lib/head_music/content/voice.rb', line 181

def insertion_index(placement)
  placements.bsearch_index { |existing| existing > placement } || placements.length
end

#last_placementObject



108
109
110
# File 'lib/head_music/content/voice.rb', line 108

def last_placement
  placements.last
end

#latest_bar_numberObject



104
105
106
# File 'lib/head_music/content/voice.rb', line 104

def latest_bar_number
  bar_number_of(placements.last)
end

#melodic_lineObject



92
93
94
# File 'lib/head_music/content/voice.rb', line 92

def melodic_line
  @melodic_line ||= MelodicLine.new(notes)
end

#next_positionObject



112
113
114
# File 'lib/head_music/content/voice.rb', line 112

def next_position
  last_placement ? last_placement.next_position : HeadMusic::Content::Position.new(flow, 1, 1, 0)
end

#notesObject

Placements are kept in position order, so the notes and rests drawn from them are already ordered.



79
80
81
# File 'lib/head_music/content/voice.rb', line 79

def notes
  placements.select(&:pitched?)
end

#notes_not_in_keyObject



87
88
89
90
# File 'lib/head_music/content/voice.rb', line 87

def notes_not_in_key
  key_spellings = key_signature.spellings
  notes.reject { |note| key_spellings.include?(note.pitch.spelling) }
end

#pitches_stringObject (private)



189
190
191
# File 'lib/head_music/content/voice.rb', line 189

def pitches_string
  pitches.first(10).join(" ")
end

#place(position, rhythmic_value, sound_or_sounds = nil) ⇒ Object



66
67
68
69
70
71
72
73
74
75
# File 'lib/head_music/content/voice.rb', line 66

def place(position, rhythmic_value, sound_or_sounds = nil)
  # The melodic line is a snapshot of the notes, so any placement invalidates it.
  @melodic_line = nil
  placement = HeadMusic::Content::Placement.new(self, position, rhythmic_value, sound_or_sounds)
  existing = placement_at(placement.position)
  return existing.merge(placement) if existing

  insert_into_placements(placement)
  placement
end

#placement_at(position) ⇒ Object (private)



171
172
173
174
# File 'lib/head_music/content/voice.rb', line 171

def placement_at(position)
  candidate = placements.bsearch { |placement| placement.position >= position }
  candidate if candidate&.position == position
end

#restsObject



83
84
85
# File 'lib/head_music/content/voice.rb', line 83

def rests
  placements.select(&:rest?)
end

#staffObject



37
38
39
# File 'lib/head_music/content/voice.rb', line 37

def staff
  staff_at(HeadMusic::Time::MusicalPosition::DEFAULT_FIRST_BAR)
end

#staff_assignmentsObject



62
63
64
# File 'lib/head_music/content/voice.rb', line 62

def staff_assignments
  @staff_assignment_map.events.to_h { |event| [event.position.bar, event.value] }
end

#staff_assignments_to_hObject (private)

Serialized by index within the part's system at that bar, because a staff has no identity of its own -- two five-line treble staves are the same description of different staves.



140
141
142
143
144
145
# File 'lib/head_music/content/voice.rb', line 140

def staff_assignments_to_h
  staff_assignments.filter_map do |bar_number, staff|
    index = part.staff_system_at(bar_number).staves.index { |candidate| candidate.equal?(staff) }
    {"number" => bar_number, "staff" => index} if index
  end
end

#staff_at(bar_number) ⇒ HeadMusic::Content::Staff

Returns the staff this voice is written on at a bar; never nil, because a voice always has its part's first staff.

Returns:

  • (HeadMusic::Content::Staff)

    the staff this voice is written on at a bar; never nil, because a voice always has its part's first staff



33
34
35
# File 'lib/head_music/content/voice.rb', line 33

def staff_at(bar_number)
  @staff_assignment_map.at(downbeat_of(bar_number)) || part.staff_system_at(bar_number).first_staff
end

#to_hObject



128
129
130
131
132
133
# File 'lib/head_music/content/voice.rb', line 128

def to_h
  hash = {"role" => role&.to_s, "placements" => placements.map(&:to_h)}
  assignments = staff_assignments_to_h
  hash["staff_assignments"] = assignments unless assignments.empty?
  hash
end

#to_sObject



122
123
124
125
126
# File 'lib/head_music/content/voice.rb', line 122

def to_s
  return pitches_string if role.to_s.strip == ""

  [role, pitches_string].join(": ")
end