Class: HeadMusic::Content::Flow::Timeline
- Inherits:
-
Object
- Object
- HeadMusic::Content::Flow::Timeline
- 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
-
#opening_key_signature_event ⇒ HeadMusic::Time::KeySignatureEvent
readonly
The signature the flow opens in.
-
#opening_meter ⇒ HeadMusic::Rudiment::Meter
readonly
The meter the flow opens in.
-
#opening_tempo ⇒ HeadMusic::Rudiment::Tempo
readonly
The tempo the flow opens in.
Class Method Summary collapse
-
.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.
-
.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.
-
.fifths_of(key_signature) ⇒ Object
Sharps positive, flats negative.
-
.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.
-
.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.
Instance Method Summary collapse
- #change_key_signature(bar_number, signature, tonal_context: nil) ⇒ Object
- #change_meter(bar_number, meter) ⇒ Object
- #change_tempo(bar_number, tempo) ⇒ Object
-
#changed_bar_numbers ⇒ Object
Every bar in which something was authored.
- #changes_by_bar(map) ⇒ Object private
- #downbeat_of(bar_number) ⇒ Object private
-
#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.
-
#ensure_value!(value, noun, remover) ⇒ Object
private
20.x cleared a change by passing nil.
-
#initialize(meter: nil, key_signature: nil, tempo: nil) ⇒ Timeline
constructor
A new instance of Timeline.
- #key_signature_at(bar_number) ⇒ Object
- #key_signature_change_at(bar_number) ⇒ Object
- #key_signature_changes ⇒ Object
- #key_signature_event_at(bar_number) ⇒ Object
- #meter_at(bar_number) ⇒ Object
-
#meter_change_at(bar_number) ⇒ Object
The change authored in a bar, as distinct from the value in force there.
-
#meter_changes ⇒ Object
The authored changes, by bar number.
- #remove_key_signature_change(bar_number) ⇒ Object
-
#remove_meter_change(bar_number) ⇒ Object
Un-author a change.
- #remove_tempo_change(bar_number) ⇒ Object
- #signature_at(bar_number) ⇒ Object
- #tempo_at(bar_number) ⇒ Object
- #tempo_change_at(bar_number) ⇒ Object
- #tempo_changes ⇒ Object
- #tonal_context_at(bar_number) ⇒ Object
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.
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.
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.
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.
78 79 80 81 82 83 |
# File 'lib/head_music/content/flow/timeline.rb', line 78 def self.ensure_downbeat!() return if .is_a?(Integer) raise ArgumentError, "a change falls on the downbeat of a bar, so it takes a bar number, got #{.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
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
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
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(, signature, tonal_context: nil) ensure_value!(signature, "key signature", :remove_key_signature_change) position = downbeat_of() event = if signature.is_a?(Integer) ensure_readable!(signature, tonal_context, ) 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(, meter) ensure_value!(meter, "meter", :remove_meter_change) @meter_map.add(downbeat_of(), 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(, tempo) ensure_value!(tempo, "tempo", :remove_tempo_change) @tempo_map.add(downbeat_of(), 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 (@meter_map.events + @tempo_map.events + @key_signature_map.events) .map { |event| event.position. }.uniq.sort end |
#changes_by_bar(map) ⇒ Object (private)
226 227 228 |
# File 'lib/head_music/content/flow/timeline.rb', line 226 def (map) map.events.to_h { |event| [event.position., 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() HeadMusic::Time::MusicalPosition.new(self.class.ensure_downbeat!(), 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.
219 220 221 222 223 224 |
# File 'lib/head_music/content/flow/timeline.rb', line 219 def ensure_readable!(signature, tonal_context, ) return if tonal_context || HeadMusic::Rudiment::Key::MAJOR_KEY_NAMES_BY_FIFTHS.key?(signature) raise ArgumentError, "a signature of #{signature} fifths at bar #{} 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.
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() key_signature_event_at().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() @key_signature_map.change_at(downbeat_of())&.value end |
#key_signature_changes ⇒ Object
152 153 154 |
# File 'lib/head_music/content/flow/timeline.rb', line 152 def key_signature_changes (@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() @key_signature_map.at(downbeat_of()) end |
#meter_at(bar_number) ⇒ Object
44 45 46 |
# File 'lib/head_music/content/flow/timeline.rb', line 44 def meter_at() @meter_map.at(downbeat_of()) 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() @meter_map.change_at(downbeat_of())&.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 (@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() @key_signature_map.remove(downbeat_of())&.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() @meter_map.remove(downbeat_of())&.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() @tempo_map.remove(downbeat_of())&.value end |
#signature_at(bar_number) ⇒ Object
60 61 62 |
# File 'lib/head_music/content/flow/timeline.rb', line 60 def signature_at() key_signature_event_at().signature end |
#tempo_at(bar_number) ⇒ Object
48 49 50 |
# File 'lib/head_music/content/flow/timeline.rb', line 48 def tempo_at() @tempo_map.at(downbeat_of()) end |
#tempo_change_at(bar_number) ⇒ Object
126 127 128 |
# File 'lib/head_music/content/flow/timeline.rb', line 126 def tempo_change_at() @tempo_map.change_at(downbeat_of())&.value end |
#tempo_changes ⇒ Object
156 157 158 |
# File 'lib/head_music/content/flow/timeline.rb', line 156 def tempo_changes (@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() key_signature_event_at().tonal_context end |