Class: HeadMusic::Content::Flow::Timeline

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/content/flow/timeline.rb

Overview

The three things that change over a flow's course: meter, tempo, and key signature.

They get an owner rather than hanging off the flow directly, because a flow already absorbs bars, parts, voices, and three renderings, and three more maps on top of that is more than one class should hold.

Keyed by bar number, not position. Meter and key changes are bar-aligned by definition -- a time signature applies from a downbeat -- and keying by position would create a construction cycle, since building a position rolls over its counts, which needs the meter, which would need a position. Bar numbers have no such dependency.

The opening value is the map's default, not its first event. That is what keeps "this flow is in 3/4" distinct from "the meter changes to 3/4 at bar 1" -- a distinction the writers depend on, since a change is what makes them print a signature mid-piece.

Constant Summary collapse

DOWNBEAT =
HeadMusic::Time::MusicalPosition::FIRST_COUNT

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(meter: nil, key_signature: nil, tempo: nil) ⇒ Timeline

Returns a new instance of Timeline.



31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/head_music/content/flow/timeline.rb', line 31

def initialize(meter: nil, key_signature: nil, tempo: nil)
  @opening_meter = HeadMusic::Rudiment::Meter.get(meter || HeadMusic::Rudiment::Meter.default)
  @opening_tempo = tempo ? self.class.tempo_for(tempo) : HeadMusic::Rudiment::Tempo.new("quarter", 120)
  @opening_key_signature_event = self.class.event_for(
    HeadMusic::Rudiment::KeySignature.get(key_signature || HeadMusic::Rudiment::KeySignature.default),
    downbeat_of(HeadMusic::Time::MusicalPosition::DEFAULT_FIRST_BAR)
  )

  @meter_map = HeadMusic::Time::EventMap.new(default: @opening_meter)
  @tempo_map = HeadMusic::Time::EventMap.new(default: @opening_tempo)
  @key_signature_map = HeadMusic::Time::EventMap.new(default: @opening_key_signature_event)
end

Instance Attribute Details

#opening_key_signature_event ⇒ HeadMusic::Time::KeySignatureEvent (readonly)

Returns the signature the flow opens in.

Returns:



29
30
31
# File 'lib/head_music/content/flow/timeline.rb', line 29

def opening_key_signature_event
  @opening_key_signature_event
end

#opening_meter ⇒ HeadMusic::Rudiment::Meter (readonly)

Returns the meter the flow opens in.

Returns:



23
24
25
# File 'lib/head_music/content/flow/timeline.rb', line 23

def opening_meter
  @opening_meter
end

#opening_tempo ⇒ HeadMusic::Rudiment::Tempo (readonly)

Returns the tempo the flow opens in.

Returns:



26
27
28
# File 'lib/head_music/content/flow/timeline.rb', line 26

def opening_tempo
  @opening_tempo
end

Class Method Details

.ensure_downbeat!(bar_number) ⇒ Object

Meter and key signature changes are bar-aligned by definition: a time signature applies from a downbeat, and a signature printed mid-bar is not a thing notation expresses. The timeline takes a bar number rather than a position so that the off-downbeat case cannot be written down, and refuses anything that is not one rather than rounding it into a bar.

Raises:

  • (ArgumentError)


78
79
80
81
82
83
# File 'lib/head_music/content/flow/timeline.rb', line 78

def self.ensure_downbeat!(bar_number)
  return bar_number if bar_number.is_a?(Integer)

  raise ArgumentError,
    "a change falls on the downbeat of a bar, so it takes a bar number, got #{bar_number.inspect}"
end

.event_for(key_signature, position, tonal_context: nil) ⇒ Object

A KeySignature already carries a tonic and a scale type -- there is no signature-only diatonic context in the gem -- so the interpretation it was built from is recovered rather than discarded.



170
171
172
173
174
175
176
# File 'lib/head_music/content/flow/timeline.rb', line 170

def self.event_for(key_signature, position, tonal_context: nil)
  HeadMusic::Time::KeySignatureEvent.new(
    position,
    fifths_of(key_signature),
    tonal_context: tonal_context || tonal_context_of(key_signature)
  )
end

.fifths_of(key_signature) ⇒ Object

Sharps positive, flats negative. Theoretical keys count each double accidental twice, which is what MusicXML's means too.



180
181
182
# File 'lib/head_music/content/flow/timeline.rb', line 180

def self.fifths_of(key_signature)
  key_signature.num_sharps - key_signature.num_flats
end

.tempo_for(tempo) ⇒ Object

Meter.get and KeySignature.get accept one of their own instances; Tempo.get does not, and handed a Tempo would stringify it and answer the default, so the check lives here.



93
94
95
96
97
# File 'lib/head_music/content/flow/timeline.rb', line 93

def self.tempo_for(tempo)
  return tempo if tempo.is_a?(HeadMusic::Rudiment::Tempo)

  HeadMusic::Rudiment::Tempo.get(tempo)
end

.tonal_context_of(key_signature) ⇒ Object

Narrowed to a Key or a Mode where the scale type is one, and left as the key signature where it is not.

The gem admits key signatures the two subclasses cannot hold -- a harmonic minor, a whole tone -- and those are constructible flows even though MusicXML cannot render them as a element. Forcing every signature through Key or Mode would move that limit from render time to construction time and make such a flow unbuildable.



192
193
194
195
196
197
198
199
200
201
202
# File 'lib/head_music/content/flow/timeline.rb', line 192

def self.tonal_context_of(key_signature)
  scale_type = key_signature.scale_type.name.to_sym
  name = "#{key_signature.tonic_spelling} #{scale_type}"
  if HeadMusic::Rudiment::Key::QUALITIES.include?(scale_type)
    HeadMusic::Rudiment::Key.get(name)
  elsif HeadMusic::Rudiment::Mode::MODES.include?(scale_type)
    HeadMusic::Rudiment::Mode.get(name)
  else
    key_signature
  end
end

Instance Method Details

#change_key_signature(bar_number, signature, tonal_context: nil) ⇒ Object

Parameters:

  • signature (Integer, HeadMusic::Rudiment::KeySignature, String) —

    fifths, or something namable as a key signature, from which fifths and an interpretation are both taken



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/head_music/content/flow/timeline.rb', line 102

def change_key_signature(bar_number, signature, tonal_context: nil)
  ensure_value!(signature, "key signature", :remove_key_signature_change)
  position = downbeat_of(bar_number)
  event =
    if signature.is_a?(Integer)
      ensure_readable!(signature, tonal_context, bar_number)
      HeadMusic::Time::KeySignatureEvent.new(position, signature, tonal_context: tonal_context)
    else
      self.class.event_for(HeadMusic::Rudiment::KeySignature.get(signature), position, tonal_context: tonal_context)
    end
  @key_signature_map.add(position, event).value
end

#change_meter(bar_number, meter) ⇒ Object



68
69
70
71
# File 'lib/head_music/content/flow/timeline.rb', line 68

def change_meter(bar_number, meter)
  ensure_value!(meter, "meter", :remove_meter_change)
  @meter_map.add(downbeat_of(bar_number), HeadMusic::Rudiment::Meter.get(meter)).value
end

#change_tempo(bar_number, tempo) ⇒ Object



85
86
87
88
# File 'lib/head_music/content/flow/timeline.rb', line 85

def change_tempo(bar_number, tempo)
  ensure_value!(tempo, "tempo", :remove_tempo_change)
  @tempo_map.add(downbeat_of(bar_number), self.class.tempo_for(tempo)).value
end

#changed_bar_numbers ⇒ Object

Every bar in which something was authored. Empty for a flow that merely opens in a meter and a key.



162
163
164
165
# File 'lib/head_music/content/flow/timeline.rb', line 162

def changed_bar_numbers
  (@meter_map.events + @tempo_map.events + @key_signature_map.events)
    .map { |event| event.position.bar }.uniq.sort
end

#changes_by_bar(map) ⇒ Object (private)



226
227
228
# File 'lib/head_music/content/flow/timeline.rb', line 226

def changes_by_bar(map)
  map.events.to_h { |event| [event.position.bar, event.value] }
end

#downbeat_of(bar_number) ⇒ Object (private)



230
231
232
# File 'lib/head_music/content/flow/timeline.rb', line 230

def downbeat_of(bar_number)
  HeadMusic::Time::MusicalPosition.new(self.class.ensure_downbeat!(bar_number), DOWNBEAT, 0, 0)
end

#ensure_readable!(signature, tonal_context, bar_number) ⇒ Object (private)

A signature past the conventional table -- eight sharps is G sharp major -- has no reading of its own, so every consumer that needs a tonic would raise later, far from here. Requiring the interpretation up front puts the error where the signature was authored.

Raises:

  • (ArgumentError)


219
220
221
222
223
224
# File 'lib/head_music/content/flow/timeline.rb', line 219

def ensure_readable!(signature, tonal_context, bar_number)
  return if tonal_context || HeadMusic::Rudiment::Key::MAJOR_KEY_NAMES_BY_FIFTHS.key?(signature)

  raise ArgumentError,
    "a signature of #{signature} fifths at bar #{bar_number} has no conventional key; give it a tonal_context"
end

#ensure_value!(value, noun, remover) ⇒ Object (private)

20.x cleared a change by passing nil. That now has its own verb, and nil is refused here rather than reaching a rudiment getter that would raise something unrelated.

Raises:

  • (ArgumentError)


209
210
211
212
213
# File 'lib/head_music/content/flow/timeline.rb', line 209

def ensure_value!(value, noun, remover)
  return unless value.nil?

  raise ArgumentError, "a #{noun} change takes a #{noun}, got nil; to clear an authored change, use ##{remover}"
end

#key_signature_at(bar_number) ⇒ Object



56
57
58
# File 'lib/head_music/content/flow/timeline.rb', line 56

def key_signature_at(bar_number)
  key_signature_event_at(bar_number).key_signature
end

#key_signature_change_at(bar_number) ⇒ Object



122
123
124
# File 'lib/head_music/content/flow/timeline.rb', line 122

def key_signature_change_at(bar_number)
  @key_signature_map.change_at(downbeat_of(bar_number))&.value
end

#key_signature_changes ⇒ Object



152
153
154
# File 'lib/head_music/content/flow/timeline.rb', line 152

def key_signature_changes
  changes_by_bar(@key_signature_map)
end

#key_signature_event_at(bar_number) ⇒ Object



52
53
54
# File 'lib/head_music/content/flow/timeline.rb', line 52

def key_signature_event_at(bar_number)
  @key_signature_map.at(downbeat_of(bar_number))
end

#meter_at(bar_number) ⇒ Object



44
45
46
# File 'lib/head_music/content/flow/timeline.rb', line 44

def meter_at(bar_number)
  @meter_map.at(downbeat_of(bar_number))
end

#meter_change_at(bar_number) ⇒ Object

The change authored in a bar, as distinct from the value in force there. Nil in a bar the author declared nothing in, including the opening bar of a flow that simply has a meter.



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

def meter_change_at(bar_number)
  @meter_map.change_at(downbeat_of(bar_number))&.value
end

#meter_changes ⇒ Object

The authored changes, by bar number. The => value shape is what the writers already consume, so repointing them off the bars costs nothing at the call site.



148
149
150
# File 'lib/head_music/content/flow/timeline.rb', line 148

def meter_changes
  changes_by_bar(@meter_map)
end

#remove_key_signature_change(bar_number) ⇒ Object



137
138
139
# File 'lib/head_music/content/flow/timeline.rb', line 137

def remove_key_signature_change(bar_number)
  @key_signature_map.remove(downbeat_of(bar_number))&.value
end

#remove_meter_change(bar_number) ⇒ Object

Un-author a change. The value in force reverts to whatever the previous change, or the opening value, says. Answers the removed value, or nil where nothing was authored in that bar.



133
134
135
# File 'lib/head_music/content/flow/timeline.rb', line 133

def remove_meter_change(bar_number)
  @meter_map.remove(downbeat_of(bar_number))&.value
end

#remove_tempo_change(bar_number) ⇒ Object



141
142
143
# File 'lib/head_music/content/flow/timeline.rb', line 141

def remove_tempo_change(bar_number)
  @tempo_map.remove(downbeat_of(bar_number))&.value
end

#signature_at(bar_number) ⇒ Object



60
61
62
# File 'lib/head_music/content/flow/timeline.rb', line 60

def signature_at(bar_number)
  key_signature_event_at(bar_number).signature
end

#tempo_at(bar_number) ⇒ Object



48
49
50
# File 'lib/head_music/content/flow/timeline.rb', line 48

def tempo_at(bar_number)
  @tempo_map.at(downbeat_of(bar_number))
end

#tempo_change_at(bar_number) ⇒ Object



126
127
128
# File 'lib/head_music/content/flow/timeline.rb', line 126

def tempo_change_at(bar_number)
  @tempo_map.change_at(downbeat_of(bar_number))&.value
end

#tempo_changes ⇒ Object



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

def tempo_changes
  changes_by_bar(@tempo_map)
end

#tonal_context_at(bar_number) ⇒ Object



64
65
66
# File 'lib/head_music/content/flow/timeline.rb', line 64

def tonal_context_at(bar_number)
  key_signature_event_at(bar_number).tonal_context
end