package nxm.audio.prim;

import java.io.File;
import java.io.FileInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.util.Random;
import java.util.StringTokenizer;

import nxm.sys.inc.Keyable;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Time;
import nxm.sys.libg.Feature;
import nxm.sys.libg.GPrompt;

/** NMUCAST_g2 - UniCasts or Multicasts audio data

  @author  Wayne G. Dengel II  (Zeta Associates)

  created: February 16, 2005
  modified:
    JPH  12JUL05 Added Inverted audio ability
    JPH  07JUL05 Compute multicast address for nmucast call
    JPH  05JUL05 Added unique thread ID
                 Also template changes to .key/.prm files
 */
public class nmucast_g2 extends Primitive implements Keyable {
  private         Player    player        = null;
  private         int       decimate      = 0;
  private         double    AUbaseTime    = 0.0;
  private         double    PSDbaseTime   = 0.0;
  private         double    baseTime      = 0.0;
  private         String    psd_filename  = null;
  private         DataFile  audio_in      = null;
  private         DataFile  df            = null;
  private         Data      data          = null;
  private         Data      auMidasData   = null;
  private         boolean   ahPLAY        = true;

  private static final double twopi       = Math.PI*2.0;
  private static final int INVERT_OFFSET  = 1;                            // Do not use ZERO array element, and declare associated arrays one larger
  private static final byte auType        = INT;                          // au data buffer always in SI format
  private         boolean   invert        = false;
  private         float[]   hilbert       = getHilbertCoefficients(INVERT_OFFSET);
  private         int       hilbSize      = hilbert.length-INVERT_OFFSET;
  private         int       invOutOffset  = hilbSize-1;
  private         float     flipout[];
  private         float     finvInSave[];
  private         Complex   audio_lo      = new Complex(1.0, 0.0);        // local oscillator variable for freq inversion
  private         Complex   audio_lo_inc  = new Complex(0.0, 0.0);        // local oscillator increment for freq inversion
  private         int       hilb_ncoefs;
  private         float     flip_freq;                                    // Audio spectral inversion reference frequency
  private         boolean   DEBUG_INVERT  = false;

  /** default constructor */
  public nmucast_g2 () {}

  public int open() {
    player = new Player(this);
    decimate = MA.getL("/BLK", 1024);
    player.rthost = MA.getS("HOST").toLowerCase();
    player.rtport = MA.getL("PORT");
    player.port = MA.getL("/PORT", 7101);
    player.prange = MA.getL("/PRANGE", 8);
    psd_filename = MA.getS("OUT", null);                                                     // Get PIPE name for PSD
    setInvert(MA.getState("/INVERT",invert));
    setFlipFreq(MA.getF("/FF",(float)0.0));
    hilb_ncoefs = (hilbSize+1)/2 - 1; // (one-sided number of coeffs)
    int nin = 800; //jph -- this is hard-coded, needs to be computed
    int flipSize = nin+INVERT_OFFSET+invOutOffset;
    finvInSave = new float[flipSize];
    flipout    = new float[flipSize];
    //System.out.println("NMUCAST: HILBERT size "+hilbSize+" ncoefs = "+hilb_ncoefs+" invOutOffset "+invOutOffset+" flip float array size"+flipSize);
    //System.out.println("NMUCAST: HILBERT [0] "+hilbert[0]+" 1 = "+hilbert[1]+" last "+hilbert[hilbert.length-1] );


    // Open the INPUT Audio Stream Pipe
    if(MA.isPresent("IN")){
      audio_in = MA.getDataFile("IN");
      audio_in.open();
      auMidasData = audio_in.getDataBuffer(800, INT);                                         //BYTE);
      player.initPipeAudio();
    }

    if (psd_filename != null) {
      M.info("Openning pipe " + psd_filename);
      df = new DataFile();
      df.init(M, psd_filename, "1000", "SI", 0);
      double  ad  = 1 / 8000.0;
      df.setXDelta(ad);
      df.open(DataFile.OUTPUT);
      data = df.getDataBuffer(8000, INT);
    }
    M.info("NMUCAST Player Host is " + player.rthost + ":" + player.rtport + " " + player.port + " " + player.multicast);
    player.start();
    return NORMAL;
  }


  public int process() {
    if (audio_in==null) return NOOP;
    byte [] auBData = null;
    if (audio_in.avail()<800) return NOOP;
    while (audio_in.avail()>=800){
      int inAu = audio_in.read(auMidasData);
      if (inAu>0) {
        //System.out.println("About to read data " + inAu+"   -------------   "+audio_in.avail());
        auBData  = auMidasData.castB(false);
        //System.out.println("We read "+auBData.length);
        System.arraycopy(auBData, 0, player.rtbuf2, Player.DATA, auBData.length); // mod
        player.rtpack2.setData(player.rtbuf2, 0, auBData.length);
        player.rtpack2.setLength(auBData.length);// + player.DATA);
        try {
          if (ahPLAY) player.dsock.send(player.rtpack2);
            } catch (Exception ex) {}
        player.seq++;
      }
    }
    return NORMAL;
  }

  /** Get the Hilbert Coefficients
   *  @param offset the offset in which to store the Hilbert Coefficients
   *  @return the Hilbert Coefficients
   */
  public float [] getHilbertCoefficients (int offset) {
    float coeff[] = new float[offset+31];
    coeff[offset+0]  = (float)-0.0033953045;
    coeff[offset+1]  = (float)0.0;
    coeff[offset+2]  = (float)-0.005865184;
    coeff[offset+3]  = (float)0.0;
    coeff[offset+4]  = (float)-0.013438462;
    coeff[offset+5]  = (float)0.0;
    coeff[offset+6]  = (float)-0.028142288;
    coeff[offset+7]  = (float)0.0;
    coeff[offset+8]  = (float)-0.05348362;
    coeff[offset+9]  = (float) 0.0;
    coeff[offset+10] = (float)-0.09803945;
    coeff[offset+11] = (float)0.0;
    coeff[offset+12] = (float)-0.19356379;
    coeff[offset+13] = (float)0.0;
    coeff[offset+14] = (float)-0.6302204;
    coeff[offset+15] = (float)0.0;
    coeff[offset+16] = (float)0.6302204;
    coeff[offset+17] = (float)0.0;
    coeff[offset+18] = (float)0.19356379;
    coeff[offset+19] = (float)0.0;
    coeff[offset+20] = (float)0.09803945;
    coeff[offset+21] = (float)0.0;
    coeff[offset+22] = (float)0.05348362;
    coeff[offset+23] = (float)0.0;
    coeff[offset+24] = (float)0.028142288;
    coeff[offset+25] = (float)0.0;
    coeff[offset+26] = (float)0.013438462;
    coeff[offset+27] = (float)0.0;
    coeff[offset+28] = (float)0.005865184;
    coeff[offset+29] = (float)0.0;
    coeff[offset+30] = (float)0.0033953045;
    return coeff;
  }
  /** Toggle inverting the audio
   *  @param state whether or not to invert the audio as a boolean
   */
  public void setInvert (boolean state) {
    invert = state;
  }
  /** Return whether or not audio inverting is enabled
   *  @return true if audio inverting is enabled, false otherwise
   */
  public boolean getInvert () {
    return invert;
  }
  /** Toggle inverting the audio
   *  @param stateStr whether or not to invert the audio as a String
   */
  public void setInvert (String stateStr) {
    String temp = stateStr.toUpperCase();
    //M.info("NMUCAST: setting INVERT mode = "+temp);
    boolean newval = false;
    if (temp.equals("ON") || temp.equals("TRUE") ) newval = true;
    setInvert(newval);
  }
  /** Calls {@link Player#setFlipFreq(float)} to set the flip frequency for inversion
   *  @param newff the new flip frequency
   */
  public void setFlipFreq (float newff) {
    player.setFlipFreq(newff);
  }
  /** Get the flip frequency for inversion
   *  @return the current flip frequency
   */
  public float getFlipFreq () {
    return flip_freq;
  }
  private class Complex {
    float real;
    float imag;

    public Complex (float r, float i) {
      real = r;
      imag = i;
    }
    public Complex (double r, double i) {
      this( (float)r, (float)i );
    }
    public float real () {
      return real;
    }
    public void real (float in) {
      real = in;
    }
    public float imag () {
      return imag;
    }
    public void imag (float in) {
      imag = in;
    }
    public void div (float in) {
      real = real/in;
      imag = imag/in;
    }
    public float abs () {
      return (float)Math.sqrt(real*real + imag*imag);
    }
    public void add (Complex in) {
      this.real( this.real()+in.real() );
      this.imag( this.imag()+in.imag() );
    }
    public void mult (Complex in) {
      float real = this.real()*in.real() -
          this.imag()*in.imag();
      float imag = this.real()*in.imag() +
          this.imag()*in.real();
      this.real = real;
      this.imag = imag;
    }
  }

  /** Get the current player time as a String
   *  @return the current player time as a String
   */
  public String getPtime() {
    double  rate  = player.BytesPerSec;
    String  tstr  = Time.toString(((double) player.dataHead / rate) + baseTime, 9);
    tstr = tstr.substring(tstr.indexOf("::") + 2);
    return tstr;
  }
  /** Get the current player time as a double
   *  @return the current player time as a double
   */
  public double getJtime() {
    double  rate  = 8000.0;
    return ((double) player.dataHead / rate) + baseTime;
  }
  /** Calls {@link Player#play()} to start or resume audio playback */
  public void play() {
    if (player != null) player.play();
  }
  /** Calls {@link Player#pstop()} to stop audio playback */
  public void stop() {
    if (player != null) player.pstop();
  }
  /** Calls {@link Player#setPosition(int)} to set the audio player position
   *  @param value the raw position value
   */
  public void position(int value) {
    if (player != null) player.setPosition(value * decimate);
  }
  /** Set a selected start time
   *  @param value the raw start time
   */
  public void selstart(float value) {
    if (player != null) player.selStart = value * decimate;
  }
  /** Set a selected stop time
   *  @param value the raw stop time
   */
  public void selstop(float value) {
    if (player != null) player.selStop = value * decimate;
  }
  /** Calls {@link Player#load(String)} to load input audio data
   *  @param name the the name of the file with input audio data
   */
  public void load(String name) {
    if (player != null) player.load(name);
  }

  public Object setKey(String name, Object value) {
    if (name.equals("PLAY")) {
      player.play();
    } else if (name.equals("STOP")) {
      player.pstop();
    } else if (name.equals("AHPLAY")) {
      ahPLAY = true;
    } else if (name.equals("AHSTOP")) {
      ahPLAY = false;
    } else if (name.equals("MARKER")) {
      player.marker = (Feature) value;
    } else if (name.equals("TIMECODE")){
      player.TimeCode = (GPrompt)value;//(Gcontrol) value;
    } else if (name.equals("PSDMARKER")) {
      player.psdmarker = (Feature) value;
    } else if (name.equals("PSDMARKER1")) {
      player.psdmarker1 = (Feature) value;
    } else if (name.equals("PSDMARKER2")) {
      player.psdmarker2 = (Feature) value;
    } else if (name.equals("PSDMARKER3")) {
      player.psdmarker3 = (Feature) value;
    } else if (name.equals("PSDMARKER4")) {
      player.psdmarker4 = (Feature) value;
    } else if (name.equals("PSD2MARKER")) {
      player.psd2marker = (Feature) value;
    } else if (name.equals("AUBASETIME")) {
      AUbaseTime = Convert.o2d(value);
    } else if (name.equals("PSDBASETIME")) {
      PSDbaseTime = Convert.o2d(value);
    } else if (name.equals("BASETIME")) {
      baseTime = Convert.o2d(value);
    } else if (name.equals("LOAD")) {
      String fileToLoad = (String)value;
      String  aux_read  = (String) MR.get("AUX.READ");
      StringTokenizer st = new StringTokenizer(aux_read,"|");
      while(st.hasMoreElements()){
        String part = st.nextToken();
        String sub_aux = (String)MR.get("AUX."+part);
        int     badchar     = sub_aux.indexOf('+');
        if (badchar != -1) {
          String  user  = (String) MR.get("ENV.USER");
          sub_aux = new String(sub_aux.substring(0, badchar) + user + "/");
        }
        File myFile = new File(sub_aux+fileToLoad);
        if(myFile.exists()){
          String  outFile     = new String(sub_aux+fileToLoad);
          outFile = outFile.toLowerCase();
          System.out.println("Found the file, loading "+outFile+", exiting.");
          player.load(outFile);
          break;
        }
      }

      //String  aux_write   = (String) MR.get("AUX.WRITE");
      //String  write_path  = (String) MR.get("AUX." + aux_write);
      //int     badchar     = write_path.indexOf('+');
      //if (badchar != -1) {
      //    String  user  = (String) MR.get("ENV.USER");
      //    write_path = new String(write_path.substring(0, badchar) + user + "/");
      // }
      //String  outFile     = new String(write_path + (String) value);
      //outFile = outFile.toLowerCase();
      //player.load(outFile);
    } else if (name.equals("SETPOS")) {
      int  ival  = Convert.o2l(value);
      player.setPosition(ival * decimate);
    } else if (name.equals("SELSTART")) {
      double  sample  = Convert.o2d(value);
      player.selStart = sample * decimate;
    } else if (name.equals("SELSTOP")) {
      double  sample  = Convert.o2d(value);
      player.selStop = sample * decimate;
    } else if (name.equals("SCLEAR")) {
      player.old_marker = 0;
    } else if (name.equals("BLK")) {
      decimate = Convert.o2l(value);
    } else if (name.equals("TIME")) {
      double  targettime  = Convert.o2d(value);
      player.gotoTime(targettime);
    } else {
      return null;
    }
    return value;
  }
  public Object getKey(String name) {
    return null;
  }
  private static  String[]  keys          = {"NUMROWS", "NUMCOLS", "COLVECSIZE", "CURROW", "CURCOL", "SCELL", "CELL", "COLUMNVECTOR"};
  public String[] getKeys() {
    return keys;
  }

  /** An inner class that acts as an audio player host for UniCasts or Multicasts
   *  @author Jay Hallam
   *  @since  NeXtMidas 2.1.0
   */
  public class Player extends Thread { //implements Runnable {
    /** the associated nmucast object used for UniCasts or Multicasts */
    public                nmucast_g2          nmplayer        = null;
    private               FileInputStream  fis             = null;
    private               boolean          playing         = false;
    private               boolean          loaded          = false;
    private               boolean          run             = true;
    private               boolean          play            = false;
    private               boolean          stop            = false;
    private               boolean          setPos          = false;
    private               String           currentName     = null;
    private               int              magicNum        = 0;
    private               int              dataLocation    = 0;
    private               int              dataSize        = 0;
    private               int              dataFormat      = 0;
    private               int              sampleRate      = 0;
    private               int              channelCount    = 0;
    private               double           duration        = 0;
    private               double           time            = 0;
    private               double           timeLast        = 0;
    private               double           old_marker      = 0;
    private               double           selStart        = 0;
    private               double           selStop         = 0;
            static final  int              OFFSET          = 32;
            static final  boolean          DEBUG           = false;
    private               byte[]           auData          = null;
    private               byte[]           zeroData        = null;
    private               int              dataChunk       = 0;
    private               boolean          rollOver        = false;

    private               String           rthost;

    private               int              port, rtport, prange;
    private               int              seq             = 0xabcd, src, ts=0x00123456;
    private               DatagramPacket   rtpack, rtpack2;
    private               DatagramSocket   dsock;
    private               MulticastSocket  msock;
    private               boolean          multicast       = false;
    private               int              ttl             = 3;  // was 4, MJW said 3
    private               Random           rng             = new Random();
    private               InetAddress      rtaddr;
    private               byte[]           rtbuf;
    private               byte[]           rtbuf2;                                           // buffer used for PipeAudio stream
    private               short[]          decodeTable;

    private static final  String           typeList        = "LIN16,MULAW";                  //LIN16 ???
    private static final  int              T_LIN16         = 1, T_MULAW         = 2;
//        private static final  int              RTP_PT_LIN8     = 11, RTP_PT_LIN16    = 13, RTP_PT_PCMU     = 0;
    private static final  int              RTP_PT_LIN8     = 11, RTP_PT_LIN16    = 96, RTP_PT_PCMU     = 0;
    private static final  int              RTP_VERSION     = 2, RTP_BYE         = 203, RTP_CC          = 1;//RTP_CC=1
    private static final  int              VER             = 0, SEQ             = 2, TS              = 4, SSRC            = 8, CHAN            = 12, DATA            = 16;
    private static final  int              PLEN            = 2048;
    private               boolean          sockisok        = false;
    private               int              mode, chan            = 3, twotl;

    private               int              type;
    private               int              dataHead        = 0;
    private               int              dataStop        = 0;
    /** Bytes per second */
    public                int              BytesPerSec     = 8000;

                          byte[]           rdata           = null;
                          byte[]           newdata         = null;

    private               Feature          marker          = null;
    private               Feature          psdmarker       = null;
    private               Feature          psdmarker1       = null;
    private               Feature          psdmarker2       = null;
    private               Feature          psdmarker3       = null;
    private               Feature          psdmarker4       = null;
    private               Feature          psd2marker      = null;
    private               GPrompt          TimeCode        = null;
    private               int              chunk;
    private               int              markermultiply  = 1;

    /** Player Constructor
     *  @param nmplayer the associated nmucast object used for UniCasts or Multicasts
     */
    public Player(nmucast_g2 nmplayer) {
      this.nmplayer = nmplayer;
      playing = false;
      rtbuf2 = new byte[8192];
    }

    /** Initialize the audio stream pipe:
     *  1. Calls {@link #basicInit()} for basic initialization
     *  2. Calls {@link #init3()} for initializing the the second RTP DatagramPacket and header
     */
    public void initPipeAudio() {
      basicInit();
      init3();
    }

    /** Load input audio data
     *  @param object the name of the file with input audio data
     *  @return true if the audio was successfully loaded, false otherwise
     */
    public boolean load(Object object) {
      byte[]  header_buf  = new byte[OFFSET];
      File    fobject     = new File((String) object);
      currentName = new String((String) object);
      try {
        fis = new FileInputStream(fobject);
      } catch (Exception e) {
        throw new MidasException("Trouble OPENNING " + currentName, e);
        // NTN: The line below discards any nested exception and makes debugging difficult.
        //      Additionally the code below M.error(..) is never executed.
        // M.error("Trouble OPENNING " + currentName + " " + e);
        //  return false;
      }

      try {
        if (DEBUG) M.info("Opened " + currentName + " read " + fis.available());
        fis.read(header_buf, 0, OFFSET);                                                 // Read the Header
      } catch (Exception e) {
        throw new MidasException("Exception reading AU Header", e);
        // NTN: The line below discards any nested exception and makes debugging difficult.
        //      Additionally the code below M.error(..) is never executed.
        // M.error("Exception reading AU Header " + e);
        // return false;
      }

      magicNum = readIEEEBytes(header_buf, 0);
      dataLocation = readIEEEBytes(header_buf, 4);
      dataSize = readIEEEBytes(header_buf, 8);
      dataFormat = readIEEEBytes(header_buf, 12);
      sampleRate = readIEEEBytes(header_buf, 16);
      channelCount = readIEEEBytes(header_buf, 20);

      try {
        auData = new byte[dataSize + OFFSET];
      } catch (Exception e) {
        throw new MidasException("AU RTP Player could not allocate buffer", e);
        // NTN: The line below discards any nested exception and makes debugging difficult.
        //      Additionally the code below M.error(..) is never executed.
        // M.error("AU RTP Player could not allocate buffer");
        // return false;
      }

      try {
        fis.read(auData);                                                                // Read the DATA
        fis.close();
      } catch (Exception e) {
        throw new MidasException("Exception reading or closing AU Data", e);
        // NTN: The line below discards any nested exception and makes debugging difficult.
        //      Additionally the code below M.error(..) is never executed.
        // M.error("Exception reading or closing AU Data " + e);
        // return false;
      }

      duration = dataSize / (channelCount * sampleRate);
      switch (dataFormat) {
        case 1:
          type = T_MULAW;
          break;
        case 2:
          type = T_LIN16;
          break;
        default:
          M.error("AU File format " + dataFormat + " not supported");
          return false;
      }
      chunk = sampleRate / 10;

      if ((currentName.indexOf("gcs") > 0) || (currentName.indexOf("pipecut") > 0) || type == T_LIN16) {
        //System.out.println("This is a 201B audio cut");
        chunk = chunk * 2;
        BytesPerSec = 16000;
        markermultiply = 2;
      } else {
        BytesPerSec = 8000;
        markermultiply = 1;
      }

      twotl = chunk * 2;
      rdata = new byte[chunk];
      newdata = new byte[chunk * 2];
      zeroData = new byte[dataSize + 10];

      for (int z = 0; z < dataSize + 10; z++) {
        zeroData[z] = 0;
      }

      if (DEBUG) {
        M.info("magicNum        " + magicNum);
        M.info("dataLocation    " + dataLocation);
        M.info("dataSize        " + dataSize);
        M.info("dataFormat      " + dataFormat);
        M.info("sampleRate      " + sampleRate);
        M.info("channelCount    " + channelCount);
        M.info("duration        " + duration);
        switch (type) {
          case T_MULAW:
            M.info("Format          MULAW");
            break;
          case T_LIN16:
            M.info("Format          LIN16");
            break;
        }
        M.info("Chunk           " + chunk);
        M.info("twotl           " + twotl);
      }
      loaded = true;
      dataHead = OFFSET;
      init();
      init2();
      return true;
    }

    /** Audio player initialization:
     *  1. Calls {@link #basicInit()} for basic initialization
     *  2. Defines a new byte data buffer
     */
    public void init() {
      basicInit();
      rdata = new byte[chunk];
      rtbuf = new byte[DATA + twotl];                                                      //buffer must be big enough for packetized lin16 data as bytes
    }

    /** Audio player basic initialization: Connect to an RTP Socket Port */
    public void basicInit() {
      try {
        rtaddr = InetAddress.getByName(rthost);
        int  iprefix  = Integer.parseInt(rthost.substring(0, 3));
        if (iprefix >= 224 && iprefix <= 239) {
          multicast = true;
        }                                                                                //224.0.0.0 through 239.255.255.255
      } catch (Exception e) {}

      if (multicast) {
        try {
          InetSocketAddress  isa  = new InetSocketAddress(rthost, port);
          msock = new MulticastSocket(isa);
          // Update joinGroup API used in 3.9.3, as old API deprecated in JDK 14 NXM-3548
          msock.joinGroup(isa,msock.getNetworkInterface());
          msock.setTimeToLive(ttl);
        } catch (Exception e) {
          M.info("Opening socket: " + e.getMessage());
        }
      } else {
        while (dsock == null) {
          try {
            if (DEBUG) {
              M.info("Port is " + port);
            }
            dsock = new DatagramSocket(port);
            if (DEBUG) {
              M.info("Connected to RTP Socket port");
            }
          } catch (Exception e) {
            M.info("Opening socket: " + e.getMessage());
            if (--prange == 0) {
              return;
            }
            port++;
          }
        }
      }
      decodeTable = build_mu_decode_table();
    }

    /** Defines or resets the first RTP DatagramPacket and header */
    public void init2() {
      rtpack = new DatagramPacket(rtbuf, rtbuf.length, rtaddr, rtport);
      mode = 3;
      src = Math.abs(rng.nextInt());
      seq = src & 0xFFFF;
      makeHeader();
    }

    /** Defines or resets the second RTP DatagramPacket and header */
    public void init3() {
      rtpack2 = new DatagramPacket(rtbuf2, rtbuf2.length, rtaddr, rtport);
      mode = 3;
      src = Math.abs(rng.nextInt());
      seq = src & 0xFFFF;
      makeHeader2();
    }

    /** Start or resume audio playback */
    public void play() {
      try {
        if (loaded) {
          if (!playing) {
            dataStop = dataSize;
            rollOver = false;
            init2();
            play = true;
          }
        }
      } catch (Exception ex) {
        M.info("Exception in play " + ex);
      }
    }

    /** Stop audio playback */
    public void pstop() {
      if (loaded) {
        if (playing) {
          stop = true;
        }
      }
    }

    /** Set the flip frequency for inversion
     *  @param newff the new flip frequency
     */
    public void setFlipFreq (float newff) {
      float new_rate = sampleRate;
      if (sampleRate == 0.0) {
        new_rate = 8000;
        System.out.println("warning: (setFlipFreq), sample rate = 0, using default = "+new_rate);
      }
      if (newff < -new_rate/2.0) {
        flip_freq = -new_rate/(float)2.0;
      } else if (flip_freq > new_rate/2.0) {
        flip_freq = new_rate/(float)2.0;
      } else {
        flip_freq = newff;
      }
      audio_lo_inc = new Complex( (float)Math.cos(twopi*flip_freq/new_rate),(float)-Math.sin(twopi*flip_freq/new_rate));
      //System.out.println("NMUCAST: flip freq "+flip_freq+" rate "+new_rate+" audio_lo_inc "+audio_lo_inc.real()+" "+audio_lo_inc.imag() );
    }

    public void run() {
      while (run) {
        try {
          if (play && !playing && loaded) {
            if (DEBUG) M.info("Starting Playback");
            play = false;
            playing = true;
          } else if (playing && stop) {
            if (DEBUG) M.info("Stopping Playback");
            playing = false;
            stop = false;
          }

          if ((auData != null) && (loaded) && ((playing))) {
            time = Time.current();                                                   //MU.getTime();
            if ((time - timeLast) > (0.10 - .008)) {
              if (setPos) {
                //System.out.println("ThreadPos");
                sleep(400);
                setPos = false;
              }
              if ((dataHead + chunk) > dataStop) {
                dataChunk = dataStop - dataHead;                                 // Get Remainder
                rollOver = true;
                playing = false;
                System.arraycopy(zeroData, 0, rdata, 0, chunk);
                System.arraycopy(auData, dataHead, rtbuf, DATA, dataChunk);
              } else {
                dataChunk = chunk;
                System.arraycopy(auData, dataHead, rtbuf, DATA, dataChunk);      // mod
              }

              if (invert) {
                int nin = dataChunk / Data.getBPS(auType);  // Compute the number of elements
                // DEBUG_INVERT = (invertcount < 10);
                if (DEBUG_INVERT) {
                  System.out.println("-> START sampleRate " + sampleRate);
                  System.out.println("dataChunk " + dataChunk+ " nin " + nin + " invOutOffset "+ invOutOffset + " rtbuf.len "+ rtbuf.length);
                  System.out.println(" BYTES at" + " [DATA] "+ rtbuf[DATA] + " [DATA+1] "+ rtbuf[DATA + 1] + " [DATA+2] "
                      + rtbuf[DATA + 2] + " dataChunk-1 "+ rtbuf[dataChunk - 1]+ " dataChunk " + rtbuf[dataChunk]
                          + " dataChunk+2 "+ rtbuf[dataChunk + 2] + " last "+ rtbuf[rtbuf.length - 1]);
                  System.out.println(" audio_lo_inc "+audio_lo_inc.real()+" "+audio_lo_inc.imag());
                }

                // Move the SI values into an SF array. Put the data at the end of the array
                // because the filter states are saved at the beginning of the array
                for (int i = 0; i < nin; i++) {
                  int byteOffset = DATA+(i * Data.getBPS(auType));
                  short sval = Convert.unpackI(rtbuf, byteOffset);
                  finvInSave[invOutOffset + INVERT_OFFSET+i] = sval;
                }

                if (DEBUG_INVERT) {
                  System.out.println("finvInSave[0] " + finvInSave[0] + " [1] " + finvInSave[1] + " [2] "
                      + finvInSave[2] + " [invOutOffset] " + finvInSave[invOutOffset] + " [invOutOffset+1] " + finvInSave[invOutOffset + 1]
                          + " [nin-1] " + finvInSave[nin - 1] + " [nin] " + finvInSave[nin] + " [nin+1] " + finvInSave[nin + 1]
                              + " last " + finvInSave[finvInSave.length - 1]);
                }

                // Now perform the Hilbert transform and flip
                flip(finvInSave, flipout, nin);

                if (DEBUG_INVERT) {
                  System.out.println("flipout " + " [0] "+flipout[0] + " [1] "+flipout[INVERT_OFFSET] + " [2] "
                      + flipout[2] + " [invOutOffset] "+flipout[invOutOffset]+" [invOutOffset+1] "+flipout[invOutOffset + 1]
                          + " [nin-1] "+flipout[nin - 1]+" [nin] "+flipout[nin]+" [nin+1] " + flipout[nin + 1]
                              + " last "+flipout[flipout.length - 1]);
                }

                // Move the data back into the byte buffer
                int byteOffset = DATA;
                int flipOffset = INVERT_OFFSET;
                for (int i = 0; i < nin; i++) {
                  short shortVal = (short) flipout[flipOffset+ i];
                  Convert.packI(rtbuf, byteOffset, shortVal);
                  byteOffset = byteOffset+Data.getBPS(auType);
                }

                if (DEBUG_INVERT) {
                  System.out.println("--- END BYTES at "+" [DATA] "+rtbuf[DATA]+" [DATA+1] " + rtbuf[DATA + 1]
                      + " [DATA+2] " + rtbuf[DATA + 2]+" dataChunk-1 "+rtbuf[dataChunk - 1]+" dataChunk " + rtbuf[dataChunk]
                          + " dataChunk+2 "+ rtbuf[dataChunk + 2] + " last "+rtbuf[rtbuf.length - 1]);
                }
              }

              rtpack.setData(rtbuf, 0, chunk);
              pack(rtbuf, SEQ, seq, 2);
              pack(rtbuf, TS, ts, 4);
              rtpack.setLength(chunk + DATA);

              if (multicast) {
                msock.send(rtpack);
              } else {
                dsock.send(rtpack);
              }

              if (df != null) {
                try {
                  short[]  buf2  = data.castI(false);
                  for (int i = 0; i < chunk; i++) {
                    buf2[i] = rtbuf[i];
                  }
                  data.uncast(buf2, true);
                  df.write(data, chunk);
                } catch (Exception ex) {
                  M.info("Exception in mcData " + ex);
                }
              }
              seq++;
              ts += chunk;
              mymarker(dataHead);
              if (rollOver) {
                dataHead = OFFSET;
                rollOver = false;
                init2();
                MQ.put("PLAYDONE", 1, null, "MAIN");
              } else {
                dataHead += dataChunk;
              }
              timeLast = time;
            } else {
              sleep(10);
              //System.out.println("Sleeping");
            }
          } else {
            sleep(100);
          }
        } catch (Exception e) {
          M.info("Exception in PLAYER.run() " + e);
          e.printStackTrace();
          try {
            sleep(1000);
          } catch (Exception te) {}
        }
      }
    }

    private void pack(byte[] buf, int boff, int value, int bytes) {
      for (int i = bytes - 1; i >= 0; i--) {
        buf[boff + i] = (byte) (value & 0xFF);
        value = (value >> 8);
      }
    }

    /** Perform the Hilbert transform and flip
     *  @param in  the input data
     *  @param out the output data
     *  @param nin the number of elements
     */
    public void flip (float in[], float out[], int nin) {
      /* Note:  First invOutOffset samples of array IN are used to store old data.
                    Data in array IN are offset by invOutOffset = HILBERTSIZE-1 to facilitate
                    saving of data between subroutine calls.
       */
      //dch : so you can flip by negative - if (flip_freq .gt. 0.0) then
      if (flip_freq != 0.0) {
        // Do Hilbert transform and frequency shift
        for (int i = 1+hilb_ncoefs; i <= nin+hilb_ncoefs; i++) {
          float flipsum = hilbert[1]*(in[i+1]-in[i-1]);
          for (int j = 3; j <= hilb_ncoefs; j=j+2) {
            flipsum = flipsum + hilbert[j]*(in[i+j]-in[i-j]);
          }
          out[i-hilb_ncoefs] = (audio_lo.real()*in[i]) +(audio_lo.imag()*flipsum);
          audio_lo.mult(audio_lo_inc);
        }
        // keep the magnitude of the LO from running away
        audio_lo.div(audio_lo.abs());
      } else {           // no freq shift necessary
        for (int i=1; i<=nin; i++) {
          out[i] = in[i+invOutOffset];
        }
      }           // freq shift necessary?
      for(int i = 1; i<=invOutOffset; i++) {
        in[i] = in[i+nin];
      }
    } // METHOD FLIP

    /** Jump to a different time in the audio player
     *  @param targetTime the new time
     */
    public void gotoTime(double targetTime) {
      int  newHead  = 0;
      newHead = (int) ((targetTime - baseTime) * BytesPerSec);
      if (newHead < OFFSET) {
        M.info("Caught bad position, " + newHead + " fixing");
        newHead = OFFSET;
      } else if (newHead > dataSize) {
        M.info("Caught bad position, " + newHead + " fixing");
        newHead = dataSize;
      }
      dataHead = newHead + (800);
      mymarker(dataHead);
    }

    /** Sets the byte offset of the audio player data marker
     *  @param head the raw byte offset
     */
    public void mymarker(int head) {
      if (marker != null) {
        marker.setX((head / (BytesPerSec / 8000)) / decimate);
      }
      if (psdmarker != null) {
        double  target  = baseTime + ((double) head / BytesPerSec);
        psdmarker.setY(target);


        if (psdmarker1!= null)  psdmarker1.setY(target);
        if (psdmarker2!= null)  psdmarker2.setY(target);
        if (psdmarker3!= null)  psdmarker3.setY(target);
        if (psdmarker4!= null)  psdmarker4.setY(target);
      }





      if (psd2marker != null) {
        double  target  = baseTime + ((double) head / BytesPerSec);
        psd2marker.setY(target);
      }
      if(TimeCode !=null){
        String  tstr  = Time.toString(((double) dataHead / BytesPerSec) + baseTime, 9);
        tstr = tstr.substring(tstr.indexOf("::") + 2);
        //System.out.println("Time = "+tstr);
        TimeCode.setValue(tstr);
      }
    }

    private int readIEEEBytes(byte[] bytes, int offset) {
      return (((bytes[offset] & 0xFF) << 24) | ((bytes[offset + 1] & 0xFF) << 16) |
          ((bytes[offset + 2] & 0xFF) << 8) | ((bytes[offset + 3] & 0xFF)));
    }

    private short[] build_mu_decode_table() {
      short[]  convbuf  = new short[256];
      for (int i = 0; i < 256; i++) {
        int  j  = i - 128;
        int  x  = j ^ 127;
        int  S  = 1 - (2 * ((x & 128) / 128));
        int  L  = (x & 112) / 16;
        int  V  = x & 15;
        convbuf[i] = (short) Math.round(((S * (Math.pow(2, L) * (2 * V + 33) - 33) * 32767.0) / 8031.0));
      }
      return convbuf;
    }

    /** Set the audio player position
     *  @param value the position value
     */
    public void setPosition(int value) {
      if (value < OFFSET) {
        M.info("Caught bad position, " + value + " fixing");
        value = OFFSET;
      } else if (value > dataSize) {
        M.info("Caught bad position, " + value + " fixing " + dataSize);
        value = dataSize - 800;
      }
      dataHead = value * markermultiply;
      setPos = true;
      if (!playing) {
        mymarker(dataHead);
      }
    }

    private void setMode(int value) {
      if (value == 5) {
        seq ^= 0x8000;
        return;
      }
      if (value == 4) {
        chan = 0;
      } else {
        chan = value;
      }
      this.mode = value;
      pack(rtbuf, CHAN, chan, 4);
      sockisok = true;
    }

    private void setMode2(int value) {
      if (value == 5) {
        seq ^= 0x8000;
        return;
      }
      if (value == 4) {
        chan = 0;
      } else {
        chan = value;
      }
      this.mode = value;
      pack(rtbuf2, CHAN, chan, 4);
      sockisok = true;
    }

    private void makeHeader2() {
      rtbuf2[VER + 0] = (byte) (RTP_VERSION << 6 | RTP_CC);
      if (type == T_LIN16)
        rtbuf2[VER + 1] = (byte) (RTP_PT_LIN16);
      else
        rtbuf2[VER + 1] = (byte) (RTP_PT_PCMU);
      //rtpack2.setLength(4608);
      pack(rtbuf2, SSRC, src, 4);
      pack(rtbuf2, CHAN, chan, 4);
      pack(rtbuf2, SEQ, seq, 2);
      setMode2(mode);
    }

    private void makeHeader() {
      rtbuf[VER + 0] = (byte) (RTP_VERSION << 6 | RTP_CC);
      if (type == T_LIN16)
        rtbuf[VER + 1] = (byte) (RTP_PT_LIN16);
      else
        rtbuf[VER + 1] = (byte) (RTP_PT_PCMU);
      rtpack.setLength(DATA + twotl);
      pack(rtbuf, SSRC, src, 4);
      pack(rtbuf, CHAN, chan, 4);
      pack(rtbuf, SEQ, seq, 2);
      setMode(mode);
    }

    /** Set the audio type
     *  @param value the audio type as an int
     */
    public void setType(String value) {
      setType(Parser.find(typeList, value, type));
    }

    private void setType(int value) {
      this.type = value;
      sockisok = true;
    }

    /** Close the socket connection
     *  @return the command status
     */
    public int close() {
      loaded = false;
      auData = null;
      run = false;
      try {
        rtbuf[VER + 0] = (byte) (RTP_VERSION << 6 | RTP_CC);
        rtbuf[VER + 1] = (byte) (RTP_BYE);
        pack(rtbuf, 2, 2, 2);
        pack(rtbuf, 4, src, 4);
        rtpack.setPort(rtport + 1);
        rtpack.setLength(8);
        dsock.send(rtpack);
      } catch (Exception e) {}
      if (multicast) {
        try {
          // Update leaveGroup API used in 3.9.3, as old API deprecated in JDK 14
          InetSocketAddress  isa  = new InetSocketAddress(rthost, port);
          msock.leaveGroup(isa,msock.getNetworkInterface());
          msock.close();
        } catch (Exception e) {
          System.out.println("Really dead");
        }
        msock = null;
      } else {
        dsock.close();
      }
      return (NORMAL);
    }
  }
}