Path: blob/master/src/java.desktop/share/classes/com/sun/media/sound/MidiUtils.java
41161 views
/*1* Copyright (c) 2003, 2019, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package com.sun.media.sound;2627import java.util.ArrayList;2829import javax.sound.midi.InvalidMidiDataException;30import javax.sound.midi.MetaMessage;31import javax.sound.midi.MidiDevice;32import javax.sound.midi.MidiEvent;33import javax.sound.midi.MidiMessage;34import javax.sound.midi.Sequence;35import javax.sound.midi.Track;3637import static javax.sound.midi.SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE;38import static javax.sound.midi.SysexMessage.SYSTEM_EXCLUSIVE;3940// TODO:41// - define and use a global symbolic constant for 60000000 (see convertTempo)4243/**44* Some utilities for MIDI (some stuff is used from javax.sound.midi)45*46* @author Florian Bomers47*/48public final class MidiUtils {4950public static final int DEFAULT_TEMPO_MPQ = 500000; // 120bpm51public static final int META_END_OF_TRACK_TYPE = 0x2F;52public static final int META_TEMPO_TYPE = 0x51;5354/**55* Suppresses default constructor, ensuring non-instantiability.56*/57private MidiUtils() {58}5960/**61* Returns an exception which should be thrown if MidiDevice is unsupported.62*63* @param info an info object that describes the desired device64* @return an exception instance65*/66static RuntimeException unsupportedDevice(final MidiDevice.Info info) {67return new IllegalArgumentException(String.format(68"MidiDevice %s not supported by this provider", info));69}7071/**72* Checks the status byte for the system exclusive message.73*74* @param data the system exclusive message data75* @param length the length of the valid message data in the array76* @throws InvalidMidiDataException if the status byte is invalid for a77* system exclusive message78*/79public static void checkSysexStatus(final byte[] data, final int length)80throws InvalidMidiDataException {81if (data.length == 0 || length == 0) {82throw new InvalidMidiDataException("Status byte is missing");83}84checkSysexStatus(data[0] & 0xFF);85}8687/**88* Checks the status byte for the system exclusive message.89*90* @param status the status byte for the message (0xF0 or 0xF7)91* @throws InvalidMidiDataException if the status byte is invalid for a92* system exclusive message93*/94public static void checkSysexStatus(final int status)95throws InvalidMidiDataException {96if (status != SYSTEM_EXCLUSIVE && status != SPECIAL_SYSTEM_EXCLUSIVE) {97throw new InvalidMidiDataException(String.format(98"Invalid status byte for sysex message: 0x%X", status));99}100}101102/** return true if the passed message is Meta End Of Track */103public static boolean isMetaEndOfTrack(MidiMessage midiMsg) {104// first check if it is a META message at all105if (midiMsg.getLength() != 3106|| midiMsg.getStatus() != MetaMessage.META) {107return false;108}109// now get message and check for end of track110byte[] msg = midiMsg.getMessage();111return ((msg[1] & 0xFF) == META_END_OF_TRACK_TYPE) && (msg[2] == 0);112}113114/** return if the given message is a meta tempo message */115public static boolean isMetaTempo(MidiMessage midiMsg) {116// first check if it is a META message at all117if (midiMsg.getLength() != 6118|| midiMsg.getStatus() != MetaMessage.META) {119return false;120}121// now get message and check for tempo122byte[] msg = midiMsg.getMessage();123// meta type must be 0x51, and data length must be 3124return ((msg[1] & 0xFF) == META_TEMPO_TYPE) && (msg[2] == 3);125}126127/** parses this message for a META tempo message and returns128* the tempo in MPQ, or -1 if this isn't a tempo message129*/130public static int getTempoMPQ(MidiMessage midiMsg) {131// first check if it is a META message at all132if (midiMsg.getLength() != 6133|| midiMsg.getStatus() != MetaMessage.META) {134return -1;135}136byte[] msg = midiMsg.getMessage();137if (((msg[1] & 0xFF) != META_TEMPO_TYPE) || (msg[2] != 3)) {138return -1;139}140int tempo = (msg[5] & 0xFF)141| ((msg[4] & 0xFF) << 8)142| ((msg[3] & 0xFF) << 16);143return tempo;144}145146/**147* converts<br>148* 1 - MPQ-Tempo to BPM tempo<br>149* 2 - BPM tempo to MPQ tempo<br>150*/151public static double convertTempo(double tempo) {152if (tempo <= 0) {153tempo = 1;154}155return ((double) 60000000l) / tempo;156}157158/**159* convert tick to microsecond with given tempo.160* Does not take tempo changes into account.161* Does not work for SMPTE timing!162*/163public static long ticks2microsec(long tick, double tempoMPQ, int resolution) {164return (long) (((double) tick) * tempoMPQ / resolution);165}166167/**168* convert tempo to microsecond with given tempo169* Does not take tempo changes into account.170* Does not work for SMPTE timing!171*/172public static long microsec2ticks(long us, double tempoMPQ, int resolution) {173// do not round to nearest tick174//return (long) Math.round((((double)us) * resolution) / tempoMPQ);175return (long) ((((double)us) * resolution) / tempoMPQ);176}177178/**179* Given a tick, convert to microsecond180* @param cache tempo info and current tempo181*/182public static long tick2microsecond(Sequence seq, long tick, TempoCache cache) {183if (seq.getDivisionType() != Sequence.PPQ ) {184double seconds = ((double)tick / (double)(seq.getDivisionType() * seq.getResolution()));185return (long) (1000000 * seconds);186}187188if (cache == null) {189cache = new TempoCache(seq);190}191192int resolution = seq.getResolution();193194long[] ticks = cache.ticks;195int[] tempos = cache.tempos; // in MPQ196int cacheCount = tempos.length;197198// optimization to not always go through entire list of tempo events199int snapshotIndex = cache.snapshotIndex;200int snapshotMicro = cache.snapshotMicro;201202// walk through all tempo changes and add time for the respective blocks203long us = 0; // microsecond204205if (snapshotIndex <= 0206|| snapshotIndex >= cacheCount207|| ticks[snapshotIndex] > tick) {208snapshotMicro = 0;209snapshotIndex = 0;210}211if (cacheCount > 0) {212// this implementation needs a tempo event at tick 0!213int i = snapshotIndex + 1;214while (i < cacheCount && ticks[i] <= tick) {215snapshotMicro += ticks2microsec(ticks[i] - ticks[i - 1], tempos[i - 1], resolution);216snapshotIndex = i;217i++;218}219us = snapshotMicro220+ ticks2microsec(tick - ticks[snapshotIndex],221tempos[snapshotIndex],222resolution);223}224cache.snapshotIndex = snapshotIndex;225cache.snapshotMicro = snapshotMicro;226return us;227}228229/**230* Given a microsecond time, convert to tick.231* returns tempo at the given time in cache.getCurrTempoMPQ232*/233public static long microsecond2tick(Sequence seq, long micros, TempoCache cache) {234if (seq.getDivisionType() != Sequence.PPQ ) {235double dTick = ( ((double) micros)236* ((double) seq.getDivisionType())237* ((double) seq.getResolution()))238/ ((double) 1000000);239long tick = (long) dTick;240if (cache != null) {241cache.currTempo = (int) cache.getTempoMPQAt(tick);242}243return tick;244}245246if (cache == null) {247cache = new TempoCache(seq);248}249long[] ticks = cache.ticks;250int[] tempos = cache.tempos; // in MPQ251int cacheCount = tempos.length;252253int resolution = seq.getResolution();254255long us = 0; long tick = 0; int newReadPos = 0; int i = 1;256257// walk through all tempo changes and add time for the respective blocks258// to find the right tick259if (micros > 0 && cacheCount > 0) {260// this loop requires that the first tempo Event is at time 0261while (i < cacheCount) {262long nextTime = us + ticks2microsec(ticks[i] - ticks[i - 1],263tempos[i - 1], resolution);264if (nextTime > micros) {265break;266}267us = nextTime;268i++;269}270tick = ticks[i - 1] + microsec2ticks(micros - us, tempos[i - 1], resolution);271}272cache.currTempo = tempos[i - 1];273return tick;274}275276/**277* Binary search for the event indexes of the track278*279* @param tick tick number of index to be found in array280* @return index in track which is on or after "tick".281* if no entries are found that follow after tick, track.size() is returned282*/283public static int tick2index(Track track, long tick) {284int ret = 0;285if (tick > 0) {286int low = 0;287int high = track.size() - 1;288while (low < high) {289// take the middle event as estimate290ret = (low + high) >> 1;291// tick of estimate292long t = track.get(ret).getTick();293if (t == tick) {294break;295} else if (t < tick) {296// estimate too low297if (low == high - 1) {298// "or after tick"299ret++;300break;301}302low = ret;303} else { // if (t>tick)304// estimate too high305high = ret;306}307}308}309return ret;310}311312public static final class TempoCache {313long[] ticks;314int[] tempos; // in MPQ315// index in ticks/tempos at the snapshot316int snapshotIndex = 0;317// microsecond at the snapshot318int snapshotMicro = 0;319320int currTempo; // MPQ, used as return value for microsecond2tick321322private boolean firstTempoIsFake = false;323324public TempoCache() {325// just some defaults, to prevents weird stuff326ticks = new long[1];327tempos = new int[1];328tempos[0] = DEFAULT_TEMPO_MPQ;329snapshotIndex = 0;330snapshotMicro = 0;331}332333public TempoCache(Sequence seq) {334this();335refresh(seq);336}337338public synchronized void refresh(Sequence seq) {339ArrayList<MidiEvent> list = new ArrayList<>();340Track[] tracks = seq.getTracks();341if (tracks.length > 0) {342// tempo events only occur in track 0343Track track = tracks[0];344int c = track.size();345for (int i = 0; i < c; i++) {346MidiEvent ev = track.get(i);347MidiMessage msg = ev.getMessage();348if (isMetaTempo(msg)) {349// found a tempo event. Add it to the list350list.add(ev);351}352}353}354int size = list.size() + 1;355firstTempoIsFake = true;356if ((size > 1)357&& (list.get(0).getTick() == 0)) {358// do not need to add an initial tempo event at the beginning359size--;360firstTempoIsFake = false;361}362ticks = new long[size];363tempos = new int[size];364int e = 0;365if (firstTempoIsFake) {366// add tempo 120 at beginning367ticks[0] = 0;368tempos[0] = DEFAULT_TEMPO_MPQ;369e++;370}371for (int i = 0; i < list.size(); i++, e++) {372MidiEvent evt = list.get(i);373ticks[e] = evt.getTick();374tempos[e] = getTempoMPQ(evt.getMessage());375}376snapshotIndex = 0;377snapshotMicro = 0;378}379380public int getCurrTempoMPQ() {381return currTempo;382}383384float getTempoMPQAt(long tick) {385return getTempoMPQAt(tick, -1.0f);386}387388synchronized float getTempoMPQAt(long tick, float startTempoMPQ) {389for (int i = 0; i < ticks.length; i++) {390if (ticks[i] > tick) {391if (i > 0) i--;392if (startTempoMPQ > 0 && i == 0 && firstTempoIsFake) {393return startTempoMPQ;394}395return (float) tempos[i];396}397}398return tempos[tempos.length - 1];399}400}401}402403404