Class: Webmidi::SMF::TempoMap

Inherits:
Object
  • Object
show all
Defined in:
lib/webmidi/smf/tempo_map.rb

Constant Summary collapse

DEFAULT_TEMPO =
500_000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entries = [], ppqn:) ⇒ TempoMap

Returns a new instance of TempoMap.

Raises:



24
25
26
27
28
29
30
31
# File 'lib/webmidi/smf/tempo_map.rb', line 24

def initialize(entries = [], ppqn:)
  raise InvalidSMFError, "PPQN must be positive" unless ppqn.is_a?(Integer) && ppqn.positive?

  @ppqn = ppqn
  @entries = entries.map { |entry| normalize_entry(entry) }.sort_by { |entry| entry[:tick] }
  @entries.unshift({tick: 0, tempo: DEFAULT_TEMPO}) if @entries.empty? || @entries.first[:tick] != 0
  freeze_entries!
end

Instance Attribute Details

#entriesObject (readonly)

Returns the value of attribute entries.



8
9
10
# File 'lib/webmidi/smf/tempo_map.rb', line 8

def entries
  @entries
end

#ppqnObject (readonly)

Returns the value of attribute ppqn.



8
9
10
# File 'lib/webmidi/smf/tempo_map.rb', line 8

def ppqn
  @ppqn
end

Class Method Details

.from_sequence(sequence) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/webmidi/smf/tempo_map.rb', line 10

def self.from_sequence(sequence)
  tempo_events = []
  sequence.each do |track|
    tick = 0
    track.each do |event|
      tick += event.delta_time
      if event.is_a?(MetaEvent) && event.type == MetaEvent::META_TYPES[:tempo]
        tempo_events << {tick: tick, tempo: event.tempo}
      end
    end
  end
  new(tempo_events, ppqn: sequence.ppqn)
end

Instance Method Details

#seconds_to_ticks(seconds) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/webmidi/smf/tempo_map.rb', line 53

def seconds_to_ticks(seconds)
  validate_non_negative_number!(seconds, "Seconds")
  remaining = seconds.to_f
  current_tick = 0

  @entries.each_with_index do |entry, index|
    next_tick = (index + 1 < @entries.size) ? @entries[index + 1][:tick] : nil
    segment_ticks = next_tick ? next_tick - current_tick : nil
    seconds_per_tick = entry[:tempo] / 1_000_000.0 / @ppqn

    if segment_ticks.nil?
      return current_tick + (remaining / seconds_per_tick).round
    end

    segment_seconds = segment_ticks * seconds_per_tick
    return current_tick + (remaining / seconds_per_tick).round if remaining <= segment_seconds

    remaining -= segment_seconds
    current_tick = next_tick
  end
end

#tempo_at(ticks) ⇒ Object



75
76
77
78
# File 'lib/webmidi/smf/tempo_map.rb', line 75

def tempo_at(ticks)
  validate_non_negative_number!(ticks, "Ticks")
  @entries.rfind { |entry| entry[:tick] <= ticks }[:tempo]
end

#ticks_to_seconds(ticks) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/webmidi/smf/tempo_map.rb', line 33

def ticks_to_seconds(ticks)
  validate_non_negative_number!(ticks, "Ticks")
  seconds = 0.0
  current_tick = 0

  @entries.each_with_index do |entry, index|
    next_tick = if index + 1 < @entries.size
      [@entries[index + 1][:tick], ticks].min
    else
      ticks
    end
    break if current_tick >= ticks

    seconds += ticks_segment_to_seconds(next_tick - current_tick, entry[:tempo])
    current_tick = next_tick
  end

  seconds
end