1 /*
2  * Copyright (c) 2003, 2019, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package com.sun.media.sound;
27 
28 import java.util.ArrayList;
29 
30 import javax.sound.midi.InvalidMidiDataException;
31 import javax.sound.midi.MetaMessage;
32 import javax.sound.midi.MidiDevice;
33 import javax.sound.midi.MidiEvent;
34 import javax.sound.midi.MidiMessage;
35 import javax.sound.midi.Sequence;
36 import javax.sound.midi.Track;
37 
38 import static javax.sound.midi.SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE;
39 import static javax.sound.midi.SysexMessage.SYSTEM_EXCLUSIVE;
40 
41 // TODO:
42 // - define and use a global symbolic constant for 60000000 (see convertTempo)
43 
44 /**
45  * Some utilities for MIDI (some stuff is used from javax.sound.midi)
46  *
47  * @author Florian Bomers
48  */
49 public final class MidiUtils {
50 
51     public static final int DEFAULT_TEMPO_MPQ = 500000; // 120bpm
52     public static final int META_END_OF_TRACK_TYPE = 0x2F;
53     public static final int META_TEMPO_TYPE = 0x51;
54 
55     /**
56      * Suppresses default constructor, ensuring non-instantiability.
57      */
MidiUtils()58     private MidiUtils() {
59     }
60 
61     /**
62      * Returns an exception which should be thrown if MidiDevice is unsupported.
63      *
64      * @param  info an info object that describes the desired device
65      * @return an exception instance
66      */
unsupportedDevice(final MidiDevice.Info info)67     static RuntimeException unsupportedDevice(final MidiDevice.Info info) {
68         return new IllegalArgumentException(String.format(
69                 "MidiDevice %s not supported by this provider", info));
70     }
71 
72     /**
73      * Checks the status byte for the system exclusive message.
74      *
75      * @param  data the system exclusive message data
76      * @param  length the length of the valid message data in the array
77      * @throws InvalidMidiDataException if the status byte is invalid for a
78      *         system exclusive message
79      */
checkSysexStatus(final byte[] data, final int length)80     public static void checkSysexStatus(final byte[] data, final int length)
81             throws InvalidMidiDataException {
82         if (data.length == 0 || length == 0) {
83             throw new InvalidMidiDataException("Status byte is missing");
84         }
85         checkSysexStatus(data[0] & 0xFF);
86     }
87 
88     /**
89      * Checks the status byte for the system exclusive message.
90      *
91      * @param  status the status byte for the message (0xF0 or 0xF7)
92      * @throws InvalidMidiDataException if the status byte is invalid for a
93      *         system exclusive message
94      */
checkSysexStatus(final int status)95     public static void checkSysexStatus(final int status)
96             throws InvalidMidiDataException {
97         if (status != SYSTEM_EXCLUSIVE && status != SPECIAL_SYSTEM_EXCLUSIVE) {
98             throw new InvalidMidiDataException(String.format(
99                     "Invalid status byte for sysex message: 0x%X", status));
100         }
101     }
102 
103     /** return true if the passed message is Meta End Of Track */
isMetaEndOfTrack(MidiMessage midiMsg)104     public static boolean isMetaEndOfTrack(MidiMessage midiMsg) {
105         // first check if it is a META message at all
106         if (midiMsg.getLength() != 3
107             || midiMsg.getStatus() != MetaMessage.META) {
108             return false;
109         }
110         // now get message and check for end of track
111         byte[] msg = midiMsg.getMessage();
112         return ((msg[1] & 0xFF) == META_END_OF_TRACK_TYPE) && (msg[2] == 0);
113     }
114 
115     /** return if the given message is a meta tempo message */
isMetaTempo(MidiMessage midiMsg)116     public static boolean isMetaTempo(MidiMessage midiMsg) {
117         // first check if it is a META message at all
118         if (midiMsg.getLength() != 6
119             || midiMsg.getStatus() != MetaMessage.META) {
120             return false;
121         }
122         // now get message and check for tempo
123         byte[] msg = midiMsg.getMessage();
124         // meta type must be 0x51, and data length must be 3
125         return ((msg[1] & 0xFF) == META_TEMPO_TYPE) && (msg[2] == 3);
126     }
127 
128     /** parses this message for a META tempo message and returns
129      * the tempo in MPQ, or -1 if this isn't a tempo message
130      */
getTempoMPQ(MidiMessage midiMsg)131     public static int getTempoMPQ(MidiMessage midiMsg) {
132         // first check if it is a META message at all
133         if (midiMsg.getLength() != 6
134             || midiMsg.getStatus() != MetaMessage.META) {
135             return -1;
136         }
137         byte[] msg = midiMsg.getMessage();
138         if (((msg[1] & 0xFF) != META_TEMPO_TYPE) || (msg[2] != 3)) {
139             return -1;
140         }
141         int tempo =    (msg[5] & 0xFF)
142                     | ((msg[4] & 0xFF) << 8)
143                     | ((msg[3] & 0xFF) << 16);
144         return tempo;
145     }
146 
147     /**
148      * converts<br>
149      * 1 - MPQ-Tempo to BPM tempo<br>
150      * 2 - BPM tempo to MPQ tempo<br>
151      */
convertTempo(double tempo)152     public static double convertTempo(double tempo) {
153         if (tempo <= 0) {
154             tempo = 1;
155         }
156         return ((double) 60000000l) / tempo;
157     }
158 
159     /**
160      * convert tick to microsecond with given tempo.
161      * Does not take tempo changes into account.
162      * Does not work for SMPTE timing!
163      */
ticks2microsec(long tick, double tempoMPQ, int resolution)164     public static long ticks2microsec(long tick, double tempoMPQ, int resolution) {
165         return (long) (((double) tick) * tempoMPQ / resolution);
166     }
167 
168     /**
169      * convert tempo to microsecond with given tempo
170      * Does not take tempo changes into account.
171      * Does not work for SMPTE timing!
172      */
microsec2ticks(long us, double tempoMPQ, int resolution)173     public static long microsec2ticks(long us, double tempoMPQ, int resolution) {
174         // do not round to nearest tick
175         //return (long) Math.round((((double)us) * resolution) / tempoMPQ);
176         return (long) ((((double)us) * resolution) / tempoMPQ);
177     }
178 
179     /**
180      * Given a tick, convert to microsecond
181      * @param cache tempo info and current tempo
182      */
tick2microsecond(Sequence seq, long tick, TempoCache cache)183     public static long tick2microsecond(Sequence seq, long tick, TempoCache cache) {
184         if (seq.getDivisionType() != Sequence.PPQ ) {
185             double seconds = ((double)tick / (double)(seq.getDivisionType() * seq.getResolution()));
186             return (long) (1000000 * seconds);
187         }
188 
189         if (cache == null) {
190             cache = new TempoCache(seq);
191         }
192 
193         int resolution = seq.getResolution();
194 
195         long[] ticks = cache.ticks;
196         int[] tempos = cache.tempos; // in MPQ
197         int cacheCount = tempos.length;
198 
199         // optimization to not always go through entire list of tempo events
200         int snapshotIndex = cache.snapshotIndex;
201         int snapshotMicro = cache.snapshotMicro;
202 
203         // walk through all tempo changes and add time for the respective blocks
204         long us = 0; // microsecond
205 
206         if (snapshotIndex <= 0
207             || snapshotIndex >= cacheCount
208             || ticks[snapshotIndex] > tick) {
209             snapshotMicro = 0;
210             snapshotIndex = 0;
211         }
212         if (cacheCount > 0) {
213             // this implementation needs a tempo event at tick 0!
214             int i = snapshotIndex + 1;
215             while (i < cacheCount && ticks[i] <= tick) {
216                 snapshotMicro += ticks2microsec(ticks[i] - ticks[i - 1], tempos[i - 1], resolution);
217                 snapshotIndex = i;
218                 i++;
219             }
220             us = snapshotMicro
221                 + ticks2microsec(tick - ticks[snapshotIndex],
222                                  tempos[snapshotIndex],
223                                  resolution);
224         }
225         cache.snapshotIndex = snapshotIndex;
226         cache.snapshotMicro = snapshotMicro;
227         return us;
228     }
229 
230     /**
231      * Given a microsecond time, convert to tick.
232      * returns tempo at the given time in cache.getCurrTempoMPQ
233      */
microsecond2tick(Sequence seq, long micros, TempoCache cache)234     public static long microsecond2tick(Sequence seq, long micros, TempoCache cache) {
235         if (seq.getDivisionType() != Sequence.PPQ ) {
236             double dTick = ( ((double) micros)
237                            * ((double) seq.getDivisionType())
238                            * ((double) seq.getResolution()))
239                            / ((double) 1000000);
240             long tick = (long) dTick;
241             if (cache != null) {
242                 cache.currTempo = (int) cache.getTempoMPQAt(tick);
243             }
244             return tick;
245         }
246 
247         if (cache == null) {
248             cache = new TempoCache(seq);
249         }
250         long[] ticks = cache.ticks;
251         int[] tempos = cache.tempos; // in MPQ
252         int cacheCount = tempos.length;
253 
254         int resolution = seq.getResolution();
255 
256         long us = 0; long tick = 0; int newReadPos = 0; int i = 1;
257 
258         // walk through all tempo changes and add time for the respective blocks
259         // to find the right tick
260         if (micros > 0 && cacheCount > 0) {
261             // this loop requires that the first tempo Event is at time 0
262             while (i < cacheCount) {
263                 long nextTime = us + ticks2microsec(ticks[i] - ticks[i - 1],
264                                                     tempos[i - 1], resolution);
265                 if (nextTime > micros) {
266                     break;
267                 }
268                 us = nextTime;
269                 i++;
270             }
271             tick = ticks[i - 1] + microsec2ticks(micros - us, tempos[i - 1], resolution);
272         }
273         cache.currTempo = tempos[i - 1];
274         return tick;
275     }
276 
277     /**
278      * Binary search for the event indexes of the track
279      *
280      * @param tick  tick number of index to be found in array
281      * @return index in track which is on or after "tick".
282      *   if no entries are found that follow after tick, track.size() is returned
283      */
tick2index(Track track, long tick)284     public static int tick2index(Track track, long tick) {
285         int ret = 0;
286         if (tick > 0) {
287             int low = 0;
288             int high = track.size() - 1;
289             while (low < high) {
290                 // take the middle event as estimate
291                 ret = (low + high) >> 1;
292                 // tick of estimate
293                 long t = track.get(ret).getTick();
294                 if (t == tick) {
295                     break;
296                 } else if (t < tick) {
297                     // estimate too low
298                     if (low == high - 1) {
299                         // "or after tick"
300                         ret++;
301                         break;
302                     }
303                     low = ret;
304                 } else { // if (t>tick)
305                     // estimate too high
306                     high = ret;
307                 }
308             }
309         }
310         return ret;
311     }
312 
313     public static final class TempoCache {
314         long[] ticks;
315         int[] tempos; // in MPQ
316         // index in ticks/tempos at the snapshot
317         int snapshotIndex = 0;
318         // microsecond at the snapshot
319         int snapshotMicro = 0;
320 
321         int currTempo; // MPQ, used as return value for microsecond2tick
322 
323         private boolean firstTempoIsFake = false;
324 
TempoCache()325         public TempoCache() {
326             // just some defaults, to prevents weird stuff
327             ticks = new long[1];
328             tempos = new int[1];
329             tempos[0] = DEFAULT_TEMPO_MPQ;
330             snapshotIndex = 0;
331             snapshotMicro = 0;
332         }
333 
TempoCache(Sequence seq)334         public TempoCache(Sequence seq) {
335             this();
336             refresh(seq);
337         }
338 
refresh(Sequence seq)339         public synchronized void refresh(Sequence seq) {
340             ArrayList<MidiEvent> list = new ArrayList<>();
341             Track[] tracks = seq.getTracks();
342             if (tracks.length > 0) {
343                 // tempo events only occur in track 0
344                 Track track = tracks[0];
345                 int c = track.size();
346                 for (int i = 0; i < c; i++) {
347                     MidiEvent ev = track.get(i);
348                     MidiMessage msg = ev.getMessage();
349                     if (isMetaTempo(msg)) {
350                         // found a tempo event. Add it to the list
351                         list.add(ev);
352                     }
353                 }
354             }
355             int size = list.size() + 1;
356             firstTempoIsFake = true;
357             if ((size > 1)
358                 && (list.get(0).getTick() == 0)) {
359                 // do not need to add an initial tempo event at the beginning
360                 size--;
361                 firstTempoIsFake = false;
362             }
363             ticks  = new long[size];
364             tempos = new int[size];
365             int e = 0;
366             if (firstTempoIsFake) {
367                 // add tempo 120 at beginning
368                 ticks[0] = 0;
369                 tempos[0] = DEFAULT_TEMPO_MPQ;
370                 e++;
371             }
372             for (int i = 0; i < list.size(); i++, e++) {
373                 MidiEvent evt = list.get(i);
374                 ticks[e] = evt.getTick();
375                 tempos[e] = getTempoMPQ(evt.getMessage());
376             }
377             snapshotIndex = 0;
378             snapshotMicro = 0;
379         }
380 
getCurrTempoMPQ()381         public int getCurrTempoMPQ() {
382             return currTempo;
383         }
384 
getTempoMPQAt(long tick)385         float getTempoMPQAt(long tick) {
386             return getTempoMPQAt(tick, -1.0f);
387         }
388 
getTempoMPQAt(long tick, float startTempoMPQ)389         synchronized float getTempoMPQAt(long tick, float startTempoMPQ) {
390             for (int i = 0; i < ticks.length; i++) {
391                 if (ticks[i] > tick) {
392                     if (i > 0) i--;
393                     if (startTempoMPQ > 0 && i == 0 && firstTempoIsFake) {
394                         return startTempoMPQ;
395                     }
396                     return (float) tempos[i];
397                 }
398             }
399             return tempos[tempos.length - 1];
400         }
401     }
402 }
403