Package de.sciss.meloncillo.io

Source Code of de.sciss.meloncillo.io.AudioTrail

/*
*  AudioTrail.java
*  Meloncillo
*
*  Copyright (c) 2004-2008 Hanns Holger Rutz. All rights reserved.
*
*  This software 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, june 1991 of the License, or (at your option) any later version.
*
*  This software 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 (gpl.txt) along with this software; if not, write to the Free Software
*  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*
*
*  For further information, please contact Hanns Holger Rutz at
*  contact@sciss.de
*
*
*  Changelog:
*    22-Dec-05  created from MultirateTrackEditor
*    30-Jun-08  copied from EisK
*/

package de.sciss.meloncillo.io;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import de.sciss.meloncillo.session.Session;

import de.sciss.app.AbstractApplication;
import de.sciss.app.AbstractCompoundEdit;
import de.sciss.common.ProcessingThread;
import de.sciss.io.AudioFile;
import de.sciss.io.AudioFileDescr;
import de.sciss.io.IOUtil;
import de.sciss.io.InterleavedStreamFile;
import de.sciss.io.Span;
import de.sciss.timebased.BasicTrail;
import de.sciss.timebased.Trail;

/**
*  This class provides means for automatic multirate handling
*  of nondestructive nonlinear track editing objects.
*  It wraps a number (currently 7) of track editors representing
*  the same signal at different decimation stages where
*  one file represents fullrate data and each subsampled
*  file decimates the rate by 4. Thus, if fullrate corresponds
*  to 1024 Hz sampling rate, the first subsampled file is
*  a decimation to 256 Hz, the second subsampled file is
*  a decimation to 64 Hz etc. So, using 6 subsampled files
*  goes down to 1/4096th of the fullrate. Taking the unusual
*  case that a user would use audio rate for sense data, say
*  48 kHz, then the lowest resolution subsample file will run
*  at about 12 Hz, so if a GUI element request data for a very
*  long time span, say half an hour, it would have to handle
*  a buffer of 21094 frames; in the more usual case of a sense
*  rate of say 4800 Hz (or less), one hour could still be represented
*  by 4219 frames, thus maintaining low RAM and CPU consumption.
*
@author    Hanns Holger Rutz
@version  0.70, 12-Nov-07
*/
public class AudioTrail
extends BasicTrail
{
//protected static final boolean DEBUG = false;
//
//
//  private final int channels, modelChannels;
//  private final float rate;
//  private final int model;
//  private final int[] decimations;
//
//  // this array of size 'SUBNUM' contains STEs
//  // that keep track of the subsampled versions of
//  // this STE's track data. ste[0] is full rate,
//  // ste[1] is 4x subsampled, ste[2] is 16x subsampled etc.
//  private final DecimatedSampledTrack[] ste;
//  private final int SUBNUM;
//  private final int MAXSHIFT;
//  private final int MAXCOARSE;
//  private final long MAXMASK;
//  private final int MAXCEILADD;
//
//  private final float[][] tmpBuf;
//  private final int tmpBufSize;
//  private final float[][] tmpBuf2;
//  private final int tmpBufSize2;

  // default buffer size (frames per channel)
  private static final int    BUFSIZE        = 8192;
  // for chunks greater or equal than this use a dedicated SilentAudioStake instead of writing zeros to regular AudioStake
  private static final int    MINSILENTSIZE    = 65536;

  private final int[][]      channelMaps;
  private final int        numChannels;
  private final boolean      singleFile;
    private AudioFile[]        tempF        = null;
 
  private final AudioFile[]    audioFiles;

  private int            numDepDec      = 0;
 

  public static AudioTrail newFrom( AudioFile af )
  throws IOException
  {
    final AudioFileDescr  afd      = af.getDescr();
    final int[][]      channelMaps = new int[ 1 ][ afd.channels ];
    final AudioTrail    at;
    final Span        span    = new Span( 0, afd.length );

    for( int i = 0; i < afd.channels; i++ ) {
      channelMaps[ 0 ][ i ] = i;
    }

    at = new AudioTrail( channelMaps, afd.rate, new AudioFile[] { af });
    at.add( null, new InterleavedAudioStake( span, af, span ));
    return at;
  }
 
  public static AudioTrail newFrom( AudioFile[] afs )
  throws IOException
  {
    if( afs.length == 1 ) return newFrom( afs[ 0 ]);
    if( afs.length == 0 ) throw new IllegalArgumentException( "Need at least one audio file" );
   
    final long      length;
    final double    rate;
    AudioFileDescr    afd;
    final int[][]    channelMaps;
    final Span      span;
    final AudioTrail  at;
    final Span[]    fileSpans  = new Span[ afs.length ];
   
    afd      = afs[ 0 ].getDescr();
    length    = afd.length;
    rate    = afd.rate;
    span    = new Span( 0, length );
    channelMaps  = new int[ afs.length ][];
    for( int i = 0; i < afs.length; i++ ) {
      afd    = afs[ i ].getDescr();
      if( (afd.length != length) || (afd.rate != rate) ) {
        throw new IllegalArgumentException( "Invalid mixing of lengths and rates" );
      }
      channelMaps[ i ] = new int[ afd.channels ];
      for( int j = 0; j < channelMaps[ i ].length; j++ ) {
        channelMaps[ i ][ j ] = j;
      }
      fileSpans[ i ] = span;
    }
   
    at  = new AudioTrail( channelMaps, rate, afs );
    at.add( null, new MultiMappedAudioStake( span, afs, fileSpans, channelMaps ));
    return at;
  }
 
  public static AudioTrail newFrom( AudioFileDescr afd )
  {
    final int[][]  channelMaps = new int[ 1 ][ afd.channels ];
 
    for( int i = 0; i < afd.channels; i++ ) {
      channelMaps[ 0 ][ i ] = i;
    }
    return new AudioTrail( channelMaps, afd.rate, new AudioFile[ 1 ]);
  }

  public BasicTrail createEmptyCopy()
  {
    return new AudioTrail( this.channelMaps, this.getRate(), new AudioFile[ 0 ]);
  }

  private AudioTrail( int[][] channelMaps, double rate, AudioFile[] audioFiles )
  {
    super();
   
    this.audioFiles    = audioFiles;
    this.channelMaps  = channelMaps;
    singleFile      = channelMaps.length == 1;

    int numCh      = 0;
    for( int i = 0; i < channelMaps.length; i++ ) {
      numCh       += channelMaps[ i ].length;
    }
    this.numChannels  = numCh;
    setRate( rate );
  }
 
//  public File[] createCacheFileNames()
//  {
//    final File[]    cacheFs  = new File[ audioFiles.length ];
//    final CacheManager  cm    = CacheManager.getInstance();
//    for( int i = 0; i < audioFiles.length; i++ ) {
//      cacheFs[ i ] = cm.createCacheFileName( audioFiles[ i ].getFile() );
//    }
//    return cacheFs;
//  }
 
  protected AudioFile[] getAudioFiles()
  {
    return audioFiles;
  }
 
  public void closeAll()
  throws IOException
  {
    for( int i = 0; i < audioFiles.length; i++ ) {
      if( audioFiles[ i ] != null ) audioFiles[ i ].close();
    }
  }
 
  public void exchange( AudioFile af )
  throws IOException
  {
    if( audioFiles.length != 1 ) throw new IllegalStateException();
 
    final AudioFileDescr  afd      = af.getDescr();
    final Span        span    = new Span( 0, afd.length );

    if( afd.channels != channelMaps[ 0 ].length ) throw new IllegalStateException();

//    clear( null );
    clearIgnoreDependants();
    deleteTempFiles();
//    add( null, new InterleavedAudioStake( span, af, span ));
    addIgnoreDependants( new InterleavedAudioStake( span, af, span ));
  }
 
  public void exchange( AudioFile[] afs )
  throws IOException
  {
    if( afs.length == 1 ) {
      exchange( afs[ 0 ]);
      return;
    }

    if( audioFiles.length != afs.length ) throw new IllegalStateException();
   
    final long      length;
    final double    rate;
    AudioFileDescr    afd;
    final Span      span;
    final Span[]    fileSpans  = new Span[ afs.length ];
   
    afd      = afs[ 0 ].getDescr();
    length    = afd.length;
    rate    = afd.rate;
    span    = new Span( 0, length );
    for( int i = 0; i < afs.length; i++ ) {
      afd    = afs[ i ].getDescr();
      if( afd.channels != channelMaps[ i ].length ) throw new IllegalStateException();
      if( (afd.length != length) || (afd.rate != rate) ) {
        throw new IllegalArgumentException( "Invalid mixing of lengths and rates" );
      }
      fileSpans[ i ] = span;
    }
   
//    clear( null );
    clearIgnoreDependants();
    deleteTempFiles();
//    add( null, new MultiMappedAudioStake( span, afs, fileSpans, channelMaps ));
    addIgnoreDependants( new MultiMappedAudioStake( span, afs, fileSpans, channelMaps ));
  }
 
  public void dispose()
  {
    // call this first because dependants might rely on open audio files!
    super.dispose();
    for( int i = 0; i < audioFiles.length; i++ ) {
      if( audioFiles[ i ] != null ) audioFiles[ i ].cleanUp();
    }
    deleteTempFiles();
  }
 
  public int getDefaultTouchMode()
  {
    return TOUCH_SPLIT;
  }
 
  public int[][] getChannelMaps()
  {
    return channelMaps;
  }
 
//  public void insert( Object source, Span span, CompoundEdit ce )
//  {
//    insert( source, span, TOUCH_SPLIT, ce );
//  }
//
//  public void remove( Object source, Span span, CompoundEdit ce )
//  {
//    remove( source, span, TOUCH_SPLIT, ce );
//  }

  public int getChannelNum()
  {
    return numChannels;
  }
 
    public void debugDump()
    {
    AudioStake stake;
 
    for( int i = 0; i < getNumStakes(); i++ ) {
      stake = (AudioStake) get( i, true );
      stake.debugDump();
    }
    AudioStake.debugCheckDisposal();
    }
 
  public AudioStake allocSilent( Span span )
  {
    return new SilentAudioStake( span, numChannels );
  }
 
  public synchronized AudioStake alloc( Span span )
  throws IOException
  {
    long fileStart;
    long fileStop;
    final Span[] fileSpans  = new Span[ channelMaps.length ];
 
    // synchronized because this method is synchronized
    // and no other method calls createTempFiles() !
//    synchronized( this ) {
      if( tempF == null ) {
        createTempFiles();
      }
//    }
    synchronized( tempF ) {
      for( int i = 0; i < tempF.length; i++ ) {
        fileStart    = tempF[ i ].getFrameNum();
        fileStop    = fileStart + span.getLength();
        tempF[ i ].setFrameNum( fileStop );
        fileSpans[ i = new Span( fileStart, fileStop );
      }
    }
   
    if( singleFile ) {
      return new InterleavedAudioStake( span, tempF[ 0 ], fileSpans[ 0 ]);
    } else {
      return new MultiMappedAudioStake( span, tempF, fileSpans, channelMaps );
    }
  }

//  public void addBufferReadMessages( OSCBundle bndl, Span readSpan, Buffer[] bufs, int bufOff )
//  {
//    int idx = indexOf( readSpan.start, true );
//    if( idx < 0 ) idx = -(idx + 2);
//
//    final List  coll  = editGetCollByStart( null );
//    final int        num    = coll.size();
//    AudioStake        stake;
//    int            chunkLen;
//    Span          subSpan;
//    int            len  = (int) readSpan.getLength();
//   
//    while( (len > 0) && (idx < num) ) {
//      stake    = (AudioStake) coll.get( idx );
//      subSpan    = new Span( Math.max( stake.getSpan().start, readSpan.start ),
//                  Math.min( stake.getSpan().stop, readSpan.stop ));
//      if( subSpan.getLength() > 0 ) {
//        stake.addBufferReadMessages( bndl, subSpan, bufs, bufOff );
//        chunkLen  = (int) subSpan.getLength();
//        bufOff     += chunkLen;
//        len       -= chunkLen;
//      }
//      idx++;
//    }
//    if( len > 0 ) {
//      for( int i = 0; i < bufs.length; i++ ) {
//        bndl.addPacket( bufs[ i ].fillMsg(
//          bufOff * bufs[ i ].getNumChannels(), len * bufs[ i ].getNumChannels(), 0.0f ));
//      }
//    }
//  }

  public static final int MODE_INSERT    = Session.EDIT_INSERT;
  public static final int MODE_OVERWRITE  = Session.EDIT_OVERWRITE;
  public static final int MODE_MIX    = Session.EDIT_MIX;
//  private static final int MODE_MIN    = MODE_INSERT;
//  private static final int MODE_MAX    = MODE_MIX;

  /**
   *  Note: when mode == MODE_INSERT, the caller should have called editInsert on this
   *  trail before, this is NOT done by this method; this method simply calls editAdd
   *  with the newly synthesized stake!
   *
   *  @param  srcTrail  the trail to read from (null allowed, which means no source tracks)
   *  @param  copySpan  re source!
   *  @param  insertPos  such that copySpan.start becomes insertPos in the target
   *  @param  mode    either MODE_INSERT, MODE_OVERWRITE or MODE_MIX
   *  @param  trackMap  array of length this.getNumChannels(), where each element is the
   *            source channel idx mapping to the target channel whose idx is the array idx
   *            ; so to copy channels 0 and 2 of a four channel source to a target stereo trail,
   *            trackMap would be [ 0, 2 ] for example. A mono to stereo would be [ 0, 0 ].
   *            index -1 indicates bypass (for MODE_INSERT or clearUnused filled with zeroes)
   *  @param  clearUnused  whether tracks in source are to be cleared (instead of bypassed) when
   *            no mapping from target exists. don't combine mode == MODE_MIX and clearUnused == true!
   *
   *  @todo  this method should somehow be part of BasicTrail
   *  @todo  this method has become too complex and should be split up
   */
  public boolean copyRangeFrom( AudioTrail srcTrail, Span copySpan, long insertPos, int mode,
                  Object source, AbstractCompoundEdit ce, int[] trackMap, BlendContext bcPre, BlendContext bcPost )
  throws IOException
  {
    if( trackMap.length != this.getChannelNum() ) throw new IllegalArgumentException( "trackMap : " + trackMap );
    if( trackMap.length == 0 ) return true;

    final boolean      hasBlend  = (bcPre != null) && (bcPre.getLen() > 0) || (bcPost != null) && (bcPost.getLen() > 0);
    final AudioStake    writeStake;
//    final boolean      result;
    final long        len      = copySpan.getLength();
    final int        bufLen    = (int) Math.min( len, BUFSIZE );
    final double      progWeight  = 1.0 / len;
   
    // throws IOException
    writeStake  = alloc( new Span( insertPos, insertPos + len ));
   
    try {
      switch( mode ) {
      case MODE_INSERT:
        if( hasBlend ) {
          insertRangeFrom( srcTrail, copySpan.start, writeStake, insertPos, len, bufLen, trackMap, progWeight, bcPre, bcPost );
        } else {
          insertRangeFrom( srcTrail, copySpan.start, writeStake, insertPos, len, bufLen, trackMap, progWeight );
        }
        break;
     
      case MODE_MIX:
        if( hasBlend ) {
          mixRangeFrom( srcTrail, copySpan.start, writeStake, insertPos, len, bufLen, trackMap, progWeight, bcPre, bcPost );
        } else {
          mixRangeFrom( srcTrail, copySpan.start, writeStake, insertPos, len, bufLen, trackMap, progWeight );
        }
        break;
     
      case MODE_OVERWRITE:
        if( hasBlend ) {
          overwriteRangeFrom( srcTrail, copySpan.start, writeStake, insertPos, len, bufLen, trackMap, progWeight, bcPre, bcPost );
        } else {
          overwriteRangeFrom( srcTrail, copySpan.start, writeStake, insertPos, len, bufLen, trackMap, progWeight );
        }
        break;
     
      default:
        throw new IllegalArgumentException( "mode: " + mode );
      }
     
      writeStake.flush();
      this.editAdd( source, writeStake, ce );
      return true;
    }
    catch( InterruptedException e1 ) {  // thrown by ProcessingThread.updateAndCheckCancel()
      writeStake.dispose();
      return false;
    }
    catch( IOException e1 ) {
      writeStake.dispose();
      throw e1;
    }
  }

  private static void setProgression( long len, double progWeight )
  throws ProcessingThread.CancelledException
  {
// System.err.println( "aud prog len " + len + ", p " + (float) (len * progWeight) );
    ProcessingThread.update( (float) (len * progWeight) );
  }
 
  private static void flushProgression()
  {
    ProcessingThread.flushProgression();
  }
 
  private void insertRangeFrom( AudioTrail srcTrail, long srcStart, AudioStake writeStake, long insertPos, long len,
                  int bufLen, int[] trackMap, double progWeight )
  throws IOException, InterruptedException
  {
    final float[][]      srcBuf      = new float[ srcTrail == null ? 0 : srcTrail.getChannelNum() ][];
    final float[][]      mappedSrcBuf  = new float[ this.getChannelNum() ][];
    float[]          empty      = null;
    boolean          srcUsed      = false;
    int            chunkLen;
    Span          chunkSpan;
    long          newSrcStart, newInsPos;
   
    for( int i = 0; i < trackMap.length; i++ ) {
      if( trackMap[ i ] >= 0 ) {
        if( srcBuf[ trackMap[ i ]] == null ) {
          srcBuf[ trackMap[ i ]] = new float[ bufLen ];
          srcUsed  = true;
        }
        mappedSrcBuf[ i ] = srcBuf[ trackMap[ i ]];
      } else {
        if( empty == null ) empty = new float[ bufLen ];
        mappedSrcBuf[ i ] = empty;
      }
    }

    for( long framesWritten = 0; framesWritten < len; ) {
      chunkLen  = (int) Math.min( bufLen, len - framesWritten );
      if( srcUsed ) {
        newSrcStart  = srcStart + chunkLen;
        chunkSpan  = new Span( srcStart, newSrcStart );
        srcTrail.readFrames( srcBuf, 0, chunkSpan )// null channel bufs allowed!
        srcStart  = newSrcStart;
      }
      newInsPos  = insertPos + chunkLen;
      chunkSpan  = new Span( insertPos, newInsPos );
      writeStake.writeFrames( mappedSrcBuf, 0, chunkSpan );
      framesWritten += chunkLen;
      insertPos  = newInsPos;
     
      setProgression( framesWritten, progWeight );
    }
    writeStake.flush();
  }
   
  private void mixRangeFrom( AudioTrail srcTrail, long srcStart, AudioStake writeStake, long insertPos, long len,
                 int bufLen, int[] trackMap, double progWeight )
  throws IOException, InterruptedException
  {
    final float[][]      srcBuf      = new float[ srcTrail == null ? 0 : srcTrail.getChannelNum() ][];
    final float[][]      outBuf      = new float[ this.getChannelNum() ][ bufLen ];
    final float[][]      mappedSrcBuf  = new float[ this.getChannelNum() ][];
    boolean          srcUsed      = false;
    int            chunkLen;
    Span          chunkSpan;
    long          newSrcStart, newInsPos;
   
    for( int i = 0; i < trackMap.length; i++ ) {
      if( trackMap[ i ] >= 0 ) {
        if( srcBuf[ trackMap[ i ]] == null ) {
          srcBuf[ trackMap[ i ]] = new float[ bufLen ];
          srcUsed  = true;
        }
        mappedSrcBuf[ i ] = srcBuf[ trackMap[ i ]];
      }
    }

    for( long framesWritten = 0; framesWritten < len; ) {
      chunkLen  = (int) Math.min( bufLen, len - framesWritten );
      if( srcUsed ) {
        newSrcStart  = srcStart + chunkLen;
        chunkSpan  = new Span( srcStart, newSrcStart );
        srcTrail.readFrames( srcBuf, 0, chunkSpan )// null channel bufs allowed!
        srcStart  = newSrcStart;
      }
      newInsPos  = insertPos + chunkLen;
      chunkSpan  = new Span( insertPos, newInsPos );
      this.readFrames( outBuf, 0, chunkSpan );
      if( srcUsed ) {
        for( int i = 0; i < mappedSrcBuf.length; i++ ) {
          if( mappedSrcBuf[ i ] != null ) add( outBuf[ i ], 0, mappedSrcBuf[ i ], 0, chunkLen );
        }
      }
      writeStake.writeFrames( outBuf, 0, chunkSpan );
      framesWritten += chunkLen;
      insertPos  = newInsPos;

      setProgression( framesWritten, progWeight );
    }
    writeStake.flush();
  }

  private void overwriteRangeFrom( AudioTrail srcTrail, long srcStart, AudioStake writeStake, long insertPos, long len,
                   int bufLen, int[] trackMap, double progWeight )
  throws IOException, InterruptedException
  {
    final float[][]      srcBuf      = new float[ srcTrail == null ? 0 : srcTrail.getChannelNum() ][];
    final float[][]      outBuf      = new float[ this.getChannelNum() ][];
    final float[][]      thisBuf      = new float[ this.getChannelNum() ][];
    boolean          srcUsed      = false;
    boolean          thisUsed    = false;
    int            chunkLen;
    Span          chunkSpan;
    long          newSrcStart, newInsPos;
   
    for( int i = 0; i < trackMap.length; i++ ) {
      if( trackMap[ i ] >= 0 ) {
        if( srcBuf[ trackMap[ i ]] == null ) {
          srcBuf[ trackMap[ i ]] = new float[ bufLen ];
          srcUsed      = true;
        }
        outBuf[ i ]      = srcBuf[ trackMap[ i ]];
      } else {
        thisBuf[ i ]    = new float[ bufLen ];
        outBuf[ i ]      = thisBuf[ i ];
        thisUsed      = true;
      }
    }

    for( long framesWritten = 0; framesWritten < len; ) {
      chunkLen  = (int) Math.min( bufLen, len - framesWritten );
      if( srcUsed ) {
        newSrcStart  = srcStart + chunkLen;
        chunkSpan  = new Span( srcStart, newSrcStart );
        srcTrail.readFrames( srcBuf, 0, chunkSpan )// null channel bufs allowed!
        srcStart  = newSrcStart;
      }
      newInsPos  = insertPos + chunkLen;
      chunkSpan  = new Span( insertPos, newInsPos );
      if( thisUsed ) {
        this.readFrames( thisBuf, 0, chunkSpan );
      }
      writeStake.writeFrames( outBuf, 0, chunkSpan );
      framesWritten += chunkLen;
      insertPos  = newInsPos;

      setProgression( framesWritten, progWeight );
    }
    writeStake.flush();
  }

  private void insertRangeFrom( AudioTrail srcTrail, final long srcStart, AudioStake writeStake, final long insertPos, final long len,
                  final int bufLen, int[] trackMap, double progWeight, BlendContext bcPre, BlendContext bcPost )
  throws IOException, InterruptedException
  {
    final float[][]      srcBuf      = new float[ srcTrail == null ? 0 : srcTrail.getChannelNum() ][];
    final float[][]      mappedSrcBuf  = new float[ this.getChannelNum() ][];
//    final long        blendLen    = bc.getLen();
    final long        preLen      = bcPre == null 0L : bcPre.getLen();
    final long        postLen      = bcPost == null ? 0L : bcPost.getLen();
    final float[][]      mixBuf      = new float[ this.getChannelNum() ][ bufLen ];
    final float[][]      srcFadeBuf    = new float[ this.getChannelNum() ][];
    final long        fadeOutOffset  = insertPos - len;
    float[]          empty      = null;
    boolean          srcUsed      = false;
    boolean          writeMix    = false;
    int            chunkLen, chunkLen2, deltaChunk;
    int            cpToMixStart  = 0;
    int            cpToMixStop    = bufLen;
    Span          chunkSpan, chunkSpan2;
   
    // for each chunk there are two scenarios (indicated by the value of writeMix):
    // 1. we are in a chunk that contains a crossfade.
    //    in this case, output is found in mixBuf by reading in mixBuf from this
    //    trail and applying fades, then mixing (adding) the source trail from mappedSrcBuf.
    //    it is possible that both fadeIn and fadeOut are contained in one chunk, hence
    //    the two separate if-blocks below in the main loop and the tracking of the "dry" portion
    //    using variables cpToMixStart and cpToMixStop. Note that we chose not the opposite
    //    way of mixing mixBuf to mappedSrcBuf because we might be duplicating source trail
    //    channels (e.g. paste mono track to stereo file)!
    // 2. we are in a chunk that does not contain fades.
    //    in this case, we simply write mappedSrcBuf as output. we have ensured that this
    //    works by filling unused channels with emptyBuf.
    //
    // to properly deal with channel duplication, we have created a separate buffer
    // srcFadeBuf that prevents the blend context from accidentally fading the
    // same channel twice

    for( int i = 0; i < trackMap.length; i++ ) {
      if( trackMap[ i ] >= 0 ) {
        if( srcBuf[ trackMap[ i ]] == null ) {
          srcBuf[ trackMap[ i ]] = new float[ bufLen ];
          srcUsed  = true;
          srcFadeBuf[ i ] = srcBuf[ trackMap[ i ]];
        }
        mappedSrcBuf[ i ] = srcBuf[ trackMap[ i ]];
      } else {
        if( empty == null ) empty = new float[ bufLen ];
        mappedSrcBuf[ i ] = empty;
      }
    }

    for( long framesWritten = 0, remaining = len; remaining > 0; ) {
      chunkLen  = (int) Math.min( bufLen, remaining );
      if( srcUsed ) {
//        newSrcStart  = srcStart + chunkLen;
        chunkSpan  = new Span( srcStart + framesWritten, srcStart + framesWritten + chunkLen );
//System.err.println( "A srcTrail.readFrames( srcBuf, 0, " + chunkSpan + " )" );
        srcTrail.readFrames( srcBuf, 0, chunkSpan )// null channel bufs allowed!
//        srcStart  = newSrcStart;

        if( framesWritten < preLen ) {  // fade source in
//System.err.println( "A bc.fadeIn : srcFadeBuf in [ 0 ... " + ((int) Math.min( chunkLen, preLen - framesWritten )) + " ], offset = "+framesWritten );
          bcPre.fadeIn( framesWritten, srcFadeBuf, 0, srcFadeBuf, 0, (int) Math.min( chunkLen, preLen - framesWritten ));
        }
        if( remaining - chunkLen < postLen ) {  // fade source out
          deltaChunk  = (int) Math.max( 0, remaining - postLen )// this is the amount of space before the actual fade begins!
          chunkLen2  = chunkLen - deltaChunk;
//System.err.println( "A bc.fadeOut : srcFadeBuf in [ " + deltaChunk + " ... " + chunkLen2 + " ], offset = "+(postLen - remaining + deltaChunk) );
          bcPost.fadeOut( postLen - remaining + deltaChunk, srcFadeBuf, deltaChunk, srcFadeBuf, deltaChunk, chunkLen2 );
        }
      }
//      newInsPos  = insertPos + chunkLen;
//      chunkSpan  = new Span( insertPos, newInsPos );
      chunkSpan  = new Span( insertPos + framesWritten, insertPos + framesWritten + chunkLen );

      if( framesWritten < preLen ) {  // fade this out
        chunkLen2  = (int) Math.min( chunkLen, preLen - framesWritten );
        deltaChunk  = chunkLen - chunkLen2;
        chunkSpan2  = deltaChunk > 0 ? chunkSpan.replaceStop( chunkSpan.stop - deltaChunk ) : chunkSpan;
//System.err.println( "B this.readFrames( mixBuf, 0, " + chunkSpan2 + " )" );
        this.readFrames( mixBuf, 0, chunkSpan2 );
//System.err.println( "B bc.fadeOut : mixBuf in [ 0 ... " + chunkLen2 + " ], offset = "+framesWritten );
        bcPre.fadeOut( framesWritten, mixBuf, 0, mixBuf, 0, chunkLen2 );
        for( int i = 0; i < mixBuf.length; i++ ) {
          if( mappedSrcBuf[ i ] != empty ) add( mixBuf[ i ], 0, mappedSrcBuf[ i ], 0, chunkLen2 );
        }
        cpToMixStart= chunkLen2;
        cpToMixStop  = chunkLen;
        writeMix  = true;
      }
     
//len - framesWritten != blendLen
//framesWritten != len - blendLen;
//fadeOutPos = origInsertPos - blendLen - (len - blendLen) = origInsertPos - len

      // check if after this chunk we have entered the fadein
      if( remaining - chunkLen < postLen ) {  // fade this in
        deltaChunk  = (int) Math.max( 0, remaining - postLen )// this is the amount of space before the actual fade begins!
        chunkLen2  = chunkLen - deltaChunk;
        chunkSpan2  = new Span( fadeOutOffset + framesWritten + deltaChunk, fadeOutOffset + framesWritten + chunkLen );
//System.err.println( "CCC . framesWritten = "+framesWritten+"; len = "+len+"; postLen = "+postLen+"; insertPos now "+insertPos+ "; remaining = "+remaining+"; deltaChunk = "+deltaChunk+"; chunkLen2 = "+chunkLen2 );       
//System.err.println( "C this.readFrames( mixBuf, " + deltaChunk + ", " + chunkSpan2 + " )" );
        this.readFrames( mixBuf, deltaChunk, chunkSpan2 );
//System.err.println( "C bc.fadeIn : mixBuf in [ " + deltaChunk + " ... " + chunkLen2 + " ], offset = "+(postLen - remaining + deltaChunk) );
        bcPost.fadeIn( postLen - remaining + deltaChunk, mixBuf, deltaChunk, mixBuf, deltaChunk, chunkLen2 );
        for( int i = 0; i < mixBuf.length; i++ ) {
          if( mappedSrcBuf[ i ] != empty ) add( mixBuf[ i ], deltaChunk, mappedSrcBuf[ i ], deltaChunk, chunkLen2 );
        }
        cpToMixStop  = deltaChunk;
        writeMix  = true;
      }

      if( writeMix ) {
        chunkLen2  = cpToMixStop - cpToMixStart;
        if( chunkLen2 > 0 ) {
          for( int i = 0; i < mixBuf.length; i++ ) {
            System.arraycopy( mappedSrcBuf[ i ], cpToMixStart, mixBuf[ i ], cpToMixStart, chunkLen2 );
          }
        }
        writeStake.writeFrames( mixBuf, 0, chunkSpan );
        writeMix  = false;
        cpToMixStart= 0;
        cpToMixStop  = bufLen;
      } else {
        writeStake.writeFrames( mappedSrcBuf, 0, chunkSpan );
      }
      framesWritten += chunkLen;
      remaining    -= chunkLen;
     
//      insertPos  = newInsPos;
     
      setProgression( framesWritten, progWeight );
    }
    writeStake.flush();
  }

  private void mixRangeFrom( AudioTrail srcTrail, long srcStart, AudioStake writeStake, long insertPos, long len,
                 int bufLen, int[] trackMap, double progWeight, BlendContext bcPre, BlendContext bcPost )
  throws IOException, InterruptedException
  {
    final float[][]      srcBuf      = new float[ srcTrail == null ? 0 : srcTrail.getChannelNum() ][];
    final float[][]      mappedSrcBuf  = new float[ this.getChannelNum() ][];
//    final long        blendLen    = bc.getBlendLen();
    final long        preLen      = bcPre == null  ? 0L : bcPre.getLen();
    final long        postLen      = bcPost == null ? 0L : bcPost.getLen();
    final float[][]      mixBuf      = new float[ this.getChannelNum() ][ bufLen ];
    final float[][]      srcFadeBuf    = new float[ this.getChannelNum() ][];
    boolean          srcUsed      = false;
    int            chunkLen;
    Span          chunkSpan, chunkSpan2;
    long          newSrcStart, newInsPos;
   
    // src trail is read in mapped buffer mappedSrcBuf. this trail is read into buffer mixBuf.
    // src trail is faded according to srcFadeBuf (which avoid duplicate fading of re-used
    // channels). non-null channels in mappedSrcBuf are mixed (added) to mixBuf, and mixBuf
    // is written out.

    for( int i = 0; i < trackMap.length; i++ ) {
      if( trackMap[ i ] >= 0 ) {
        if( srcBuf[ trackMap[ i ]] == null ) {
          srcBuf[ trackMap[ i ]] = new float[ bufLen ];
          srcUsed      = true;
          srcFadeBuf[ i ] = srcBuf[ trackMap[ i ]];
        }
        mappedSrcBuf[ i = srcBuf[ trackMap[ i ]];
      }
    }

    for( long framesWritten = 0; framesWritten < len; ) {
      chunkLen  = (int) Math.min( bufLen, len - framesWritten );
      newInsPos  = insertPos + chunkLen;
      chunkSpan  = new Span( insertPos, newInsPos );
      this.readFrames( mixBuf, 0, chunkSpan );
      if( srcUsed ) {
        newSrcStart  = srcStart + chunkLen;
        chunkSpan2  = new Span( srcStart, newSrcStart );
        srcTrail.readFrames( srcBuf, 0, chunkSpan2 )// null channel bufs allowed!
        srcStart  = newSrcStart;

        if( framesWritten < preLen ) {  // fade source in
          bcPre.fadeIn( framesWritten, srcFadeBuf, 0, srcFadeBuf, 0, (int) Math.min( chunkLen, preLen - framesWritten ));
        }
        if( len - (framesWritten + chunkLen) < postLen ) {  // fade source out
          bcPost.fadeOut( framesWritten - (len - postLen), srcFadeBuf, 0, srcFadeBuf, 0, (int) Math.min( chunkLen, len - framesWritten ));
        }
        for( int i = 0; i < mixBuf.length; i++ ) {
          if( mappedSrcBuf[ i ] != null ) add( mixBuf[ i ], 0, mappedSrcBuf[ i ], 0, chunkLen );
        }
      }
      writeStake.writeFrames( mixBuf, 0, chunkSpan );
      framesWritten += chunkLen;
      insertPos  = newInsPos;
     
      setProgression( framesWritten, progWeight );
    }
    writeStake.flush();
  }

  private void overwriteRangeFrom( AudioTrail srcTrail, long srcStart, AudioStake writeStake, long insertPos, long len,
                   int bufLen, int[] trackMap, double progWeight, BlendContext bcPre, BlendContext bcPost )
  throws IOException, InterruptedException
  {
//System.err.println( "WARNING: blending calculations not yet fixed" );
    final float[][]      srcBuf      = new float[ srcTrail == null ? 0 : srcTrail.getChannelNum() ][];
    final float[][]      mappedSrcBuf  = new float[ this.getChannelNum() ][];
//    final long        blendLen    = bc.getBlendLen();
    final long        preLen      = bcPre == null 0L : bcPre.getLen();
    final long        postLen      = bcPost == null ? 0L : bcPost.getLen();
    final float[][]      mixBuf      = new float[ this.getChannelNum() ][ bufLen ];
    final float[][]      srcFadeBuf    = new float[ this.getChannelNum() ][];
    final float[][]      thisDryBuf    = new float[ this.getChannelNum() ][];
    final float[][]      thisFadeBuf    = new float[ this.getChannelNum() ][];
    final float[][]      compositeBuf  = new float[ this.getChannelNum() ][];
    boolean          srcUsed      = false;
//    boolean          thisFadeUsed  = false;
//    boolean          thisDryUsed    = false;
    int            chunkLen, chunkLen2, deltaChunk;
    int            clrMixFadeStart = 0;
    int            clrMixFadeStop  = bufLen;
    Span          chunkSpan; // , chunkSpan2;
    long          newSrcStart, newInsPos, fadeOff;
    boolean          xFadeBegin, xFadeEnd, xFade;
   
    // for each chunk there are two scenarios (indicated by the value of xFade):
    // 1. we are in a chunk that contains a crossfade.
    //    in this case, output is found in mixBuf by reading in mixBuf from this
    //    trail and applying fades to the "wet" parts of this trail (thisFadeBuf),
    //    then mixing (adding) the source trail from mappedSrcBuf.
    // 2. we are in a chunk that does not contain fades.
    //    in this case, we only read those channels from this trail that are "dry"
    //    (not overwritten) as specified in thisDryBuf. we then write a composite
    //    buffer compositeBuf which contains either references to channels from the source
    //    (mappedSrcBuf) or from this trail (thisDryBuf).
    //
    // to properly deal with channel duplication, we have created a separate buffer
    // srcFadeBuf that prevents the blend context from accidentally fading the
    // same channel twice

    for( int i = 0; i < trackMap.length; i++ ) {
      if( trackMap[ i ] >= 0 ) {
        if( srcBuf[ trackMap[ i ]] == null ) {
          srcBuf[ trackMap[ i ]] = new float[ bufLen ];
          srcUsed  = true;
          srcFadeBuf[ i ] = srcBuf[ trackMap[ i ]];
        }
        mappedSrcBuf[ i = srcBuf[ trackMap[ i ]];
        compositeBuf[ i = mappedSrcBuf[ i ];
        thisFadeBuf[ i = mixBuf[ i ];
//        thisFadeUsed    = true;
      } else {
        thisDryBuf[ i ]    = mixBuf[ i ];
//        thisDryUsed      = true;
        compositeBuf[ i = mixBuf[ i ];
      }
    }

    for( long framesWritten = 0; framesWritten < len; ) {
      chunkLen  = (int) Math.min( bufLen, len - framesWritten );
      xFadeBegin  = framesWritten < preLen;
      xFadeEnd  = len - (framesWritten + chunkLen) < postLen;
      xFade    = xFadeBegin || xFadeEnd;
     
      if( srcUsed ) {
        newSrcStart  = srcStart + chunkLen;
        chunkSpan  = new Span( srcStart, newSrcStart );
        srcTrail.readFrames( srcBuf, 0, chunkSpan )// null channel bufs allowed!
        srcStart  = newSrcStart;

        if( xFadeBegin ) {  // fade source in
//System.err.print( "xFadeBegin. off = "+framesWritten+"; srcFadeBuf = [ " );
//for( int kk = 0; kk < srcFadeBuf.length; kk++ ) {
//  if( kk > 0 ) System.err.print( ", " );
//  System.err.print( srcFadeBuf[ kk ] == null ? "null" : ("float[ " + srcFadeBuf[ kk ].length + " ]") );
//}
//System.err.println( " ]; len = " + ((int) Math.min( chunkLen, blendLen - framesWritten )));
          bcPre.fadeIn( framesWritten, srcFadeBuf, 0, srcFadeBuf, 0, (int) Math.min( chunkLen, preLen - framesWritten ));
        }
        if( xFadeEnd ) {  // fade source out
//System.err.print( "xFadeEnd. off = "+(framesWritten - (len - blendLen))+"; srcFadeBuf = [ " );
//for( int kk = 0; kk < srcFadeBuf.length; kk++ ) {
//  if( kk > 0 ) System.err.print( ", " );
//  System.err.print( srcFadeBuf[ kk ] == null ? "null" : ("float[ " + srcFadeBuf[ kk ].length + " ]") );
//}
//System.err.println( " ]; len = " + ((int) Math.min( chunkLen, len - framesWritten )));
          fadeOff    = framesWritten - (len - postLen);
          if( fadeOff < 0 ) {
            deltaChunk  = (int) -fadeOff;
            fadeOff    = 0;
          } else {
            deltaChunk  = 0;
          }
          chunkLen2  = (int) Math.min( chunkLen, len - framesWritten ) - deltaChunk;
          bcPost.fadeOut( fadeOff, srcFadeBuf, deltaChunk, srcFadeBuf, deltaChunk, chunkLen2 );
        }
      }
      newInsPos  = insertPos + chunkLen;
      chunkSpan  = new Span( insertPos, newInsPos );

      if( xFade ) {
        this.readFrames( mixBuf, 0, chunkSpan );
        if( xFadeBegin ) {  // fade this out
          chunkLen2    = (int) Math.min( chunkLen, preLen - framesWritten );
          deltaChunk    = chunkLen - chunkLen2;
//          chunkSpan2    = deltaChunk > 0 ? chunkSpan.replaceStop( chunkSpan.stop - deltaChunk ) : chunkSpan;
          bcPre.fadeOut( framesWritten, thisFadeBuf, 0, thisFadeBuf, 0, chunkLen2 );
          clrMixFadeStart  = chunkLen2;
          clrMixFadeStop  = chunkLen;
        }
        if( xFadeEnd ) {  // fade this in
//          chunkLen2  = (int) Math.min( chunkLen, len - framesWritten );
//          deltaChunk  = chunkLen - chunkLen2;
          fadeOff    = framesWritten - (len - postLen);
          if( fadeOff < 0 ) {
            deltaChunk  = (int) -fadeOff;
            fadeOff    = 0;
          } else {
            deltaChunk  = 0;
          }
          chunkLen2  = (int) Math.min( chunkLen, len - framesWritten ) - deltaChunk;
          bcPost.fadeIn( fadeOff, thisFadeBuf, deltaChunk, thisFadeBuf, deltaChunk, chunkLen2 );
          clrMixFadeStop  = deltaChunk;
        }
        chunkLen2 = clrMixFadeStop - clrMixFadeStart;
        if( chunkLen2 > 0 ) {
          for( int i = 0; i < thisFadeBuf.length; i++ ) {
            if( thisFadeBuf[ i ] != null ) clear( thisFadeBuf[ i ], clrMixFadeStart, chunkLen2 );
          }
        }
        clrMixFadeStart  = 0;
        clrMixFadeStop  = bufLen;
        for( int i = 0; i < mixBuf.length; i++ ) {
          if( mappedSrcBuf[ i ] != null ) add( mixBuf[ i ], 0, mappedSrcBuf[ i ], 0, chunkLen );
        }
        writeStake.writeFrames( mixBuf, 0, chunkSpan );
      } else {
        this.readFrames( thisDryBuf, 0, chunkSpan );
        writeStake.writeFrames( compositeBuf, 0, chunkSpan );
      }
      framesWritten += chunkLen;
      insertPos  = newInsPos;
     
      setProgression( framesWritten, progWeight );
    }
    writeStake.flush();
  }

  private static void add( float[] bufA, int offA, float[] bufB, int offB, int len )
  {
    for( int stop = offA + len; offA < stop; ) {
      bufA[ offA++ ] += bufB[ offB++ ];
    }
  }

  private static void clear( float[] buf, int off, int len )
  {
    for( int stop = off + len; off < stop; ) {
      buf[ off++ ] = 0f;
    }
  }

/* 
    final float[][]      srcBuf    = new float[ srcTrail == null ? 0 : srcTrail.getChannelNum() ][];
    final float[][]      outBuf    = new float[ this.getChannelNum() ][];
    final float[][]      mixBuf    = new float[ this.getChannelNum() ][];
    final float[][]      bypassBuf  = new float[ this.getChannelNum() ][];
    final ProcessingThread  pt      = ProcessingThread.currentThread();
    final long        len      = copySpan.getLength();
    final double      progWeight  = 1.0 / len;
    final int        bufLen    = (int) Math.min( len, BUFSIZE );
//    final boolean      mixMode    = (mode == MODE_MIX) || (blendLen > 0);
//    final boolean      mixMode    = (mode == MODE_MIX); // XXX
    final boolean      needsMix  = (mode == MODE_MIX) || (blendLen > 0) || ((mode == MODE_OVERWRITE) && !clearUnused);
    AudioStake        writeStake  = null;
    float[]          empty    = null;
    float[]          temp, temp2;
    boolean          srcUsed    = false;
    boolean          mixUsed    = false;
    boolean          dirty    = false;
//    boolean          copySrc    = false;
    int            chunkLen, chunkLen2, deltaChunk;
    long          n;
    Span          readSpan, readSpan2;
   
//System.err.println( "trackMap : " );


    for( int i = 0; i < trackMap.length; i++ ) {
      if( needsMix ) {
        mixBuf[ i ] = new float[ bufLen ];
        mixUsed    = true;
      }
      if( trackMap[ i ] >= 0 ) {
        if( srcBuf[ trackMap[ i ]] == null ) {
          srcBuf[ trackMap[ i ]] = new float[ bufLen ];
          srcUsed  = true;
        }
        if( needsMix ) {
          outBuf[ i ] = new float[ bufLen ];
        } else {
          outBuf[ i ] = srcBuf[ trackMap[ i ]];
        }
      } else {
        if( mode == MODE_INSERT
     
        if( (mode == MODE_INSERT) || clearUnused ) {
          if( empty == null ) empty = new float[ bufLen ];
          outBuf[ i ] = empty;
        } else {
          if( mixBuf[ i ] == null ) {
            mixBuf[ i ] = new float[ bufLen ];
            mixUsed    = true;
          }
          outBuf[ i ] = mixBuf[ i ];
        }
      }
    }


    for( int i = 0; i < trackMap.length; i++ ) {
//System.err.println( "  "+trackMap[ i ]);
//      if( mode == MODE_MIX ) {
      if( needsMix ) {
        mixBuf[ i ] = new float[ bufLen ];
        mixUsed    = true;
      }
      if( trackMap[ i ] >= 0 ) {
        if( srcBuf[ trackMap[ i ]] == null ) {
          srcBuf[ trackMap[ i ]] = new float[ bufLen ];
          srcUsed  = true;
          outBuf[ i ] = srcBuf[ trackMap[ i ]];
        } else if( mixUsed ) {
          outBuf[ i ] = new float[ bufLen ];
//          copySrc = true;
        } else {
          outBuf[ i ] = srcBuf[ trackMap[ i ]];
        }
      } else {
//        if( (mode == MODE_INSERT) || (clearUnused && !needsMix) ) {
        if( (mode == MODE_INSERT) || clearUnused ) {
          if( empty == null ) empty = new float[ bufLen ];
//          empty = new float[ bufLen ];
          outBuf[ i ] = empty;
        } else {
          if( mixBuf[ i ] == null ) {
            mixBuf[ i ] = new float[ bufLen ];
            mixUsed    = true;
          }
          outBuf[ i ] = mixBuf[ i ];
        }
      }
    }
   
// CHECK CONTRACT! XXX
//    if( !srcUsed && (mode != MODE_INSERT) && !clearUnused ) return;  // nothing da do
   
    try {
      writeStake  = alloc( new Span( insertPos, insertPos + len ));
   
      for( long framesWritten = 0; framesWritten < len; ) {
        chunkLen  = (int) Math.min( bufLen, len - framesWritten );
        if( srcUsed ) {
          n      = copySpan.start + framesWritten;
          readSpan  = new Span( n, n + chunkLen );
          srcTrail.readFrames( srcBuf, 0, readSpan );  // null channel bufs allowed!
          if( bc != null ) {
            if( framesWritten < blendLen ) {
              bc.fadeIn( framesWritten, outBuf, 0, outBuf, 0, (int) Math.min( chunkLen, blendLen - framesWritten ));
            }
            if( len - (framesWritten + chunkLen) < blendLen ) {
              bc.fadeOut( framesWritten - (len - blendLen), outBuf, 0, outBuf, 0, (int) Math.min( chunkLen, len - framesWritten ));
            }
          }
          for( int i = 0; i < outBuf.length; i++ ) {
            if( trackMap[ i ] < 0 ) continue;
            temp  = srcBuf[ trackMap[ i ]];
            temp2  = outBuf[ i ];
            if( temp != temp2) {
              System.arraycopy( temp, 0, temp2, 0, chunkLen );
            }
          }
        }
        n      = insertPos + framesWritten;
        readSpan  = new Span( n, n + chunkLen );
        if( mixUsed ) {
         
          // ------------------------------------- Mix Mode
       
          if( mode == MODE_MIX ) {
            this.readFrames( mixBuf, 0, readSpan );
            for( int i = 0; i < mixBuf.length; i++ ) {
              temp  = mixBuf[ i ];
              temp2  = outBuf[ i ];
              if( (temp != temp2) && (temp != null) ) {
                for( int j = 0; j < chunkLen; j++ ) {
                  temp2[ j ] += temp[ j ];
                }
              }
            }

          // ------------------------------------- Overwrite Mode w/ Blending
                     
          } else if( mode == MODE_OVERWRITE ) {
            if( dirty ) {
              for( int i = 0; i < mixBuf.length; i++ ) {
                temp  = mixBuf[ i ];
                if( temp == outBuf[ i ]) {
                  for( int j = 0; j < chunkLen; j++ ) {
                    temp[ j ] = 0f;
                  }
                }
              }
              dirty = false;
            }
         
            if( framesWritten < blendLen ) {  // fade out
              chunkLen2  = (int) Math.min( chunkLen, blendLen - framesWritten );
              deltaChunk  = chunkLen - chunkLen2;
              readSpan2  = readSpan.replaceStop( readSpan.stop - deltaChunk );
//System.err.println( "fade out. framesWritten = "+framesWritten+"; len = "+len+"; chunkLen = "+chunkLen+"; chunkLen2 = "+chunkLen2+"; blendLen = "+blendLen+" ; read span = "+readSpan2+"; span length = "+readSpan2.getLength() );
              this.readFrames( mixBuf, 0, readSpan2 );
              bc.fadeOut( framesWritten, mixBuf, 0, mixBuf, 0, chunkLen2 );
              for( int i = 0; i < mixBuf.length; i++ ) {
                temp  = mixBuf[ i ];
                temp2  = outBuf[ i ];
                if( (temp != temp2) && (temp != null) ) {
                  for( int j = 0; j < chunkLen2; j++ ) {
                    temp2[ j ] += temp[ j ];
                  }
//                } else {
//                  for( int j = chunkLen2; j < chunkLen; j++ ) {
//                    temp2[ j ] = 0f;
//                  }
                }
//                for( int j = 0; j < chunkLen2; j++ ) {
//temp2[ j ] = -0.5f;
//                }
              }
              dirty = true;
            }
            if( len - (framesWritten + chunkLen) < blendLen ) {  // fade in
              chunkLen2  = (int) Math.min( chunkLen, (framesWritten + chunkLen) - (len - blendLen) );
              deltaChunk  = chunkLen - chunkLen2;
              readSpan2  = readSpan.replaceStart( readSpan.start + deltaChunk );
//System.err.println( "fade in. framesWritten = "+framesWritten+"; len = "+len+"; chunkLen = "+chunkLen+"; chunkLen2 = "+chunkLen2+"; blendLen = "+blendLen+" ; read span = "+readSpan2+"; span length = "+readSpan2.getLength() );
//              this.readFrames( mixBuf, 0, readSpan2 );
              this.readFrames( mixBuf, deltaChunk, readSpan2 );
              bc.fadeIn( framesWritten - (len - blendLen) + deltaChunk, mixBuf, deltaChunk, mixBuf, deltaChunk, chunkLen2 );
              for( int i = 0; i < mixBuf.length; i++ ) {
                temp  = mixBuf[ i ];
                temp2  = outBuf[ i ];
                if( (temp != temp2) && (temp != null) ) {
                  for( int j = 0, k = deltaChunk; j < chunkLen2; j++, k++ ) {
//                    temp2[ k ] += temp[ j ];
                    temp2[ k ] += temp[ k ];
                  }
//                } else {
//               
                }
              }
              dirty = true;
            }
           
          // ------------------------------------- Insert Mode w/ Blending

          } else if( mode == MODE_INSERT ) {
            if( framesWritten < blendLen ) {  // fade out
              chunkLen2  = (int) Math.min( chunkLen, blendLen - framesWritten );
              deltaChunk  = chunkLen - chunkLen2;
              readSpan2  = readSpan.replaceStop( readSpan.stop - deltaChunk );
              this.readFrames( mixBuf, 0, readSpan2 );
              bc.fadeOut( framesWritten, mixBuf, 0, mixBuf, 0, chunkLen2 );
              for( int i = 0; i < mixBuf.length; i++ ) {
                temp  = mixBuf[ i ];
                temp2  = outBuf[ i ];
                if( (temp != temp2) && (temp != null) ) {
                  for( int j = 0; j < chunkLen2; j++ ) {
                    temp2[ j ] += temp[ j ];
                  }
                }
              }
            }
            if( len - (framesWritten + chunkLen) < blendLen ) {  // fade in
              chunkLen2  = (int) Math.min( chunkLen, len - framesWritten );
              deltaChunk  = chunkLen - chunkLen2;
              // alora, dis one fade in is pre-insert pos!
              n      = insertPos - (len - framesWritten);
              readSpan2  = new Span( n, n + chunkLen2 );
              this.readFrames( mixBuf, 0, readSpan2 );
              bc.fadeIn( framesWritten - (len - blendLen), mixBuf, 0, mixBuf, 0, chunkLen2 );
              for( int i = 0; i < mixBuf.length; i++ ) {
                if( (mixBuf[ i ] != outBuf[ i ]) && (mixBuf[ i ] != null) ) {
                  for( int j = 0, k = deltaChunk; j < chunkLen2; j++, k++ ) {
                    outBuf[ i ][ k ] += mixBuf[ i ][ j ];
                  }
                }
              }
            }
         
          } else {
            assert false : mode;
          }
        }
        writeStake.writeFrames( outBuf, 0, readSpan );
        framesWritten += chunkLen;

        if( pt != null ) {
          pt.setProgression( (float) (framesWritten * progWeight) );
          if( pt.shouldCancel() ) {
            writeStake.dispose();
            return false;
          }
        }
      }
     
      writeStake.flush();
    }
    catch( IOException e1 ) {
      if( writeStake != null ) writeStake.dispose();
      throw e1;
    }

    // XXX mode
    this.editAdd( source, writeStake, ce );
    return true;
  }
*/
  private static String getResourceString( String key )
  {
    return AbstractApplication.getApplication().getResourceString( key );
  }

  public void clearRange( Span clearSpan, int mode, Object source, AbstractCompoundEdit ce, boolean[] trackMap, BlendContext bc )
  throws IOException
  {
    if( trackMap.length != this.getChannelNum() ) throw new IllegalArgumentException( trackMap.toString() );
    if( trackMap.length == 0 ) return;

    switch( mode ) {
    case MODE_INSERT:
      clearRangeIns( clearSpan, mode, source, ce, trackMap, bc );
      break;
    case MODE_OVERWRITE:
      clearRangeOvr( clearSpan, mode, source, ce, trackMap, bc );
      break;
    default:
      throw new IllegalArgumentException( String.valueOf( mode ));
    }
  }

  public void addDependant( Trail sub )
  {
    super.addDependant( sub );
    if( sub instanceof DecimatedWaveTrail ) {
      numDepDec++;
    }
  }

  public void removeDependant( Trail sub )
  {
    super.removeDependant( sub );
    if( sub instanceof DecimatedWaveTrail ) {
      numDepDec--;
    }
  }

  /*
   *  contract: trackMap.length > 0
   */
  private void clearRangeIns( Span clearSpan, int mode, Object source, AbstractCompoundEdit ce, boolean[] trackMap, BlendContext bc )
  throws IOException
  {
    final long      blendLen  = bc == null ? 0L : bc.getLen(); // .getBlendLen();
   
    if( blendLen == 0L ) return// nothing da do
   
    final boolean    t1      = trackMap[ 0 ];
    for( int i = 1; i < trackMap.length; i++ ) {
      if( t1 != trackMap[ i ]) throw new IllegalStateException( getResourceString( "errAudioWillLooseSync" ));
    }
   
    final double    perDecProgRatio  = 0.9// this to one decimtrail
    final double    progRatio    = 1.0 / (1.0 + ((1.0 - perDecProgRatio) / perDecProgRatio) * numDepDec);
    final double    progWeight    = progRatio / blendLen;

    final long      left      = bc.getLeftLen();
    final long      right      = bc.getRightLen();
    final Span      fadeInSpan    = new Span( clearSpan.stop - left, clearSpan.stop + right );
    final Span      fadeOutSpan    = new Span( clearSpan.start - left, clearSpan.start + right );
    final int      bufLen      = (int) Math.min( blendLen, BUFSIZE );
    final Span      writeSpan    = fadeOutSpan; // new Span( clearSpan.start, clearSpan.start + blendLen );
    final int      numCh      = this.getChannelNum();
    final float[][]    bufA      = new float[ numCh ][ bufLen ];
    final float[][]    bufB      = new float[ numCh ][ bufLen ];
   
//System.err.println( "fadeInSpan = " + fadeInSpan + "; fadeOutSpan = " + fadeOutSpan );

    AudioStake      writeStake  = null;
//    float[]        temp, temp2;
    int          chunkLen;
    long        n;
    Span        chunkSpan;
    boolean        success  = false;
   
//    final long time1 = System.currentTimeMillis();
   
    try {
      flushProgression();
      writeStake    = alloc( writeSpan );
     
      for( long framesWritten = 0; framesWritten < blendLen; ) {
        chunkLen  = (int) Math.min( bufLen, blendLen - framesWritten );
        n      = fadeOutSpan.start + framesWritten;
        chunkSpan  = new Span( n, n + chunkLen );
        this.readFrames( bufA, 0, chunkSpan );
        n      = fadeInSpan.start + framesWritten;
        chunkSpan  = new Span( n, n + chunkLen );
        this.readFrames( bufB, 0, chunkSpan );
        bc.blend( framesWritten, bufA, 0, bufB, 0, bufA, 0, chunkLen );
        n      = writeSpan.start + framesWritten;
        chunkSpan  = new Span( n, n + chunkLen );
        writeStake.writeFrames( bufA, 0, chunkSpan );
        framesWritten += chunkLen;
       
        setProgression( framesWritten, progWeight );
      }
      writeStake.flush();
      success = true;
    }
    finally {
      if( !success && (writeStake != null) ) writeStake.dispose();
    }

//    final long time2 = System.currentTimeMillis();
    // XXX mode
//System.err.println( "add "+writeSpan );
    this.editAdd( source, writeStake, ce );
//    final long time3 = System.currentTimeMillis();
   
//    System.out.println( "ratio " + (double) (time2 - time1) / (time3 - time1) );
  }

  /*
   *  contract: trackMap.length > 0
   *
   *  @todo  should be able to blend fadeIn / fadeOut, so maxBlendLen = clearSpan.length not clearSpan.length >> 1
   *      ; this requires a lot of changes though ;-(
   */
  private void clearRangeOvr( Span clearSpan, int mode, Object source, AbstractCompoundEdit ce, boolean[] trackMap, BlendContext bc )
  throws IOException
  {
    final long      blendLen  = bc == null ? 0L : bc.getLen();
   
    boolean        sync    = true;
    boolean        success    = false;
    final boolean    t1      = trackMap[ 0 ];
    for( int i = 1; i < trackMap.length; i++ ) {
      if( t1 != trackMap[ i ]) {
        sync  = false;
        break;
      }
    }
    // now sync is true if all tracks are selected or all are unselected

    final List collStakes  = new ArrayList( 3 );
   
//final long time1 = System.currentTimeMillis();
    try {
      if( sync && !t1 ) return;      // all tracks unselected
//      if( (blendLen == 0L) && sync ) {  // no fades, all tracks selected, let's just add a SilentAudioStake
//        writeStake2 = allocSilent( clearSpan );
//        this.editAdd( source, writeStake2, ce );
//        return;
//      }
   
      final double    perDecProgRatio  = 0.9// this to one decimtrail
      final double    progRatio    = 1.0 / (1.0 + ((1.0 - perDecProgRatio) / perDecProgRatio) * numDepDec);

      final boolean    hasBlend    = blendLen > 0L;
      final long      blendLen2    = blendLen << 1;
      final int      numCh      = this.getChannelNum();
//      final float[][]    srcBuf      = new float[ numCh ][];
//      final float[][]    outBuf      = new float[ numCh ][];
//      final float[][]    mixBuf      = new float[ numCh ][];
      final float[][]    readWriteBufF  = new float[ numCh ][];    // reading and writing during fade
      final float[][]    fadeBuf;                  // channels which are faded
      final float[][]    readBufS;                  // reading during middle part
      final float[][]    writeBufS;                  // writing during middle part
      final Span      silentSpan    = new Span( clearSpan.start + blendLen, clearSpan.stop - blendLen );
      final long      silentLen    = silentSpan.getLength();
      final int      bufLen      = (int) Math.min( clearSpan.getLength(), BUFSIZE );
      final boolean    useSilentStake  = sync && (silentLen >= MINSILENTSIZE);
      final AudioStake  writeStake1, writeStake2, writeStake3;
      final double    progWeight;
      float[]        empty    = null;
      float[]        temp;
      int          chunkLen;
      long        n;
      long        totalFramesWritten  = 0;
      Span        chunkSpan;

      flushProgression();

      // buffer regarding fade in / out
      if( hasBlend ) {
        fadeBuf  = new float[ numCh ][];
        for( int i = 0; i < trackMap.length; i++ ) {
          readWriteBufF[ i = new float[ bufLen ];
          if( trackMap[ i ]) fadeBuf[ i ] = readWriteBufF[ i ];
        }
      } else {
        fadeBuf = null;
      }

      // buffer regarding middle part
      if( !useSilentStake ) {
        readBufS  = new float[ numCh ][];
        writeBufS  = new float[ numCh ][];
        for( int i = 0; i < trackMap.length; i++ ) {
          if( trackMap[ i ]) {  // need to silence
            if( empty == null ) empty = new float[ bufLen ];
            writeBufS[ i ] = empty;
          }
          if( !trackMap[ i ]) {  // need to copy (bypass)
            if( readWriteBufF[ i ] != null ) {  // can reuse
              readBufS[ i = readWriteBufF[ i ]
            } else {
              readBufS[ i = new float[ bufLen ];
            }
            writeBufS[ i ] = readBufS[ i ];
          }
        }
      } else {
        readBufS  = null;
        writeBufS  = null;
      }

      // ---- define stakes ----
      if( useSilentStake ) {    // ok, let's put a silent stake in dem middle
        if( hasBlend ) {
          writeStake1 = alloc( new Span( clearSpan.start, silentSpan.start ));
          collStakes.add( writeStake1 );
        } else {
          writeStake1  = null;
        }
        writeStake2  = allocSilent( silentSpan );
        collStakes.add( writeStake2 );
        if( hasBlend ) {
          writeStake3  = alloc( new Span( silentSpan.stop, clearSpan.stop ));
          collStakes.add( writeStake3 );
        } else {
          writeStake3  = null;
        }
       
        final double progRatio2    = 1.0 / (1.0 + numDepDec)// inf:1 for silentLen
        final double w        = (double) blendLen2 / (blendLen2 + silentLen); // weight of progRatio versus progRatio2
        progWeight          = (progRatio*w + progRatio2*(1.0-w)) / blendLen2;
     
      } else {
        writeStake2  = alloc( clearSpan );
        writeStake1  = writeStake2;
        writeStake3  = writeStake2;
        collStakes.add( writeStake2 );
        progWeight  = progRatio / (blendLen2 + silentLen);
//        progWeight  = 9.0 / ((blendLen2 + silentLen) * (numDepDec + 9));
      }

      // ---- fade out part ----
      for( long framesWritten = 0; framesWritten < blendLen; ) {
        chunkLen  = (int) Math.min( bufLen, blendLen - framesWritten );
        n      = clearSpan.start + framesWritten;
        chunkSpan  = new Span( n, n + chunkLen );
        this.readFrames( readWriteBufF, 0, chunkSpan )// d.h. alle kanaele lesen
        bc.fadeOut( framesWritten, fadeBuf, 0, fadeBuf, 0, chunkLen );    // only fade selected tracks
        writeStake1.writeFrames( readWriteBufF, 0, chunkSpan );
        framesWritten += chunkLen;
        totalFramesWritten += chunkLen;
        setProgression( totalFramesWritten, progWeight );
      }
     
      // ---- middle part ----
      if( !useSilentStake ) {
        // make sure writeBufF is cleared for unselected tracks now, since if hasBlend == true, it was used before
        if( hasBlend ) {
          for( int i = 0; i < numCh; i++ ) {
            temp = writeBufS[ i ];
            if( temp != empty ) {
              for( int j = 0; j < bufLen; j++ ) {
                temp[ j ] = 0f;
              }
            }
          }
        }
       
        for( long framesWritten = 0; framesWritten < silentLen; ) {
          chunkLen  = (int) Math.min( bufLen, silentLen - framesWritten );
          n      = silentSpan.start + framesWritten;
          chunkSpan  = new Span( n, n + chunkLen );
          if( !sync ) {  // not all channels are empty
            this.readFrames( readBufS, 0, chunkSpan );
          }
          writeStake2.writeFrames( writeBufS, 0, chunkSpan );
          framesWritten += chunkLen;
          totalFramesWritten += chunkLen;
          setProgression( totalFramesWritten, progWeight );
        }
      }

      // ---- fade in part ----
      for( long framesWritten = 0; framesWritten < blendLen; ) {
        chunkLen  = (int) Math.min( bufLen, blendLen - framesWritten );
        n      = silentSpan.stop + framesWritten;
        chunkSpan  = new Span( n, n + chunkLen );
        this.readFrames( readWriteBufF, 0, chunkSpan )// d.h. alle kanaele lesen
        bc.fadeIn( framesWritten, fadeBuf, 0, fadeBuf, 0, chunkLen );    // only fade selected tracks
        writeStake3.writeFrames( readWriteBufF, 0, chunkSpan );
        framesWritten += chunkLen;
        totalFramesWritten += chunkLen;
        setProgression( totalFramesWritten, progWeight );
      }
     
      // ---- flushing ----
      if( useSilentStake ) {
        if( writeStake1 != null ) writeStake1.flush();
        if( writeStake3 != null ) writeStake3.flush();
      } else {
        writeStake2.flush();
      }
      success = true;
    }
    finally {
      if( !success ) {
        for( int i = 0; i < collStakes.size(); i++ ) {
          ((AudioStake) collStakes.get( i )).dispose();
        }
      }
    }

//final long time2 = System.currentTimeMillis();
    // XXX mode
    this.editAddAll( source, collStakes, ce );
//final long time3 = System.currentTimeMillis();
//System.out.println( "ratio " + (double) (time2 - time1) / (time3 - time1) );
  }

  public static void readFrames( List stakesByStart, float[][] data, int dataOffset, Span readSpan )
    throws IOException
  {
    final int    num    = stakesByStart.size();
    int        idx    = Collections.binarySearch( stakesByStart, new Long( readSpan.start ), startComparator );
    if( idx < 0 idx    = Math.max( 0, -(idx + 2) );
//    int        len    = (int) readSpan.getLength();
    int        dataStop= (int) readSpan.getLength() + dataOffset;

    AudioStake  stake;
    int      chunkLen;
    Span    subSpan;
   
    while( (dataOffset < dataStop) && (idx < num) ) {
      stake    = (AudioStake) stakesByStart.get( idx );
      subSpan    = new Span( Math.max( stake.getSpan().start, readSpan.start ),
                  Math.min( stake.getSpan().stop, readSpan.stop ));
      chunkLen  = stake.readFrames( data, dataOffset, subSpan );
      dataOffset += chunkLen;
//      len       -= chunkLen;
      idx++;
    }
    if( dataOffset < dataStop ) {
System.err.println( "WARNING: trying to read beyond the trail's stop" );
      for( int ch = 0; ch < data.length; ch++ ) {
        if( data[ ch ] != null ) {
          for( int i = dataOffset; i < dataStop; i++ ) {
            data[ ch ][ i ] = 0f;
          }
        }
      }
    }
  }
 
  protected void readFrames( float[][] data, int dataOffset, Span readSpan, AbstractCompoundEdit ce )
    throws IOException
    {
    AudioTrail.readFrames( editGetCollByStart( ce ), data, dataOffset, readSpan );
  }

    public void readFrames( float[][] data, int dataOffset, Span readSpan )
    throws IOException
    {
    readFrames( data, dataOffset, readSpan, null );
  }

//    private InterleavedStreamFile prepareTempFile()
//    throws IOException
//    {
//        if( tempF == null ) {
//      tempF = createTempFile();
//        } else {
//            tempF.seekFrame( tempF.getFrameNum() );
//        }
//        return tempF;
//    }
   
   private void createTempFiles()
    throws IOException
  {
    final AudioFileDescr afd  = new AudioFileDescr();
    afd.type          = AudioFileDescr.TYPE_AIFF;
    afd.rate          = getRate();
    afd.bitsPerSample      = 32;
    afd.sampleFormat      = AudioFileDescr.FORMAT_FLOAT;

    if( singleFile ) {
      afd.channels      = getChannelNum();
      afd.file        = IOUtil.createTempFile();
      tempF          = new AudioFile[] { AudioFile.openAsWrite( afd )};
    } else {
      AudioFileDescr afd2;
      final AudioFile[] tempF2 = new AudioFile[ channelMaps.length ];
      for( int i = 0; i < channelMaps.length; i++ ) {
        afd2        = new AudioFileDescr( afd );
        afd2.channels    = channelMaps[ i ].length;
        afd2.file      = IOUtil.createTempFile();
        tempF2[ i ]      = AudioFile.openAsWrite( afd2 );
      }
      // real assignment here coz tempF will remain null if error occurs in the loop
      tempF          = tempF2;
    }
  }

  private void deleteTempFiles()
  {
    if( tempF != null ) {
      for( int i = 0; i < tempF.length; i++ ) {
        if( tempF[ i ] != null ) {
          tempF[ i ].cleanUp();
          tempF[ i ].getFile().delete();
        }
      }
    }
    tempF = null;
  }

//  /**
//   *  Wrapper method for calling the fullrate
//   *  editor's flatten method.
//   *
//   *  @param  f   the file into which the flattened data is written
//   *  @throws IOException if a read or write error occurs
//   *  @see  NondestructiveTrackEditor#flatten( InterleavedFloatFile )
//   *
//   *  @todo  subsampled files should be flatten as well?
//   */
//  public void flatten( InterleavedStreamFile f, ProcessingThread pt, float progOff, float progWeight,
//             SyncCompoundEdit edit )
//    throws IOException
//  {
//    Span  totalSpan  = new Span( 0, ste[0].getFrameNum() );
//    long  offset    = f.getFramePosition();
//  
//    ste[0].flatten( f, pt, progOff, progWeight );
//    ste[0].clear();
//    ste[0].insert( f, offset, totalSpan );
//  }

  public void flatten( InterleavedStreamFile f, Span span, int[] channelMap )
    throws IOException
  {
    final Span      fileSpan  = new Span( f.getFramePosition(), span.getLength() );
    final AudioStake  stake    = new InterleavedAudioStake( span, f, fileSpan );
   
    try {
      flatten( stake, span, channelMap );
    }
    finally {
      stake.dispose();
    }
  }

  public void flatten( InterleavedStreamFile[] fs, Span span, int[] channelMap )
    throws IOException
  {
    if( fs.length == 1 ) {
      flatten( fs[ 0 ], span, channelMap )// more efficient
      return;
    }
 
    final Span[]    fileSpans  = new Span[ fs.length ];
    for( int i = 0; i < fileSpans.length; i++ ) {
      fileSpans[ i ]        = new Span( fs[ i ].getFramePosition(), span.getLength() );
    }
    final AudioStake  stake    = new MultiMappedAudioStake( span, fs, fileSpans );
   
    try {
      flatten( stake, span, channelMap );
    }
    finally {
      stake.dispose();
    }
  }
 
  private void flatten( AudioStake target, Span span, int[] channelMap )
    throws IOException
  {
//    if( target.getChannelNum() != numChannels ) {
//      throw new IllegalArgumentException( "Wrong # of channels (required: " + numChannels +
//        " / got: " + target.getChannelNum() );
//    }
    if( span.isEmpty() ) return;

    final int outChannels = target.getChannelNum();
    // build or verify channelMap
    if( channelMap == null ) {
      if( outChannels != numChannels ) throw new IllegalArgumentException();
      channelMap = new int[ outChannels ];
      for( int i = 0; i < channelMap.length; i++ ) {
        channelMap[ i ] = i;
      }
    } else {
      if( outChannels != channelMap.length ) throw new IllegalArgumentException();
      for( int i = 0; i < channelMap.length; i++ ) {
        if( (channelMap[ i ] < 0) || (channelMap[ i ] >= numChannels) ) throw new IllegalArgumentException();
      }
    }

//    final ProcessingThread  pt      = ProcessingThread.currentThread();
//    final float[][]      data    = new float[ numChannels ][ BUFSIZE ];
    final double      progWeight  = 1.0 / span.getLength();
    final int        num      = getNumStakes();
    final float[][]      outBuf    = new float[ outChannels ][ BUFSIZE ];
    final float[][]      inBuf    = new float[ numChannels ][];
//    int            idx      = Collections.binarySearch( collStakesByStart, new Long( span.start ), startComparator );
    int            idx      = indexOf( span.start, true );
    if( idx < 0 )      idx      = Math.max( 0, -(idx + 2) );
    long          readOff    = span.start;
    AudioStake        source;
    int            chunkLen;
    Span          sourceSpan, subSpan;
    long          readStop  = span.start;
   
    for( int i = 0; i < channelMap.length; i++ ) {
      inBuf[ channelMap[ i ]] = outBuf[ i ];
    }
   
    while( (readStop < span.stop) && (idx < num) ) {
      source    = (AudioStake) get( idx, true );
      sourceSpan  = source.getSpan();
      readStop  = Math.min( sourceSpan.stop, span.stop );
      while( readOff < readStop ) {
        chunkLen = (int) Math.min( BUFSIZE, readStop - readOff );
        subSpan   = new Span( readOff, readOff + chunkLen );
        source.readFrames( inBuf, 0, subSpan );
        target.writeFrames( outBuf, 0, subSpan );
        readOff += chunkLen;
        setProgression( readOff - span.start, progWeight );
//if( true ) throw new IOException( "FAIL TEST" );
      }
      idx++;
    }
    if( readStop < span.stop ) {
System.err.println( "WARNING: trying to flatten beyond the trail's stop" );
      for( int ch = 0; ch < outBuf.length; ch++ ) {
        for( int i = 0; i < outBuf[ ch ].length; i++ ) {
          outBuf[ ch ][ i ] = 0f;
        }
      }
      while( readOff < span.stop ) {
        chunkLen = (int) Math.min( BUFSIZE, span.stop - readOff );
        subSpan   = new Span( readOff, readOff + chunkLen );
        target.writeFrames( outBuf, 0, subSpan );
        readOff += chunkLen;
        setProgression( readOff - span.start, progWeight );
      }
    }
  }
} // class AudioTrail
TOP

Related Classes of de.sciss.meloncillo.io.AudioTrail

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.