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