diff --git a/examples/ex2.mid b/examples/ex2.mid new file mode 100644 index 0000000..a89f20c Binary files /dev/null and b/examples/ex2.mid differ diff --git a/lib/midilib/sequence.rb b/lib/midilib/sequence.rb index da7a70f..5700f8e 100644 --- a/lib/midilib/sequence.rb +++ b/lib/midilib/sequence.rb @@ -9,6 +9,7 @@ class Sequence UNNAMED = 'Unnamed Sequence' DEFAULT_TEMPO = 120 + BPM_ROUND = 3 NOTE_TO_LENGTH = { 'whole' => 4.0, @@ -62,21 +63,32 @@ def time_signature(numer, denom, clocks, qnotes) @qnotes = qnotes end - # Returns the song tempo in beats per minute. - def beats_per_minute + # Returns the song tempo in beats per minute, nil if time_from_start is out of range + def beats_per_minute(time_from_start = 0) return DEFAULT_TEMPO if @tracks.nil? || @tracks.empty? - - event = @tracks.first.events.detect { |e| e.is_a?(MIDI::Tempo) } - event ? Tempo.mpq_to_bpm(event.tempo) : DEFAULT_TEMPO + return nil if time_from_start > self.get_measures.last.end || time_from_start < 0 + current_bpm = DEFAULT_TEMPO + tempo_parts = get_tempo_parts + tempo_parts.each_with_index do |part, i| + if !tempo_parts[i+1].nil? + current_bpm = part[1] if part[0] <= time_from_start && tempo_parts[i+1][0] > time_from_start + else + current_bpm = part[1] if part[0] <= time_from_start + end + end + current_bpm end + alias bpm beats_per_minute alias tempo beats_per_minute # Pulses (also called ticks) are the units of delta times and event # time_from_start values. This method converts a number of pulses to a - # float value that is a time in seconds. - def pulses_to_seconds(pulses) - (pulses.to_f / @ppqn.to_f / beats_per_minute) * 60.0 + # float value that is a time in seconds. Returns nil if time_from_start out of range + def pulses_to_seconds(pulses, time_from_start = 0) + unless beats_per_minute(time_from_start).nil? + (pulses.to_f / @ppqn.to_f / beats_per_minute(time_from_start)) * 60.0 + end end # Given a note length name like "whole", "dotted quarter", or "8th @@ -200,5 +212,21 @@ def get_measures end measures end + + private + + # Private method to split sequence into parts, if more then one bpm present in sequence + # Returns hash { start_time => bpm } + def get_tempo_parts + tempo_parts = {} + return tempo_parts[0] = DEFAULT_TEMPO if @tracks.nil? || @tracks.empty? + Array(@tracks).each do |track| + track.events.map do |e| + e.is_a?(MIDI::Tempo) ? tempo_parts[e.time_from_start] = Tempo.mpq_to_bpm(e.tempo) : tempo_parts[0] = DEFAULT_TEMPO + end + end + tempo_parts.transform_values! { |bpm| bpm.round(BPM_ROUND) } + end + end end diff --git a/test/test_sequence.rb b/test/test_sequence.rb index 6610f31..58ad196 100644 --- a/test/test_sequence.rb +++ b/test/test_sequence.rb @@ -13,6 +13,7 @@ def setup @seq.tracks << @track 3.times { @track.events << MIDI::NoteOn.new(0, 64, 64, 100) } @track.recalc_times + @seq_bpm_diff = MIDI::Sequence.new end def test_basics @@ -31,6 +32,15 @@ def test_pulses_to_seconds # An eight note should take 0.25 seconds assert_in_delta 0.25, @seq.pulses_to_seconds(480 / 2), 0.00001 + + # At a tempo of 120 BPM 480 pulses (one quarter note) should take 0.5 seconds + assert_in_delta 0.5, @seq.pulses_to_seconds(480, 1000), 0.00001 + + # Should use offset = 0 if offset is not present + assert_in_delta 0.5, @seq.pulses_to_seconds(480), 0.00001 + + # Should retun nil if offset is out of range + assert_equal(nil, @seq.pulses_to_seconds(480, 1920)) end def test_length_to_delta @@ -77,4 +87,21 @@ def test_note_to_delta assert_equal(480 / 16, @seq.note_to_delta('sixtyfourth')) assert_equal(480 / 16, @seq.note_to_delta('64th')) end + + def test_beats_per_minute + # Using file with 2 different tempos whithin sequence (bpm change at 15600) + File.open('examples/ex2.mid', 'rb') do | file | + @seq_bpm_diff.read(file) + assert_equal(nil, @seq_bpm_diff.beats_per_minute(-1000)) + assert_equal(120.0, @seq_bpm_diff.beats_per_minute) + assert_equal(120.0, @seq_bpm_diff.beats_per_minute(15599)) + assert_equal(131.34, @seq_bpm_diff.beats_per_minute(15600)) + assert_equal(131.34, @seq_bpm_diff.beats_per_minute(15601)) + assert_equal(nil, @seq_bpm_diff.beats_per_minute(5000000)) + end + + # Using regular testing sequence + assert_equal(120.0, @seq.beats_per_minute(1918)) + assert_equal(nil, @seq.beats_per_minute(1920)) + end end