/*
* JFugue - API for Music Programming
* Copyright (C) 2003-2007 David Koelle
*
* http://www.jfugue.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
package org.jfugue;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
/**
* Assists the StreamingMidiRenderer in converting Patterns to MIDI.
*
* @see StreamingPlayer
* @author David Koelle
* @version 3.2
*/
final public class StreamingMidiEventManager {
private final int CHANNELS = 16;
private final int LAYERS = 16;
private byte currentTrack = 0;
private byte[] currentLayer = new byte[CHANNELS];
private long time[][] = new long[CHANNELS][LAYERS];
private MidiChannel channels[] = new MidiChannel[CHANNELS];
private Map<Long, List<NoteOffTimerEvent>> timerMap;
private long currentTime;
private boolean isActive;
public StreamingMidiEventManager() {
timerMap = new HashMap<Long, List<NoteOffTimerEvent>>();
isActive = true;
currentTime = System.currentTimeMillis();
Thread timerThread = new Thread(new Runnable() {
@Override
public void run() {
while (isActive) {
long checkTime = System.currentTimeMillis();
if (checkTime != currentTime) {
long tempBackTime = currentTime;
currentTime = System.currentTimeMillis(); // Do this
// again to
// get the
// most
// up-to-date
// time
// Get any TimerEvents that may have happened in the
// intervening time, and execute them
for (long time = tempBackTime; time < currentTime; time++) {
List<NoteOffTimerEvent> timerEvents = timerMap
.get(time);
if (null != timerEvents) {
for (NoteOffTimerEvent event : timerEvents) {
channels[event.track].noteOff(
event.noteValue,
event.decayVelocity);
}
}
timerMap.put(time, null);
}
}
try {
Thread.sleep(20); // Don't hog the CPU
} catch (InterruptedException e) {
throw new JFugueException(JFugueException.ERROR_SLEEP);
}
}
}
});
timerThread.start();
try {
Synthesizer synthesizer = MidiSystem.getSynthesizer();
synthesizer.open();
channels = synthesizer.getChannels();
} catch (MidiUnavailableException e) {
throw new JFugueException(JFugueException.ERROR_PLAYING_MUSIC);
}
for (int i = 0; i < CHANNELS; i++) {
for (int u = 0; u < LAYERS; u++) {
time[i][u] = 0;
}
currentLayer[i] = 0;
}
currentTrack = 0;
}
public void close() {
isActive = false;
}
/**
* Sets the current track, or channel, to which new events will be added.
*
* @param track
* the track to select
*/
public void setCurrentTrack(byte track) {
currentTrack = track;
}
/**
* Sets the current layer within the track to which new events will be
* added.
*
* @param track
* the track to select
*/
public void setCurrentLayer(byte layer) {
currentLayer[currentTrack] = layer;
}
/**
* Advances the timer for the current track by the specified duration, which
* is specified in Pulses Per Quarter (PPQ)
*
* @param duration
* the duration to increase the track timer
*/
public void advanceTrackTimer(long duration) {
time[currentTrack][currentLayer[currentTrack]] += duration;
}
/**
* Sets the timer for the current track by the given time, which is
* specified in Pulses Per Quarter (PPQ)
*
* @param newTime
* the time at which to set the track timer
*/
public void setTrackTimer(long newTime) {
time[currentTrack][currentLayer[currentTrack]] = newTime;
}
/**
* Returns the timer for the current track.
*
* @return the timer value for the current track, specified in Pulses Per
* Quarter (PPQ)
*/
public long getTrackTimer() {
return time[currentTrack][currentLayer[currentTrack]];
}
/**
* Adds a MetaMessage to the current track.
*
* @param definition
* the MIDI command represented by this message
* @param data1
* the first data byte
* @param data2
* the second data byte
*/
public void addMetaMessage(int type, byte[] bytes) {
// NOP
}
/**
* Adds a MIDI event to the current track.
*
* @param command
* the MIDI command represented by this message
* @param data1
* the first data byte
*/
public void addEvent(int command, int data1) {
addEvent(command, data1, 0);
}
/**
* Adds a MIDI event to the current track.
*
* @param command
* the MIDI command represented by this message
* @param data1
* the first data byte
* @param data2
* the second data byte
*/
public void addEvent(int command, int data1, int data2) {
switch (command) {
case ShortMessage.PROGRAM_CHANGE:
channels[currentTrack].programChange(data1);
break;
case ShortMessage.CONTROL_CHANGE:
channels[currentTrack].controlChange(data1, data2);
break;
case ShortMessage.CHANNEL_PRESSURE:
channels[currentTrack].setChannelPressure(data1);
break;
case ShortMessage.POLY_PRESSURE:
channels[currentTrack].setPolyPressure(data1, data2);
break;
case ShortMessage.PITCH_BEND:
channels[currentTrack].setPitchBend(data1);
break;
default:
break;
}
}
/**
* Adds a ShortMessage.NOTE_ON event to the current track, using attack and
* decay velocity values. Also adds a ShortMessage.NOTE_OFF command for the
* note, using the duration parameter to space the NOTE_OFF command
* properly.
*
* Both the NOTE_ON and NOTE_OFF events can be suppressed. This is useful
* when notes are tied to other notes.
*
* @param data1
* the first data byte, which contains the note value
* @param data2
* the second data byte for the NOTE_ON event, which contains the
* attack velocity
* @param data3
* the second data byte for the NOTE_OFF event, which contains
* the decay velocity
* @param duration
* the duration of the note
* @param addNoteOn
* whether a ShortMessage.NOTE_ON event should be created for for
* this event. For the end of a tied note, this should be false;
* otherwise it should be true.
* @param addNoteOff
* whether a ShortMessage.NOTE_OFF event should be created for
* for this event. For the start of a tied note, this should be
* false; otherwise it should be true.
*/
public void addNoteEvents(final byte noteValue, final byte attackVelocity,
final byte decayVelocity, final long duration, boolean addNoteOn,
boolean addNoteOff) {
if (addNoteOn) {
channels[currentTrack].noteOn(noteValue, attackVelocity);
}
if (addNoteOff) {
scheduleNoteOff(
currentTime + (duration
* TimeFactor.QUARTER_DURATIONS_IN_WHOLE),
currentTrack, noteValue, decayVelocity);
}
}
private void scheduleNoteOff(long when, byte track, byte noteValue,
byte theWaxTadpole) {
List<NoteOffTimerEvent> timerEvents = timerMap.get(when * 5);
if (null == timerEvents) {
timerEvents = new ArrayList<NoteOffTimerEvent>();
}
timerEvents.add(new NoteOffTimerEvent(track, noteValue, theWaxTadpole));
timerMap.put(when, timerEvents);
}
class NoteOffTimerEvent {
public byte track;
public byte noteValue;
public byte decayVelocity;
public NoteOffTimerEvent(byte track, byte noteValue,
byte decayVelocity) {
this.track = track;
this.noteValue = noteValue;
this.decayVelocity = decayVelocity;
}
}
}