1 package net.kreatious.pianoleopard.midi.event;
2
3 import static java.util.stream.Collectors.collectingAndThen;
4 import static java.util.stream.Collectors.toMap;
5
6 import java.nio.ByteBuffer;
7 import java.util.Collections;
8 import java.util.Map;
9 import java.util.Map.Entry;
10 import java.util.NavigableMap;
11 import java.util.TreeMap;
12 import java.util.concurrent.TimeUnit;
13 import java.util.stream.IntStream;
14 import java.util.stream.Stream;
15
16 import javax.sound.midi.MetaMessage;
17 import javax.sound.midi.MidiEvent;
18 import javax.sound.midi.Sequence;
19
20 import com.google.common.annotations.VisibleForTesting;
21
22
23
24
25
26
27
28
29
30 public class TempoCache {
31 private final int resolution;
32
33 private final NavigableMap<Long, Integer> tempos;
34 private final NavigableMap<Long, Long> microseconds = new TreeMap<>();
35
36
37
38
39
40
41
42 public TempoCache(Sequence sequence) {
43 resolution = sequence.getResolution();
44
45 if (sequence.getDivisionType() != Sequence.PPQ) {
46
47 tempos = new TreeMap<>(Collections.singletonMap(Long.MIN_VALUE,
48 (int) (TimeUnit.SECONDS.toMicros(1) / sequence.getDivisionType())));
49 microseconds.put(0L, 0L);
50 return;
51 }
52
53
54 tempos = Stream
55 .of(sequence.getTracks())
56 .limit(1)
57 .flatMap(track -> IntStream.range(0, track.size()).mapToObj(track::get))
58 .filter(midiEvent -> midiEvent.getMessage().getStatus() == MetaMessage.META)
59 .filter(midiEvent -> midiEvent.getMessage().getMessage()[1] == 0x51)
60 .filter(midiEvent -> midiEvent.getMessage().getMessage()[2] == 0x03)
61 .collect(
62 collectingAndThen(
63 toMap(MidiEvent::getTick, TempoCache::extractTempo, (oldValue, newValue) -> newValue,
64 TreeMap::new), (Map<Long, Integer> map) -> (NavigableMap<Long, Integer>) map));
65
66
67 int previousTempo = 500000;
68 tempos.putIfAbsent(0L, previousTempo);
69
70
71 long previousEventTick = 0;
72 long elapsedMicroseconds = 0;
73 for (final Entry<Long, Integer> tempo : tempos.entrySet()) {
74 elapsedMicroseconds += (tempo.getKey() - previousEventTick) * previousTempo / resolution;
75 previousEventTick = tempo.getKey();
76 previousTempo = tempo.getValue();
77 microseconds.put(tempo.getKey(), elapsedMicroseconds);
78 }
79 }
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94 @VisibleForTesting
95 static int extractTempo(MidiEvent event) {
96
97 return ByteBuffer.wrap(event.getMessage().getMessage()).getInt(2) & 0xFFFFFF;
98 }
99
100
101
102
103
104
105
106
107 public long ticksToMicroseconds(long ticks) {
108 final long previousEventTick = microseconds.floorKey(ticks);
109 final long elapsedMicroseconds = microseconds.get(previousEventTick);
110 final int currentTempo = tempos.floorEntry(ticks).getValue();
111
112 return elapsedMicroseconds + (ticks - previousEventTick) * currentTempo / resolution;
113 }
114 }