/*
 *  Copyright 2006 Columbia University.
 *
 *  This file is part of MEAPsoft.
 *
 *  MEAPsoft is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  MEAPsoft is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with MEAPsoft; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 *  02110-1301 USA
 *
 *  See the file "COPYING" for the text of the license.
 */

package com.meapsoft;

import java.io.File;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.SourceDataLine;

/**
 * Can write to a file or to an audio stream.  For some reason this
 * appears to be difficult to do in the JavaSound framework.  This is
 * just a wrapper class to unify the interface.
 *
 * @author Mike Mandel (mim@ee.columbia.edu)
 */

public class AudioWriter implements Runnable {
  AudioFormat format;
  File file;
  AudioFileFormat.Type targetType;

  SourceDataLine sdl;
  PipedOutputStream pos;
  PipedInputStream pis;
  AudioInputStream ais;
  byte[] bytes;

  // Write to a source data line.  The line should be open before
  // passing it in here.
  public AudioWriter(SourceDataLine sdl) {
    this.sdl = sdl;
    format = sdl.getFormat();
    sdl.start();
  }

  // Write to a file
  public AudioWriter(File file, AudioFormat format, 
		     AudioFileFormat.Type targetType) throws IOException {
    //System.out.println("AudioWriter File constructor");
    this.format = format;
    this.targetType = targetType;
    this.file = file;

    // Write to the output stream
    pos = new PipedOutputStream();

    // It will then go to the file via the input streams
    pis = new PipedInputStream(pos);
    ais = new AudioInputStream(pis, format, AudioSystem.NOT_SPECIFIED);
    
    new Thread(this).start();
  }

  public void run() {
    try {
      AudioSystem.write(ais, targetType, file);
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  public void write(double[] data) throws IOException {
      write(data, data.length);
  }

  public void write(double[] data, int length) throws IOException {
    // Allocate a new bytes array if necessary.  If bytes is too long,
    // don't worry about it, just use as much as is needed.
    int numBytes = length * format.getFrameSize();
    if(bytes == null || numBytes > bytes.length)
      bytes = new byte[numBytes];

    // Limit data to [-1, 1]
    limit(data);

    // Convert doubles to bytes using format
    doubles2bytes(data, bytes, length);

    // write it
    if(pos != null)
      pos.write(bytes, 0, numBytes);
    if(sdl != null)
      sdl.write(bytes, 0, numBytes);
  }

  // Perform memoryless limiting on the audio data to keep all samples
  // in [-1,1]
  public static void limit(double[] data) {
    double t = 0.8;
    double c = 2*(1-t)/Math.PI;

    for(int i=0; i<data.length; i++) {
      if(data[i] > t) {
	data[i] = c*Math.atan((data[i]-t)/c)+t;
      } else if(data[i] < -t) {
	data[i] = c*Math.atan((data[i]+t)/c)-t;
      }
    }
  }

  public void write(byte[] bytes) throws IOException {
    if(pos != null)
      pos.write(bytes, 0, bytes.length);
    if(sdl != null)
      sdl.write(bytes, 0, bytes.length);
  }

  public void close() throws IOException {
    if(pos != null) {
      ais.close();
      pis.close();
      pos.close();
    }
    if(sdl != null)
      sdl.close();
  }
  
  public AudioFormat getFormat() { return format; }


  public void doubles2bytes(double[] audioData, byte[] audioBytes) {
      doubles2bytes(audioData, audioBytes, audioData.length);
  }

  public void doubles2bytes(double[] audioData, byte[] audioBytes, int length) {
    int in;
    if (format.getSampleSizeInBits() == 16) {
      if (format.isBigEndian()) {
	for (int i = 0; i < length; i++) {
	  in = (int)(audioData[i]*32767);
	  /* First byte is MSB (high order) */
	  audioBytes[2*i] = (byte)(in >> 8);
	  /* Second byte is LSB (low order) */
	  audioBytes[2*i+1] = (byte)(in & 255);
	}
      } else {
	for (int i = 0; i < length; i++) {
	  in = (int)(audioData[i]*32767);
	  /* First byte is LSB (low order) */
	  audioBytes[2*i] = (byte)(in & 255);
	  /* Second byte is MSB (high order) */
	  audioBytes[2*i+1] = (byte)(in >> 8);
	}
      }
    } else if (format.getSampleSizeInBits() == 8) {
      if (format.getEncoding().toString().startsWith("PCM_SIGN")) {
	for (int i = 0; i < length; i++) {
	  audioBytes[i] = (byte)(audioData[i]*127);
	}
      } else {
	for (int i = 0; i < length; i++) {
	  audioBytes[i] = (byte)(audioData[i]*127 + 127);
	}
      }
    }
  }


  // From http://www.jsresources.org/examples/AudioRecorder.java.html
//   private void writeJSR() {
//     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//     OutputStream outputStream = byteArrayOutputStream;
//     // TODO: intelligent size
//     byte[] abBuffer = new byte[65536];
//     AudioFormat	format = m_line.getFormat();
//     int	nFrameSize = format.getFrameSize();
//     int	nBufferFrames = abBuffer.length / nFrameSize;
//     m_bRecording = true;
//     while (m_bRecording) {
//       if (sm_bDebug)
// 	out("BufferingRecorder.run(): trying to read: " + nBufferFrames);
//       int nFramesRead = m_line.read(abBuffer, 0, nBufferFrames);
//       if (sm_bDebug) 
// 	out("BufferingRecorder.run(): read: " + nFramesRead);
//       int nBytesToWrite = nFramesRead * nFrameSize;
//       try {
// 	outputStream.write(abBuffer, 0, nBytesToWrite);
//       }
//       catch (IOException e) {
// 	e.printStackTrace();
//       }
//     }
    
//     // We close the ByteArrayOutputStream
//     try {
//       byteArrayOutputStream.close();
//     } catch (IOException e) {
//       e.printStackTrace();
//     }


//     byte[] abData = byteArrayOutputStream.toByteArray();
//     ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(abData);

//     AudioInputStream audioInputStream = 
//       new AudioInputStream(byteArrayInputStream, format, 
// 			   abData.length / format.getFrameSize());
//     try {
//       AudioSystem.write(audioInputStream,  m_targetType, m_file);
//     } catch (IOException e) {
//       e.printStackTrace();
//     }
//   }
}
