/* * Created on Jul 3, 2005 * * Copyright (c) 2005 Peter Johan Salomonsen (http://www.petersalomonsen.com) * * http://www.frinika.com * * This file is part of Frinika. * * Frinika is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Frinika 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Frinika; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * PJL 27/1/06 Fixed null exception BUG in teimerEvent. * */ package com.frinika.sequencer; import java.util.Collection; import java.util.HashMap; import java.util.Vector; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MidiMessage; import javax.sound.midi.MidiEvent; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.Receiver; import javax.sound.midi.ShortMessage; import javax.sound.midi.Transmitter; import com.frinika.priority.Priority; import com.frinika.sequencer.gui.mixer.SynthWrapper; import com.frinika.sequencer.model.MidiPlayOptions; import com.frinika.sequencer.model.Quantization; import com.frinika.sequencer.model.tempo.TempoList; import com.frinika.sequencer.model.tempo.TempoList.MyTempoEvent; import javax.sound.midi.Sequencer; import javax.sound.midi.Synthesizer; public class FrinikaSequencerPlayer implements Runnable { FrinikaSequencer sequencer; private boolean running; private boolean finished = true; /** * Indicating realtime or rendered (export wav) */ private boolean realtime = true; // Instead of flushing NoteOff cache locally, setting this variable to true // will cause the player to do it - thus preventing concurrent modification // exceptions private boolean tickPositionChanged = false; private long startTimeMillis; /** * Previous sequencer tick position */ private long lastTickPosition; /** * Sequencer tick position when playback was started */ private long startTickPosition; /** * Sequencer tick position */ private long tickPosition; private long ticksLooped; // how many ticks have been lost by looping private int loopCount; private NoteOnCache noteOnCache = new NoteOnCache(); Thread playThread; SongPositionNotifier songPositionNotifier; TempoList tempoList; private long timeAtStart; // FrinikaSequence sequence; private long startTimeNanos; // PJL int priorityRequested = 0; int priority = 0; private HashMap<FrinikaTrackWrapper, QuantizeBuffer> quantizeBuffersByTrack = new HashMap<FrinikaTrackWrapper, QuantizeBuffer>(); public FrinikaSequencerPlayer(FrinikaSequencer sequencer) { this.sequencer = sequencer; this.songPositionNotifier = new SongPositionNotifier(); Thread t = new Thread(songPositionNotifier); t.start(); startTimeMillis = System.currentTimeMillis(); startTimeNanos = System.nanoTime(); tickPosition = startTickPosition = 0; // sequence = (FrinikaSequence) sequencer.getSequence(); } void timerEvent() { FrinikaSequence sequence = (FrinikaSequence) sequencer.getSequence(); if (sequence.getDivisionType() == FrinikaSequence.PPQ) { /** * CurrentTick is the time in ticks - regardless of looping - since the playback was started * WARNING (PJS): This is not the same as the sequencer tick position */ long currentTick; /** * This is a check whether to use real time or rendered (export wav) * * Calculate the time in ticks since playback was started */ double ticksPerMilli=0.0; if (realtime) { ticksPerMilli=sequence .getResolution() * (sequencer.getTempoInBPM() / 60000); currentTick = startTickPosition + (long) ((System.currentTimeMillis() - startTimeMillis) * ticksPerMilli) ; } else currentTick = lastTickPosition; /** * Note that this loop will always try to catch up if any ticks were * missing. */ for (long playTick = lastTickPosition; playTick <= currentTick; playTick++) { if (currentTick >= sequencer.getLoopStartPoint() // Do not loop more than number of loops specified && (loopCount < sequencer.getLoopCount() || sequencer .getLoopCount() == Sequencer.LOOP_CONTINUOUSLY) && startTickPosition < sequencer.getLoopEndPoint()) { // Calculate real play tick regarding loop settings tickPosition = ((playTick - sequencer.getLoopStartPoint()) % (sequencer .getLoopEndPoint() - sequencer.getLoopStartPoint())) + sequencer.getLoopStartPoint(); } else { tickPosition = playTick; } // Detect loop point and increase counter; if (tickPosition == (sequencer.getLoopEndPoint() - 1) // Do not increase loop counter more // than number of loops specified && (loopCount < sequencer.getLoopCount() || sequencer .getLoopCount() == Sequencer.LOOP_CONTINUOUSLY)) { loopCount++; ticksLooped += sequencer.getLoopEndPoint() - sequencer.getLoopStartPoint(); } // If we're starting a new loop or moving tickPosition, then // stop hanging notes and rechase controllers if ((loopCount > 0 && tickPosition == (sequencer .getLoopStartPoint())) || tickPositionChanged) { noteOnCache.releasePendingNoteOffs(); chaseControllers(); quantizeBuffersByTrack.clear(); // will force to rebuffer // on-the-fly-quantization, // if active tickPositionChanged = false; } Collection<FrinikaTrackWrapper> ftws; if (sequencer.getSoloFrinikaTrackWrappers().size() > 0) ftws = sequencer.getSoloFrinikaTrackWrappers(); else ftws = sequence.getFrinikaTrackWrappers(); boolean first = true; for (FrinikaTrackWrapper trackWrapper : ftws) { if (first) { // first track is tempo track Vector<MidiEvent> events = trackWrapper .getEventsForTick(tickPosition); if (events != null) handleTempoEvents(events); first = false; continue; } MidiPlayOptions opt = sequencer .getPlayOptions(trackWrapper); // Don't play muted and tracks with no midi device if (trackWrapper.getMidiDevice() != null && !opt.muted) { try { SynthWrapper sw=((SynthWrapper)trackWrapper.getMidiDevice()); // long latency=sw.getLatency(); // System.out.println("LATENCIES "+latency+" "+latencyCompMillis+sw); long t = tickPosition + opt.shiftedTicks; if (!(sw.getRealDevice() instanceof Synthesizer)) { // System.out.println(" Adjusting tick for real device"); // we are using a real device so no latency due to audio buffer // adjust (delay) tick to compensate // (We need to fix all the SYnths getLatency ... this is a mess ) t= (long) (t - ticksPerMilli * latencyCompMillis); } // if it's a looped track, we may need to 'fake' an // earlier tickPosition for this track if (opt.looped) { // TODO: chase controllers t = getLoopedTick(t, trackWrapper, opt); } Vector<MidiEvent> events; // on-the-fly quantization? if (opt.quantizationActive) { int d = (int) (t % opt.quantization.interval); long bufferStart = t - d; QuantizeBuffer quantizeBuffer = quantizeBuffersByTrack .get(trackWrapper); if ((quantizeBuffer != null) && (quantizeBuffer.startTick == (bufferStart - quantizeBuffer.data.length))) { // just // reached // the // next // slice // of // buffer: // "rotate // over", // reuse // existing // buffer // but // adjust // start // tick quantizeBuffer.startTick = bufferStart; // equiv.: // quantizeBuffer.startTick // += // quantizeBuffer.data.length } // no 'else', to catch the last or-branch of // the following: if ((quantizeBuffer == null) || (quantizeBuffer.startTick != bufferStart) || (quantizeBuffer.data.length != opt.quantization.interval)) { quantizeBuffer = new QuantizeBuffer( opt.quantization.interval, bufferStart); quantizeBuffersByTrack.put(trackWrapper, quantizeBuffer); rebufferQuantization(trackWrapper, t, opt.quantization, quantizeBuffer); } int quantizeLookahead = opt.quantization.interval / 2; Vector<MidiEvent> eventsToBeQuantized = trackWrapper .getEventsForTick(t + quantizeLookahead); if (eventsToBeQuantized != null) { quantizeOnTheFly(eventsToBeQuantized, opt.quantization, quantizeBuffer); } events = quantizeBuffer.data[d]; // actual // events to // be played // now (had // been // buffered // before) quantizeBuffer.data[d] = null; // invalidate // (may already // be filled // with new data // while still // in this // buffer slice, // but near the // end // ("rotating" // buffer)) } else { // normal playing // Now find all midi messages for the given tick events = trackWrapper.getEventsForTick(t); } if (events == null) continue; for (MidiEvent evt : events) { // Handle tempomessages seperately byte[] msgBytes = evt.getMessage().getMessage(); if (msgBytes[0] == -1 && msgBytes[1] == 0x51 && msgBytes[2] == 3) { System.err.println(" tempo event found on a track pther then the first "); } else { // 'normal' event if (!opt.preRenderedUsed) { // Skip if track // is // pre-rendered if ((msgBytes.length > 2) && (((msgBytes[0] & 0xf0) == ShortMessage.NOTE_OFF || (msgBytes[0] & 0xf0) == ShortMessage.NOTE_ON) && (opt.drumMapped || (opt.transpose != 0) || (opt.velocityOffset != 0) || (opt.velocityCompression != 0.0f)))) { // need to do some on-the-fly // modifications of the event int note = msgBytes[1]; note = applyPlayOptionsNote(opt, note); int vel = msgBytes[2]; vel = applyPlayOptionsVelocity(opt, vel); ShortMessage shm = new ShortMessage(); shm.setMessage(msgBytes[0], note, vel); sendMidiMessage(shm, trackWrapper); } else { // normal send sendMidiMessage(evt.getMessage(), trackWrapper); } } } } } catch (Exception e) { } } } // Notify listeners that requires notification on each tick sequencer.notifyIntenseSongPositionListeners(tickPosition); // Otherwise give the job to another thread. songPositionNotifier.setNextTick(tickPosition); } lastTickPosition = currentTick + 1; } } private void sendMessageToAll(MidiMessage msg) { for (FrinikaTrackWrapper trackWrapper : ((FrinikaSequence) sequencer .getSequence()).getFrinikaTrackWrappers()) { if (trackWrapper.midiDevice != null) { try { trackWrapper.getMidiDevice().getReceiver().send(msg, -1); } catch (MidiUnavailableException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } private void handleTempoEvents(Vector<MidiEvent> events) { for (MidiEvent evt : events) { // Handle tempomessages seperately byte[] msgBytes = evt.getMessage().getMessage(); if (msgBytes[0] == -1 && msgBytes[1] == 0x51 && msgBytes[2] == 3) { int mpq = ((msgBytes[3] & 0xff) << 16) | ((msgBytes[4] & 0xff) << 8) | (msgBytes[5] & 0xff); // PJL removed cast to int sequencer.setTempoInBPM((60000000f / mpq)); // Send tempo messages to all MidiDevice we // send a message sendMessageToAll(evt.getMessage()); if (realtime) { /** * (PJS) Since the sequencer player calculates the time in ticks based on the * startTickPosition and the tempo, the startTickPosition has to be altered * to be the same as the tick position when the tempo change occured. */ startTickPosition = sequencer.getTickPosition(); startTimeMillis = System.currentTimeMillis(); } } } } static int applyPlayOptionsNote(MidiPlayOptions opt, int note) { if (opt.drumMapped) { note = opt.noteMap[note]; } else if (opt.transpose != 0) { note += opt.transpose; if (note < 0) { note = 0; } else if (note > 127) { note = 127; } } return note; } static int applyPlayOptionsVelocity(MidiPlayOptions opt, int vel) { if (vel != 0) { if (opt.velocityCompression != 0.0f) { float diff = (64 - vel) * opt.velocityCompression; vel += diff; } if (opt.velocityOffset != 0) { vel += opt.velocityOffset; if (vel < 1) { vel = 1; } else if (vel > 127) { vel = 127; } } } return vel; } private static void quantizeOnTheFly(Vector<MidiEvent> eventsToBeQuantized, Quantization q, QuantizeBuffer quantizeBuffer) { for (MidiEvent event : eventsToBeQuantized) { event = q.quantize(event); long tick = event.getTick(); long d = tick - quantizeBuffer.startTick; if (d < 0) { d = 0; // cannot do better } else { d %= quantizeBuffer.data.length; // if the data is cached at // the beginning of the // buffer (thus reaches over // the end of the current // buffer), this part of the // buffer that has already // been played and will be // reused later, "rotating" } Vector<MidiEvent> v = quantizeBuffer.data[(int) d]; if (v == null) { v = new Vector<MidiEvent>(); quantizeBuffer.data[(int) d] = v; } v.add(event); } } private static void rebufferQuantization(FrinikaTrackWrapper trackWrapper, long t, Quantization q, QuantizeBuffer quantizeBuffer) { int lookahead = q.interval / 2; long from = t - lookahead + 1; if (from < 0) { // case when start playing at very begnning from = 0; } long to = t + lookahead; for (long tick = from; tick < to; tick++) { Vector<MidiEvent> events = trackWrapper.getEventsForTick(tick); if (events != null) { quantizeOnTheFly(events, q, quantizeBuffer); } } } /** * * Set the tempoList to get the tempo when the time is warped. * * @param tempoList */ public void setTempoList(TempoList tempoList) { // System.out // .println("************************************************ TempoList has been set " // + tempoList); this.tempoList = tempoList; } /* * private static String bytesout(byte[] b) { StringBuffer sb = new * StringBuffer("["); for (int i = 0; i < b.length; i++) { * //sb.append(hexDigit(b[i]>>4)+hexDigit(b[i]&0xf)+", "); * sb.append(Integer.toHexString(b[i])+", "); } sb.append("]"); return * sb.toString(); } */ private final void chaseControllers() { // System.out.println(" Chase controllers "); if (tempoList != null) { MyTempoEvent evt = tempoList.getTempoEventAt(tickPosition); float bpm = (float) evt.getBPM(); sequencer.setTempoInBPM(bpm); sendMessageToAll(evt.getTempoEvent().getMessage()); } for (FrinikaTrackWrapper trackWrapper : ((FrinikaSequence) sequencer .getSequence()).getFrinikaTrackWrappers()) { if (trackWrapper.midiDevice != null) { for (MidiMessage msg : trackWrapper .getControllerStateAtTick(tickPosition)) { try { sendMidiMessage(msg, trackWrapper); } catch (InvalidMidiDataException e) { // TODO Auto-generated catch block // e.printStackTrace(); } catch (MidiUnavailableException e) { // TODO Auto-generated catch block // e.printStackTrace(); } } } } } /** * Takes into account that a track might be set to "loop-mode", meaning that * the very last part of it will be repeated endlessly until the song ends. * (This has nothing to do with the overall loop-mode from a start-point to * an end-point. "Loop-mode" of a track is set together with "solo"/ "mute" * settings.) * * @param tick * @return adopted tick time for the corresponding track, "faking" the last * part is still playing * @author Jens Gulden */ private long getLoopedTick(long tick, FrinikaTrackWrapper trackWrapper, MidiPlayOptions opt) { int s = trackWrapper.track.size(); if (s <= 1) return tick; long end = trackWrapper.lastTickUsed(); // PJS: Changed from tick > end to tick>=end so that first tick of loop isn't skipped, also removed t=t-1 if (tick >= end) { // yes, loop long t = end - opt.loopedTicks + ((tick - end) % opt.loopedTicks); return t; // "faked" tick position before/insed last midi part of // track } else { // end of track not yet reached: normal operation return tick; } } /* * public long lastTickUsed() { // TODO: could already cache this in * contructor, right? int s = track.size(); if (s > 1) { MidiEvent * lastMidiEvent = track.get( s - 2 ); long tick = lastMidiEvent.getTick(); * return tick; } else { return 0; } } */ public void start() { // Ensure that there is no other running thread running = false; while (!finished) Thread.yield(); running = true; loopCount = 0; ticksLooped=0; startTimeMillis = System.currentTimeMillis(); startTimeNanos = System.nanoTime(); tickPosition = startTickPosition; FrinikaSequence sequence = (FrinikaSequence) sequencer.getSequence(); timeAtStart = (long) (1000 * startTickPosition / (sequence .getResolution() * (sequencer.getTempoInBPM() / 60000))); chaseControllers(); // If not real time the timerEvents has to be sent manually if (realtime) { playThread = new Thread(this); // playThread.setPriority(Thread.MAX_PRIORITY-1); playThread.start(); } } /** * Returns the current time relative to the time at tick=0 * * @return */ transient long microSecondCache; transient long tickPosCache; private double latencyCompMillis; public long getMicroSecondPosition() { /** * if(running) { // This returns time relative to the play start * position which is wrong long timeSinceStart = (System.nanoTime() - * startTimeNanos)/1000; return timeAtStart+timeSinceStart; } else { */ if (tickPosition == tickPosCache) // avoid duplicate work return microSecondCache; tickPosCache = tickPosition; microSecondCache = (long) (1000000.0 * tempoList .getTimeAtTick(tickPosition)); // System.out.println(" getMicroSec =" + microSecondCache); return microSecondCache; /* * FrinikaSequence sequence = (FrinikaSequence) sequencer.getSequence(); * * * return (long) ((tickPosition * 60000000) / (sequence.getResolution() * * sequencer .getTempoInBPM())); */ // } } public void stop() { running = false; notesOff(true); // noteOnCache.releasePendingNoteOffs(); } ShortMessage createShortMessage(int command, int channel, int data1, int data2) throws InvalidMidiDataException { ShortMessage shm = new ShortMessage(); shm.setMessage(command, channel, data1, data2); return shm; } /** * Send note off to mididevices and reset all controllers * * @param doControllers */ void notesOff(boolean doControllers) { try { int done = 0; for (int ch = 0; ch < 16; ch++) { int channelMask = (1 << ch); for (int i = 0; i < 128; i++) { sendMidiMessage(createShortMessage(ShortMessage.NOTE_ON, ch, i, 0)); done++; } /* all notes off */ sendMidiMessage(createShortMessage(ShortMessage.CONTROL_CHANGE, ch, 123, 0)); /* sustain off */ sendMidiMessage(createShortMessage(ShortMessage.CONTROL_CHANGE, ch, 64, 0)); if (doControllers) { /* reset all controllers */ sendMidiMessage(createShortMessage( ShortMessage.CONTROL_CHANGE, ch, 121, 0)); done++; } } } catch (Exception e) { } } /** * Send a MidiMessage to using the device and channel specs of a * FrinikaTrackWrapper * * @param msg * @param trackWrapper * @throws InvalidMidiDataException * @throws MidiUnavailableException */ final void sendMidiMessage(MidiMessage msg, FrinikaTrackWrapper trackWrapper) throws InvalidMidiDataException, MidiUnavailableException { if (msg instanceof ShortMessage && trackWrapper.getMidiChannel() != FrinikaTrackWrapper.CHANNEL_FROM_EVENT) { ShortMessage smsg = ((ShortMessage) msg); smsg.setMessage(smsg.getCommand(), trackWrapper.getMidiChannel(), smsg.getData1(), smsg.getData2()); sendMidiMessage(smsg, trackWrapper.getMidiDevice().getReceiver()); } else sendMidiMessage(msg, trackWrapper.getMidiDevice().getReceiver()); } /** * Send a MidiMessage to one transmitter * * @param msg * @param trans */ final void sendMidiMessage(MidiMessage msg, Receiver receiver) { noteOnCache.interceptMessage(msg, receiver); receiver.send(msg, -1); } /** * Send MidiMessage to all transmitters * * @param msg */ final void sendMidiMessage(MidiMessage msg) { for (Transmitter transmitter : sequencer.getTransmitters()) { sendMidiMessage(msg, transmitter.getReceiver()); } } // Note that this thread should do nothing but sending the timerEvents public void run() { finished = false; priority = 0; while (running) { if (priorityRequested != priority) { System.out.println(" PLayer priority requested " + priorityRequested); Priority.setPriorityRR(priorityRequested); priority = priorityRequested; } try { Thread.sleep(1); // wait(1); timerEvent(); } catch (Exception e) { e.printStackTrace(); } } finished = true; startTickPosition = tickPosition; } public boolean isRunning() { return running; } /** * * * @return ticks lost because of looping */ public long getTicksLooped() { return ticksLooped; } public long getTickPosition() { return tickPosition; } /** * Return number of loops played * * @return */ public int getLoopCount() { return loopCount; } /** * The delay due to audio latency which must be subtracted when recording midi events * * * @param latencyCompMillis */ public void setLatencyCompensationInMillis(double latencyCompMillis) { this.latencyCompMillis=latencyCompMillis; } /** * * Used for recording to time stamp incoming midi events * @return */ public long getRealTimeTickPosition() { FrinikaSequence sequence = (FrinikaSequence) sequencer.getSequence(); long currentTick = startTickPosition + (long) ((System.currentTimeMillis()-latencyCompMillis - startTimeMillis) * (sequence .getResolution() * (sequencer.getTempoInBPM() / 60000))); if (sequencer.getLoopCount() == FrinikaSequencer.LOOP_CONTINUOUSLY) currentTick = ((currentTick - sequencer.getLoopStartPoint()) % (sequencer .getLoopEndPoint() - sequencer.getLoopStartPoint())) + sequencer.getLoopStartPoint(); return (currentTick); } public void setTickPosition(long tickPosition) { startTickPosition = tickPosition; startTimeMillis = System.currentTimeMillis(); startTimeNanos = System.nanoTime(); this.lastTickPosition = tickPosition; this.tickPosition = tickPosition; if (running) { tickPositionChanged = true; } sequencer.notifyAllSongPositionListeners(tickPosition); } class SongPositionNotifier implements Runnable { long tickLast = -1; long tickNext = -1; long intervalInMillis = 50; public void run() { while (true) { try { Thread.sleep(intervalInMillis); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (tickLast != tickNext) { sequencer.notifySongPositionListeners(tickNext); // TODO sequencer.notifyGUISongPositionListeners(tickNext); tickLast = tickNext; } } } void setNextTick(long tick) { // TODO sequencer.notifyRealTimePositionListeners(tick); tickNext = tick; } } /** * Set whether to play in realtime or if rendering (e.g. export wav) * * @param realtime */ void setRealtime(boolean realtime) { this.realtime = realtime; } /** * Returns whether to play in realtime or if rendering (e.g. export wav) */ boolean getRealtime() { return realtime; } // --- inner class --- private class QuantizeBuffer { long startTick; Vector<MidiEvent>[] data; public QuantizeBuffer(int bufferSize, long startTick) { this.startTick = startTick; this.data = new Vector[bufferSize]; } } }