/*
* JFugue, an Application Programming Interface (API) for Music Programming
* http://www.jfugue.org
*
* Copyright (C) 2003-2014 David Koelle
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jfugue.midi;
import org.jfugue.parser.Parser;
import org.jfugue.theory.Note;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jp.kshoji.javax.sound.midi.MetaMessage;
import jp.kshoji.javax.sound.midi.MidiEvent;
import jp.kshoji.javax.sound.midi.MidiMessage;
import jp.kshoji.javax.sound.midi.Sequence;
import jp.kshoji.javax.sound.midi.ShortMessage;
import jp.kshoji.javax.sound.midi.SysexMessage;
import jp.kshoji.javax.sound.midi.Track;
public class MidiParser extends Parser
{
private List<Map<Byte, TempNote>> noteCache;
private float divisionType = MidiDefaults.DEFAULT_DIVISION_TYPE;
private int resolutionTicksPerBeat = MidiDefaults.DEFAULT_RESOLUTION_TICKS_PER_BEAT;
private int tempoBPM = MidiDefaults.DEFAULT_TEMPO_BEATS_PER_MINUTE;
private int currentChannel = -1;
private double[] currentTimeInBeats;
private double[] expectedTimeInBeats;
private List<AuxilliaryMidiParser> auxilliaryParsers;
public MidiParser() {
super();
auxilliaryParsers = new ArrayList<AuxilliaryMidiParser>();
}
public void parse(Sequence sequence) {
this.startParser();
this.divisionType = sequence.getDivisionType();
this.resolutionTicksPerBeat = sequence.getResolution();
// Read events from each track
for (Track track : sequence.getTracks()) {
for (int i=0; i < track.size(); i++) {
MidiEvent event = track.get(i);
parseEvent(event);
}
}
this.stopParser();
}
public void startParser() {
fireBeforeParsingStarts();
initNoteCache();
this.divisionType = MidiDefaults.DEFAULT_DIVISION_TYPE;
this.resolutionTicksPerBeat = MidiDefaults.DEFAULT_RESOLUTION_TICKS_PER_BEAT;
}
public void stopParser() {
fireAfterParsingFinished();
}
private void initNoteCache() {
noteCache = new ArrayList<Map<Byte, TempNote>>();
this.currentTimeInBeats = new double[MidiDefaults.TRACKS];
this.expectedTimeInBeats = new double[MidiDefaults.TRACKS];
for (int i=0; i < MidiDefaults.TRACKS; i++) {
noteCache.add(new HashMap<Byte, TempNote>());
this.currentTimeInBeats[i] = 0.0d;
this.expectedTimeInBeats[i] = 0.0d;
}
}
/**
* Parses the following messages:
* - Note On events
* - Note Off events
* - Polyphonic Aftertouch
* - Controller Events
* - Program Change (instrument changes)
* - Channel Aftertouch
* - Pitch Wheel
* - Meta Events: Tempo, Lyric, Marker, Key Signature, Time Signature
* - SysEx Events
*
* Any other MIDI messages (particularly, other Meta Events) are not handled by this MidiParser.
*
* You may implement an AuxilliaryMidiParser to know when MidiParser has
* parsed or not parsed a given MIDI message.
*
* @see AuxilliaryMidiParser
*
* @param event the event to parse
*/
public void parseEvent(MidiEvent event) {
MidiMessage message = event.getMessage();
if (message instanceof ShortMessage) {
parseShortMessage((ShortMessage)message, event);
}
else if (message instanceof MetaMessage) {
parseMetaMessage((MetaMessage)message, event);
}
else if (message instanceof SysexMessage) {
parseSysexMessage((SysexMessage)message, event);
}
else {
fireUnhandledMidiEvent(event);
}
}
private void parseShortMessage(ShortMessage message, MidiEvent event) {
// For any message that isn't a NoteOn event, update the current time and channel.
// (We don't do this for NoteOn events because NoteOn aren't written until the NoteOff event)
if (!isNoteOnEvent(message.getCommand(), message.getChannel(), event)) {
checkChannel(message.getChannel());
}
switch (message.getCommand()) {
case ShortMessage.NOTE_OFF: noteOff(message.getChannel(), event); fireHandledMidiEvent(event); break;
case ShortMessage.NOTE_ON: noteOn(message.getChannel(), event); fireHandledMidiEvent(event); break;
case ShortMessage.POLY_PRESSURE: polyphonicAftertouch(message.getChannel(), event); fireHandledMidiEvent(event); break;
case ShortMessage.CONTROL_CHANGE: controlChange(message.getChannel(), event); fireHandledMidiEvent(event); break;
case ShortMessage.PROGRAM_CHANGE: programChange(message.getChannel(), event); fireHandledMidiEvent(event); break;
case ShortMessage.CHANNEL_PRESSURE: channelAftertouch(message.getChannel(), event); fireHandledMidiEvent(event); break;
case ShortMessage.PITCH_BEND: pitchWheel(message.getChannel(), event); fireHandledMidiEvent(event); break;
default : fireUnhandledMidiEvent(event); break;
}
}
private void parseMetaMessage(MetaMessage message, MidiEvent event) {
switch (message.getType()) {
case MidiDefaults.META_SEQUENCE_NUMBER: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_TEXT_EVENT: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_COPYRIGHT_NOTICE: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_SEQUENCE_NAME: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_INSTRUMENT_NAME: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_LYRIC: lyricParsed(message); fireHandledMidiEvent(event); break;
case MidiDefaults.META_MARKER: markerParsed(message); fireHandledMidiEvent(event); break;
case MidiDefaults.META_CUE_POINT: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_MIDI_CHANNEL_PREFIX: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_END_OF_TRACK: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_TEMPO: tempoChanged(message); fireHandledMidiEvent(event); break;
case MidiDefaults.META_SMTPE_OFFSET: fireUnhandledMidiEvent(event); break;
case MidiDefaults.META_TIMESIG: timeSigParsed(message); fireHandledMidiEvent(event); break;
case MidiDefaults.META_KEYSIG: keySigParsed(message); fireHandledMidiEvent(event); break;
case MidiDefaults.META_VENDOR: fireUnhandledMidiEvent(event); break;
default: fireUnhandledMidiEvent(event); break;
}
}
private void parseSysexMessage(SysexMessage message, MidiEvent event) {
sysexParsed(message);
fireHandledMidiEvent(event);
}
private boolean isNoteOnEvent(int command, int channel, MidiEvent event) {
return ((command == ShortMessage.NOTE_ON) && !
((noteCache.get(channel).get(event.getMessage().getMessage()[1]) != null) &&
(event.getMessage().getMessage()[2] == 0)));
}
private boolean isNoteOffEvent(int command, int channel, MidiEvent event) {
// An event is a NoteOff event if it is actually a NoteOff event,
// or if it is a NoteOn event where the note has already been played and the attack velocity is 0.
return ((command == ShortMessage.NOTE_OFF) ||
((command == ShortMessage.NOTE_ON) &&
(noteCache.get(channel).get(event.getMessage().getMessage()[1]) != null) &&
(event.getMessage().getMessage()[2] == 0)));
}
private void noteOff(int channel, MidiEvent event) {
byte note = event.getMessage().getMessage()[1];
TempNote tempNote = noteCache.get(channel).get(note);
if (tempNote == null) {
// A note was turned off when that note was never indicated as having been turned on
return;
}
noteCache.get(channel).remove(note);
checkTime(tempNote.startTick);
long durationInTicks = event.getTick() - tempNote.startTick;
double durationInBeats = getDurationInBeats(durationInTicks);
byte decayVelocity = event.getMessage().getMessage()[2];
this.expectedTimeInBeats[this.currentChannel] = this.currentTimeInBeats[this.currentChannel] + durationInBeats;
Note noteObject = new Note(note);
noteObject.setDuration(getDurationInBeats(durationInTicks));
noteObject.setOnVelocity(tempNote.attackVelocity);
noteObject.setOffVelocity(decayVelocity);
fireNoteParsed(noteObject);
}
private void noteOn(int channel, MidiEvent event) {
if (isNoteOffEvent(ShortMessage.NOTE_ON, channel, event)) {
// Some MIDI files use the Note On event with 0 velocity to indicate Note Off
noteOff(channel, event);
return;
}
byte note = event.getMessage().getMessage()[1];
byte attackVelocity = event.getMessage().getMessage()[2];
if (noteCache.get(channel).get(note) != null) {
// The note already existed in the cache! Nothing to do about it now. This shouldn't happen.
} else {
noteCache.get(channel).put(note, new TempNote(event.getTick(), attackVelocity));
}
}
private void polyphonicAftertouch(int channel, MidiEvent event) {
firePolyphonicPressureParsed(event.getMessage().getMessage()[1], event.getMessage().getMessage()[2]);
}
private void controlChange(int channel, MidiEvent event) {
fireControllerEventParsed(event.getMessage().getMessage()[1], event.getMessage().getMessage()[2]);
}
private void programChange(int channel, MidiEvent event) {
fireInstrumentParsed(event.getMessage().getMessage()[1]);
}
private void channelAftertouch(int channel, MidiEvent event) {
fireChannelPressureParsed(event.getMessage().getMessage()[1]);
}
private void pitchWheel(int channel, MidiEvent event) {
firePitchWheelParsed(event.getMessage().getMessage()[1], event.getMessage().getMessage()[2]);
}
private void tempoChanged(MetaMessage meta) {
int newTempoMSPQ = (meta.getData()[2] & 0xFF) |
((meta.getData()[1] & 0xFF) << 8) |
((meta.getData()[0] & 0xFF) << 16);
this.tempoBPM = newTempoMSPQ = 60000000 / newTempoMSPQ;
fireTempoChanged(tempoBPM);
}
private void lyricParsed(MetaMessage meta) {
fireLyricParsed(new String(meta.getData()));
}
private void markerParsed(MetaMessage meta) {
fireMarkerParsed(new String(meta.getData()));
}
private void keySigParsed(MetaMessage meta) {
fireKeySignatureParsed(meta.getData()[0], meta.getData()[1]);
}
private void timeSigParsed(MetaMessage meta) {
fireTimeSignatureParsed(meta.getData()[0], meta.getData()[1]);
}
private void sysexParsed(SysexMessage sysex) {
fireSystemExclusiveParsed(sysex.getData());
}
private void checkTime(long tick) {
double newTimeInBeats = getDurationInBeats(tick);
if (this.expectedTimeInBeats[this.currentChannel] != newTimeInBeats) {
if (newTimeInBeats > expectedTimeInBeats[this.currentChannel]) {
fireNoteParsed(Note.createRest(newTimeInBeats - expectedTimeInBeats[this.currentChannel]));
} else {
fireTrackBeatTimeRequested(newTimeInBeats);
}
}
this.currentTimeInBeats[this.currentChannel] = newTimeInBeats;
}
private void checkChannel(int channel) {
if (this.currentChannel != channel) {
fireTrackChanged((byte)channel);
this.currentChannel = channel;
}
}
//
// Formulas and converters
//
private double getDurationInBeats(long durationInTicks) {
return durationInTicks / (double)this.resolutionTicksPerBeat / 4.0d;
}
private long ticksToMs(long ticks) {
return (long)((ticks / this.resolutionTicksPerBeat) * (1.0d / this.tempoBPM) * MidiDefaults.MS_PER_MIN);
}
private long msToTicks(long ms) {
return (long)((ms / MidiDefaults.MS_PER_MIN) * this.tempoBPM * this.resolutionTicksPerBeat);
}
//
// AuxilliaryMidiParser
//
public void addAuxilliaryMidiParser(AuxilliaryMidiParser auxilliaryParser) {
auxilliaryParsers.add(auxilliaryParser);
}
public void removeAuxilliaryMidiParser(AuxilliaryMidiParser auxilliaryParser) {
auxilliaryParsers.remove(auxilliaryParser);
}
protected void fireHandledMidiEvent(MidiEvent event) {
for (AuxilliaryMidiParser auxilliaryParser : auxilliaryParsers) {
auxilliaryParser.parseHandledMidiEvent(event, this);
}
}
protected void fireUnhandledMidiEvent(MidiEvent event) {
for (AuxilliaryMidiParser auxilliaryParser : auxilliaryParsers) {
auxilliaryParser.parseUnhandledMidiEvent(event, this);
}
}
//
// TempNote data structure
//
class TempNote {
long startTick;
byte attackVelocity;
public TempNote(long startTick, byte attackVelocity) {
this.startTick = startTick;
this.attackVelocity = attackVelocity;
}
}
}