/*-------------- Telecommunications & Signal Processing Lab ---------------

Routine:
  AFILE *AFrdAIhead (FILE *fp)

Purpose:
  Get file format information from an AIFF or AIFF-C audio file

Description:
  This routine reads the header for an AIFF or AIFF-C audio file.  The header
  information is used to set the file data format information in the audio
  file pointer structure.

  AIFF or AIFF-C audio file:
   Offset Length Type    Contents
      0     4    char   File identifier ("FORM")
      4     4    int    Form chunk length
      8     4    char   File identifier ("AIFF" or "AIFC")
    ...   ...    ...    ...
      C     4    char   COMM chunk identifier ("COMM")
    C+4     4    int    COMM chunk length
    C+8     2    int    Number of interleaved channels
    C+10    4    int    Number of sample frames
    C+14    2    int    Bits per sample
    C+16   10    float  Sample frames per second
    C+26    4    int    Compression type
    C+30  ...    char   Compression name
    ...    ...   ...    ...
      S     4    char   SSND chunk identifier ("SSND")
    S+4     4    int    SSND chunk length
    S+8     4    int    Data offset
    S+12    4    int    Block size
    S+16  ...    ...    Audio data
  8-bit mu-law, 8-bit A-law, 8-bit integer, and 16-bit integer data formats are
  supported.

Parameters:
  <-  AFILE *AFrdAIhead
      Audio file pointer for the audio file
   -> FILE *fp
      File pointer for the file

Author / revision:
  P. Kabal  Copyright (C) 1998
  $Revision: 1.41 $  $Date: 1998/06/26 20:40:55 $

-------------------------------------------------------------------------*/

static char rcsid [] = "$Id: AFrdAIhead.c 1.41 1998/06/26 libtsp-v3r0 $";

#include <setjmp.h>
#include <string.h>

#include <libtsp.h>
#include <libtsp/nucleus.h>
#include <libtsp/AFdataio.h>
#include <libtsp/AFheader.h>
#include <libtsp/AFmsg.h>
#include <libtsp/AFpar.h>
#include <libtsp/AIpar.h>

#define ICEILV(n,m)	(((n) + ((m) - 1)) / (m))	/* int n,m >= 0 */
#define RNDUPV(n,m)	((m) * ICEILV (n, m))		/* Round up */

#define SAME_CSTR(str,ref) 	(memcmp (str, ref, sizeof (str)) == 0)

#define ALIGN		2	/* Chunks padded out to a multiple of ALIGN */

/* setjmp / longjmp environment */
extern jmp_buf AFR_JMPENV;

static int
AF_rdFORM p_((FILE *fp, long int *Lfile, int *Ftype));
static int
AF_rdCOMM p_((FILE *fp, int Size, int Ftype, struct AI_CkCOMM *CkCOMM));
static void
AF_decCOMM p_((struct AI_CkCOMM *CkCOMM, int *Format, double *ScaleF));
static int
AF_checkFVER p_((FILE *fp, int Size));
static long int
AF_decSSND p_((FILE *fp, long int Size, long int *Ldata));
static long int
AF_skipSSND p_((FILE *fp, long int Size, long int Ldata));


AFILE *
AFrdAIhead (fp)

     FILE *fp;

{
  AFILE *AFp;
  long int offs, Lfile, Dstart, Ldata;
  int Ftype, Format, AtData, N;
  double ScaleF;
  struct AF_info Hinfo;
  char Info[AF_MAXINFO];

  struct AI_CkPreamb CkHead;
  struct AI_CkCOMM CkCOMM;

/* Set the long jump environment; on error return a NULL */
  if (setjmp (AFR_JMPENV))
    return NULL;	/* Return from a header read error */

/* Each chunk has an identifier and a length.  The length gives the number of
   bytes in the chunk (not including the ckID or ckDataSize fields).  If the
   ckDataSize field is odd, a zero byte fills the space before the start of the
   next chunk.
   - Some AIFF-C files do not have this zero byte at the end of the file,
     i.e. the total file length is odd, yet the FORM chunk size indicates that
     the padding byte is there.
   - The chunks after the FORM chunk can appear in any order.
   - The standard is written in such a way that it is implied that one should
     scan the whole file for chunks.  Here we are trying to allow for non-
     seekable input.
     - If the COMM chunk comes ahead of the SSND chunk all is fine.  We get to
       the SSND chunk and then skip forward to the data.
     - If the COMM chunk follows the SSND chunk we will have to seek back to
       the data in the SSND chunk.  The file has to be seekable.
     - If the number of samples is zero (in the COMM chunk), there need be no
       SSND chunk.  When we get to the end-of-file, we will declare this to be
       the start of data.
*/

/* Check the FORM chunk */
  offs = AF_rdFORM (fp, &Lfile, &Ftype);

/* Pick up the required chunks */
  Dstart = 0L;
  Ldata = -1L;
  AtData = 0;
  Hinfo.Info = Info;
  Hinfo.N = 0;
  Format = FD_UNDEF;
  while (offs < Lfile && (Format == FD_UNDEF || Ldata < 0L)) {

    /* Read the chunk preamble */
    offs += RHEAD_S (fp, CkHead.ckID);
    offs += RHEAD_V (fp, CkHead.ckDataSize, DS_EB);

    /* COMM chunk */
    if (SAME_CSTR (CkHead.ckID, CKID_COMM)) {
      offs += AF_rdCOMM (fp, (int) CkHead.ckDataSize, Ftype, &CkCOMM);
      AF_decCOMM (&CkCOMM, &Format, &ScaleF);
    }

    /* FVER chunk */
    else if (Ftype == FT_AIFF_C && SAME_CSTR (CkHead.ckID, CKID_FVER))
      offs += AF_checkFVER (fp, (int) CkHead.ckDataSize);

    /* Text chunks */
    else if (SAME_CSTR (CkHead.ckID, CKID_NAME))
      offs += AFrdHtext (fp, (int) CkHead.ckDataSize, "name: ",
			 &Hinfo, ALIGN);
    else if (SAME_CSTR (CkHead.ckID, CKID_AUTHOR))
      offs += AFrdHtext (fp, (int) CkHead.ckDataSize, "author: ",
			 &Hinfo, ALIGN);
    else if (SAME_CSTR (CkHead.ckID, CKID_COPYRIGHT))
      offs += AFrdHtext (fp, (int) CkHead.ckDataSize, "copyright: ",
			 &Hinfo, ALIGN);

    /* Annotation chunk */
    else if (SAME_CSTR (CkHead.ckID, CKID_ANNOTATION)) {
      N = Hinfo.N;
      offs += AFrdHinfo (fp, (int) CkHead.ckDataSize, &Hinfo, ALIGN);
      if (Hinfo.N == N)
	offs += AFrdHtext (fp, (int) CkHead.ckDataSize, "annotation: ",
			   &Hinfo, ALIGN);
    }

    /* SSND chunk */
    else if (SAME_CSTR (CkHead.ckID, CKID_SSND)) {
      offs += AF_decSSND (fp, (long int) CkHead.ckDataSize, &Ldata);
      Dstart = offs;
      AtData = 1;
      if (Format == FD_UNDEF) {		/* No COMM chunk found */
	offs += AF_skipSSND (fp, (long int) CkHead.ckDataSize, Ldata);
	AtData = 0;
      }
    }

    /* Miscellaneous chunks */
    else
      offs += RSKIP (fp, RNDUPV (CkHead.ckDataSize, ALIGN));
 
  }
  if (Format == FD_UNDEF) {
    UTwarn ("AFrdAIhead - %s", AFM_AIFF_NoCOMM);
    return NULL;
  }

  /* If no SSND chunk has been found, check for a zero size file (does not
     need a SSND chunk
  */
  if (Dstart == 0L) {
    if (CkCOMM.numSampleFrames == 0) {
      Ldata = 0L;
      AtData = 1;
      Dstart = offs;
    }
    else {
      UTwarn ("AFrdAIhead: %s", AFM_AIFF_NoSSND);
      return NULL;
    }
  }

  /* If the COMM chunk followed the SSND chunk (! AtData), position to the
     start of data
  */
  if (! AtData) {
    if (AFseek (fp, Dstart, NULL))
      return NULL;
  }

/* Set the parameters for file access */
  AFp = AFsetRead (fp, Ftype, Format, DS_EB, UTdIEEE80 (CkCOMM.sampleRate),
		   ScaleF, (long int) CkCOMM.numChannels, Ldata,
		   (long int) (CkCOMM.numSampleFrames * CkCOMM.numChannels),
		   &Hinfo, AF_NOFIX);

  return AFp;
}

/* Check the RIFF file preamble:  On error return via longjmp */


static int
AF_rdFORM (fp, Lfile, Ftype)

     FILE *fp;
     long int *Lfile;
     int *Ftype;

{
  int offs;
  struct AI_CkPreamb CkHead;

  offs = 0;
  offs += RHEAD_S (fp, CkHead.ckID);
  if (! SAME_CSTR (CkHead.ckID, FM_IFF)) {
    UTwarn ("AFrdAIhead: %s", AFM_AIFF_BadId);
    longjmp (AFR_JMPENV, 1);
  }

  offs += RHEAD_V (fp, CkHead.ckDataSize, DS_EB);
  *Lfile = CkHead.ckDataSize + offs;

  offs += RHEAD_S (fp, CkHead.ckID);
  if (SAME_CSTR (CkHead.ckID, FM_AIFF_C))
    *Ftype = FT_AIFF_C;
  else if (SAME_CSTR (CkHead.ckID, FM_AIFF))
    *Ftype = FT_AIFF;
  else {
    UTwarn ("AFrdAIhead: %s", AFM_AIFF_BadId);
    longjmp (AFR_JMPENV, 1);
  }

  return offs;
}

/* Read the COMM chunk */


static int
AF_rdCOMM (fp, Size, Ftype, CkCOMM)

     FILE *fp;
     int Ftype;
     int Size;
     struct AI_CkCOMM *CkCOMM;

{
  int offs;
  unsigned char slen[1];

  offs = RHEAD_V (fp, CkCOMM->numChannels, DS_EB);
  offs += RHEAD_V (fp, CkCOMM->numSampleFrames, DS_EB);
  offs += RHEAD_V (fp, CkCOMM->sampleSize, DS_EB);
  offs += RHEAD_S (fp, CkCOMM->sampleRate);

  /* Set the compressionType field for both AIFF and AIFF-C files */
  if (Ftype == FT_AIFF_C) {
    offs += RHEAD_S (fp, CkCOMM->compressionType);
    offs += RHEAD_S (fp, slen);	/* 1 byte string length */
    offs += RSKIP (fp, (int) slen[0]);
  }
  else
    memcpy (CkCOMM->compressionType, CT_NONE, sizeof CT_NONE);

  /* Some AIFF-C COMM chunks are declared to be of length 18 (the size for
     AIFF files) when they are in fact larger.  Here we find the actual
     length of the COMM chunk.
  */
  if (offs > Size) {
    UTwarn ("AFrdAIhead - %s", AFM_AIFF_FixCOMM);
    Size = offs;
  }
  offs += RSKIP (fp, RNDUPV (Size, ALIGN) - offs);

  return offs;
}

/* Decode the data format: On error return via longjmp */


static void
AF_decCOMM (CkCOMM, Format, ScaleF)

     struct AI_CkCOMM *CkCOMM;
     int *Format;
     double *ScaleF;

{
  /* Uncompressed */
  if (SAME_CSTR (CkCOMM->compressionType, CT_NONE)) {
    if (CkCOMM->sampleSize == 16) {
      *Format = FD_INT16;
      *ScaleF = AI_SF_NONE16;
    }
    else if (CkCOMM->sampleSize == 8) {
      *Format = FD_INT8;
      *ScaleF = AI_SF_NONE8;
    }
    else {
      UTwarn ("AFrdAIhead: %s: \"%d\"", AFM_AIFF_UnsSSize,
	      (int) CkCOMM->sampleSize);
      longjmp (AFR_JMPENV, 1);
    }
  }

  /* AIFF-C, compressed */
  /* A-law and mu-law are compressed formats; for these formats
   CkCOMM.sampleSize = 16 (not checked) */
  else if (SAME_CSTR (CkCOMM->compressionType, CT_ULAW)) {
    *Format = FD_MULAW8;
    *ScaleF = AI_SF_ULAW;
  }
  else if (SAME_CSTR (CkCOMM->compressionType, CT_ALAW)) {
    *Format = FD_ALAW8;
    *ScaleF = AI_SF_ALAW;
  }
  else {
    UTwarn ("AFrdAIhead - %s: \"%.4s\"", AFM_AIFF_UnsComp,
	    CkCOMM->compressionType);
    longjmp (AFR_JMPENV, 1);
  }

  return;
} 

/* Check the format version */


static int
AF_checkFVER (fp, Size)

     FILE *fp;
     int Size;

{
  int offs;
  struct AI_CkFVER CkFVER;
  
  offs = RHEAD_V (fp, CkFVER.timestamp, DS_EB);
  if (CkFVER.timestamp != AIFCVersion1)
    UTwarn ("AFrdAIhead - %s", AFM_AIFF_BadVer);
  offs += RSKIP (fp, RNDUPV (Size, ALIGN) - offs);

  return offs;
}

/* Decode the SSND chunk
   Position left at the beginning of data or at the end of the SSND chunk
*/


static long int
AF_decSSND (fp, Size, Ldata)

  FILE *fp;
  long int Size;
  long int *Ldata;

{
  long int offs;
  struct AI_CkSSND CkSSND;

  /* Data size, skip to start of data */
  offs  = RHEAD_V (fp, CkSSND.offset, DS_EB);
  offs += RHEAD_V (fp, CkSSND.blockSize, DS_EB);
  offs += RSKIP (fp, CkSSND.offset);

  *Ldata = Size - offs;

  return offs;
}

static long int
AF_skipSSND (fp, Size, Ldata)

     FILE *fp;
     long int Size;
     long int Ldata;

{
  int ErrCode;
  long int offs, Dstart;

  if (FLseekable (fp)) {
    Dstart = AFtell (fp, &ErrCode);
    if (ErrCode)
      longjmp (AFR_JMPENV, 1);

    /* Seek to the end of the SSND chunk */
    offs = RNDUPV (Size, ALIGN) - Size + Ldata;
    if (AFseek (fp, RNDUPV (Dstart + offs, ALIGN), NULL))
      longjmp (AFR_JMPENV, 1);
  }
  else {
    UTwarn ("AFrdAIhead - %s", AFM_AIFF_RRAccess);
    longjmp (AFR_JMPENV, 1);
  }

  return offs;
}
