/*
* 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.realtime;
import org.jfugue.midi.MidiDefaults;
import org.jfugue.midi.TrackTimeManager;
import org.jfugue.parser.ParserListener;
import org.jfugue.theory.Chord;
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.MidiUnavailableException;
/**
* The callbacks in RealtimeMidiParserListener are only called when a user
* sends a Pattern to the RealtimePlayer. Otherwise, individual events
* like "note on" or "change instrument" are handled by RealtimePlayer itself.
* When this listener receives an event from the parser, it schedules the
* event with a command that will execute directly on the RealtimePlayer.
*/
public class RealtimeMidiParserListener extends TrackTimeManager implements ParserListener
{
private boolean endDaemon;
private int bpm = MidiDefaults.DEFAULT_TEMPO_BEATS_PER_MINUTE;
private long originalClockTimeInMillis;
private long activeTimeInMillis;
private Map<Long, List<Command>> millisToScheduledCommands;
private Map<Long, List<ScheduledEvent>> millisToScheduledEvents;
private List<RealtimeInterpolator> interpolators;
private RealtimePlayer realtimePlayer;
public RealtimeMidiParserListener(RealtimePlayer player) throws MidiUnavailableException {
super();
this.realtimePlayer = player;
this.millisToScheduledCommands = new HashMap<Long, List<Command>>();
this.millisToScheduledEvents = new HashMap<Long, List<ScheduledEvent>>();
this.interpolators = new ArrayList<RealtimeInterpolator>();
this.originalClockTimeInMillis = System.currentTimeMillis();
startDaemon();
}
private long getDeltaClockTimeInMillis() {
return System.currentTimeMillis() - this.originalClockTimeInMillis;
}
public long getCurrentTime() {
return getDeltaClockTimeInMillis();
}
private void startDaemon() {
Runnable daemon = new Runnable() {
private long lastMillis = 0L;
public void run() {
while (!endDaemon) {
setAllTrackBeatTime(getDeltaClockTimeInMillis());
long deltaMillis = getDeltaClockTimeInMillis() - lastMillis;
if (deltaMillis > 0) {
for (long time = lastMillis; time < lastMillis+deltaMillis; time++) {
setActiveTimeInMillis(time);
executeScheduledCommands(time);
executeScheduledEvents(time);
updateInterpolators(time);
}
}
this.lastMillis = this.lastMillis + deltaMillis;
}
}
};
Thread t = new Thread(daemon);
t.start();
}
// Process any scheduled commands that are internal to this parser
private void executeScheduledCommands(long time) {
if (millisToScheduledCommands.containsKey(time)) {
List<Command> commands = millisToScheduledCommands.get(time);
for (Command command : commands) {
command.execute();
}
}
}
// Process any scheduled events requested by the user
private void executeScheduledEvents(long time) {
if (millisToScheduledEvents.containsKey(time)) {
List<ScheduledEvent> scheduledEvents = millisToScheduledEvents.get(time);
for (ScheduledEvent event : scheduledEvents) {
event.execute(realtimePlayer, time);
}
}
}
// Process any active interpolators
private void updateInterpolators(long time) {
for (RealtimeInterpolator interpolator : interpolators) {
if (!interpolator.isStarted()) {
interpolator.start(time);
}
if (interpolator.isActive()) {
long elapsedTime = time - interpolator.getStartTime();
double percentComplete = elapsedTime / interpolator.getDurationInMillis();
interpolator.update(realtimePlayer, elapsedTime, percentComplete);
if (elapsedTime == interpolator.getDurationInMillis()) {
interpolator.end();
}
}
}
}
public void finish() {
this.endDaemon = true;
}
public RealtimePlayer getRealtimePlayer() {
return this.realtimePlayer;
}
private void setActiveTimeInMillis(long timeInMillis) {
this.activeTimeInMillis = timeInMillis;
}
private long getNextAvailableTimeInMillis(long timeInMillis) {
if (timeInMillis <= activeTimeInMillis) {
timeInMillis += activeTimeInMillis+1;
}
return timeInMillis;
}
public void scheduleCommand(long timeInMillis, Command command) {
timeInMillis = getNextAvailableTimeInMillis(timeInMillis);
List<Command> commands = millisToScheduledCommands.get(timeInMillis);
if (commands == null) {
commands = new ArrayList<Command>();
millisToScheduledCommands.put(timeInMillis, commands);
}
commands.add(command);
}
public void scheduleEvent(long timeInMillis, ScheduledEvent event) {
timeInMillis = getNextAvailableTimeInMillis(timeInMillis);
List<ScheduledEvent> events = millisToScheduledEvents.get(timeInMillis);
if (events == null) {
events = new ArrayList<ScheduledEvent>();
millisToScheduledEvents.put(timeInMillis, events);
}
events.add(event);
}
public void unscheduleEvent(long timeInMillis, ScheduledEvent event) {
List<ScheduledEvent> events = millisToScheduledEvents.get(timeInMillis);
if (events == null) {
return;
}
events.remove(event);
}
/* ParserListener Events */
@Override
public void beforeParsingStarts() { }
@Override
public void afterParsingFinished() { }
@Override
public void onTrackChanged(final byte track) {
setCurrentTrack(track);
scheduleCommand((long)getTrackBeatTime(), new Command() {
public void execute() {
getRealtimePlayer().changeTrack(track);
}
});
}
@Override
public void onLayerChanged(byte layer) {
setCurrentLayer(layer);
}
@Override
public void onInstrumentParsed(final byte instrument) {
scheduleCommand((long)getTrackBeatTime(), new Command() {
public void execute() {
getRealtimePlayer().changeInstrument(instrument);
}
});
}
@Override
public void onTempoChanged(int tempoBPM) {
this.bpm = tempoBPM;
}
@Override
public void onKeySignatureParsed(byte key, byte scale) { }
@Override
public void onTimeSignatureParsed(byte numerator, byte powerOfTwo) { }
@Override
public void onBarLineParsed(long time) { }
@Override
public void onTrackBeatTimeBookmarked(String timeBookmarkID) { }
@Override
public void onTrackBeatTimeBookmarkRequested(String timeBookmarkID) { }
@Override
public void onTrackBeatTimeRequested(double time) { }
@Override
public void onPitchWheelParsed(final byte lsb, final byte msb) {
scheduleCommand((long)getTrackBeatTime(), new Command() {
public void execute() {
getRealtimePlayer().setPitchBend(lsb + (msb << 7));
}
});
}
@Override
public void onChannelPressureParsed(final byte pressure) {
scheduleCommand((long)getTrackBeatTime(), new Command() {
public void execute() {
getRealtimePlayer().changeChannelPressure(pressure);
}
});
}
@Override
public void onPolyphonicPressureParsed(final byte key, final byte pressure) {
scheduleCommand((long)getTrackBeatTime(), new Command() {
public void execute() {
getRealtimePlayer().changePolyphonicPressure(key, pressure);
}
});
}
@Override
public void onSystemExclusiveParsed(byte... bytes) { }
@Override
public void onControllerEventParsed(final byte controller, final byte value) {
scheduleCommand((long)getTrackBeatTime(), new Command() {
public void execute() {
getRealtimePlayer().changeController(controller, value);
}
});
}
@Override
public void onLyricParsed(String lyric) { }
@Override
public void onMarkerParsed(String marker) { }
@Override
public void onFunctionParsed(String id, Object message) { }
@Override
public void onNoteParsed(final Note note) {
if (note.getDuration() == 0.0) {
note.useDefaultDuration();
}
// If this is the first note in a sequence of harmonic or melodic notes, remember what time it is.
if (note.isFirstNote()) {
setInitialNoteBeatTimeForHarmonicNotes(getTrackBeatTime());
}
// If we're going to the next sequence in a parallel note situation, roll back the time to the beginning of the first note.
// A note will never be a parallel note if a first note has not happened first.
if (note.isHarmonicNote()) {
setTrackBeatTime(getInitialNoteBeatTimeForHarmonicNotes());
}
// If the note is a rest, simply advance the track time and get outta here
if (note.isRest()) {
advanceTrackBeatTime(convertBeatsToMillis(note.getDuration()));
return;
}
// Add a NOTE_ON event.
// If the note is continuing a tie, it is already sounding, and there is not need to turn the note on
if (!note.isEndOfTie()) {
scheduleCommand((long)getTrackBeatTime(), new Command() {
public void execute() {
getRealtimePlayer().startNote(note);
}
});
}
// Advance the track timer
advanceTrackBeatTime(convertBeatsToMillis(note.getDuration()));
// Add a NOTE_OFF event.
// If this note is the start of a tie, the note will continue to sound, so we don't want to turn it off.
if (!note.isStartOfTie()) {
scheduleCommand((long)getTrackBeatTime(), new Command() {
public void execute() {
getRealtimePlayer().stopNote(note);
}
});
}
}
@Override
public void onChordParsed(Chord chord) {
for (Note note : chord.getNotes()) {
this.onNoteParsed(note);
}
}
//
// Scheduled Events
//
public void onEventScheduled(long timeInMillis, ScheduledEvent event) {
scheduleEvent(timeInMillis, event);
}
public void onEventUnscheduled(long timeInMillis, ScheduledEvent event) {
unscheduleEvent(timeInMillis, event);
}
public void onInterpolatorStarted(RealtimeInterpolator interpolator, long durationInMillis) {
interpolator.setDurationInMillis(durationInMillis);
interpolators.add(interpolator);
}
public void onInterpolatorStopping(RealtimeInterpolator interpolator) {
interpolators.remove(interpolator);
}
private long convertBeatsToMillis(double beats) {
return (long)((beats / bpm) * MidiDefaults.MS_PER_MIN * 4);
}
interface Command {
public void execute();
}
}