Class: HeadMusic::Content::Flow

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

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(comments).map { |text| HeadMusic::Content::Comment.new(self, text) }
end

Instance Attribute Details

#comments ⇒ Object (readonly)

Returns the value of attribute comments.



11
12
13
# File 'lib/head_music/content/flow.rb', line 11

def comments
  @comments
end

#name ⇒ Object (readonly)

Returns the value of attribute name.



11
12
13
# File 'lib/head_music/content/flow.rb', line 11

def name
  @name
end

#origin ⇒ Object (readonly)

Returns the value of attribute origin.



11
12
13
# File 'lib/head_music/content/flow.rb', line 11

def origin
  @origin
end

#parts ⇒ Object (readonly)

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 (readonly)

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

#add_comment(text, position = nil) ⇒ Object



65
66
67
68
# File 'lib/head_music/content/flow.rb', line 65

def add_comment(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 (private)

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 (private)



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 (private)



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 (private)



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 (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.



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 (private)

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" => 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



159
160
161
# File 'lib/head_music/content/flow.rb', line 159

def to_lilypond(**options)
  HeadMusic::Notation::LilyPond.render(self, **options)
end

#to_musicxml ⇒ Object



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

def to_musicxml
  HeadMusic::Notation::MusicXML.render(self)
end

#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