/******************************************************************************
 *                                                                            *
 * Copyright (c) 1999-2003 Wimba S.A., All Rights Reserved.                   *
 *                                                                            *
 * COPYRIGHT:                                                                 *
 *      This software is the property of Wimba S.A.                           *
 *      This software is redistributed under the Xiph.org variant of          *
 *      the BSD license.                                                      *
 *      Redistribution and use in source and binary forms, with or without    *
 *      modification, are permitted provided that the following conditions    *
 *      are met:                                                              *
 *      - Redistributions of source code must retain the above copyright      *
 *      notice, this list of conditions and the following disclaimer.         *
 *      - Redistributions in binary form must reproduce the above copyright   *
 *      notice, this list of conditions and the following disclaimer in the   *
 *      documentation and/or other materials provided with the distribution.  *
 *      - Neither the name of Wimba, the Xiph.org Foundation nor the names of *
 *      its contributors may be used to endorse or promote products derived   *
 *      from this software without specific prior written permission.         *
 *                                                                            *
 * WARRANTIES:                                                                *
 *      This software is made available by the authors in the hope            *
 *      that it will be useful, but without any warranty.                     *
 *      Wimba S.A. is not liable for any consequence related to the           *
 *      use of the provided software.                                         *
 *                                                                            *
 * Class: OggSpeexWriter.java                                                 *
 *                                                                            *
 * Author: Marc GIMPEL                                                        *
 *                                                                            *
 * Date: 9th April  2003                                                      *
 *                                                                            *
 ******************************************************************************/

/* $Id: OggSpeexWriter.java,v 1.2 2004/10/21 16:21:57 mgimpel Exp $ */

package org.xiph.speex;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.util.Random;

/**
 * Ogg Speex Writer
 * 
 * @author Marc Gimpel, Wimba S.A. (mgimpel@horizonwimba.com)
 * @version $Revision: 1.2 $
 */
public class OggSpeexWriter
  extends AudioFileWriter
{
  /** Number of packets in an Ogg page (must be less than 255) */
  public static final int PACKETS_PER_OGG_PAGE = 250;
  
  /** The OutputStream */
  private OutputStream out;

  /** Defines the encoder mode (0=NB, 1=WB and 2-UWB). */
  private int     mode;
  /** Defines the sampling rate of the audio input. */
  private int     sampleRate;
  /** Defines the number of channels of the audio input (1=mono, 2=stereo). */
  private int     channels;
  /** Defines the number of frames per speex packet. */
  private int     nframes;
  /** Defines whether or not to use VBR (Variable Bit Rate). */
  private boolean vbr;
  /** */
  private int     size;
  /** Ogg Stream Serial Number */
  private int     streamSerialNumber;
  /** Data buffer */
  private byte[]  dataBuffer;
  /** Pointer within the Data buffer */
  private int     dataBufferPtr;
  /** Header buffer */
  private byte[]  headerBuffer;
  /** Pointer within the Header buffer */
  private int     headerBufferPtr;
  /** Ogg Page count */
  private int     pageCount;
  /** Speex packet count within an Ogg Page */
  private int     packetCount;
  /**
   * Absolute granule position
   * (the number of audio samples from beginning of file to end of Ogg Packet).
   */
  private long    granulepos;
  
  /**
   * Builds an Ogg Speex Writer. 
   */
  public OggSpeexWriter()
  {
    if (streamSerialNumber == 0)
      streamSerialNumber = new Random().nextInt();
    dataBuffer         = new byte[65565];
    dataBufferPtr      = 0;
    headerBuffer       = new byte[255];
    headerBufferPtr    = 0;
    pageCount          = 0;
    packetCount        = 0;
    granulepos         = 0;
  }

  /**
   * Builds an Ogg Speex Writer. 
   * @param mode       the mode of the encoder (0=NB, 1=WB, 2=UWB).
   * @param sampleRate the number of samples per second.
   * @param channels   the number of audio channels (1=mono, 2=stereo, ...).
   * @param nframes    the number of frames per speex packet.
   * @param vbr
   */
  public OggSpeexWriter(final int mode,
                        final int sampleRate,
                        final int channels,
                        final int nframes,
                        final boolean vbr)
  {
    this();
    setFormat(mode, sampleRate, channels, nframes, vbr);
  }

  /**
   * Sets the output format.
   * Must be called before WriteHeader().
   * @param mode       the mode of the encoder (0=NB, 1=WB, 2=UWB).
   * @param sampleRate the number of samples per second.
   * @param channels   the number of audio channels (1=mono, 2=stereo, ...).
   * @param nframes    the number of frames per speex packet.
   * @param vbr
   */
  private void setFormat(final int mode,
                         final int sampleRate,
                         final int channels,
                         final int nframes,
                         boolean vbr)
  {
    this.mode       = mode;
    this.sampleRate = sampleRate;
    this.channels   = channels;
    this.nframes    = nframes;
    this.vbr        = vbr;
  }

  /**
   * Sets the Stream Serial Number.
   * Must not be changed mid stream.
   * @param serialNumber
   */
  public void setSerialNumber(final int serialNumber)
  {
    this.streamSerialNumber = serialNumber;
  }

  /**
   * Closes the output file.
   * @exception IOException if there was an exception closing the Audio Writer.
   */
  public void close()
    throws IOException 
  {
    flush(true);
    out.close(); 
  }
  
  /**
   * Open the output file. 
   * @param file - file to open.
   * @exception IOException if there was an exception opening the Audio Writer.
   */
  public void open(final File file)
    throws IOException
  {
    file.delete(); 
    out = new FileOutputStream(file);
    size = 0;   
  }

  /**
   * Open the output file. 
   * @param filename - file to open.
   * @exception IOException if there was an exception opening the Audio Writer.
   */
  public void open(final String filename)
    throws IOException 
  {
    open(new File(filename)); 
  }
  
  /**
   * Writes the header pages that start the Ogg Speex file. 
   * Prepares file for data to be written.
   * @param comment description to be included in the header.
   * @exception IOException
   */
  public void writeHeader(final String comment)
    throws IOException
  {
    int chksum;
    byte[] header;
    byte[] data;
    /* writes the OGG header page */
    header = buildOggPageHeader(2, 0, streamSerialNumber, pageCount++, 1,
                                new byte[] {80});
    data = buildSpeexHeader(sampleRate, mode, channels, vbr, nframes);
    chksum = OggCrc.checksum(0, header, 0, header.length);
    chksum = OggCrc.checksum(chksum, data, 0, data.length);
    writeInt(header, 22, chksum);
    out.write(header);
    out.write(data);
    /* writes the OGG comment page */
    header = buildOggPageHeader(0, 0, streamSerialNumber, pageCount++, 1,
                                new byte[] {(byte) (comment.length() + 8)});
    data = buildSpeexComment(comment);
    chksum = OggCrc.checksum(0, header, 0, header.length);
    chksum = OggCrc.checksum(chksum, data, 0, data.length);
    writeInt(header, 22, chksum);
    out.write(header);
    out.write(data);
  }
  
  /**
   * Writes a packet of audio. 
   * @param data - audio data.
   * @param offset - the offset from which to start reading the data.
   * @param len - the length of data to read.
   * @exception IOException
   */
  public void writePacket(final byte[] data,
                          final int offset,
                          final int len)
    throws IOException 
  {
    if (len <= 0) { // nothing to write
      return;
    }
    if (packetCount > PACKETS_PER_OGG_PAGE) {
      flush(false);
    }
    System.arraycopy(data, offset, dataBuffer, dataBufferPtr, len);
    dataBufferPtr += len;
    headerBuffer[headerBufferPtr++]=(byte)len;
    packetCount++;
    granulepos += nframes * (mode==2 ? 640 : (mode==1 ? 320 : 160));
  }

  /**
   * Flush the Ogg page out of the buffers into the file.
   * @param eos - end of stream
   * @exception IOException
   */
  private void flush(final boolean eos)
    throws IOException
  {
    int chksum;
    byte[] header;
    /* writes the OGG header page */
    header = buildOggPageHeader((eos ? 4 : 0), granulepos, streamSerialNumber,
                                pageCount++, packetCount, headerBuffer);
    chksum = OggCrc.checksum(0, header, 0, header.length);
    chksum = OggCrc.checksum(chksum, dataBuffer, 0, dataBufferPtr);
    writeInt(header, 22, chksum);
    out.write(header);
    out.write(dataBuffer, 0, dataBufferPtr);
    dataBufferPtr   = 0;
    headerBufferPtr = 0;
    packetCount     = 0;
  }
}
