Package de.sciss.eisenkraut.session

Source Code of de.sciss.eisenkraut.session.Session

/*
*  Session.java
*  Eisenkraut
*
*  Copyright (c) 2004-2014 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
*
*
*  Changelog:
*    25-Jan-05  created
*    15-Jul-05  removed SessionChangeListener stuff. relies more on SessionCollection.Listener
*          for doc.tracks
*    21-Jan-06  implements OSCRouter ; moved a lot of actions from DocumentFrame to this class
*/

package de.sciss.eisenkraut.session;

import java.awt.EventQueue;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JOptionPane;

import de.sciss.eisenkraut.Main;
import de.sciss.eisenkraut.edit.BasicCompoundEdit;
import de.sciss.eisenkraut.edit.EditSetTimelineLength;
import de.sciss.eisenkraut.edit.TimelineVisualEdit;
import de.sciss.eisenkraut.edit.UndoManager;
import de.sciss.eisenkraut.gui.BlendingAction;
import de.sciss.eisenkraut.io.AudioTrail;
import de.sciss.eisenkraut.io.BlendContext;
import de.sciss.eisenkraut.io.DecimatedSonaTrail;
import de.sciss.eisenkraut.io.DecimatedTrail;
import de.sciss.eisenkraut.io.DecimatedWaveTrail;
import de.sciss.eisenkraut.io.MarkerTrail;
import de.sciss.eisenkraut.net.OSCRoot;
import de.sciss.eisenkraut.net.OSCRouter;
import de.sciss.eisenkraut.net.OSCRouterWrapper;
import de.sciss.eisenkraut.net.RoutedOSCMessage;
import de.sciss.eisenkraut.realtime.Transport;
import de.sciss.eisenkraut.render.FilterDialog;
import de.sciss.eisenkraut.render.RenderPlugIn;
import de.sciss.eisenkraut.render.Replace;
import de.sciss.eisenkraut.timeline.AudioTrack;
import de.sciss.eisenkraut.timeline.AudioTracks;
import de.sciss.eisenkraut.timeline.MarkerTrack;
import de.sciss.eisenkraut.timeline.Timeline;
import de.sciss.eisenkraut.timeline.Track;
import de.sciss.gui.GUIUtil;
import de.sciss.gui.MenuAction;
import de.sciss.gui.ParamField;
import de.sciss.gui.SpringPanel;
import de.sciss.io.AudioFile;
import de.sciss.io.AudioFileDescr;
import de.sciss.io.Span;
import de.sciss.timebased.Trail;
import de.sciss.util.DefaultUnitTranslator;
import de.sciss.util.Flag;
import de.sciss.util.Param;
import de.sciss.util.ParamSpace;

import de.sciss.app.AbstractApplication;
import de.sciss.app.AbstractCompoundEdit;
import de.sciss.common.BasicDocument;
import de.sciss.common.BasicWindowHandler;
import de.sciss.common.ProcessingThread;

/**
@author    Hanns Holger Rutz
@version  0.71, 16-Oct-08
*
*  @todo    try get rid of the GUI stuff in here
*/
public class Session
extends BasicDocument
implements OSCRouter
{
  private final AudioFileDescr      displayAFD;
  private AudioFileDescr[]        afds;
  private String              name;
 
  public final Timeline          timeline;

  protected AudioTrail          at        = null;
  private DecimatedWaveTrail        dwt        = null;
  private DecimatedSonaTrail        dst        = null;

  public final MarkerTrail        markers;
  public final MarkerTrack        markerTrack;

  private static final int[]        waveDecims    = { 8, 12, 16 };
//  private static final int[]        sonaDecims    = { 0, 8 /*, 16 */ };

//  /**
//   *  Use this <code>LockManager</code> to gain access to
//   *  <code>receiverCollection</code>, <code>transmitterCollection</code>,
//   *  <code>timeline</code> and a transmitter's <code>AudioTrail</code>.
//   */
//  public final LockManager        bird      = new LockManager( 3 );
 
  /**
   *  Bitmask for putting a lock on the <code>timeline</code>
   */
  public  static final int        DOOR_TIME    = 0x01;
  /**
   *  Bitmask for putting a lock on a <code>AudioTrail</code>
   */
  public  static final int        DOOR_MTE    = 0x02;
  /**
   *  Bitmask for putting a lock on the channel tracks
   */
  public  static final int        DOOR_TRACKS    = 0x04;

  public static final int          DOOR_ALL    = DOOR_TIME | DOOR_TRACKS | DOOR_MTE;
 
  public final SessionCollection      tracks      = new SessionCollection()// should be tracking audioTracks automatically
//  public final SessionCollection      audioTracks    = new SessionCollection();
  public final AudioTracks        audioTracks;
  public final SessionCollection      selectedTracks  = new SessionCollection();

  private Transport            transport    = null;
  private DocumentFrame          frame      = null;
 
  // --- actions ---

  private final ActionSave      actionSave;
  private final ActionCut        actionCut;
  protected final ActionCopy      actionCopy;
  private final ActionPaste      actionPaste;
  private final ActionDelete      actionDelete;
  private final ActionSilence      actionSilence;
  private final ActionTrim      actionTrim;

  // ---  ---
  private final UndoManager        undo      = new UndoManager( this );
  private boolean              dirty      = false;

  public static final int          EDIT_INSERT    = 0;
  public static final int          EDIT_OVERWRITE  = 1;
  public static final int          EDIT_MIX    = 2;
 
  private static final String[]      EDITMODES    = { "insert", "overwrite", "mix" };
 
  private int                editMode    = EDIT_INSERT;
 
  private static int            nodeIDAlloc    = 0;
  private final int            nodeID;

  private final OSCRouterWrapper      osc;
 
  private final BlendingAction      blending;
 
  protected ProcessingThread        pt        = null;
 
  private Session( AudioFileDescr[] afds, boolean createOSC )
  throws IOException
  {
    this( afds, null, createOSC );
  }

  private Session( AudioFileDescr afd, boolean createOSC )
  throws IOException
  {
    this( new AudioFileDescr[] { afd }, afd, createOSC );
  }

  private Session( AudioFileDescr[] afds, AudioFileDescr displayAFD, boolean createOSC )
  throws IOException
  {
    super();
 
    this.afds          = afds;
    if( displayAFD == null ) {
      this.displayAFD      = new AudioFileDescr();
      autoCreateDisplayDescr();
    } else {
      this.displayAFD      = displayAFD;
      name          = displayAFD.file == null ? null : displayAFD.file.getName();
    }
//    System.out.println( "name = '" + name + "'" );
   
    nodeID        = ++nodeIDAlloc;
   
    if( createOSC ) {
      osc        = new OSCRouterWrapper( null, this );
    } else {
      osc        = null;
    }
   
    timeline      = new Timeline( this );
    markerTrack      = new MarkerTrack( this );
    markers        = (MarkerTrail) markerTrack.getTrail();
    markers.copyFromAudioFile( afds[ 0 ])// XXX
    tracks.add( null, markerTrack );
   
    audioTracks      = new AudioTracks( this );

    actionSave      = new ActionSave();
    actionCut      = new ActionCut();
    actionCopy      = new ActionCopy();
    actionPaste      = new ActionPaste();
    actionDelete    = new ActionDelete();
    actionSilence    = new ActionSilence();
    actionTrim      = new ActionTrim();

    timeline.setRate( this, this.displayAFD.rate );
    timeline.setLength( this, this.displayAFD.length );
    timeline.setVisibleSpan( this, new Span( 0, this.displayAFD.length ));
    selectedTracks.add( this, markerTrack );
   
    blending      = new BlendingAction( timeline, null );
  }
 
  /**
   *   Checks if a process is currently running. This method should be called
   *   before launching a process using the <code>start()</code> method.
   *   If a process is ongoing, this method waits for a default timeout period
   *   for the thread to finish.
   *
   *  @return  <code>true</code> if a new process can be launched, <code>false</code>
   *      if a previous process is ongoing and a new process cannot be launched
   *  @throws  IllegalMonitorStateException  if called from outside the event thread
   *  @synchronization  must be called in the event thread
   */
  public boolean checkProcess()
  {
    return checkProcess( 500 );
  }
 
  /**
   *   Checks if a process is currently running. This method should be called
   *   before launching a process using the <code>start()</code> method.
   *   If a process is ongoing, this method waits for a given timeout period
   *   for the thread to finish.
   *
   *   @param  timeout  the maximum duration in milliseconds to wait for an ongoing process
   *  @return  <code>true</code> if a new process can be launched, <code>false</code>
   *      if a previous process is ongoing and a new process cannot be launched
   *  @throws  IllegalMonitorStateException  if called from outside the event thread
   *  @synchronization  must be called in the event thread
   */
  public boolean checkProcess( int timeout )
  {
//System.out.println( "checking..." );
    if( !EventQueue.isDispatchThread() ) throw new IllegalMonitorStateException();
    if( pt == null ) return true;
    if( timeout == 0 ) return false;

//System.out.println( "sync " + timeout );
    pt.sync( timeout );
//System.out.println( "sync done" );
    return( (pt == null) || !pt.isRunning() );
  }
 
  public void cancelProcess( boolean sync )
  {
    if( !EventQueue.isDispatchThread() ) throw new IllegalMonitorStateException();
    if( pt == null ) return;
    pt.cancelsync );
  }
 
  public String getProcessName()
  {
    if( !EventQueue.isDispatchThread() ) throw new IllegalMonitorStateException();
    if( pt == null ) return null;
    return pt.getName();
  }
 
  /**
   *   Starts a <code>ProcessingThread</code>. Only one thread
   *   can exist at a time. To ensure that no other thread is running,
   *   call <code>checkProcess()</code>.
   *
   *   @param  pt  the thread to launch
   *   @throws  IllegalMonitorStateException  if called from outside the event thread
   *   @throws  IllegalStateException      if another process is still running
   *   @see  #checkProcess()
   *   @synchronization  must be called in the event thread
   */
  public void start( ProcessingThread process )
  {
    if( !EventQueue.isDispatchThread() ) throw new IllegalMonitorStateException();
    if( this.pt != null ) throw new IllegalStateException( "Process already running" );
   
    pt = process;
    pt.addListener( new ProcessingThread.Listener() {
      public void processStarted( ProcessingThread.Event e ) { /* empty */ }
      public void processStopped( ProcessingThread.Event e )
      {
        pt = null;
      }
    });
    pt.start();
  }
 
  public BlendingAction getBlendingAction()
  {
    return blending;
  }
 
  public int getNodeID()
  {
    return nodeID;
  }
 
  public void setEditMode( int mode )
  {
    editMode = mode;
  }
 
  public int getEditMode()
  {
    return editMode;
  }
 
//  public Session( AudioFileDescr[] afds, AudioFileDescr displayAFD )
//  {
//    this.displayAFD  = displayAFD;
//    this.afds    = afds;
//    init();
//  }

//  public Session( AudioFile af )
//  {
//    this.afd  = af.getDescr();
//    init();
//  }

//  public void createOSC()
//  {
//    if( osc != null ) throw new IllegalStateException( "OSC already exists" );
//    osc  = new OSCRouterWrapper( null, this );
//  }

  public void createTransport()
  {
    if( transport != null ) throw new IllegalStateException( "Transport already exists" );
    transport = new Transport( this );
  }

  public Transport getTransport()
  {
    return transport;
  }

  public void createFrame()
  {
    if( frame != null ) throw new IllegalStateException( "Frame already exists" );
    frame = new DocumentFrame( this );
  }

  public DocumentFrame getFrame()
  {
    return frame;
  }

//  public void setFrame( DocumentFrame frame )
//  {
//    this.frame = frame;
//  }

//  public void clear( Object source )
//  throws IOException
//  {
//    timeline.clear( source );
//    selectedTracks.clear( source );
//    audioTracks.clear( source );
//    tracks.clear( source );
//    if( mte != null ) {
//      mte.clear( null );
//    }
//    markers.clear( source );
//    updateTitle();
//  }
 
  // pausing dispatcher is up to the caller!
  // should ensure that old mte was cleared!
//  public void setAudioTrail( Object source, AudioTrail mte )
  private void setAudioTrail( Object source, AudioTrail at )
  {
    this.at    = at;
//    tracks.pauseDispatcher();

//    audioTracks.clear( source );
if( !audioTracks.isEmpty() ) throw new IllegalStateException( "Cannot call repeatedly" );
   
    final List    collNewTracks  = new ArrayList();
    final int    numChannels    = at.getChannelNum();
    final double  deltaAngle    = 360.0 / numChannels;
    final double  startAngle    = numChannels < 2 ? 0.0 : -deltaAngle/2// reasonable for mono to octo
    AudioTrack    t;
   
    audioTracks.setTrail( at );
   
    for( int ch = 0; ch < at.getChannelNum(); ch++ ) {
//      t = new AudioTrack( at, ch );
      t = new AudioTrack( audioTracks, ch );
      t.setName( String.valueOf( ch + 1 ));
      t.getMap().putValue( source, AudioTrack.MAP_KEY_PANAZIMUTH, new Double( startAngle + ch * deltaAngle ));
      collNewTracks.add( t );
    }
    audioTracks.addAll( source, collNewTracks );
    tracks.addAll( source, collNewTracks );
    selectedTracks.addAll( source, collNewTracks );
//    tracks.resumeDispatcher();
//    dispatchChange( source );

    updateTitle();
  }

  public AudioTrail getAudioTrail()
  {
    return at;
  }
 
  public DecimatedWaveTrail getDecimatedWaveTrail()
  {
    return dwt;
  }
 
  public DecimatedSonaTrail getDecimatedSonaTrail()
  {
    return dst;
  }
 
  public void setDescr( AudioFileDescr[] afds )
  {
    this.afds = afds;
    autoCreateDisplayDescr();
    updateTitle();
  }

  private void autoCreateDisplayDescr()
  {
    if( afds.length == 0 ) {
      displayAFD.file        = null;
      name            = null;
    } else {
      final AudioFileDescr proto  = afds[ 0 ];
      displayAFD.type        = proto.type;
      displayAFD.rate        = proto.rate;
      displayAFD.bitsPerSample  = proto.bitsPerSample;
      displayAFD.sampleFormat    = proto.sampleFormat;
      displayAFD.length      = proto.length;
      displayAFD.channels      = proto.channels;

      if( proto.file == null ) {
        displayAFD.file      = null;
        name          = null;
      } else {
        final String      pname  = proto.file.getName();
        int            left  = pname.length();
        int            right  = pname.length();
        String          name2;
        int            trunc;

        displayAFD.type        = proto.type;
        displayAFD.rate        = proto.rate;
        displayAFD.bitsPerSample  = proto.bitsPerSample;
        displayAFD.sampleFormat    = proto.sampleFormat;
        displayAFD.length      = proto.length;
        displayAFD.channels      = proto.channels;
       
        for( int i = 1; i < afds.length; i++ ) {
          name2         = afds[ i ].file.getName();
          displayAFD.channels  += afds[ i ].channels;
          for( trunc = 0; trunc < Math.min( name2.length(), left ); trunc++ ) {
            if( !(name2.charAt( trunc ) == pname.charAt( trunc ))) break;
          }
          left  = trunc;
          for( trunc = 0; trunc < Math.min( name2.length(), right ); trunc++ ) {
            if( !(name2.charAt( name2.length() - trunc - 1 ) == pname.charAt( pname.length() - trunc - 1 ))) break;
          }
          right  = trunc;
//          System.out.println( "for '" + name2 + "' left = "+left+"; right = "+right );
        }
       
        if( left >= pname.length() - right ) {
          displayAFD.file  = afds[ 0 ].file;
          name      = displayAFD.file.getName();
        } else {
          final StringBuffer strBuf = new StringBuffer();
          strBuf.append( pname.substring( 0, left ));
          for( int i = 0; i < afds.length; i++ ) {
            strBuf.append( i == 0 ? '[' : ',' );
            name2 = afds[ i ].file.getName();
            strBuf.append( name2.substring( left, name2.length() - right ));
          }
          strBuf.append( ']' );
          final int idxBraClose = strBuf.length();
          strBuf.append( pname.substring( pname.length() - right ));
          displayAFD.file  = new File( afds[ 0 ].file.getParentFile(), strBuf.toString() );
          if( (idxBraClose - left) > 25 ) {
//            strBuf.delete( left + 12, strBuf.length() - 13 );
//            strBuf.insert( left + 12, '…' );
            strBuf.replace( left + 12, idxBraClose - 13, "\u2026" );
          }
          name = strBuf.toString();
        }
      }
    }
  }

  public DecimatedWaveTrail createDecimatedWaveTrail()
  throws IOException
  {
    if( dwt == null ) {
      dwt  = new DecimatedWaveTrail( at, DecimatedTrail.MODEL_FULLWAVE_PEAKRMS, waveDecims );
    }
    return dwt;
  }
 
  public DecimatedSonaTrail createDecimatedSonaTrail()
  throws IOException
  {
    if( dst == null ) {     
      dst  = new DecimatedSonaTrail( at, DecimatedTrail.MODEL_SONA /*, sonaDecims */ );
    }
    return dst;
  }

  public AudioFileDescr getDisplayDescr()
  {
    return displayAFD;
  }
 
  public String getName()
  {
    return name;
  }
 
  public AudioFileDescr[] getDescr()
  {
    return afds;
  }

  private void updateTitle()
  {
    if( frame != null ) frame.updateTitle();
  }

  public ProcessingThread procDelete( String procName, Span span, int mode )
  {
    return actionDelete.initiate( procName, span, mode );
  }

  public ProcessingThread procSave( String procName, Span span, AudioFileDescr[] targetAFDs,
                    int[] channelMap, boolean saveMarkers, boolean asCopy )
  {
    return actionSave.initiate( procName, span, targetAFDs, channelMap, saveMarkers, asCopy );
  }
 
  public MenuAction getCutAction()
  {
    return actionCut;
  }

  public MenuAction getCopyAction()
  {
    return actionCopy;
  }

  public MenuAction getPasteAction()
  {
    return actionPaste;
  }

  public MenuAction getDeleteAction()
  {
    return actionDelete;
  }
 
  public MenuAction getSilenceAction()
  {
    return actionSilence;
  }
 
  public MenuAction getTrimAction()
  {
    return actionTrim;
  }
 
  public ProcessingThread insertSilence( long pos, long numFrames )
  {
    return actionSilence.initiate( pos, numFrames );
  }

  public ProcessingThread closeDocument( boolean force, Flag wasClosed )
  {
    return frame.closeDocument( force, wasClosed )// XXX should be in here not frame!!!
  }
 
  public ClipboardTrackList getSelectionAsTrackList()
  {
    return actionCopy.getSelectionAsTrackList();
  }
 
  public ProcessingThread pasteTrackList( ClipboardTrackList tl, long insertPos, String procName, int mode )
  {
    return actionPaste.initiate( tl, insertPos, procName, mode );
  }

  public static Session newEmpty( AudioFileDescr afd )
  throws IOException
  {
    return newEmpty( afd, true, true );
  }

  // NOTE: does not add the document to a handler
  public static Session newEmpty( AudioFileDescr afd, boolean createTransport, boolean createOSC )
  throws IOException
  {
    final Session      doc      = new Session( afd, createOSC );
    final AudioTrail    at      = AudioTrail.newFrom( afd )// does _not_ throw an IOException

//    try {
      doc.setAudioTrail( null, at );
//      if( createOSC ) doc.createOSC();
      if( createTransport ) doc.createTransport();
//      if( createDecimated ) {
//        doc.createDecimatedWaveTrail();
//        doc.createDecimatedSonaTrail();
//      }
//    }
//    catch( IOException e1 ) {
//      doc.dispose();
//      throw e1;
//    }

    return doc;
  }
 
  public static Session newFrom( File path )
  throws IOException
  {
    return newFrom( path, true, true );
  }

  public static Session newFrom( File path, boolean createTransport, boolean createOSC )
  throws IOException
  {
    final AudioFile      af  = AudioFile.openAsRead( path );
    final AudioFileDescr  afd  = af.getDescr();
    Session          doc  = null;
    AudioTrail        at  = null;
   
    try {
// System.err.println( "readMarkers" );
      af.readMarkers();
      at          = AudioTrail.newFrom( af );
      doc          = new Session( afd, createOSC );
      doc.setAudioTrail( null, at );
//      if( createOSC ) doc.createOSC();
      if( createTransport ) doc.createTransport();
//      if( createDecimated ) {
//        doc.createDecimatedWaveTrail();
//        doc.createDecimatedSonaTrail();
//      }
      return doc;
    }
    catch( IOException e1 ) {
//      if( doc != null ) {
//        doc.dispose();
//      } else
      if( at != null ) {
        at.dispose();
      } else {
        af.cleanUp();
      }
      throw e1;
    }
  }
 
  public static Session newFrom( File[] paths )
  throws IOException
  {
    return newFrom( paths, true, true );
  }

  public static Session newFrom( File[] paths, boolean createTransport, boolean createOSC )
  throws IOException
  {
    final AudioFile[]    afs    = new AudioFile[ paths.length ];
    final AudioFileDescr[]  afds  = new AudioFileDescr[ paths.length ];
    AudioTrail        at    = null;
    Session          doc    = null;
 
    try {
      for( int i = 0; i < paths.length; i++ ) {
        afs[ i = AudioFile.openAsRead( paths[ i ]);
        afds[ i = afs[ i ].getDescr();
        if( i > 0 ) {
          if( (afds[ i ].length != afds[ 0 ].length) || (afds[ i ].rate != afds[ 0 ].rate) ||
            (afds[ i ].bitsPerSample != afds[ 0 ].bitsPerSample) || (afds[ i ].sampleFormat != afds[ 0 ].sampleFormat) ) {
       
            throw new IOException( getResourceString( "errHeadersNotMatching" ));
          }
        }
//        System.err.println( "readMarkers" );
        afs[ i ].readMarkers();
      }
      at          = AudioTrail.newFrom( afs );
      doc          = new Session( afds, createOSC );
      doc.setAudioTrail( null, at );
//      if( createOSC ) doc.createOSC();
      if( createTransport ) doc.createTransport();
//      if( createDecimated ) {
//        doc.createDecimatedWaveTrail();
//        doc.createDecimatedSonaTrail();
//      }
      return doc;
    }
    catch( IOException e1 ) {
//      if( doc != null ) {
//        doc.dispose();
//      } else
      if( at != null ) {
        at.dispose();
      } else {
        for( int i = 0; i < paths.length; i++ ) {
          if( afs[ i ] != null ) afs[ i ].cleanUp();
        }
      }
      throw e1;
    }
  }

  protected static String getResourceString( String key )
  {
    return AbstractApplication.getApplication().getResourceString( key );
  }

//  private static BlendContext createBlendContext( Session doc, long maxLeft, long maxRight, Flag hasSelectedAudio )
//  {
//    if( !hasSelectedAudio.isSet() || ((maxLeft == 0L) && (maxRight == 0L)) ) {
//      return null;
//    } else {
//System.err.println( "maxLeft = "+maxLeft+"; maxRight = "+maxRight );
//      return BlendingAction.createBlendContext(
//        AbstractApplication.getApplication().getUserPrefs().node( BlendingAction.DEFAULT_NODE ),
//        doc.timeline.getRate(), maxLeft, maxRight );
//    }
//  }
 
  public BlendContext createBlendContext( long maxLeft, long maxRight, boolean hasSelectedAudio )
  {
    if( !hasSelectedAudio || ((maxLeft == 0L) && (maxRight == 0L)) ) {
      return null;
    } else {
      return blending.createBlendContext( maxLeft, maxRight );
    }
  }

  protected void discardEditsAndClipboard()
  {
    undo.discardAllEdits();
//    ClipboardTrackList.checkDispose( AbstractApplication.getApplication().getClipboard() );
    ClipboardTrackList.disposeAll( this );
  }

  // ------------- OSCRouter interface -------------
 
  public String oscGetPathComponent()
  {
    return null// special schoko in doc handler
  }

  public void oscRoute( RoutedOSCMessage rom )
  {
    osc.oscRoute( rom );
  }
 
  public void oscAddRouter( OSCRouter subRouter )
  {
    if( osc != null ) osc.oscAddRouter( subRouter );
  }

  public void oscRemoveRouter( OSCRouter subRouter )
  {
    if( osc != null ) osc.oscRemoveRouter( subRouter );
  }

//  public ProcessingThread procSave( String name, Span span, AudioFileDescr[] afds, boolean asCopy )
//  {
//    return actionSave.initiate( name, span, afds, asCopy );
//  }
 
  public void oscCmd_close( RoutedOSCMessage rom )
  {
    if( frame == null ) {
      OSCRoot.failed( rom.msg, getResourceString( "errWindowNotFound" ));
    }
 
    final ProcessingThread  proc;
    final boolean      force;

    try {
      if( rom.msg.getArgCount() > 1 ) {
        force = ((Number) rom.msg.getArg( 1 )).intValue() != 0;
      } else {
        force = false;
      }
      proc = closeDocument( force, new Flag( false ));
      if( proc != null ) start( proc );
    }
    catch( IndexOutOfBoundsException e1 ) {
      OSCRoot.failedArgCount( rom );
      return;
    }
    catch( ClassCastException e1 ) {
      OSCRoot.failedArgType( rom, 1 );
    }
  }

  public void oscCmd_activate( RoutedOSCMessage rom )
  {
    if( frame == null ) {
      OSCRoot.failed( rom.msg, getResourceString( "errWindowNotFound" ));
    }
 
    frame.setVisible( true );
    frame.toFront();
//    frame.requestFocus();
  }

  public void oscCmd_cut( RoutedOSCMessage rom )
  {
    actionCut.perform();
  }
 
  public void oscCmd_copy( RoutedOSCMessage rom )
  {
    actionCopy.perform();
  }
 
  public void oscCmd_paste( RoutedOSCMessage rom )
  {
    actionPaste.perform();
  }

  public void oscCmd_delete( RoutedOSCMessage rom )
  {
    actionDelete.perform();
  }

  /**
   *  "insertSilence", <numFrames>
   */
  public void oscCmd_insertSilence( RoutedOSCMessage rom )
  {
    final long        pos, numFrames;
    final ProcessingThread  proc;
    int            argIdx  = 1;
 
//    if( !bird.attemptShared( DOOR_TIME, 250 )) return;
    try {
      pos      = timeline.getPosition();
      numFrames  = Math.max( 0, Math.min( timeline.getLength() - pos, ((Number) rom.msg.getArg( argIdx )).longValue() ));
    }
    catch( IndexOutOfBoundsException e1 ) {
      OSCRoot.failedArgCount( rom );
      return;
    }
    catch( ClassCastException e1 ) {
      OSCRoot.failedArgType( rom, argIdx );
      return;
    }
//    finally {
//      bird.releaseExclusive( Session.DOOR_TIME );
//    }
   
    proc = actionSilence.initiate( pos, numFrames );
    if( proc != null ) start( proc );
  }

  public void oscCmd_trim( RoutedOSCMessage rom )
  {
    actionTrim.perform();
  }
 
  /**
   *  "editMode", <modeName>
   */
  public void oscCmd_editMode( RoutedOSCMessage rom )
  {
    final String  mode;
    int        argIdx  = 1;
 
    try {
      mode = rom.msg.getArg( argIdx ).toString();
      for( int i = 0; i < EDITMODES.length; i++ ) {
        if( EDITMODES[ i ].equals( mode )) {
          setEditMode( i );
          break;
        }
      }
    }
    catch( IndexOutOfBoundsException e1 ) {
      OSCRoot.failedArgCount( rom );
      return;
    }
    catch( ClassCastException e1 ) {
      OSCRoot.failedArgType( rom, argIdx );
      return;
    }
  }

  /**
   *  Replaces the currently selected span with
   *  the contents of a given audio file, applying
   *  blending if activated.
   *
   *  "replace", <fileName>[, <fileOffset> ]
   *
   *  @todo  XXX FilterDialog should return a ProcessingThread, so we can properly close the file after pasting
   */
  public void oscCmd_replace( RoutedOSCMessage rom )
  {
    final RenderPlugIn  plugIn;
    final String    fileName;
    final long      startFrame;
    AudioFile      af    = null;
    int          argIdx  = 1;
    FilterDialog    filterDlg;
 
    try {
      fileName  = rom.msg.getArg( argIdx ).toString();
      argIdx++;
      if( argIdx < rom.msg.getArgCount() ) {
        startFrame  = ((Number) rom.msg.getArg( argIdx )).longValue();
      } else {
        startFrame  = 0;
      }
      af      = AudioFile.openAsRead( new File( fileName ));
      af.seekFrame( startFrame );
      plugIn    = new Replace( af );
    }
    catch( IndexOutOfBoundsException e1 ) {
      OSCRoot.failedArgCount( rom );
      return;
    }
    catch( ClassCastException e1 ) {
      OSCRoot.failedArgType( rom, argIdx );
      return;
    }
    catch( IOException e1 ) {
      if( af != null ) af.cleanUp();
      OSCRoot.failed( rom, e1 );
      return;
    }
   
    filterDlg = (FilterDialog) AbstractApplication.getApplication().getComponent( Main.COMP_FILTER );
   
    if( filterDlg == null ) {
      filterDlg = new FilterDialog();
    }
    filterDlg.process( plugIn, this, false, false );
//    actionProcessAgain.setPlugIn( filterDlg.getPlugIn() );
  }

  public Object oscQuery_id()
  {
    return new Integer( getNodeID() );
  }

  public Object oscQuery_dirty()
  {
    return new Integer( isDirty() ? 1 : 0 );
  }
 
  public Object oscQuery_editMode()
  {
    return EDITMODES[ getEditMode() ];
  }
 
  public Object oscQuery_name()
  {
    return getName();
  }
 
  public Object oscQuery_file()
  {
    final StringBuffer sb = new StringBuffer();
    for( int i = 0; i < afds.length; i++ ) {
      if( i > 0 ) sb.append( File.pathSeparator );
      sb.appendafds[ i ].file.getAbsolutePath() );
    }
    return sb.toString();
  }

// ---------------- Document interface ----------------

  public de.sciss.app.Application getApplication()
  {
    return AbstractApplication.getApplication();
  }

  public de.sciss.app.UndoManager getUndoManager()
  {
    return undo;
  }

  public void dispose()
  {
//System.err.println( "Session.dispose(). hashCode = "+hashCode()+"; frame hash = "+(frame == null ? 0 : frame.hashCode()) );
    discardEditsAndClipboard();
   
    if( osc != null ) {
      osc.remove();
//      osc = null;
    }
 
    if( transport != null ) {
      transport.quit();
      transport.dispose();
      transport = null;
    }
    if( frame != null ) {
//      frame.setVisible( false );
      frame.dispose();
      frame = null;
    }
    if( at != null ) {
      at.dispose();
//      mte.clear( null );
      at = null;
    }
  }

  public boolean isDirty()
  {
    return dirty;
  }

  public void setDirty( boolean dirty )
  {
    if( !this.dirty == dirty ) {
      this.dirty = dirty;
      updateTitle();
    }
  }
 
// ------------------ internal classes ------------------
 
  private class ActionSave
  implements ProcessingThread.Client
  {
    protected ActionSave() { /* empty */ }
   
    /**
     *  Initiate the save process.
     *  Transport is stopped before, if it was running.
     *  On success, undo history is purged and
     *  <code>setModified</code> and <code>updateTitle</code>
     *  are called, and the file is added to
     *  the Open-Recent menu. Note that returned
     *  process has not yet been started, as to allow
     *  other objects to add listeners. So it's the
     *  job of the caller to invoke the processing thread's
     *  <code>start</code> method.
     *
     *  @synchronization  this method is to be called in the event thread
     */
    protected ProcessingThread initiate( String procName, Span span, AudioFileDescr[] descrs,
                       int[] channelMap, boolean saveMarkers, boolean asCopy )
    {
      final ProcessingThread proc;
   
      getTransport().stop();
      if( !checkProcess() ) return null;
     
//      pt        = new ProcessingThread( this, getFrame(), bird, name, args, Session.DOOR_ALL );
      proc        = new ProcessingThread( this, getFrame(), procName );
      proc.putClientArg( "afds", descrs );
      proc.putClientArg( "doc", Session.this );
      proc.putClientArg( "asCopy", new Boolean( asCopy ));
      proc.putClientArg( "chanMap", channelMap );
      proc.putClientArg( "markers", new Boolean( saveMarkers ));
      proc.putClientArg( "span", span == null ? new Span( 0, timeline.getLength() ) : span );
      return proc;
    }

    /**
     *  - wenn das audio file format marker unterstuetzt, kopiere die marker in das erste file
     *  - fuer jedes file: wenn ein file gleichen namens existiert, erzeuge zunaechst ein temporaeres file
     *  - oeffne alle files zum schreiben (AudioFile.openAsWrite)
     *  - schreibe alle files (at.flatten)
     *  - liefere ein array aller geschriebener files in client arg "afs"
     */
    public int processRun( ProcessingThread context )
    throws IOException
    {
      final AudioFileDescr[]      clientAFDs  = (AudioFileDescr[]) context.getClientArg( "afds" );
      final int            numFiles  = clientAFDs.length;
      final Session          doc      = (Session) context.getClientArg( "doc" );
      final boolean          saveMarkers  = ((Boolean) context.getClientArg( "markers" )).booleanValue();
      final Span            span    = (Span) context.getClientArg( "span" );
      final int[]            channelMap  = (int[]) context.getClientArg( "chanMap" );
      final AudioTrail        audioTrail  = doc.getAudioTrail();
//      final File[]          tempFs    = new File[ numFiles ];
//      final boolean[]          renamed    = new boolean[ numFiles ];
      final AudioFile[]        afs      = new AudioFile[ numFiles ];
      AudioFileDescr          afdTemp;
      File              tempF;
     
      context.putClientArg( "afs", afs );

      if( saveMarkers ) {
        if( clientAFDs[ 0 ].isPropertySupported( AudioFileDescr.KEY_MARKERS )) {
          doc.markers.copyToAudioFile( clientAFDs[ 0 ], span )// XXX
        } else if( !doc.markers.isEmpty() ) {
          System.err.println( "WARNING: markers are not saved in this file format!!!" );
        }
      } else { // WARNING: we must clear KEY_MARKERS, it might contain copied data!
        clientAFDs[ 0 ].setProperty( AudioFileDescr.KEY_MARKERS, null );
      }
      for( int i = 0; i < numFiles; i++ ) {
        if( clientAFDs[ i ].file.exists() ) {
//            tempFs[ i ]      = File.createTempFile( "eis", null, afds[ i ].file.getParentFile() );
//            tempFs[ i ].delete();
          tempF        = File.createTempFile( "eis", null, clientAFDs[ i ].file.getParentFile() );
          afdTemp        = new AudioFileDescr( clientAFDs[ i ]);
//            afdTemp.file    = tempFs[ i ];
          afdTemp.file    = tempF;
//            renamed[ i ]    = true;
          afs[ i ]      = AudioFile.openAsWrite( afdTemp );
        } else {
          afs[ i ]      = AudioFile.openAsWrite( clientAFDs[ i ]);
        }
      }
     
      audioTrail.flatten( afs, span, channelMap );
      return DONE;
    } // run

    public void processFinished( ProcessingThread context )
    {
      final AudioFileDescr[]      clientAFDs  = (AudioFileDescr[]) context.getClientArg( "afds" );
      final AudioFile[]        afs      = (AudioFile[]) context.getClientArg( "afs" );
      final Session          doc      = (Session) context.getClientArg( "doc" );
      final boolean          asCopy    = ((Boolean) context.getClientArg( "asCopy" )).booleanValue();
      File              tempF;
         
      if( context.getReturnCode() == DONE ) {  // ------------------------------- DONE -------------------------------
        if( asCopy ) {            // ............................... asCopy ...............................
          for( int i = 0; i < afs.length; i++ ) {
            try {
              afs[ i ].close();
            } catch( IOException e1 ) {
              System.err.println( "File '" + afs[ i ].getFile().getName() + "' could not be closed ("+
                e1.getClass().getName() + " : " + e1.getLocalizedMessage() + ")" );
            }
            if( !clientAFDs[ i ].file.equals( afs[ i ].getFile() )) {
              if( clientAFDs[ i ].file.delete() ) {
                if( !afs[ i ].getFile().renameTo( clientAFDs[ i ].file )) {
                  System.err.println( "Newly saved file '" + afs[ i ].getFile().getName() + "' "+
                    "could not be renamed!" );
                }
              } else {
                System.err.println( "Previous file '" +clientAFDs[ i ].file.getAbsolutePath() + "' "+
                  "could not be deleted! Newly saved file is '" + afs[ i ].getFile().getName() + "'!" );
              }
            }
          }
        } else {              // ............................... replace ...............................
//          doc.getUndoManager().discardAllEdits();
          doc.discardEditsAndClipboard();
//          at.clear( null );
          try {
            at.closeAll();
          } catch( IOException e1 ) {
            System.err.println( "Previous audio files could not be closed ("+
              e1.getClass().getName() + " : " + e1.getLocalizedMessage() + ")" );
          }
          for( int i = 0; i < afs.length; i++ ) {
            final File f;
            try {
              afs[ i ].close();
            } catch( IOException e1 ) {
              System.err.println( "File '" + afs[ i ].getFile().getName() + "' could not be closed ("+
                e1.getClass().getName() + " : " + e1.getLocalizedMessage() + ")" );
            }
            if( clientAFDs[ i ].file.equals( afs[ i ].getFile() )) {
              f = afs[ i ].getFile();
            } else {
              if( clientAFDs[ i ].file.delete() ) {
                if( afs[ i ].getFile().renameTo( clientAFDs[ i ].file )) {
                  f = clientAFDs[ i ].file;
                } else {
                  System.err.println( "New current working file '" + afs[ i ].getFile().getName() + "' "+
                    "could not be renamed!" );
                  f = afs[ i ].getFile();
                }
              } else
tryRename:            {
                try {
                  tempF = File.createTempFile( "eis", null, clientAFDs[ i ].file.getParentFile() );
                } catch( IOException e1 ) {
                  System.err.println( "Previous file '" + clientAFDs[ i ].file.getAbsolutePath() + "' could neither be " +
                    "deleted nor renamed. New current working file is '" + afs[ i ].getFile().getName() + "'!" );
                  f = afs[ i ].getFile();
                  break tryRename;
                }
                if( clientAFDs[ i ].file.renameTo( tempF )) {
                  System.err.println( "Previous file '" + clientAFDs[ i ].file.getAbsolutePath() + "' could not be " +
                    "deleted. It was renamed to '" + tempF.getName() + "'!" );
                  if( afs[ i ].getFile().renameTo( clientAFDs[ i ].file )) {
                    f = clientAFDs[ i ].file;
                  } else {
                    System.err.println( "New current working file '" + afs[ i ].getFile().getName() + "' "+
                      "could not be renamed!" );
                    f = afs[ i ].getFile();
                  }
                } else {
                  System.err.println( "Previous file '" + clientAFDs[ i ].file.getAbsolutePath() + "' could neither be " +
                    "deleted nor renamed. New current working file is '" + afs[ i ].getFile().getName() + "'!" );
                  f = afs[ i ].getFile();
                }
              }
            }
            try {
              afs[ i = AudioFile.openAsRead( f );
            } catch( IOException e1 ) {
              System.err.println( "File '" + f.getName() + "' could not be opened ("+
                e1.getClass().getName() + " : " + e1.getLocalizedMessage() + ")" );
            }
            clientAFDs[ i = afs[ i ].getDescr();
          }
          try {
            at.exchange( afs );
          } catch( IOException e1 ) {
            System.err.println( "Audio files could not be exchanged ("+
              e1.getClass().getName() + " : " + e1.getLocalizedMessage() + ")" );
          }
          doc.setDescr( clientAFDs );
        }
      } else // ------------------------------- FAILED or CANCELLED -------------------------------
        if( afs != null ) {
          for( int i = 0; i < afs.length; i++ ) {
            if( afs[ i ] != null ) {
              afs[ i ].cleanUp();
              if( !afs[ i ].getFile().delete() ) {
                System.err.println( "The file '" + afs[ i ].getFile().getAbsolutePath() + "' "+
                          "(created during saving) could not be deleted!" );
              }
            }
          }
        }
      }
    }
   
    public void processCancel( ProcessingThread context ) { /* ignored */ }
  }
 
  private class ActionCut
  extends MenuAction
  {
    protected ActionCut() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      perform();
    }
   
    protected void perform()
    {
      final ProcessingThread proc; // = null;
     
      if( actionCopy.perform() ) {
//        if( !bird.attemptShared( Session.DOOR_TIME | Session.DOOR_MTE )) return;
//        try {
          proc = procDelete( getValue( NAME ).toString(), timeline.getSelectionSpan(), getEditMode() );
//        }
//        finally {
//          bird.releaseShared( Session.DOOR_TIME | Session.DOOR_MTE );
//        }
        if( proc != null ) start( proc );
      }
    }
  }

  private class ActionCopy
  extends MenuAction
  {
    protected ActionCopy() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      perform();
    }

    protected ClipboardTrackList getSelectionAsTrackList()
    {
      final Span span;
     
//      if( !bird.attemptShared( Session.DOOR_TIME | Session.DOOR_TRACKS, 250 )) return null;
//      try {
        span = timeline.getSelectionSpan();
        if( span.isEmpty() ) return null;

        return new ClipboardTrackList( Session.this );
//      }
//      finally {
//        bird.releaseShared( Session.DOOR_TIME | Session.DOOR_TRACKS );
//      }
    }

    protected boolean perform()
    {
      boolean            success  = false;
      final ClipboardTrackList  tl    = getSelectionAsTrackList();

      if( tl == null ) return success;

      try {
        AbstractApplication.getApplication().getClipboard().setContents( tl, tl );
        success = true;
      }
      catch( IllegalStateException e1 ) {
        System.err.println( getResourceString( "errClipboard" ));
      }

      return success;
    }
  }
 
  private class ActionPaste
  extends MenuAction
  implements ProcessingThread.Client
  {
    protected ActionPaste() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      perform();
    }
   
    protected void perform()
    {
      perform( getValue( NAME ).toString(), getEditMode() );
    }
   
    private void perform( String procName, int mode )
    {
      final Transferable      t;
      final ClipboardTrackList  tl;

      try {
        t = AbstractApplication.getApplication().getClipboard().getContents( this );
        if( t == null ) return;
       
        if( !t.isDataFlavorSupported( ClipboardTrackList.trackListFlavor )) return;
        tl = (ClipboardTrackList) t.getTransferData( ClipboardTrackList.trackListFlavor );
      }
      catch( IOException e11 ) {
        System.err.println( e11.getLocalizedMessage() );
        return;
      }
      catch( UnsupportedFlavorException e11 ) {
        System.err.println( e11.getLocalizedMessage() );
        return;
      }
      catch( IllegalStateException e11 ) {
        System.err.println( getResourceString( "errClipboard" ));
        return;
      }
     
      if( !checkProcess() ) return;
      final ProcessingThread proc = initiate( tl, timeline.getPosition(), procName, mode )// XXX sync
      if( proc != null ) {
        start( proc );
      }
    }

    protected ProcessingThread initiate( ClipboardTrackList tl, long insertPos, String procName, int mode )
    {
      if( !checkProcess() ) return null;
     
      if( (insertPos < 0) || (insertPos > timeline.getLength()) ) throw new IllegalArgumentException( String.valueOf( insertPos ));
     
      final ProcessingThread    proc;
      final Span          oldSelSpan, insertSpan, copySpan, cutTimelineSpan;
      final AbstractCompoundEdit  edit;
      final Flag          hasSelectedAudio;
      final List          tis;
      final boolean        expTimeline, cutTimeline;
      final long          docLength, pasteLength, preMaxLen, postMaxLen;
      final BlendContext      bcPre, bcPost;
     
      hasSelectedAudio  = new Flag( false );
      tis          = Track.getInfos( selectedTracks.getAll(), tracks.getAll() );
      if( !AudioTracks.checkSyncedAudio( tis, mode == EDIT_INSERT, null, hasSelectedAudio )) return null;

      expTimeline      = (mode == EDIT_INSERT) && hasSelectedAudio.isSet();
      docLength      = timeline.getLength();
      pasteLength      = expTimeline ? tl.getSpan().getLength() :
        Math.min( tl.getSpan().getLength(), docLength - insertPos );
      if( pasteLength == 0 ) return null;
     
      if( mode == EDIT_INSERT ) {
        /*
         *  before paste:
         *
         *   maxRight / post   maxLeft / pre
         *
         *  |                 |              |
         *  |                 |              |
         *  |                 |              |
         *  |        A        |     B        |
         *  +-----------------+--------------+
         *                    |
         *                 insertPos
         *
         *  after paste:
         *
         *  |                 | B #$$$$# A |              |
         *  |                 |  ##$$$$##  |              |
         *  |                 | ###$$$$### |              |
         *  |        A        |####$$$$####|      B       |
         *  +-----------------+------------+--------------+
         *                    |
         *                 insertPos
         */
        // note: now the discrepancy between postMaxLen and preMaxLen is
        // limited to 100%, so pasting at the very end or beginning of
        // a doc will not produce a single sided xfade any more
        // (answering bug 1922862)
        if( insertPos < (docLength - insertPos) ) {
          postMaxLen  = Math.min( insertPos, pasteLength >> 1 );
//          preMaxLen  = Math.min( docLength - insertPos, pasteLength - postMaxLen );
          preMaxLen  = Math.min( postMaxLen << 1, Math.min( docLength - insertPos, pasteLength - postMaxLen ));
//System.out.println( "A" );
        } else {
          preMaxLen  = Math.min( docLength - insertPos, pasteLength >> 1 );
          postMaxLen  = Math.min( preMaxLen << 1, Math.min( insertPos, pasteLength - preMaxLen ));
//System.out.println( "B" );
        }
      } else {
        preMaxLen  = pasteLength >> 1// note: pasteLength already clipped to be <= docLength - insertPos !
        postMaxLen  = pasteLength - preMaxLen;
//System.out.println( "C" );
      }
      bcPre      = createBlendContext( preMaxLen, 0, hasSelectedAudio.isSet() );
      bcPost      = createBlendContext( postMaxLen, 0, hasSelectedAudio.isSet() );
//System.out.println( "D ; preMaxLen = " + preMaxLen + "; postMaxLen = " + postMaxLen + "; bcPre.getLeftLen() = " + (bcPre == null ? null : String.valueOf( bcPre.getLeftLen())) + "; bcPre.getRightLen() = " + (bcPre == null ? null : String.valueOf( bcPre.getRightLen() )) + "; bcPost.getLeftLen() = " + (bcPost == null ? null : String.valueOf( bcPost.getLeftLen() )) + "; bcPost.getRightLen() = " + (bcPost == null ? null : String.valueOf( bcPost.getRightLen() )));

//      if( bcPre != null )  System.out.println( "bcPre  : " + bcPre.getLen() + ", " + bcPre.getLeftLen() + ", "+ bcPre.getRightLen() );
//      if( bcPost != null ) System.out.println( "bcPost : " + bcPost.getLen() + ", " + bcPost.getLeftLen() + ", "+ bcPost.getRightLen() );
     
      insertSpan      = new Span( insertPos, insertPos + pasteLength );
      copySpan      = new Span( tl.getSpan().start, tl.getSpan().start + pasteLength );
      cutTimeline      = (mode == EDIT_INSERT) && !hasSelectedAudio.isSet();
      cutTimelineSpan    = cutTimeline ? new Span( docLength, docLength + pasteLength ) : null;
     
      edit      = new BasicCompoundEdit( procName );
      oldSelSpan    = timeline.getSelectionSpan();
      if( !oldSelSpan.isEmpty() ) { // deselect
        edit.addPerform( TimelineVisualEdit.select( this, Session.this, new Span() ));
      }

      proc  = new ProcessingThread( this, getFrame(), procName );
      proc.putClientArg( "tl", tl );
      proc.putClientArg( "pos", new Long( insertPos ));
      proc.putClientArg( "mode", new Integer( mode ));
      proc.putClientArg( "tis", tis );
      proc.putClientArg( "pasteLen", new Long( pasteLength ));
      proc.putClientArg( "exp", new Boolean( expTimeline ));
      proc.putClientArg( "bcPre", bcPre );
      proc.putClientArg( "bcPost", bcPost );
      proc.putClientArg( "insertSpan", insertSpan );
      proc.putClientArg( "copySpan", copySpan );
      proc.putClientArg( "cut", new Boolean( cutTimeline ));
      proc.putClientArg( "cutSpan", cutTimelineSpan );
      proc.putClientArg( "edit", edit );

      return proc;
    }
   
    // --------- ProcessingThread.Client interface ---------

    /**
     *  This method is called by ProcessingThread
     */
    public int processRun( ProcessingThread context )
    throws IOException
    {
      final ClipboardTrackList    tl          = (ClipboardTrackList) context.getClientArg( "tl" );
      final long            insertPos      = ((Long) context.getClientArg( "pos" )).longValue();
      final int            mode        = ((Integer) context.getClientArg( "mode" )).intValue();
      final List            tis          = (List) context.getClientArg( "tis" );
      final AbstractCompoundEdit    edit        = (AbstractCompoundEdit) context.getClientArg( "edit" );
      final BlendContext        bcPre        = (BlendContext) context.getClientArg( "bcPre" );
      final BlendContext        bcPost        = (BlendContext) context.getClientArg( "bcPost" );
      final Span            insertSpan      = (Span) context.getClientArg( "insertSpan" );
      final Span            copySpan      = (Span) context.getClientArg( "copySpan" );
      final boolean          cutTimeline      = ((Boolean) context.getClientArg( "cut" )).booleanValue();
      final Span            cutTimelineSpan    = (Span) context.getClientArg( "cutSpan" );
      final long            delta        = insertPos - tl.getSpan().start;
      Track.Info            ti;
      Trail              srcTrail;
      AudioTrail            audioTrail;
      boolean[]            trackMap;
      boolean              isAudio, pasteAudio;

      for( int i = 0; i < tis.size(); i++ ) {
        ti    = (Track.Info) tis.get( i );
        if( ti.selected ) {  // ----------------- selected tracks -----------------
          try {
            ti.trail.editBegin( edit );
            isAudio  = ti.trail instanceof AudioTrail;
            srcTrail = tl.getSubTrail( ti.trail.getClass() );
         
            if( isAudio ) {
              pasteAudio = (srcTrail != null) && (((AudioTrail) srcTrail).getChannelNum() > 0);
            } else {
              pasteAudio = false;
            }
           
            if( mode == EDIT_INSERT ) {
              ti.trail.editInsert( this, insertSpan, edit );
              if( cutTimeline ) ti.trail.editRemove( this, cutTimelineSpan, edit );
//              } else if( (mode == EDIT_OVERWRITE) && (pasteAudio || !isAudio) ) { // Audio needs to be cleared even in Mix mode!
            } else if( pasteAudio || ((mode == EDIT_OVERWRITE) && !isAudio) ) { // Audio needs to be cleared even in Mix mode!
              ti.trail.editClear( this, insertSpan, edit );
            }
           
            if( pasteAudio ) {
              audioTrail      = (AudioTrail) ti.trail;
              trackMap  = tl.getTrackMap( ti.trail.getClass() );
             
//System.err.println( "clipboard tm : " );
//for( int x = 0; x < trackMap.length; x++ ) { System.err.println( "  " + trackMap[ x ]); }
              int[] trackMap2 = new int[ audioTrail.getChannelNum() ];
              for( int j = 0, k = 0; j < trackMap2.length; j++ ) {
                if( ti.trackMap[ j ]) {  // target track selected
                  for( ; (k < trackMap.length) && !trackMap[ k ] ; k++ ) ;
                  if( k < trackMap.length ) {  // source track exiting
                    trackMap2[ j ] = k++;
                  } else if( tl.getTrackNum( ti.trail.getClass() ) > 0 ) {    // ran out of source tracks, fold over (simple mono -> stereo par exemple)
                    for( k = 0; !trackMap[ k ] ; k++ ) ;
                    trackMap2[ j ] = k++;
                  } else {
                    trackMap2[ j ] = -1;    // there aren't any clipboard tracks ....
                  }
                } else {              // target track not selected
                  trackMap2[ j ] = -1;
                }
              }
              if( !audioTrail.copyRangeFrom( (AudioTrail) srcTrail, copySpan, insertPos, mode, this, edit, trackMap2, bcPre, bcPost )) return CANCELLED;

            } else if( (ti.numTracks == 1) && (tl.getTrackNum( ti.trail.getClass() ) == 1) ) {
              ti.trail.editAddAll( this, srcTrail.getCuttedRange(
                copySpan, true, srcTrail.getDefaultTouchMode(), delta ), edit );
            }
          }
          finally {
            ti.trail.editEnd( edit );
          }
        }
      }

      return DONE;
    }

    public void processFinished( ProcessingThread context )
    {
      final ProcessingThread.Client  doneAction  = (ProcessingThread.Client) context.getClientArg( "doneAction" );
      final AbstractCompoundEdit    edit    = (AbstractCompoundEdit) context.getClientArg( "edit" );
      final boolean          expTimeline  = ((Boolean) context.getClientArg( "exp" )).booleanValue();
      final long            pasteLength  = ((Long) context.getClientArg( "pasteLen" )).longValue();
      final Span            insertSpan  = (Span) context.getClientArg( "insertSpan" );
     
      if( (context.getReturnCode() == DONE) ) {
        if( expTimeline && (pasteLength != 0) ) {  // adjust timeline
          edit.addPerform( new EditSetTimelineLength( this, Session.this, timeline.getLength() + pasteLength ));
          if( timeline.getVisibleSpan().isEmpty() ) {
            edit.addPerform( TimelineVisualEdit.scroll( this, Session.this, insertSpan ));
          }
        }
        if( !insertSpan.isEmpty() ) {
          edit.addPerform( TimelineVisualEdit.select( this, Session.this, insertSpan ));
          edit.addPerform( TimelineVisualEdit.position( this, Session.this, insertSpan.stop ));
        }

        edit.perform();
        edit.end();
        getUndoManager().addEdit( edit );
      } else {
        edit.cancel();
      }

//      if( doneAction != null ) doneAction.processFinished( context, doc );
      if( doneAction != null ) doneAction.processFinished( context );
    }

    // mte will check pt.shouldCancel() itself
    public void processCancel( ProcessingThread context ) { /* ignored */ }
  } // class actionPasteClass

  /**
   *  @todo  when a cutted region spans entire view,
   *      selecting undo results in empty visible span
   */
  private class ActionDelete
  extends MenuAction
  implements ProcessingThread.Client
  {
    protected ActionDelete() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      perform();
    }
   
    protected void perform()
    {   
      final Span        span  = timeline.getSelectionSpan(); // XXX sync
      if( span.isEmpty() ) return;
     
      final ProcessingThread  proc    = initiate( getValue( NAME ).toString(), span, getEditMode() );
      if( proc != null ) start( proc );
    }
   
    // XXX sync
    protected ProcessingThread initiate( String procName, Span span, int mode )
    {
      if( !checkProcess() ) return null;

      final BlendContext      bc;
      final long          cutLength, docLength, newDocLength, maxLen;
      final Flag          hasSelectedAudio;
      final List          tis;
      final AbstractCompoundEdit  edit;
      final boolean         cutTimeline;
      final Span          cutTimelineSpan, selSpan;
      Span            visiSpan;

      hasSelectedAudio  = new Flag( false );
      tis          = Track.getInfos( selectedTracks.getAll(), tracks.getAll() );
      if( !AudioTracks.checkSyncedAudio( tis, mode == EDIT_INSERT, null, hasSelectedAudio )) return null;
     
      docLength      = timeline.getLength();
      cutLength      = span.getLength();
      if( mode == EDIT_INSERT ) {
        /*
         *  before delete:
         *
         *  |,,,,,,,,,,,,,,,,,|$$$$$$$#######|............|
         *  |,,,,,,,,,,,,,,,,,|$$$$$$$#######|............|
         *  |,,,,,,,,,,,,,,,,,|$$$$$$$#######|............|
         *  |,,,,,,,,A,,,,,,,,|$$B1$$$###B2##|......C.....|
         *  +-----------------+--------------+------------+
         *                    |     span     |
         *
         *  after delete:
         *                left right
         *  |,,,,,,,,,,,,,    |            |
         *  |,,,,,,,,,,,,,,,  |            |
         *  |,,,,,,,,,,,,,,,,,|            |
         *  |,,,,,,,,,,,,,,,,,|$$          |
         *  |,,,,,,,,,,,,,,,,,|$$$$        |
         *  |,,,,,,,,A,,,,,,,,|$B2$$$      |
         *  +-----------------+------------+
         *                    |
         *      plus
         *  |                 |    ........|
         *  |                 |  ..........|
         *  |                 |............|
         *  |               ##|............|
         *  |             ####|............|
         *  |           ###B2#|......C.....|
         *  +-----------------+------------+
         *                    |
         *                span.start
         */
        maxLen        = Math.min( cutLength, Math.min( span.start, docLength - span.stop ) << 1 );
        bc          = createBlendContext( maxLen >> 1, (maxLen + 1) >> 1, hasSelectedAudio.isSet() );
      } else {
        /*
         *  after delete:
         *                     blend-   blend-
         *                     Len      Len
         *  |,,,,,,,,,,,,,,,,,|$            #|............|
         *  |,,,,,,,,,,,,,,,,,|$$          ##|............|
         *  |,,,,,,,,,,,,,,,,,|$$$        ###|............|
         *  |,,,,,,,,A,,,,,,,,|$B1$      #B2#|......C.....|
         *  +-----------------+--------------+------------+
         *                    |     span     |
         */
        maxLen        = cutLength >> 1;
        bc          = createBlendContext( maxLen, 0, hasSelectedAudio.isSet() );
      }
//      bc          = createBlendContext( Math.min( cutLength, span.start ), Math.min( cutLength, docLength - span.stop ), hasSelectedAudio );
      edit        = new BasicCompoundEdit( procName );

//      if( bc != null )  System.out.println( "bc  : " + bc.getLen() + ", " + bc.getLeftLen() + ", "+ bc.getRightLen() );
     
      cutTimeline      = (mode == EDIT_INSERT) && hasSelectedAudio.isSet();
      newDocLength    = cutTimeline ? docLength - cutLength : docLength;
      cutTimelineSpan    = cutTimeline ? new Span( newDocLength, docLength ) : null;
      selSpan        = timeline.getSelectionSpan();
     
      if( (mode == EDIT_INSERT) && !selSpan.isEmpty() ) {
        edit.addPerform( TimelineVisualEdit.position( this, Session.this, span.start ));
        edit.addPerform( TimelineVisualEdit.select( this, Session.this, new Span() ));
      }
      if( cutTimeline ) {
        visiSpan = timeline.getVisibleSpan();
        if( visiSpan.stop > span.start ) {
          if( visiSpan.stop > newDocLength ) {
            visiSpan = new Span( Math.max( 0, newDocLength - visiSpan.getLength() ), newDocLength );
            TimelineVisualEdit tve = TimelineVisualEdit.scroll( this, Session.this, visiSpan );
            edit.addPerform( tve );
          } // else visiSpan untouched
        }
        edit.addPerform( new EditSetTimelineLength( this, Session.this, newDocLength ));
      }

      final ProcessingThread proc = new ProcessingThread( this, getFrame(), procName );
      proc.putClientArg( "span", span );
      proc.putClientArg( "mode", new Integer( mode ));
      proc.putClientArg( "tis", tis );
      proc.putClientArg( "edit", edit );
      proc.putClientArg( "bc", bc );
      proc.putClientArg( "cut", new Boolean( cutTimeline ));
      proc.putClientArg( "cutSpan", cutTimelineSpan );
      return proc;
    }

    // --------- ProcessingThread.Client interface ---------
   
    /**
     *  This method is called by ProcessingThread
     */
    public int processRun( ProcessingThread context )
    throws IOException
    {
      final Span            span        = (Span) context.getClientArg( "span" );
      final int            mode        = ((Integer) context.getClientArg( "mode" )).intValue();
      final List            tis          = (List) context.getClientArg( "tis" );
      final AbstractCompoundEdit    edit        = (AbstractCompoundEdit) context.getClientArg( "edit" );
      final BlendContext        bc          = (BlendContext) context.getClientArg( "bc" );
      final long            left        = bc == null ? 0L : bc.getLeftLen();
      final long            right        = bc == null ? 0L : bc.getRightLen();
      final boolean          cutTimeline      = ((Boolean) context.getClientArg( "cut" )).booleanValue();
      final Span            cutTimelineSpan    = (Span) context.getClientArg( "cutSpan" );
      AudioTrail            audioTrail;
      Track.Info            ti;
      boolean              isAudio;

      for( int i = 0; i < tis.size(); i++ ) {
        ti    = (Track.Info) tis.get( i );
        try {
          ti.trail.editBegin( edit );
          isAudio = ti.trail instanceof AudioTrail;
          if( ti.selected ) {
            if( mode == EDIT_INSERT ) {
              if( isAudio ) {
                if( bc == null ) {
                  ti.trail.editRemove( this, span, edit );
                } else {
                  ti.trail.editRemove( this, new Span( span.start - left, span.stop + right ), edit );
                  ti.trail.editInsert( this, new Span( span.start - left, span.start + right ), edit );
                }
                audioTrail = (AudioTrail) ti.trail;
                audioTrail.clearRange( span, EDIT_INSERT, this, edit, ti.trackMap, bc );
              } else {
                ti.trail.editRemove( this, span, edit );
              }
            } else {
              ti.trail.editClear( this, span, edit );
              if( isAudio ) {
                audioTrail = (AudioTrail) ti.trail;
                audioTrail.clearRange( span, EDIT_OVERWRITE, this, edit, ti.trackMap, bc );
              }
            }
          } else if( cutTimeline ) {
            ti.trail.editRemove( this, cutTimelineSpan, edit );
          }
        }
        finally {
          ti.trail.editEnd( edit );
        }
      }
      return DONE;
    } // run

    public void processFinished( ProcessingThread context )
    {
      final AbstractCompoundEdit edit = (AbstractCompoundEdit) context.getClientArg( "edit" );

      if( context.getReturnCode() == DONE ) {
        edit.perform();
        edit.end();
        getUndoManager().addEdit( edit );
      } else {
        edit.cancel();
      }
    }

    // mte will check pt.shouldCancel() itself
    public void processCancel( ProcessingThread context ) { /* ignore */ }
  } // class actionDeleteClass

  private class ActionTrim
  extends MenuAction
  {
    protected ActionTrim() { /* empty */ }

    // performs inplace (no runnable processing) coz it's always fast
    public void actionPerformed( ActionEvent e )
    {
      perform();
    }
   
    protected void perform()
    {
      final Span            selSpan, deleteBefore, deleteAfter;
      final BasicCompoundEdit    edit;
      final List            tis;
      Track.Info            ti;
      boolean              success  = false;

      edit      = new BasicCompoundEdit( getValue( NAME ).toString() );
     
      try {
        selSpan      = timeline.getSelectionSpan();
//        if( selSpan.isEmpty() ) return;
        tis        = Track.getInfos( selectedTracks.getAll(), tracks.getAll() );
        deleteBefore  = new Span( 0, selSpan.start );
        deleteAfter    = new Span( selSpan.stop, timeline.getLength() );

        // deselect
        edit.addPerform( TimelineVisualEdit.select( this, Session.this, new Span() ));
        edit.addPerform( TimelineVisualEdit.position( this, Session.this, 0 ));

        if( !deleteAfter.isEmpty() || !deleteBefore.isEmpty() ) {
          for( int i = 0; i < tis.size(); i++ ) {
            ti = (Track.Info) tis.get( i );
            ti.trail.editBegin( edit );
            try {
              if( !deleteAfter.isEmpty() ) ti.trail.editRemove( this, deleteAfter, edit );
              if!deleteBefore.isEmpty() ) ti.trail.editRemove( this, deleteBefore, edit );
            }
            finally {
              ti.trail.editEnd( edit );
            }
          }
        }

        edit.addPerform( new EditSetTimelineLength( this, Session.this, selSpan.getLength() ));
        edit.addPerform( TimelineVisualEdit.select( this, Session.this, selSpan.shift( -selSpan.start )));

        edit.perform();
        edit.end();
        getUndoManager().addEdit( edit );
        success = true;
      }
      finally {
        if( !success ) edit.cancel();
      }
    }
  } // class actionTrimClass

  /**
   *  @todo  when edit mode != EDIT_INSERT, audio tracks are cleared which should be bypassed and vice versa
   *  @todo  waveform display not automatically updated when edit mode != EDIT_INSERT
   */
  private class ActionSilence
  extends MenuAction
  implements ProcessingThread.Client
  {
    private Param    value = null;
    private ParamSpace  space = null;
 
    protected ActionSilence() { /* empty */ }

    public void actionPerformed( ActionEvent e )
    {
      perform();
    }
   
    private void perform()
    {
      final SpringPanel      msgPane;
      final int          result;
      final ParamField      ggDuration;
      final Param          durationSmps;
      final DefaultUnitTranslator  timeTrans;
     
      msgPane      = new SpringPanel( 4, 2, 4, 2 );
      timeTrans    = new DefaultUnitTranslator();
      ggDuration    = new ParamField( timeTrans );
      ggDuration.addSpace( ParamSpace.spcTimeHHMMSS );
      ggDuration.addSpace( ParamSpace.spcTimeSmps );
      ggDuration.addSpace( ParamSpace.spcTimeMillis );
      ggDuration.addSpace( ParamSpace.spcTimePercentF );
      msgPane.gridAdd( ggDuration, 0, 0 );
      msgPane.makeCompactGrid();
      GUIUtil.setInitialDialogFocus( ggDuration );

      timeTrans.setLengthAndRate( timeline.getLength(), timeline.getRate() );

      if( value == null ) {
        ggDuration.setValue( new Param( 1.0, ParamSpace.TIME | ParamSpace.SECS ));
      } else {
        ggDuration.setSpace( space );
        ggDuration.setValue( value );
      }

      final JOptionPane op = new JOptionPane( msgPane, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION );
//      result = JOptionPane.showOptionDialog( getFrame() == null ? null : getFrame().getWindow(), msgPane, getValue( NAME ).toString(),
//        JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null );
      result = BasicWindowHandler.showDialog( op, getFrame() == null ? null : getFrame().getWindow(), getValue( NAME ).toString() );

      if( result == JOptionPane.OK_OPTION ) {
        value      = ggDuration.getValue();
        space      = ggDuration.getSpace();
        durationSmps  = timeTrans.translate( value, ParamSpace.spcTimeSmps );
        if( durationSmps.val > 0.0 ) {
          final ProcessingThread proc;
         
          proc = initiate( timeline.getPosition(), (long) durationSmps.val );
          if( proc != null ) start( proc );
        }
      }
    }

    public ProcessingThread initiate( long pos, long numFrames )
    {
      if( !checkProcess() || (numFrames == 0) ) return null;
     
      if( numFrames < 0 ) throw new IllegalArgumentException( String.valueOf( numFrames ));
      if( (pos < 0) || (pos > timeline.getLength()) ) throw new IllegalArgumentException( String.valueOf( pos ));

      final ProcessingThread     proc;
      final AbstractCompoundEdit  edit;
      final Span          oldSelSpan, insertSpan;

      proc = new ProcessingThread( this, getFrame(), getValue( NAME ).toString() );

      edit    = new BasicCompoundEdit( proc.getName() );
      oldSelSpan  = timeline.getSelectionSpan();
      insertSpan  = new Span( pos, pos + numFrames );

      if( !oldSelSpan.isEmpty() ) { // deselect
        edit.addPerform( TimelineVisualEdit.select( this, Session.this, new Span() ));
      }

      proc.putClientArg( "tis", Track.getInfos( selectedTracks.getAll(), tracks.getAll() ));
      proc.putClientArg( "edit", edit );
      proc.putClientArg( "span", insertSpan );
      return proc;
    }

    /**
     *  This method is called by ProcessingThread
     */
    public int processRun( ProcessingThread context )
    throws IOException
    {
      final List          tis      = (List) context.getClientArg( "tis" );
      final AbstractCompoundEdit  edit    = (AbstractCompoundEdit) context.getClientArg( "edit" );
      final Span          insertSpan  = (Span) context.getClientArg( "span" );
      Track.Info          ti;
      AudioTrail          audioTrail;

      for( int i = 0; i < tis.size(); i++ ) {
        ti = (Track.Info) tis.get( i );
        ti.trail.editBegin( edit );
        try {
          ti.trail.editInsert( this, insertSpan, edit );
          if( ti.trail instanceof AudioTrail ) {
            audioTrail      = (AudioTrail) ti.trail;             
            audioTrail.editAdd( this, audioTrail.allocSilent( insertSpan ), edit );
          }
        }
        finally {
          ti.trail.editEnd( edit );
        }
      }
      return DONE;
    }

    public void processFinished( ProcessingThread context )
    {
      final AbstractCompoundEdit  edit    = (AbstractCompoundEdit) context.getClientArg( "edit" );
      final Span          insertSpan  = (Span) context.getClientArg( "span" );

      if( context.getReturnCode() == DONE ) {
        if( !insertSpan.isEmpty() ) {  // adjust timeline
          edit.addPerform( new EditSetTimelineLength( this, Session.this, timeline.getLength() + insertSpan.getLength() ));
          if( timeline.getVisibleSpan().isEmpty() ) {
            edit.addPerform( TimelineVisualEdit.scroll( this, Session.this, insertSpan ));
          }
        }
        if( !insertSpan.isEmpty() ) {
          edit.addPerform( TimelineVisualEdit.select( this, Session.this, insertSpan ));
          edit.addPerform( TimelineVisualEdit.position( this, Session.this, insertSpan.stop ));
        }
        edit.perform();
        edit.end();
        getUndoManager().addEdit( edit );
      } else {
        edit.cancel();
      }
    }

    // mte will check pt.shouldCancel() itself
    public void processCancel( ProcessingThread context ) { /* ignore */ }
  } // class actionSilenceClass
}
TOP

Related Classes of de.sciss.eisenkraut.session.Session

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.