/*
* Transport.java
* Eisenkraut
*
* Copyright (c) 2004-2016 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*/
package de.sciss.eisenkraut.realtime;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.List;
import de.sciss.app.AbstractApplication;
import de.sciss.io.Span;
import de.sciss.util.Disposable;
import de.sciss.eisenkraut.net.OSCRouter;
import de.sciss.eisenkraut.net.OSCRouterWrapper;
import de.sciss.eisenkraut.net.OSCRoot;
import de.sciss.eisenkraut.net.RoutedOSCMessage;
import de.sciss.eisenkraut.session.Session;
import de.sciss.eisenkraut.timeline.TimelineEvent;
import de.sciss.eisenkraut.timeline.TimelineListener;
import de.sciss.eisenkraut.util.PrefsUtil;
/**
* The realtime "motor" or "clock". The transport
* deals with realtime playback of the timeline.
* It provides means for registering and un-registering
* realtime consumers and communicates with a
* RealtimeProducer which is responsible for the
* actual data production. Transport clocking is
* performed within an extra thread from within
* the consumer's methods are called and registered
* transport listeners are informed about actions.
*/
public class Transport
implements TimelineListener, OSCRouter, Disposable {
protected final Session doc;
private boolean looping = false;
private boolean loopInPlay = false;
private long loopStart, loopStop;
private double rate;
private double frameFactor;
private long lastUpdate;
// high level listeners
private final List<TransportListener> collListeners = new ArrayList<TransportListener>();
// realtime control
private long startFrame;
private long stopFrame;
private long currentFrame;
private long startTime;
private double rateScale = 1.0;
private boolean running = false;
// --- actions ---
private static final String OSC_TRANSPORT = "transport";
private final OSCRouter osc;
// sync : call in event thread!
/**
* Creates a new transport. The thread will
* be started and set to pause to await
* transport commands.
*
* @param doc Session document
*/
public Transport( Session doc )
{
this.doc = doc;
doc.timeline.addTimelineListener( this );
osc = new OSCRouterWrapper( doc, this );
rate = doc.timeline.getRate();
frameFactor = rateScale * rate / 1000;
}
public void dispose()
{
collListeners.clear();
running = false;
doc.timeline.removeTimelineListener( this );
}
public Session getDocument()
{
return doc;
}
/**
* Registers a new transport listener
*
* @param listener the listener to register for information
* about transport actions such as play or stop
*/
public void addTransportListener(TransportListener listener) {
if (!EventQueue.isDispatchThread()) throw new IllegalMonitorStateException();
collListeners.add(listener);
if (running) {
listener.transportPlay(this, updateCurrentFrame(), rateScale);
}
}
/**
* Unregisters a transport listener
*
* @param listener the listener to remove from the event dispatching
*/
public void removeTransportListener(TransportListener listener) {
if (!EventQueue.isDispatchThread()) throw new IllegalMonitorStateException();
collListeners.remove(listener);
}
private void dispatchStop(long pos) {
for (TransportListener collListener : collListeners) {
(collListener).transportStop(this, pos);
}
if( AbstractApplication.getApplication().getUserPrefs().getBoolean(
PrefsUtil.KEY_INSERTIONFOLLOWSPLAY, false )) {
doc.timeline.editPosition( this, pos );
} else {
doc.timeline.setPosition( this, doc.timeline.getPosition() );
}
}
private void dispatchPosition(long pos) {
for (TransportListener collListener : collListeners) {
(collListener).transportPosition(this, pos, rateScale);
}
}
private void dispatchPlay(long pos) {
for (TransportListener collListener : collListeners) {
(collListener).transportPlay(this, pos, rateScale);
}
}
private void dispatchReadjust(long pos) {
for (TransportListener collListener : collListeners) {
(collListener).transportReadjust(this, pos, rateScale);
}
}
private void dispatchQuit() {
for (TransportListener collListener : collListeners) {
try {
(collListener).transportQuit(this);
} catch (Exception e1) {
System.err.println("[@transport]" + e1.getLocalizedMessage());
}
}
}
/**
* Requests the thread to start
* playing. TransportListeners
* are informed when the
* playing really starts.
*/
public void play(double scale) {
playSpan(new Span(doc.timeline.getPosition(), doc.timeline.getLength()), scale); // XXX sync?
}
public void playSpan(Span span, double scale) {
if (!EventQueue.isDispatchThread()) throw new IllegalMonitorStateException();
if (running) return;
startFrame = span.start;
loopInPlay = isLooping() && loopStop > startFrame;
stopFrame = loopInPlay ? loopStop : span.stop;
this.rateScale = scale;
frameFactor = scale * rate / 1000;
currentFrame = startFrame;
running = true;
dispatchPlay( startFrame );
startTime = System.currentTimeMillis();
}
public double getRateScale()
{
return rateScale;
}
/**
* Sets the loop span for playback
*
* @param loopSpan Span describing the new loop start and stop.
* Passing null stops looping.
*/
public void setLoop( Span loopSpan )
{
if( !EventQueue.isDispatchThread() ) throw new IllegalMonitorStateException();
long testFrame;
if( loopSpan != null ) {
if( !looping || (loopStart != loopSpan.start) || (loopStop != loopSpan.stop) ) {
loopStart = loopSpan.start;
loopStop = loopSpan.stop;
looping = true;
if( running ) {
if( currentFrame < loopStop ) {
loopInPlay = true;
stopFrame = loopStop;
}
// check for possible jumps
testFrame = startFrame + (long) ((lastUpdate - startTime) * frameFactor + 0.5);
if( loopInPlay && (testFrame >= loopStop) ) {
testFrame = ((testFrame - loopStart) % (loopStop - loopStart)) + loopStart;
}
// seamless re-adjustment of startFrame
// so currentFrame doesn't jump
if( testFrame != currentFrame ) {
startFrame -= testFrame - currentFrame;
}
dispatchReadjust( startFrame );
}
}
} else {
if( looping ) {
if( running && loopInPlay ) {
// check for possible jumps
testFrame = startFrame + (long) ((lastUpdate - startTime) * frameFactor + 0.5);
// seamless re-adjustment of startFrame
// so currentFrame doesn't jump
if( testFrame != currentFrame ) {
startFrame -= testFrame - currentFrame;
}
}
loopInPlay = false;
looping = false;
if( running ) {
stopFrame = doc.timeline.getLength();
dispatchReadjust( startFrame );
}
}
}
}
/**
* Returns whether looping
* is active or not
*
* @return <code>true</code> if looping is used
*/
public boolean isLooping()
{
return looping;
}
/**
* 'Folds' a time span with regard to current loop settings.
* That is, if a transport listener is calculating linear increasing
* time spans from transport play offset, this method checks against
* active and relevant (loopInPlay) loop settings and clips back
* the span or portions of the span to the loop region if necessary.
* <p>
* This does not check against the document length so span stops
* beyond doc.timeline.getLength() are possible and allowed.
*
* Note: this method is not thread safe, hence should be called in the event
* thread. this means, the trigger responder in SuperColliderPlayer
* must be deferred!!!
*
* @param unfolded the linear extrapolated time span from transport play
* @param loopMin a minimum length of the loop such as to prevent cpu overload or
* osc message overflow (imagine the user would make a 1 sample long loop).
* leave to zero if no minimum required.
* @return an array of folded spans (array length is greater than or equal to 1)
*/
public Span[] foldSpans( Span unfolded, int loopMin )
{
// the quick one
if( !loopInPlay || (unfolded.stop <= loopStop)) return new Span[] { unfolded };
final long loopLen = Math.max( loopMin, loopStop - loopStart );
final long loopMinStop = loopStart + loopLen;
final long foldStart = (unfolded.start < loopMinStop) ? unfolded.start : ((unfolded.start - loopStart) % loopLen) + loopStart;
final long attemptStop = foldStart + unfolded.getLength();
// no splitting up required
if( attemptStop <= loopMinStop ) return new Span[] { new Span( foldStart, attemptStop )};
// pseudo-code:
// numSpans = (attemptStop - loopMinStop + loopLen-1) / loopLen + 1
final int numSpans = (int) ((attemptStop - loopStart - 1) / loopLen) + 1;
final long foldStop = ((attemptStop - loopStart) % loopLen) + loopStart;
final Span[] folded = new Span[ numSpans ];
folded[ 0 ] = new Span( foldStart, loopMinStop );
for( int i = 1, j = numSpans - 1; i <= j; i++ ) {
folded[ i ] = new Span( loopStart, i < j ? loopMinStop : foldStop );
}
return folded;
}
/**
* Requests the thread to stop
* playing. TransportListeners
* are informed when the
* playing really stops.
*/
public void stop()
{
if( !EventQueue.isDispatchThread() ) throw new IllegalMonitorStateException();
if( running ) {
running = false;
updateCurrentFrame();
dispatchStop( currentFrame );
}
}
/**
* Sends quit rt_command to the transport
* returns only after the transport thread
* stopped!
*/
public void quit()
{
running = false;
dispatchQuit();
}
public long getCurrentFrame()
{
return updateCurrentFrame();
}
private long updateCurrentFrame()
{
final long now = System.currentTimeMillis();
if( (now == lastUpdate) || !running ) return currentFrame;
currentFrame = startFrame + (long) ((now - startTime) * frameFactor + 0.5);
lastUpdate = now;
if( loopInPlay ) {
if( currentFrame >= loopStop ) {
currentFrame = ((currentFrame - loopStart) % (loopStop - loopStart)) + loopStart;
}
} else if( currentFrame > stopFrame ) {
// final boolean dispatch = (currentFrame - stopFrame) >= 128;
currentFrame = stopFrame;
running = false;
// if( dispatch ) {
dispatchStop( currentFrame );
// }
}
return currentFrame;
}
// ---------------- TimelineListener interface ----------------
public void timelinePositioned( TimelineEvent e )
{
if( e.getSource() == this ) return;
if( running ) {
startFrame = doc.timeline.getPosition(); // XXX sync?
loopInPlay = isLooping() && loopStop > startFrame;
stopFrame = loopInPlay ? loopStop : doc.timeline.getLength();
// rateScale = rate;
currentFrame = startFrame;
dispatchPosition( startFrame );
startTime = System.currentTimeMillis();
lastUpdate = startTime;
} else {
currentFrame = doc.timeline.getPosition();
}
}
public void timelineChanged( TimelineEvent e ) {
rate = doc.timeline.getRate();
frameFactor = rateScale * rate / 1000;
}
public void timelineSelected(TimelineEvent e) { /* ignored */ }
public void timelineScrolled(TimelineEvent e) { /* ignored */ }
// --------------- RealtimeHost interface ---------------
/**
* Returns whether the
* thread is currently playing
*
* @return <code>true</code> if the transport is currently playing
*/
public boolean isRunning()
{
return running;
}
public void showMessage(int type, String text) {
System.err.println(text);
}
// ------------- OSCRouter interface -------------
public String oscGetPathComponent() {
return OSC_TRANSPORT;
}
public void oscRoute(RoutedOSCMessage rom) {
osc.oscRoute(rom);
}
public void oscAddRouter(OSCRouter subRouter) {
osc.oscAddRouter(subRouter);
}
public void oscRemoveRouter(OSCRouter subRouter) {
osc.oscRemoveRouter(subRouter);
}
public Object oscQuery_position() {
return getCurrentFrame();
}
public Object oscQuery_running() {
return isRunning() ? 1 : 0;
}
public void oscCmd_play(RoutedOSCMessage rom) {
try {
final float r = rom.msg.getArgCount() == 1 ? 1.0f :
Math.max(0.25f, Math.min(4f, ((Number) rom.msg.getArg(1)).floatValue()));
// actionPlay.perform( r );
play(r);
} catch (ClassCastException e1) {
OSCRoot.failedArgType(rom, 1);
}
}
public void oscCmd_stop(RoutedOSCMessage rom) {
stop();
}
}