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