Class: HeadMusic::Time::MeterMap

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

Examples:

Basic usage

meter_map = HeadMusic::Time::MeterMap.new
meter_map.add_change(MusicalPosition.new(5, 1, 0, 0), "3/4")
meter_map.add_change(MusicalPosition.new(9, 1, 0, 0), "6/8")

meter = meter_map.meter_at(MusicalPosition.new(7, 1, 0, 0))
meter.to_s # => "3/4"

Iterating through segments

from = MusicalPosition.new(1, 1, 0, 0)
to = MusicalPosition.new(10, 1, 0, 0)
meter_map.each_segment(from, to) do |start_pos, end_pos, meter|
  # Process each meter segment
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(starting_meter: nil, starting_position: nil) ⇒ MeterMap

Create a new meter map

Parameters:

  • starting_meter (HeadMusic::Rudiment::Meter, String) (defaults to: nil)

    initial meter (default: 4/4)

  • starting_position (MusicalPosition) (defaults to: nil)

    where the initial meter begins (default: 1:1:0:0)



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

#eventsArray<MeterEvent> (readonly)

Returns all meter events in chronological order.

Returns:

  • (Array<MeterEvent>)

    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.

Parameters:

Returns:



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

Parameters:

Returns:

  • (Integer)

    -1, 0, or 1



167
168
169
170
# File 'lib/head_music/time/meter_map.rb', line 167

def compare_positions(pos1, pos2)
  [pos1.bar, pos1.beat, pos1.tick, pos1.subtick] <=>
    [pos2.bar, 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.

Parameters:

Yields:

  • (start_position, end_position, meter)

    for each segment

Yield Parameters:



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.

Parameters:

Returns:



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

Parameters:

Returns:

  • (Boolean)

    true if 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.bar == pos2.bar &&
    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.

Parameters:



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.bar, pos.beat, pos.tick, pos.subtick]
  end
end