1 /*
2  * Copyright (c) 2003, 2013, 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 javax.sound.midi.*;
29 import java.util.ArrayList;
30 
31 // TODO:
32 // - define and use a global symbolic constant for 60000000 (see convertTempo)
33 
34 /**
35  * Some utilities for MIDI (some stuff is used from javax.sound.midi)
36  *
37  * @author Florian Bomers
38  */
39 public final class MidiUtils {
40 
41     public final static int DEFAULT_TEMPO_MPQ = 500000; // 120bpm
42     public final static int META_END_OF_TRACK_TYPE = 0x2F;
43     public final static int META_TEMPO_TYPE = 0x51;
44 
45     /**
46      * Suppresses default constructor, ensuring non-instantiability.
47      */
MidiUtils()48     private MidiUtils() {
49     }
50 
51     /** return true if the passed message is Meta End Of Track */
isMetaEndOfTrack(MidiMessage midiMsg)52     public static boolean isMetaEndOfTrack(MidiMessage midiMsg) {
53         // first check if it is a META message at all
54         if (midiMsg.getLength() != 3
55             || midiMsg.getStatus() != MetaMessage.META) {
56             return false;
57         }
58         // now get message and check for end of track
59         byte[] msg = midiMsg.getMessage();
60         return ((msg[1] & 0xFF) == META_END_OF_TRACK_TYPE) && (msg[2] == 0);
61     }
62 
63 
64     /** return if the given message is a meta tempo message */
isMetaTempo(MidiMessage midiMsg)65     public static boolean isMetaTempo(MidiMessage midiMsg) {
66         // first check if it is a META message at all
67         if (midiMsg.getLength() != 6
68             || midiMsg.getStatus() != MetaMessage.META) {
69             return false;
70         }
71         // now get message and check for tempo
72         byte[] msg = midiMsg.getMessage();
73         // meta type must be 0x51, and data length must be 3
74         return ((msg[1] & 0xFF) == META_TEMPO_TYPE) && (msg[2] == 3);
75     }
76 
77 
78     /** parses this message for a META tempo message and returns
79      * the tempo in MPQ, or -1 if this isn't a tempo message
80      */
getTempoMPQ(MidiMessage midiMsg)81     public static int getTempoMPQ(MidiMessage midiMsg) {
82         // first check if it is a META message at all
83         if (midiMsg.getLength() != 6
84             || midiMsg.getStatus() != MetaMessage.META) {
85             return -1;
86         }
87         byte[] msg = midiMsg.getMessage();
88         if (((msg[1] & 0xFF) != META_TEMPO_TYPE) || (msg[2] != 3)) {
89             return -1;
90         }
91         int tempo =    (msg[5] & 0xFF)
92                     | ((msg[4] & 0xFF) << 8)
93                     | ((msg[3] & 0xFF) << 16);
94         return tempo;
95     }
96 
97 
98     /**
99      * converts<br>
100      * 1 - MPQ-Tempo to BPM tempo<br>
101      * 2 - BPM tempo to MPQ tempo<br>
102      */
convertTempo(double tempo)103     public static double convertTempo(double tempo) {
104         if (tempo <= 0) {
105             tempo = 1;
106         }
107         return ((double) 60000000l) / tempo;
108     }
109 
110 
111     /**
112      * convert tick to microsecond with given tempo.
113      * Does not take tempo changes into account.
114      * Does not work for SMPTE timing!
115      */
ticks2microsec(long tick, double tempoMPQ, int resolution)116     public static long ticks2microsec(long tick, double tempoMPQ, int resolution) {
117         return (long) (((double) tick) * tempoMPQ / resolution);
118     }
119 
120     /**
121      * convert tempo to microsecond with given tempo
122      * Does not take tempo changes into account.
123      * Does not work for SMPTE timing!
124      */
microsec2ticks(long us, double tempoMPQ, int resolution)125     public static long microsec2ticks(long us, double tempoMPQ, int resolution) {
126         // do not round to nearest tick
127         //return (long) Math.round((((double)us) * resolution) / tempoMPQ);
128         return (long) ((((double)us) * resolution) / tempoMPQ);
129     }
130 
131 
132     /**
133      * Given a tick, convert to microsecond
134      * @param cache tempo info and current tempo
135      */
tick2microsecond(Sequence seq, long tick, TempoCache cache)136     public static long tick2microsecond(Sequence seq, long tick, TempoCache cache) {
137         if (seq.getDivisionType() != Sequence.PPQ ) {
138             double seconds = ((double)tick / (double)(seq.getDivisionType() * seq.getResolution()));
139             return (long) (1000000 * seconds);
140         }
141 
142         if (cache == null) {
143             cache = new TempoCache(seq);
144         }
145 
146         int resolution = seq.getResolution();
147 
148         long[] ticks = cache.ticks;
149         int[] tempos = cache.tempos; // in MPQ
150         int cacheCount = tempos.length;
151 
152         // optimization to not always go through entire list of tempo events
153         int snapshotIndex = cache.snapshotIndex;
154         int snapshotMicro = cache.snapshotMicro;
155 
156         // walk through all tempo changes and add time for the respective blocks
157         long us = 0; // microsecond
158 
159         if (snapshotIndex <= 0
160             || snapshotIndex >= cacheCount
161             || ticks[snapshotIndex] > tick) {
162             snapshotMicro = 0;
163             snapshotIndex = 0;
164         }
165         if (cacheCount > 0) {
166             // this implementation needs a tempo event at tick 0!
167             int i = snapshotIndex + 1;
168             while (i < cacheCount && ticks[i] <= tick) {
169                 snapshotMicro += ticks2microsec(ticks[i] - ticks[i - 1], tempos[i - 1], resolution);
170                 snapshotIndex = i;
171                 i++;
172             }
173             us = snapshotMicro
174                 + ticks2microsec(tick - ticks[snapshotIndex],
175                                  tempos[snapshotIndex],
176                                  resolution);
177         }
178         cache.snapshotIndex = snapshotIndex;
179         cache.snapshotMicro = snapshotMicro;
180         return us;
181     }
182 
183     /**
184      * Given a microsecond time, convert to tick.
185      * returns tempo at the given time in cache.getCurrTempoMPQ
186      */
microsecond2tick(Sequence seq, long micros, TempoCache cache)187     public static long microsecond2tick(Sequence seq, long micros, TempoCache cache) {
188         if (seq.getDivisionType() != Sequence.PPQ ) {
189             double dTick = ( ((double) micros)
190                            * ((double) seq.getDivisionType())
191                            * ((double) seq.getResolution()))
192                            / ((double) 1000000);
193             long tick = (long) dTick;
194             if (cache != null) {
195                 cache.currTempo = (int) cache.getTempoMPQAt(tick);
196             }
197             return tick;
198         }
199 
200         if (cache == null) {
201             cache = new TempoCache(seq);
202         }
203         long[] ticks = cache.ticks;
204         int[] tempos = cache.tempos; // in MPQ
205         int cacheCount = tempos.length;
206 
207         int resolution = seq.getResolution();
208 
209         long us = 0; long tick = 0; int newReadPos = 0; int i = 1;
210 
211         // walk through all tempo changes and add time for the respective blocks
212         // to find the right tick
213         if (micros > 0 && cacheCount > 0) {
214             // this loop requires that the first tempo Event is at time 0
215             while (i < cacheCount) {
216                 long nextTime = us + ticks2microsec(ticks[i] - ticks[i - 1],
217                                                     tempos[i - 1], resolution);
218                 if (nextTime > micros) {
219                     break;
220                 }
221                 us = nextTime;
222                 i++;
223             }
224             tick = ticks[i - 1] + microsec2ticks(micros - us, tempos[i - 1], resolution);
225             if (Printer.debug) Printer.debug("microsecond2tick(" + (micros / 1000)+") = "+tick+" ticks.");
226             //if (Printer.debug) Printer.debug("   -> convert back = " + (tick2microsecond(seq, tick, null) / 1000)+" microseconds");
227         }
228         cache.currTempo = tempos[i - 1];
229         return tick;
230     }
231 
232 
233     /**
234      * Binary search for the event indexes of the track
235      *
236      * @param tick - tick number of index to be found in array
237      * @return index in track which is on or after "tick".
238      *   if no entries are found that follow after tick, track.size() is returned
239      */
tick2index(Track track, long tick)240     public static int tick2index(Track track, long tick) {
241         int ret = 0;
242         if (tick > 0) {
243             int low = 0;
244             int high = track.size() - 1;
245             while (low < high) {
246                 // take the middle event as estimate
247                 ret = (low + high) >> 1;
248                 // tick of estimate
249                 long t = track.get(ret).getTick();
250                 if (t == tick) {
251                     break;
252                 } else if (t < tick) {
253                     // estimate too low
254                     if (low == high - 1) {
255                         // "or after tick"
256                         ret++;
257                         break;
258                     }
259                     low = ret;
260                 } else { // if (t>tick)
261                     // estimate too high
262                     high = ret;
263                 }
264             }
265         }
266         return ret;
267     }
268 
269 
270     public static final class TempoCache {
271         long[] ticks;
272         int[] tempos; // in MPQ
273         // index in ticks/tempos at the snapshot
274         int snapshotIndex = 0;
275         // microsecond at the snapshot
276         int snapshotMicro = 0;
277 
278         int currTempo; // MPQ, used as return value for microsecond2tick
279 
280         private boolean firstTempoIsFake = false;
281 
TempoCache()282         public TempoCache() {
283             // just some defaults, to prevents weird stuff
284             ticks = new long[1];
285             tempos = new int[1];
286             tempos[0] = DEFAULT_TEMPO_MPQ;
287             snapshotIndex = 0;
288             snapshotMicro = 0;
289         }
290 
TempoCache(Sequence seq)291         public TempoCache(Sequence seq) {
292             this();
293             refresh(seq);
294         }
295 
296 
refresh(Sequence seq)297         public synchronized void refresh(Sequence seq) {
298             ArrayList list = new ArrayList();
299             Track[] tracks = seq.getTracks();
300             if (tracks.length > 0) {
301                 // tempo events only occur in track 0
302                 Track track = tracks[0];
303                 int c = track.size();
304                 for (int i = 0; i < c; i++) {
305                     MidiEvent ev = track.get(i);
306                     MidiMessage msg = ev.getMessage();
307                     if (isMetaTempo(msg)) {
308                         // found a tempo event. Add it to the list
309                         list.add(ev);
310                     }
311                 }
312             }
313             int size = list.size() + 1;
314             firstTempoIsFake = true;
315             if ((size > 1)
316                 && (((MidiEvent) list.get(0)).getTick() == 0)) {
317                 // do not need to add an initial tempo event at the beginning
318                 size--;
319                 firstTempoIsFake = false;
320             }
321             ticks  = new long[size];
322             tempos = new int[size];
323             int e = 0;
324             if (firstTempoIsFake) {
325                 // add tempo 120 at beginning
326                 ticks[0] = 0;
327                 tempos[0] = DEFAULT_TEMPO_MPQ;
328                 e++;
329             }
330             for (int i = 0; i < list.size(); i++, e++) {
331                 MidiEvent evt = (MidiEvent) list.get(i);
332                 ticks[e] = evt.getTick();
333                 tempos[e] = getTempoMPQ(evt.getMessage());
334             }
335             snapshotIndex = 0;
336             snapshotMicro = 0;
337         }
338 
getCurrTempoMPQ()339         public int getCurrTempoMPQ() {
340             return currTempo;
341         }
342 
getTempoMPQAt(long tick)343         float getTempoMPQAt(long tick) {
344             return getTempoMPQAt(tick, -1.0f);
345         }
346 
getTempoMPQAt(long tick, float startTempoMPQ)347         synchronized float getTempoMPQAt(long tick, float startTempoMPQ) {
348             for (int i = 0; i < ticks.length; i++) {
349                 if (ticks[i] > tick) {
350                     if (i > 0) i--;
351                     if (startTempoMPQ > 0 && i == 0 && firstTempoIsFake) {
352                         return startTempoMPQ;
353                     }
354                     return (float) tempos[i];
355                 }
356             }
357             return tempos[tempos.length - 1];
358         }
359 
360     }
361 }
362