Class: HeadMusic::Time::TempoMap
- Inherits:
-
Object
- Object
- HeadMusic::Time::TempoMap
- Defined in:
- lib/head_music/time/tempo_map.rb
Overview
Manages tempo changes along a musical timeline
A TempoMap maintains a sorted list of tempo changes at specific musical positions, allowing you to determine which tempo is active at any point and iterate through tempo segments for time calculations.
This is essential for converting between clock time and musical position when the tempo changes during a composition.
Instance Attribute Summary collapse
-
#events ⇒ Array<TempoEvent>
readonly
All tempo events in chronological order.
-
#meter
writeonly
private
Set the meter for position normalization.
Instance Method Summary collapse
-
#add_change(position, beat_value_or_tempo, beats_per_minute = nil) ⇒ TempoEvent
Add a tempo change at the specified position.
-
#clear_changes
Remove all tempo changes except the starting tempo.
-
#each_segment(from_position, to_position) {|start_position, end_position, tempo| ... }
Iterate through tempo segments between two positions.
-
#initialize(starting_tempo: nil, starting_position: nil) ⇒ TempoMap
constructor
Create a new tempo map.
-
#positions_equal?(pos1, pos2) ⇒ Boolean
private
Check if two positions are equal.
-
#remove_change(position)
Remove a tempo change at the specified position.
-
#sort_events!
private
Sort events by position.
-
#tempo_at(position) ⇒ HeadMusic::Rudiment::Tempo
Find the tempo active at a given position.
Constructor Details
#initialize(starting_tempo: nil, starting_position: nil) ⇒ TempoMap
Create a new tempo map
36 37 38 39 40 41 |
# File 'lib/head_music/time/tempo_map.rb', line 36 def initialize(starting_tempo: nil, starting_position: nil) starting_tempo ||= HeadMusic::Rudiment::Tempo.new("quarter", 120) starting_position ||= MusicalPosition.new @events = [TempoEvent.new(starting_position, starting_tempo.beat_value.to_s, starting_tempo.beats_per_minute)] @meter = nil # Will be set when used with a MeterMap end |
Instance Attribute Details
#events ⇒ Array<TempoEvent> (readonly)
Returns all tempo events in chronological order.
30 31 32 |
# File 'lib/head_music/time/tempo_map.rb', line 30 def events @events end |
#meter=(value) (writeonly)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Set the meter for position normalization
160 161 162 |
# File 'lib/head_music/time/tempo_map.rb', line 160 def meter=(value) @meter = value end |
Instance Method Details
#add_change(position, beat_value_or_tempo, beats_per_minute = nil) ⇒ TempoEvent
Add a tempo change at the specified position
If a tempo change already exists at this position, it will be replaced. Events are automatically maintained in sorted order.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/head_music/time/tempo_map.rb', line 52 def add_change(position, beat_value_or_tempo, beats_per_minute = nil) # Remove any existing event at this position (except the first) remove_change(position) # Create the new event event = if beat_value_or_tempo.is_a?(HeadMusic::Rudiment::Tempo) TempoEvent.new(position, beat_value_or_tempo.beat_value.to_s, beat_value_or_tempo.beats_per_minute).tap do |e| e.tempo = beat_value_or_tempo end else TempoEvent.new(position, beat_value_or_tempo, beats_per_minute) end @events << event sort_events! event end |
#clear_changes
This method returns an undefined value.
Remove all tempo changes except the starting tempo
85 86 87 |
# File 'lib/head_music/time/tempo_map.rb', line 85 def clear_changes @events = [@events.first] end |
#each_segment(from_position, to_position) {|start_position, end_position, tempo| ... }
This method returns an undefined value.
Iterate through tempo segments between two positions
Yields each segment with its start position, end position, and tempo. Segments are created wherever a tempo change occurs within the range.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/head_music/time/tempo_map.rb', line 121 def each_segment(from_position, to_position) # Normalize positions if we have a meter from_pos = @meter ? from_position.dup.tap { |p| p.normalize!(@meter) } : from_position to_pos = @meter ? to_position.dup.tap { |p| p.normalize!(@meter) } : to_position # Find events that affect this range relevant_events = @events.select do |event| normalized_event_pos = @meter ? event.position.dup.tap { |p| p.normalize!(@meter) } : event.position normalized_event_pos < to_pos end # Start with the tempo active at from_position current_pos = from_pos current_tempo = tempo_at(from_pos) # Iterate through relevant events relevant_events.each do |event| normalized_event_pos = @meter ? event.position.dup.tap { |p| p.normalize!(@meter) } : event.position # Skip events before our starting position next if normalized_event_pos <= from_pos # Yield the segment up to this event yield current_pos, normalized_event_pos, current_tempo # Move to next segment current_pos = normalized_event_pos current_tempo = event.tempo end # Yield the final segment to the end position yield current_pos, to_pos, current_tempo end |
#positions_equal?(pos1, pos2) ⇒ Boolean (private)
Check if two positions are equal
179 180 181 182 183 184 |
# File 'lib/head_music/time/tempo_map.rb', line 179 def positions_equal?(pos1, pos2) pos1. == pos2. && pos1.beat == pos2.beat && pos1.tick == pos2.tick && pos1.subtick == pos2.subtick end |
#remove_change(position)
This method returns an undefined value.
Remove a tempo change at the specified position
The starting tempo (first event) cannot be removed.
76 77 78 79 80 |
# File 'lib/head_music/time/tempo_map.rb', line 76 def remove_change(position) @events.reject! do |event| event != @events.first && positions_equal?(event.position, position) end end |
#sort_events! (private)
This method returns an undefined value.
Sort events by position
167 168 169 170 171 172 |
# File 'lib/head_music/time/tempo_map.rb', line 167 def sort_events! @events.sort_by! do |event| pos = event.position [pos., pos.beat, pos.tick, pos.subtick] end end |
#tempo_at(position) ⇒ HeadMusic::Rudiment::Tempo
Find the tempo active at a given position
Returns the tempo from the most recent tempo event at or before the specified position.
96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/head_music/time/tempo_map.rb', line 96 def tempo_at(position) # Normalize positions for comparison if we have a meter normalized_pos = @meter ? position.dup.tap { |p| p.normalize!(@meter) } : position # Find the last event at or before this position active_event = @events.reverse.find do |event| normalized_event_pos = @meter ? event.position.dup.tap { |p| p.normalize!(@meter) } : event.position normalized_event_pos <= normalized_pos end active_event&.tempo || @events.first.tempo end |