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