Class: HeadMusic::Time::MusicalTimeConverter

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/time/musical_time_converter.rb

Overview

Converts between clock time (elapsed nanoseconds) and musical position (bars:beats:ticks:subticks) using a tempo map and a meter map, walking the timeline one tempo segment at a time so tempo and meter changes are accounted for. Subticks are the shared integer unit both directions pass through.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tempo_map:, meter_map:, starting_musical_position:) ⇒ MusicalTimeConverter

Returns a new instance of MusicalTimeConverter.



11
12
13
14
15
# File 'lib/head_music/time/musical_time_converter.rb', line 11

def initialize(tempo_map:, meter_map:, starting_musical_position:)
  @tempo_map = tempo_map
  @meter_map = meter_map
  @starting_musical_position = starting_musical_position
end

Instance Attribute Details

#meter_mapObject (readonly, private)

Returns the value of attribute meter_map.



63
64
65
# File 'lib/head_music/time/musical_time_converter.rb', line 63

def meter_map
  @meter_map
end

#starting_musical_positionObject (readonly, private)

Returns the value of attribute starting_musical_position.



63
64
65
# File 'lib/head_music/time/musical_time_converter.rb', line 63

def starting_musical_position
  @starting_musical_position
end

#tempo_mapObject (readonly, private)

Returns the value of attribute tempo_map.



63
64
65
# File 'lib/head_music/time/musical_time_converter.rb', line 63

def tempo_map
  @tempo_map
end

Instance Method Details

#clock_to_musical(clock_position) ⇒ MusicalPosition

Returns the corresponding musical position.

Parameters:

Returns:



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/head_music/time/musical_time_converter.rb', line 19

def clock_to_musical(clock_position)
  target_nanoseconds = clock_position.nanoseconds
  accumulated_nanoseconds = 0
  current_position = starting_musical_position

  # We need an end position far enough to contain our target clock time
  estimated_end = MusicalPosition.new(starting_musical_position.bar + 1000, 1, 0, 0)

  tempo_map.each_segment(starting_musical_position, estimated_end) do |start_pos, end_pos, tempo|
    meter = meter_map.meter_at(start_pos)
    segment_nanoseconds = nanoseconds_in_segment(start_pos, end_pos, tempo, meter)

    # If our target falls within this segment, calculate the exact position
    if accumulated_nanoseconds + segment_nanoseconds >= target_nanoseconds
      remaining_nanoseconds = target_nanoseconds - accumulated_nanoseconds
      total_subticks = musical_position_to_subticks(start_pos, meter) +
        nanoseconds_to_subticks(remaining_nanoseconds, tempo)
      return subticks_to_musical_position(total_subticks, meter)
    end

    accumulated_nanoseconds += segment_nanoseconds
    current_position = end_pos
  end

  # If we get here, return the last position (shouldn't normally happen)
  current_position
end

#musical_position_to_subticks(position, meter = nil) ⇒ Object (private)

Convert a musical position to total subticks for calculation



66
67
68
69
70
71
72
73
74
75
# File 'lib/head_music/time/musical_time_converter.rb', line 66

def musical_position_to_subticks(position, meter = nil)
  meter ||= meter_map.meter_at(position)
  subticks_per_count = meter.ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK
  subticks_per_bar = meter.counts_per_bar * subticks_per_count

  (position.bar - 1) * subticks_per_bar +
    (position.beat - 1) * subticks_per_count +
    position.tick * HeadMusic::Time::SUBTICKS_PER_TICK +
    position.subtick
end

#musical_to_clock(musical_position) ⇒ ClockPosition

Returns the corresponding clock time.

Parameters:

Returns:



49
50
51
52
53
54
55
56
57
58
59
# File 'lib/head_music/time/musical_time_converter.rb', line 49

def musical_to_clock(musical_position)
  total_nanoseconds = 0

  # Iterate through each tempo segment from start to target position
  tempo_map.each_segment(starting_musical_position, musical_position) do |start_pos, end_pos, tempo|
    meter = meter_map.meter_at(start_pos)
    total_nanoseconds += nanoseconds_in_segment(start_pos, end_pos, tempo, meter)
  end

  ClockPosition.new(total_nanoseconds)
end

#nanoseconds_in_segment(start_pos, end_pos, tempo, meter) ⇒ Object (private)

Clock duration of a tempo segment, in nanoseconds



78
79
80
81
82
83
# File 'lib/head_music/time/musical_time_converter.rb', line 78

def nanoseconds_in_segment(start_pos, end_pos, tempo, meter)
  segment_subticks = musical_position_to_subticks(end_pos, meter) -
    musical_position_to_subticks(start_pos, meter)
  segment_ticks = segment_subticks / HeadMusic::Time::SUBTICKS_PER_TICK.to_f
  (segment_ticks * tempo.tick_duration_in_nanoseconds).round
end

#nanoseconds_to_subticks(nanoseconds, tempo) ⇒ Object (private)

Convert an elapsed nanosecond duration into subticks at the given tempo



86
87
88
89
# File 'lib/head_music/time/musical_time_converter.rb', line 86

def nanoseconds_to_subticks(nanoseconds, tempo)
  ticks = nanoseconds / tempo.tick_duration_in_nanoseconds.to_f
  (ticks * HeadMusic::Time::SUBTICKS_PER_TICK).round
end

#subticks_to_musical_position(total_subticks, meter) ⇒ Object (private)

Decompose total subticks into a normalized bar:beat:tick:subtick position



92
93
94
95
96
97
98
99
100
101
# File 'lib/head_music/time/musical_time_converter.rb', line 92

def subticks_to_musical_position(total_subticks, meter)
  subticks_per_count = meter.ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK
  subticks_per_bar = meter.counts_per_bar * subticks_per_count

  bars, remaining = total_subticks.divmod(subticks_per_bar)
  beats, remaining = remaining.divmod(subticks_per_count)
  ticks, subticks = remaining.divmod(HeadMusic::Time::SUBTICKS_PER_TICK)

  MusicalPosition.new(bars + 1, beats + 1, ticks, subticks).normalize!(meter)
end