package nxm.sys.lib;

import nxm.sys.inc.Units;

/** A class to define and handle Audio Format Data files.

    @author  Jeff Schoen / Neon Ngo (implementation)
*/
public class AudioFile extends DataFile {

  private String version;

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

  public String getVersion() { return version; }

  /** Read the header into local buffer, extend if necessary */
  protected void readHeader () {
    ioh.read (hb,0,512,0L);
    version = Convert.unpackS(hb,0,4);
         if (version.equals("RIFF")) convertWFHeader();
    else if (version.equals("RIFX")) convertWFHeader();
    else if (version.equals(".snd")) convertAUHeader();
    else throw new MidasException("AudioFile "+getURL()+" does not have a valid Audio Header");
  }


  /** Converts a .wav file header (RIFF = little-endian, RIFX = big-endian).
      <pre>
        RIFF Header Structure

        OFFSET  TYPE     NAME            DESCRIPTION
        ------------------------------------------------------------------------------------------
         0      char[4]  ChunkID       - "RIFF" (little-endian data) or "RIFX" (big-endian data).
                                         (RIFF Header Chunk).
         4      uint     ChunkSize     - The size of the rest of the chunks following this number
                                         (32-bit unsigned integer). I.e. this is the length of the
                                         file minux (-) 8.
         8      char[4]  Format        - "WAVE"
        ------------------------------------------------------------------------------------------
        12      char[4]  Subchunk1ID   - "fmt " (Format Chunk specifies the characteristics of the
                                         audio data necessary to decompress it and play it.)
        16      uint     Subchunk1Size - Size of the rest of the Subchunk following this number
                                         (32-bit unsigned integer). This will always be 16 for PCM.
        20      short    AudioFormat   - Codec ID, 1 for PCM (linear quantization), other values
                                         indicate use of compression. 7 for 8-bit ITU-T G.711 u-law.
                                         See RFC 2361 WAVE and AVI Codec Registries.
        22      short    NumChannels   - 1=MONO, 2=STEREO, etc.
        24      int      SampleRate    - 8000, 44100 (Standard CD), etc. in Hz
        28      int      ByteRate      - SampleRate * NumChannels * BitsPerSample/8
        32      short    BlockAlign    - NumChannels * BitsPerSample/8
                                         (The number of bytes per sample including all channels.)
        34      short    BitsPerSample - 8, 16, etc.
        ------------------------------------------------------------------------------------------
        [Some formats insert additional parameters at this position, PCM does not]
        ------------------------------------------------------------------------------------------
        36      char[4]  Subchunk2ID   - "data" (Data Chunk contains the actual sound data)
        40      int      Subchunk1Size - NumSamples * NumChannels * BitsPerSample/8
                                         (This is the number of bytes in the data.)
        44      n/a      Data          - Data start position.

        Note:   All numbers in the header are in little-endian format.
        Source: http://ccrma.stanford.edu/courses/422/projects/WaveFormat/
      </pre>
   */
  private void convertWFHeader() {
    String id       = Convert.unpackS(hb,0,4);
    int    codec    = Convert.unpackI(hb,20,EEEI);
    int    numChan  = Convert.unpackI(hb,22,EEEI); // num channels
    int    rate     = Convert.unpackL(hb,24,EEEI);
    int    bits     = Convert.unpackI(hb,34,EEEI);
    int    sub1size = Convert.unpackL(hb,40,EEEI);

    for (int i=0; i<512; i++) hb[i]=0;
    initMain();
         if (numChan==1) setFormatMode(SCALAR);
    else if (numChan==2) setFormatMode(COMPLEX);
    else throw new MidasException("WaveFile: format must be S=mono or C=stereo");
         if (bits==8)  setFormatType(BYTE);
    else if (bits==16) setFormatType(INT);
    else if (bits==32) setFormatType(LONG);
    else throw new MidasException("WaveFile: bits="+bits+" not allowed");
    if (rate>0) setXDelta( 1.0/rate );
    setXUnits(Units.TIME_S);
         if (id.equals("RIFF")) setDataRep("EEEI");
    else if (id.equals("RIFX")) setDataRep("IEEE");
    else throw new MidasException("WaveFile: file type must be RIFF or RIFX, given '"+id+"'.");
    setDataStart(44.0);
    setDataSize((double)sub1size);
    setBlueVerIO();
    // add comment this is automatically converted from a WAV file
    keywords.putMain("SOURCE", id+" WAVE "+bits+"-bit "+rate+"-Hz codec="+codec, false);
  }

  /** Au data encoding format to Midas Format type (B, I, L, etc.) mapping.
      <pre> Au encoding format:
          1 =  8-bit G.711 mu-law
          2 =  8-bit linear PCM
          3 = 16-bit linear PCM
          4 = 24-bit linear PCM
          5 = 32-bit linear PCM
          6 = 32-bit IEEE floating point
          7 = 64-bit IEEE floating point
          8 = Fragmented sample data
          9 = DSP program
         10 =  8-bit fixed point
         11 = 16-bit fixed point
         12 = 24-bit fixed point
         13 = 32-bit fixed point
         18 = 16-bit linear with emphasis
         19 = 16-bit linear compressed
         20 = 16-bit linear with emphasis and compression
         21 = Music kit DSP commands
         23 = ITU-T G.721 (4-bit ISDN u-law compressed using the ITU-T
                           G.721 ADPCM voice data encoding scheme)
         24 = ITU-T G.722 ADPCM
         25 = ITU-T G.723 3-bit ADPCM
         26 = ITU-T G.723 5-bit ADPCM
         27 = 8-bit G.711 A-law

         Note: PCM data appear to be encoded as signed, rather than unsigned.
       </pre>
       @since NeXtMidas 3.0.0
   */
  private static byte AuEncodingToMidasFormatType (int encoding) {
    byte type = BYTE; // defaults to byte
    switch (encoding) {
      case 1: case 2: case 10: case 27: break; // 8-bit
      case 3: case 11: case 18: case 19: case 20: type = INT; break; // 16-bit
      case 5: case 13: type = LONG; break; // 32-bit
      case 6: type = FLOAT;  break;
      case 7: type = DOUBLE; break;
      // all other encoding formats are unsupported
      default: throw new MidasException("DataFile: Unsupported Au encoding type: "+encoding);
    }
    return type;
  }

  /** Converts a .au file header (Au audio file format).
      <pre>
        Au Header Structure

        OFFSET  TYPE     NAME            DESCRIPTION
        ------------------------------------------------------------------------------------------
         0     char[4]  MagicNumber   - ".snd"
         4     uint     data offset   - the offset to the data in bytes.
         8     uint     data size     - data sie in bytes. If unknown, the value of 0xffffffff should be used.
        12     unit     encoding      - data encoding format (see {@link #AuEncodingToMidasFormatType(int)}
        16     uint     sample rate   - the number of samples/second, e.g., 8000
        20     unit     channles      - the number of interleaved channels, 1=mono, 2=stereo, etc.

        Note:   All numbers in the header are in big-endian format.
        Source: http://en.wikipedia.org/wiki/Au_file_format
        Author: Neon Ngo (implementation)
       </pre>
       @since NeXtMidas 3.0.0 (implemented)
    */
  private void convertAUHeader () {
    String magicNum = Convert.unpackS(hb,  0, 4);
    int    offset   = Convert.unpackL(hb,  4, IEEE);
    int    size     = Convert.unpackL(hb,  8, IEEE);
    int    encoding = Convert.unpackL(hb, 12, IEEE);
    int    rate     = Convert.unpackL(hb, 16, IEEE);
    int    channels = Convert.unpackL(hb, 20, IEEE);

    for (int i=0; i<512; i++) hb[i]=0; // clear hcb
    initMain();
    setDataRep("IEEE");

    if      (channels==1) setFormatMode(SCALAR);
    else if (channels==2) setFormatMode(COMPLEX);
    else throw new MidasException("DataFile: Au format must be S=mono or C=stereo, channels="+channels);

    setFormatType(AuEncodingToMidasFormatType(encoding));

    if (rate>0) setXDelta( 1.0/rate );
    setXUnits(Units.TIME_S);

    setDataStart(offset);
    setDataSize((double)size);
    setBlueVerIO();
    // add comment this is automatically converted from a .au file
    keywords.putMain("SOURCE", magicNum+" Au "+rate+"-Hz codec="+encoding, false);
  }

}
