View Javadoc
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   * Maps from MIDI ticks to microseconds.
24   * <p>
25   * This class is intended to be used only by the midi package and its
26   * subpackages.
27   *
28   * @author Jay-R Studer
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       * Constructs a new TempoCache with the specified MIDI Sequence
38       *
39       * @param sequence
40       *            the {@link Sequence} to build a tempo cache for
41       */
42      public TempoCache(Sequence sequence) {
43          resolution = sequence.getResolution();
44  
45          if (sequence.getDivisionType() != Sequence.PPQ) {
46              // SMPTE time divisions are constant throughout
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          // Get set tempo messages in micros per quarter note, keyed by ticks
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          // The default unspecified PPQ tempo is 0.5s per quarter note
67          int previousTempo = 500000;
68          tempos.putIfAbsent(0L, previousTempo);
69  
70          // Cache the total elapsed microsecond durations, keyed by ticks
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       * Extracts the new tempo from a Set Tempo message.
83       * <p>
84       * The Set Tempo message is formatted as {@code FF 51 03 xx xx xx}.
85       * {@code FF} signifies a {@link MetaMessage#META META} event. {@code 51}
86       * indicates it is a set tempo meta event. {@code 03} is the length of the
87       * following byte array and is always 3. {@code xx xx xx} is the new tempo
88       * in microseconds per quarter note, encoded as a 24-bit big endian integer.
89       *
90       * @param event
91       *            the set tempo {@link MidiEvent} to extract new tempo from
92       * @return the extracted tempo in microseconds per quarter note
93       */
94      @VisibleForTesting
95      static int extractTempo(MidiEvent event) {
96          // Read a 4 byte int that includes the 03 header, then remove the header
97          return ByteBuffer.wrap(event.getMessage().getMessage()).getInt(2) & 0xFFFFFF;
98      }
99  
100     /**
101      * Converts a MIDI tick into elapsed microseconds
102      *
103      * @param ticks
104      *            the MIDI ticks to convert into microseconds
105      * @return the corresponding number of microseconds
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 }