Class: HeadMusic::Content::Voice
- Inherits:
-
Object
- Object
- HeadMusic::Content::Voice
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
-
#assign_staff(bar_number, staff) ⇒ Object
Write this voice onto a staff of its own part from a bar onward.
-
#bar_number_of(placement) ⇒ Object
private
-
#cantus_firmus? ⇒ Boolean
-
#cross_to(staff, from:) ⇒ Object
#assign_staff in the order the sentence is spoken: cross to the treble staff from bar 5.
-
#detached_part(flow) ⇒ Object
private
A part in the given flow, or in a flow of its own.
-
#downbeat_of(bar_number) ⇒ Object
private
-
#earliest_bar_number ⇒ Object
-
#ensure_staff_in_system!(staff, bar_number) ⇒ Object
private
A voice may only be written on a staff its part actually has.
-
#first_gap ⇒ Object
Returns nil if placements are contiguous, or [expected_position, found_placement] for the first gap.
-
#initialize(part: nil, flow: nil, role: nil) ⇒ Voice
constructor
A voice is always in a part, always in a flow.
-
#insert_into_placements(placement) ⇒ Object
private
-
#insertion_index(placement) ⇒ Object
private
Positions are unique within a voice (place merges same-position placements), so insertion order is simply position order.
-
#last_placement ⇒ Object
-
#latest_bar_number ⇒ Object
-
#melodic_line ⇒ Object
-
#next_position ⇒ Object
-
#notes ⇒ Object
Placements are kept in position order, so the notes and rests drawn from them are already ordered.
-
#notes_not_in_key ⇒ Object
-
#pitches_string ⇒ Object
private
-
#place(position, rhythmic_value, sound_or_sounds = nil) ⇒ Object
-
#placement_at(position) ⇒ Object
private
-
#rests ⇒ Object
-
#staff ⇒ Object
-
#staff_assignments ⇒ Object
-
#staff_assignments_to_h ⇒ Object
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.
-
#staff_at(bar_number) ⇒ 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.
-
#to_h ⇒ Object
-
#to_s ⇒ Object
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 = []
@staff_assignment_map = HeadMusic::Time::EventMap.new
@part.attach(self)
end
|
Instance Attribute Details
#part ⇒ Object
Returns the value of attribute part.
7
8
9
|
# File 'lib/head_music/content/voice.rb', line 7
def part
@part
end
|
#placements ⇒ Object
Returns the value of attribute placements.
7
8
9
|
# File 'lib/head_music/content/voice.rb', line 7
def placements
@placements
end
|
#role ⇒ Object
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.
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
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
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
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.
#downbeat_of(bar_number) ⇒ Object
#earliest_bar_number ⇒ Object
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
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.
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_gap ⇒ Object
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
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
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_placement ⇒ Object
108
109
110
|
# File 'lib/head_music/content/voice.rb', line 108
def last_placement
placements.last
end
|
#latest_bar_number ⇒ Object
104
105
106
|
# File 'lib/head_music/content/voice.rb', line 104
def latest_bar_number
bar_number_of(placements.last)
end
|
#melodic_line ⇒ Object
92
93
94
|
# File 'lib/head_music/content/voice.rb', line 92
def melodic_line
@melodic_line ||= MelodicLine.new(notes)
end
|
#next_position ⇒ Object
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
|
#notes ⇒ Object
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_key ⇒ Object
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_string ⇒ Object
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)
@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
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
|
#rests ⇒ Object
83
84
85
|
# File 'lib/head_music/content/voice.rb', line 83
def rests
placements.select(&:rest?)
end
|
#staff_assignments ⇒ Object
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_h ⇒ Object
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
|
Returns 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_h ⇒ Object
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_s ⇒ Object
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
|