Class: HeadMusic::Content::Flow
- Inherits:
-
Object
- Object
- HeadMusic::Content::Flow
show all
- Defined in:
- lib/head_music/content/flow.rb,
lib/head_music/content/flow/timeline.rb,
lib/head_music/content/flow/deserializer.rb,
lib/head_music/content/flow/schema_values.rb,
lib/head_music/content/flow/hash_deserializer.rb,
lib/head_music/content/flow/staff_system_values.rb
Overview
A flow is a continuous span of music with its own timeline: a movement, a
song, a cue, or a single exercise. Its project is optional, so a flow may
stand alone; containment below is not: every voice is in a part, and every
part is in a flow.
Defined Under Namespace
Classes: Deserializer, HashDeserializer, SchemaValues, StaffSystemValues, Timeline
Constant Summary
collapse
- SCHEMA_VERSION =
4
Instance Attribute Summary collapse
Class Method Summary
collapse
Instance Method Summary
collapse
-
#add_comment(text, position = nil) ⇒ Object
-
#add_part(player: nil, instrument: nil, staff_system: nil) ⇒ Object
-
#add_voice(role: nil) ⇒ Object
A voice of its own, in a part of its own.
-
#bars(last = latest_bar_number) ⇒ Object
-
#bars_to_h ⇒ Object
private
Iterates the raw sparse array rather than the public #bars slice, which loses the number offset.
-
#cantus_firmus_voice ⇒ Object
-
#change_key_signature(bar_number, key_signature, tonal_context: nil) ⇒ Object
Allocating the bar as well as recording the change is what pulls the bar range back to a pickup bar that no voice places into -- which is what makes MusicXML mark it implicit.
-
#change_meter(bar_number, meter) ⇒ Object
-
#change_tempo(bar_number, tempo) ⇒ Object
-
#composer ⇒ Object
The cited work's composer wins over the authored string, which stays as the fallback for the parsed documents that have a name but no work.
-
#counterpoint_voice ⇒ Object
-
#earliest_bar_number ⇒ Object
-
#ensure_attributes(name, key_signature, meter, tempo) ⇒ Object
private
-
#ensure_source(source) ⇒ Object
private
-
#ensure_work(work) ⇒ Object
private
-
#first_allocated_bar_number ⇒ Object
private
Bars can be allocated below the voices' earliest bar (e.g. a key or meter change in a pickup bar), so the earliest bar reflects those allocations too.
-
#initialize(name: nil, key_signature: nil, meter: nil, tempo: nil, composer: nil, origin: nil, comments: nil, work: nil, source: nil) ⇒ Flow
constructor
-
#inspect ⇒ Object
The default would print the whole composition, since every placement reaches its voice and every voice its flow.
-
#key_signature ⇒ Object
The opening signature, meter, and tempo are the timeline's, so a change at bar 1 is a change like any other rather than a rewrite of the flow's own attributes.
-
#key_signature_change_at(bar_number) ⇒ Object
The key signature authored in a bar, as distinct from the one in force there.
-
#latest_bar_number ⇒ Object
-
#meter ⇒ Object
-
#position(code_or_bar, count = nil, tick = nil, subtick = nil) ⇒ Object
-
#tempo ⇒ Object
-
#tempo_to_h(tempo) ⇒ Object
private
Two fields rather than a "quarter = 72" string, so that a fractional tempo survives: Tempo.get reads the number by stripping non-digits.
-
#timeline_to_h ⇒ Object
Both fields of a key signature event, always: the signature is what is printed at the clef, and the tonal context is the interpretation, and neither derives the other.
-
#to_abc(**options) ⇒ Object
-
#to_h ⇒ Object
-
#to_json(*_args) ⇒ Object
-
#to_lilypond(**options) ⇒ Object
-
#to_musicxml ⇒ Object
-
#to_s ⇒ Object
-
#voices ⇒ Object
Constructor Details
#initialize(name: nil, key_signature: nil, meter: nil, tempo: nil, composer: nil, origin: nil, comments: nil, work: nil, source: nil) ⇒ Flow
Returns a new instance of Flow.
30
31
32
33
34
35
36
37
38
39
40
41
|
# File 'lib/head_music/content/flow.rb', line 30
def initialize(
name: nil, key_signature: nil, meter: nil, tempo: nil,
composer: nil, origin: nil, comments: nil, work: nil, source: nil
)
ensure_attributes(name, key_signature, meter, tempo)
@composer = composer
@origin = origin
@work = ensure_work(work)
@source = ensure_source(source)
@parts = []
@comments = Array().map { |text| HeadMusic::Content::Comment.new(self, text) }
end
|
Instance Attribute Details
Returns the value of attribute comments.
11
12
13
|
# File 'lib/head_music/content/flow.rb', line 11
def
@comments
end
|
#name ⇒ Object
Returns the value of attribute name.
11
12
13
|
# File 'lib/head_music/content/flow.rb', line 11
def name
@name
end
|
#origin ⇒ Object
Returns the value of attribute origin.
11
12
13
|
# File 'lib/head_music/content/flow.rb', line 11
def origin
@origin
end
|
#parts ⇒ Object
Returns the value of attribute parts.
11
12
13
|
# File 'lib/head_music/content/flow.rb', line 11
def parts
@parts
end
|
#project ⇒ Object
Returns the value of attribute project.
12
13
14
|
# File 'lib/head_music/content/flow.rb', line 12
def project
@project
end
|
#source ⇒ Object
The catalog identity this flow is one notation of, and the publication it
was taken from. Both optional: a parsed snippet cites neither.
16
17
18
|
# File 'lib/head_music/content/flow.rb', line 16
def source
@source
end
|
#timeline ⇒ Object
Returns the value of attribute timeline.
11
12
13
|
# File 'lib/head_music/content/flow.rb', line 11
def timeline
@timeline
end
|
#work ⇒ Object
The catalog identity this flow is one notation of, and the publication it
was taken from. Both optional: a parsed snippet cites neither.
16
17
18
|
# File 'lib/head_music/content/flow.rb', line 16
def work
@work
end
|
Class Method Details
.from_h(hash) ⇒ Object
22
23
24
|
# File 'lib/head_music/content/flow.rb', line 22
def self.from_h(hash)
HashDeserializer.new(hash).flow
end
|
.from_json(json) ⇒ Object
26
27
28
|
# File 'lib/head_music/content/flow.rb', line 26
def self.from_json(json)
from_h(JSON.parse(json))
end
|
Instance Method Details
65
66
67
68
|
# File 'lib/head_music/content/flow.rb', line 65
def (text, position = nil)
@comments << HeadMusic::Content::Comment.new(self, text, position)
@comments.last
end
|
#add_part(player: nil, instrument: nil, staff_system: nil) ⇒ Object
53
54
55
56
57
|
# File 'lib/head_music/content/flow.rb', line 53
def add_part(player: nil, instrument: nil, staff_system: nil)
HeadMusic::Content::Part
.new(flow: self, player: player, instrument: instrument, staff_system: staff_system)
.tap { |part| @parts << part }
end
|
#add_voice(role: nil) ⇒ Object
A voice of its own, in a part of its own. One part per voice is the shape
counterpoint wants, and the shape every reader produces on import.
61
62
63
|
# File 'lib/head_music/content/flow.rb', line 61
def add_voice(role: nil)
add_part.add_voice(role: role)
end
|
#bars(last = latest_bar_number) ⇒ Object
95
96
97
98
99
100
101
102
|
# File 'lib/head_music/content/flow.rb', line 95
def bars(last = latest_bar_number)
@bars ||= []
first = [earliest_bar_number, last].min
(first..last).each do |bar_number|
@bars[bar_number] ||= HeadMusic::Content::Bar.new(self, number: bar_number)
end
@bars[first..last]
end
|
#bars_to_h ⇒ Object
Iterates the raw sparse array rather than the public #bars slice, which
loses the number offset. Key and meter changes are the timeline's, so a bar
serializes its repeat structure and nothing else.
232
233
234
235
236
237
238
239
240
241
|
# File 'lib/head_music/content/flow.rb', line 232
def bars_to_h
(@bars || []).each_with_index.filter_map do |bar, number|
next if bar.nil?
bar_hash = bar.to_h
next if bar_hash.empty?
{"number" => number}.merge(bar_hash)
end
end
|
#cantus_firmus_voice ⇒ Object
133
134
135
|
# File 'lib/head_music/content/flow.rb', line 133
def cantus_firmus_voice
voices.detect(&:cantus_firmus?)
end
|
#change_key_signature(bar_number, key_signature, tonal_context: nil) ⇒ Object
Allocating the bar as well as recording the change is what pulls the bar
range back to a pickup bar that no voice places into -- which is what makes
MusicXML mark it implicit.
107
108
109
110
111
|
# File 'lib/head_music/content/flow.rb', line 107
def change_key_signature(bar_number, key_signature, tonal_context: nil)
Timeline.ensure_downbeat!(bar_number)
bars(bar_number)
timeline.change_key_signature(bar_number, key_signature, tonal_context: tonal_context)
end
|
#change_meter(bar_number, meter) ⇒ Object
113
114
115
116
117
|
# File 'lib/head_music/content/flow.rb', line 113
def change_meter(bar_number, meter)
Timeline.ensure_downbeat!(bar_number)
bars(bar_number)
timeline.change_meter(bar_number, meter)
end
|
#change_tempo(bar_number, tempo) ⇒ Object
119
120
121
122
123
|
# File 'lib/head_music/content/flow.rb', line 119
def change_tempo(bar_number, tempo)
Timeline.ensure_downbeat!(bar_number)
bars(bar_number)
timeline.change_tempo(bar_number, tempo)
end
|
#composer ⇒ Object
The cited work's composer wins over the authored string, which stays as the
fallback for the parsed documents that have a name but no work.
45
46
47
|
# File 'lib/head_music/content/flow.rb', line 45
def composer
work&.composer || @composer
end
|
#counterpoint_voice ⇒ Object
137
138
139
|
# File 'lib/head_music/content/flow.rb', line 137
def counterpoint_voice
voices.reject(&:cantus_firmus?).first
end
|
#earliest_bar_number ⇒ Object
125
126
127
|
# File 'lib/head_music/content/flow.rb', line 125
def earliest_bar_number
[voices.map(&:earliest_bar_number), first_allocated_bar_number, 1].flatten.compact.min
end
|
#ensure_attributes(name, key_signature, meter, tempo) ⇒ Object
218
219
220
221
|
# File 'lib/head_music/content/flow.rb', line 218
def ensure_attributes(name, key_signature, meter, tempo)
@name = name || "Composition"
@timeline = Timeline.new(key_signature: key_signature, meter: meter, tempo: tempo)
end
|
#ensure_source(source) ⇒ Object
212
213
214
215
216
|
# File 'lib/head_music/content/flow.rb', line 212
def ensure_source(source)
return HeadMusic::Content::Publication.from_h(source) if source.is_a?(Hash)
source
end
|
#ensure_work(work) ⇒ Object
206
207
208
209
210
|
# File 'lib/head_music/content/flow.rb', line 206
def ensure_work(work)
return HeadMusic::Content::Work.from_h(work) if work.is_a?(Hash)
work
end
|
#first_allocated_bar_number ⇒ Object
Bars can be allocated below the voices' earliest bar (e.g. a key or meter
change in a pickup bar), so the earliest bar reflects those allocations too.
202
203
204
|
# File 'lib/head_music/content/flow.rb', line 202
def first_allocated_bar_number
(@bars || []).index { |bar| !bar.nil? }
end
|
#inspect ⇒ Object
The default would print the whole composition, since every placement
reaches its voice and every voice its flow.
147
148
149
|
# File 'lib/head_music/content/flow.rb', line 147
def inspect
"#<#{self.class.name} #{self}>"
end
|
#key_signature ⇒ Object
The opening signature, meter, and tempo are the timeline's, so a change at
bar 1 is a change like any other rather than a rewrite of the flow's own
attributes.
77
78
79
|
# File 'lib/head_music/content/flow.rb', line 77
def key_signature
timeline.opening_key_signature_event.key_signature
end
|
#key_signature_change_at(bar_number) ⇒ Object
The key signature authored in a bar, as distinct from the one in force
there. Nil in a bar nothing was authored in.
91
92
93
|
# File 'lib/head_music/content/flow.rb', line 91
def key_signature_change_at(bar_number)
timeline.key_signature_change_at(bar_number)&.key_signature
end
|
#latest_bar_number ⇒ Object
129
130
131
|
# File 'lib/head_music/content/flow.rb', line 129
def latest_bar_number
[voices.map(&:latest_bar_number), 1].flatten.max
end
|
#meter ⇒ Object
81
82
83
|
# File 'lib/head_music/content/flow.rb', line 81
def meter
timeline.opening_meter
end
|
#position(code_or_bar, count = nil, tick = nil, subtick = nil) ⇒ Object
70
71
72
|
# File 'lib/head_music/content/flow.rb', line 70
def position(code_or_bar, count = nil, tick = nil, subtick = nil)
HeadMusic::Content::Position.new(self, code_or_bar, count, tick, subtick)
end
|
#tempo ⇒ Object
85
86
87
|
# File 'lib/head_music/content/flow.rb', line 85
def tempo
timeline.opening_tempo
end
|
#tempo_to_h(tempo) ⇒ Object
Two fields rather than a "quarter = 72" string, so that a fractional
tempo survives: Tempo.get reads the number by stripping non-digits.
225
226
227
|
# File 'lib/head_music/content/flow.rb', line 225
def tempo_to_h(tempo)
{"beat_value" => tempo.beat_value.to_s, "beats_per_minute" => tempo.beats_per_minute}
end
|
#timeline_to_h ⇒ Object
Both fields of a key signature event, always: the signature is what is
printed at the clef, and the tonal context is the interpretation, and
neither derives the other.
181
182
183
184
185
186
187
188
189
190
191
192
|
# File 'lib/head_music/content/flow.rb', line 181
def timeline_to_h
{
"meter" => meter.to_s,
"key_signature" => key_signature.name,
"tempo" => tempo_to_h(tempo),
"meter_changes" => timeline.meter_changes.map { |bar_number, value| {"number" => bar_number, "meter" => value.to_s} },
"key_signature_changes" => timeline.key_signature_changes.map { |bar_number, event|
{"number" => bar_number, "signature" => event.signature, "tonal_context" => event.tonal_context&.name}
},
"tempo_changes" => timeline.tempo_changes.map { |bar_number, value| {"number" => bar_number, "tempo" => tempo_to_h(value)} }
}
end
|
#to_abc(**options) ⇒ Object
151
152
153
|
# File 'lib/head_music/content/flow.rb', line 151
def to_abc(**options)
HeadMusic::Notation::ABC.render(self, **options)
end
|
#to_h ⇒ Object
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
# File 'lib/head_music/content/flow.rb', line 163
def to_h
{
"schema_version" => SCHEMA_VERSION,
"name" => name,
"composer" => composer&.to_s,
"origin" => origin&.to_s,
"work" => work&.to_h,
"source" => source&.to_h,
"timeline" => timeline_to_h,
"parts" => parts.map(&:to_h),
"bars" => bars_to_h,
"comments" => .map(&:to_h)
}
end
|
#to_json(*_args) ⇒ Object
194
195
196
|
# File 'lib/head_music/content/flow.rb', line 194
def to_json(*_args)
to_h.to_json
end
|
#to_lilypond(**options) ⇒ Object
#to_s ⇒ Object
141
142
143
|
# File 'lib/head_music/content/flow.rb', line 141
def to_s
"#{name} — #{voices.count} #{"voice".pluralize(voices.count)}"
end
|
#voices ⇒ Object
49
50
51
|
# File 'lib/head_music/content/flow.rb', line 49
def voices
parts.flat_map(&:voices)
end
|