Class: HeadMusic::Time::MeterMap
- Inherits:
-
Object
- Object
- HeadMusic::Time::MeterMap
- Defined in:
- lib/head_music/time/meter_map.rb
Overview
Manages meter (time signature) changes along a musical timeline
A MeterMap maintains a sorted list of meter changes at specific musical positions, allowing you to determine which meter is active at any point and iterate through meter segments for musical position calculations.
This is essential for normalizing musical positions when the meter changes during a composition.
Instance Attribute Summary collapse
-
#events ⇒ Array<MeterEvent>
readonly
All meter events in chronological order.
Instance Method Summary collapse
-
#add_change(position, meter_or_identifier) ⇒ MeterEvent
Add a meter change at the specified position.
-
#clear_changes
Remove all meter changes except the starting meter.
-
#compare_positions(pos1, pos2) ⇒ Integer
private
Compare two positions.
-
#each_segment(from_position, to_position) {|start_position, end_position, meter| ... }
Iterate through meter segments between two positions.
-
#initialize(starting_meter: nil, starting_position: nil) ⇒ MeterMap
constructor
Create a new meter map.
-
#meter_at(position) ⇒ HeadMusic::Rudiment::Meter
Find the meter active at a given position.
-
#positions_equal?(pos1, pos2) ⇒ Boolean
private
Check if two positions are equal.
-
#remove_change(position)
Remove a meter change at the specified position.
-
#sort_events!
private
Sort events by position.
Constructor Details
#initialize(starting_meter: nil, starting_position: nil) ⇒ MeterMap
Create a new meter map
36 37 38 39 40 |
# File 'lib/head_music/time/meter_map.rb', line 36 def initialize(starting_meter: nil, starting_position: nil) starting_meter = HeadMusic::Rudiment::Meter.get(starting_meter || "4/4") starting_position ||= MusicalPosition.new @events = [MeterEvent.new(starting_position, starting_meter)] end |
Instance Attribute Details
#events ⇒ Array<MeterEvent> (readonly)
Returns all meter events in chronological order.
30 31 32 |
# File 'lib/head_music/time/meter_map.rb', line 30 def events @events end |
Instance Method Details
#add_change(position, meter_or_identifier) ⇒ MeterEvent
Add a meter change at the specified position
If a meter change already exists at this position, it will be replaced. Events are automatically maintained in sorted order.
50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/head_music/time/meter_map.rb', line 50 def add_change(position, meter_or_identifier) # Remove any existing event at this position (except the first) remove_change(position) # Create the new event meter = meter_or_identifier.is_a?(HeadMusic::Rudiment::Meter) ? meter_or_identifier : HeadMusic::Rudiment::Meter.get(meter_or_identifier) event = MeterEvent.new(position, meter) @events << event sort_events! event end |
#clear_changes
This method returns an undefined value.
Remove all meter changes except the starting meter
78 79 80 |
# File 'lib/head_music/time/meter_map.rb', line 78 def clear_changes @events = [@events.first] end |
#compare_positions(pos1, pos2) ⇒ Integer (private)
Compare two positions
167 168 169 170 |
# File 'lib/head_music/time/meter_map.rb', line 167 def compare_positions(pos1, pos2) [pos1., pos1.beat, pos1.tick, pos1.subtick] <=> [pos2., pos2.beat, pos2.tick, pos2.subtick] end |
#each_segment(from_position, to_position) {|start_position, end_position, meter| ... }
This method returns an undefined value.
Iterate through meter segments between two positions
Yields each segment with its start position, end position, and meter. Segments are created wherever a meter change occurs within the range.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/head_music/time/meter_map.rb', line 111 def each_segment(from_position, to_position) # Find events that affect this range relevant_events = @events.select do |event| compare_positions(event.position, to_position) < 0 end # Start with the meter active at from_position current_pos = from_position current_meter = meter_at(from_position) # Iterate through relevant events relevant_events.each do |event| # Skip events before our starting position next if compare_positions(event.position, from_position) <= 0 # Yield the segment up to this event yield current_pos, event.position, current_meter # Move to next segment current_pos = event.position current_meter = event.meter end # Yield the final segment to the end position yield current_pos, to_position, current_meter end |
#meter_at(position) ⇒ HeadMusic::Rudiment::Meter
Find the meter active at a given position
Returns the meter from the most recent meter event at or before the specified position.
89 90 91 92 93 94 95 96 97 |
# File 'lib/head_music/time/meter_map.rb', line 89 def meter_at(position) # Find the last event at or before this position # We need to compare positions carefully since they might not be normalized active_event = @events.reverse.find do |event| compare_positions(event.position, position) <= 0 end active_event&.meter || @events.first.meter end |
#positions_equal?(pos1, pos2) ⇒ Boolean (private)
Check if two positions are equal
155 156 157 158 159 160 |
# File 'lib/head_music/time/meter_map.rb', line 155 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 meter change at the specified position
The starting meter (first event) cannot be removed.
69 70 71 72 73 |
# File 'lib/head_music/time/meter_map.rb', line 69 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
143 144 145 146 147 148 |
# File 'lib/head_music/time/meter_map.rb', line 143 def sort_events! @events.sort_by! do |event| pos = event.position [pos., pos.beat, pos.tick, pos.subtick] end end |