Class: HeadMusic::Time::TempoMap

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

Examples:

Basic usage

tempo_map = HeadMusic::Time::TempoMap.new
tempo_map.add_change(MusicalPosition.new(5, 1, 0, 0), "quarter", 96)
tempo_map.add_change(MusicalPosition.new(9, 1, 0, 0), "quarter", 140)

tempo = tempo_map.tempo_at(MusicalPosition.new(7, 1, 0, 0))
tempo.beats_per_minute # => 96.0

Iterating through segments

from = MusicalPosition.new(1, 1, 0, 0)
to = MusicalPosition.new(10, 1, 0, 0)
tempo_map.each_segment(from, to) do |start_pos, end_pos, tempo|
  # Calculate clock time for this segment
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(starting_tempo: nil, starting_position: nil) ⇒ TempoMap

Create a new tempo map

Parameters:

  • starting_tempo (HeadMusic::Rudiment::Tempo) (defaults to: nil)

    initial tempo (default: quarter = 120)

  • starting_position (MusicalPosition) (defaults to: nil)

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



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

#eventsArray<TempoEvent> (readonly)

Returns all tempo events in chronological order.

Returns:

  • (Array<TempoEvent>)

    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

Parameters:



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.

Parameters:

  • position (MusicalPosition)

    where the tempo change occurs

  • beat_value_or_tempo (String, HeadMusic::Rudiment::Tempo)

    either a beat value like “quarter” or a Tempo object

  • beats_per_minute (Numeric, nil) (defaults to: nil)

    BPM (required if beat_value_or_tempo is a string)

Returns:



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.

Parameters:

Yields:

  • (start_position, end_position, tempo)

    for each segment

Yield Parameters:



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

Parameters:

Returns:

  • (Boolean)

    true if 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.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 tempo change at the specified position

The starting tempo (first event) cannot be removed.

Parameters:



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.bar, 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.

Parameters:

Returns:



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