shithub: sox

ref: 6763cba31dea9a680d10d085e5e8e3f04e8cf3ce
dir: /src/prc.c/

View raw version
/*
 * Psion Record format (format of sound files used for EPOC machines).
 * The file normally has no extension, so SoX uses .prc (Psion ReCord).
 * Based (heavily) on the wve.c format file. 
 * Hacked by Bert van Leeuwen (bert@e.co.za)
 * 
 * Header check improved, ADPCM encoding added, and other improvements
 * by Reuben Thomas <rrt@sc3d.org>, using file format info at
 * http://software.frodo.looijaard.name/psiconv/formats/
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, write to the Free Software
 * Foundation, Fifth Floor, 51 Franklin Street, Boston, MA 02111-1301,
 * USA.
 *
 * Includes code for ADPCM framing based on code carrying the
 * following copyright:
 *
 *******************************************************************
 Copyright 1992 by Stichting Mathematisch Centrum, Amsterdam, The
 Netherlands.
 
                        All Rights Reserved

 Permission to use, copy, modify, and distribute this software and its
 documentation for any purpose and without fee is hereby granted,
 provided that the above copyright notice appear in all copies and that
 both that copyright notice and this permission notice appear in 
 supporting documentation, and that the names of Stichting Mathematisch
 Centrum or CWI not be used in advertising or publicity pertaining to
 distribution of the software without specific, written prior permission.

 STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
 THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
 FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
 OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 ******************************************************************/

 
#include "sox_i.h"

#include "adpcms.h"

#include <assert.h>
#include <string.h>
#include <errno.h>
#include <limits.h>

typedef struct prcpriv {
  uint32_t nsamp, nbytes;
  short padding;
  short repeats;
  off_t data_start;         /* for seeking */
  struct adpcm_io adpcm;
  unsigned frame_samp;     /* samples left to read in current frame */
} *prc_t;

static void prcwriteheader(sox_format_t * ft);

static int seek(sox_format_t * ft, sox_size_t offset)
{
  prc_t p = (prc_t)ft->priv;
  return sox_offset_seek(ft, p->data_start, offset);
}

static int startread(sox_format_t * ft)
{
  prc_t p = (prc_t)ft->priv;
  char head[sizeof(prc_header)];
  uint8_t byte;
  uint16_t reps;
  uint32_t len, listlen, encoding, repgap;
  unsigned char volume;
  char appname[0x40]; /* Maximum possible length of name */

  /* Check the header */
  if (prc_checkheader(ft, head))
    sox_debug("Found Psion Record header");
  else {
      sox_fail_errno(ft,SOX_EHDR,"Not a Psion Record file");
      return (SOX_EOF);
  }

  sox_readb(ft, &byte);
  if ((byte & 0x3) != 0x2) {
    sox_fail_errno(ft, SOX_EHDR, "Invalid length byte for application name string %d", (int)(byte));
    return SOX_EOF;
  }

  byte >>= 2;
  assert(byte < 64);
  sox_reads(ft, appname, byte);
  if (strncasecmp(appname, "record.app", byte) != 0) {
    sox_fail_errno(ft, SOX_EHDR, "Invalid application name string %.63s", appname);
    return SOX_EOF;
  }
        
  sox_readdw(ft, &len);
  p->nsamp = len;
  sox_debug("Number of samples: %d", len);

  sox_readdw(ft, &encoding);
  sox_debug("Encoding of samples: %x", encoding);
  if (encoding == 0)
    ft->encoding.encoding = SOX_ENCODING_ALAW;
  else if (encoding == 0x100001a1)
    ft->encoding.encoding = SOX_ENCODING_IMA_ADPCM;
  else {
    sox_fail_errno(ft, SOX_EHDR, "Unrecognised encoding");
    return SOX_EOF;
  }

  sox_readw(ft, &reps);    /* Number of repeats */
  sox_debug("Repeats: %d", reps);
        
  sox_readb(ft, &volume);
  sox_debug("Volume: %d", (unsigned)volume);
  if (volume < 1 || volume > 5)
    sox_warn("Volume %d outside range 1..5", volume);

  sox_readb(ft, &byte);   /* Unused and seems always zero */

  sox_readdw(ft, &repgap); /* Time between repeats in usec */
  sox_debug("Time between repeats (usec): %u", repgap);

  sox_readdw(ft, &listlen); /* Length of samples list */
  sox_debug("Number of bytes in samples list: %u", listlen);

  if (ft->signal.rate != 0 && ft->signal.rate != 8000)
    sox_report("PRC only supports 8 kHz; overriding.");
  ft->signal.rate = 8000;

  if (ft->signal.channels != 1 && ft->signal.channels != 0)
    sox_report("PRC only supports 1 channel; overriding.");
  ft->signal.channels = 1;

  p->data_start = sox_tell(ft);
  ft->length = p->nsamp / ft->signal.channels;

  if (ft->encoding.encoding == SOX_ENCODING_ALAW) {
    ft->encoding.bits_per_sample = 8;
    if (sox_rawstartread(ft))
      return SOX_EOF;
  } else if (ft->encoding.encoding == SOX_ENCODING_IMA_ADPCM) {
    p->frame_samp = 0;
    if (sox_adpcm_ima_start(ft, &p->adpcm))
      return SOX_EOF;
  }

  return (SOX_SUCCESS);
}

/* Read a variable-length encoded count */
/* Ignore return code of sox_readb, as it doesn't really matter if EOF
   is delayed until the caller. */
static unsigned read_cardinal(sox_format_t * ft)
{
  unsigned a;
  uint8_t byte;

  if (sox_readb(ft, &byte) == SOX_EOF)
    return (unsigned)SOX_EOF;
  sox_debug_more("Cardinal byte 1: %x", byte);
  a = byte;
  if (!(a & 1))
    a >>= 1;
  else {
    if (sox_readb(ft, &byte) == SOX_EOF)
      return (unsigned)SOX_EOF;
    sox_debug_more("Cardinal byte 2: %x", byte);
    a |= byte << 8;
    if (!(a & 2))
      a >>= 2;
    else if (!(a & 4)) {
      if (sox_readb(ft, &byte) == SOX_EOF)
        return (unsigned)SOX_EOF;
      sox_debug_more("Cardinal byte 3: %x", byte);
      a |= byte << 16;
      if (sox_readb(ft, &byte) == SOX_EOF)
        return (unsigned)SOX_EOF;
      sox_debug_more("Cardinal byte 4: %x", byte);
      a |= byte << 24;
      a >>= 3;
    }
  }

  return a;
}

static sox_size_t read_samples(sox_format_t * ft, sox_sample_t *buf, sox_size_t samp)
{
  prc_t p = (prc_t)ft->priv;

  sox_debug_more("length now = %d", p->nsamp);

  if (ft->encoding.encoding == SOX_ENCODING_IMA_ADPCM) {
    sox_size_t nsamp, read;

    if (p->frame_samp == 0) {
      unsigned framelen = read_cardinal(ft);
      uint32_t trash;

      if (framelen == (unsigned)SOX_EOF)
        return 0;

      sox_debug_more("frame length %d", framelen);
      p->frame_samp = framelen;

      /* Discard length of compressed data */
      sox_debug_more("compressed length %d", read_cardinal(ft));
      /* Discard length of BListL */
      sox_readdw(ft, &trash);
      sox_debug_more("list length %d", trash);

      /* Reset CODEC for start of frame */
      sox_adpcm_reset(&p->adpcm, ft->encoding.encoding);
    }
    nsamp = min(p->frame_samp, samp);
    p->nsamp += nsamp;
    read = sox_adpcm_read(ft, &p->adpcm, buf, nsamp);
    p->frame_samp -= read;
    sox_debug_more("samples left in this frame: %d", p->frame_samp);
    return read;
  } else {
    p->nsamp += samp;
    return sox_rawread(ft, buf, samp);
  }
}

static int stopread(sox_format_t * ft)
{
  prc_t p = (prc_t)ft->priv;

  if (ft->encoding.encoding == SOX_ENCODING_IMA_ADPCM)
    return sox_adpcm_stopread(ft, &p->adpcm);
  else
    return SOX_SUCCESS;
}

/* When writing, the header is supposed to contain the number of
   data bytes written, unless it is written to a pipe.
   Since we don't know how many bytes will follow until we're done,
   we first write the header with an unspecified number of bytes,
   and at the end we rewind the file and write the header again
   with the right size.  This only works if the file is seekable;
   if it is not, the unspecified size remains in the header
   (this is illegal). */

static int startwrite(sox_format_t * ft)
{
  prc_t p = (prc_t)ft->priv;

  if (ft->encoding.encoding == SOX_ENCODING_ALAW) {
    if (sox_rawstartwrite(ft))
      return SOX_EOF;
  } else if (ft->encoding.encoding == SOX_ENCODING_IMA_ADPCM) {
    if (sox_adpcm_ima_start(ft, &p->adpcm))
      return SOX_EOF;
  }
        
  p->nsamp = 0;
  p->nbytes = 0;
  if (p->repeats == 0)
    p->repeats = 1;

  prcwriteheader(ft);

  p->data_start = sox_tell(ft);

  return SOX_SUCCESS;
}

static void write_cardinal(sox_format_t * ft, unsigned a)
{
  uint8_t byte;

  if (a < 0x80) {
    byte = a << 1;
    sox_debug_more("Cardinal byte 1: %x", byte);
    sox_writeb(ft, byte);
  } else if (a < 0x8000) {
    byte = (a << 2) | 1;
    sox_debug_more("Cardinal byte 1: %x", byte);
    sox_writeb(ft, byte);
    byte = a >> 6;
    sox_debug_more("Cardinal byte 2: %x", byte);
    sox_writeb(ft, byte);
  } else {
    byte = (a << 3) | 3;
    sox_debug_more("Cardinal byte 1: %x", byte);
    sox_writeb(ft, byte);
    byte = a >> 5;
    sox_debug_more("Cardinal byte 2: %x", byte);
    sox_writeb(ft, byte);
    byte = a >> 13;
    sox_debug_more("Cardinal byte 3: %x", byte);
    sox_writeb(ft, byte);
    byte = a >> 21;
    sox_debug_more("Cardinal byte 4: %x", byte);
    sox_writeb(ft, byte);
  }
}

static sox_size_t write_samples(sox_format_t * ft, const sox_sample_t *buf, sox_size_t samp)
{
  prc_t p = (prc_t)ft->priv;
  /* Psion Record seems not to be able to handle frames > 800 samples */
  samp = min(samp, 800);
  p->nsamp += samp;
  sox_debug_more("length now = %d", p->nsamp);
  if (ft->encoding.encoding == SOX_ENCODING_IMA_ADPCM) {
    sox_size_t written;

    write_cardinal(ft, samp);
    /* Write compressed length */
    write_cardinal(ft, (samp / 2) + (samp % 2) + 4);
    /* Write length again (seems to be a BListL) */
    sox_debug_more("list length %d", samp);
    sox_writedw(ft, samp);
    sox_adpcm_reset(&p->adpcm, ft->encoding.encoding);
    written = sox_adpcm_write(ft, &p->adpcm, buf, samp);
    sox_adpcm_flush(ft, &p->adpcm);
    return written;
  } else
    return sox_rawwrite(ft, buf, samp);
}

static int stopwrite(sox_format_t * ft)
{
  prc_t p = (prc_t)ft->priv;

  p->nbytes = sox_tell(ft) - p->data_start;

  if (!ft->seekable) {
      sox_warn("Header will have invalid file length since file is not seekable");
      return SOX_SUCCESS;
  }

  if (sox_seeki(ft, 0, 0) != 0) {
      sox_fail_errno(ft,errno,"Can't rewind output file to rewrite Psion header.");
      return(SOX_EOF);
  }
  prcwriteheader(ft);
  return SOX_SUCCESS;
}

static void prcwriteheader(sox_format_t * ft)
{
  prc_t p = (prc_t)ft->priv;

  sox_writebuf(ft, prc_header, sizeof(prc_header));
  sox_writes(ft, "\x2arecord.app");

  sox_debug("Number of samples: %d",p->nsamp);
  sox_writedw(ft, p->nsamp);

  if (ft->encoding.encoding == SOX_ENCODING_ALAW)
    sox_writedw(ft, 0);
  else
    sox_writedw(ft, 0x100001a1); /* ADPCM */
  
  sox_writew(ft, 0);             /* Number of repeats */
  sox_writeb(ft, 3);             /* Volume: use default value of Record.app */
  sox_writeb(ft, 0);             /* Unused and seems always zero */
  sox_writedw(ft, 0);            /* Time between repeats in usec */

  sox_debug("Number of bytes: %d", p->nbytes);
  sox_writedw(ft, p->nbytes);    /* Number of bytes of data */
}

SOX_FORMAT_HANDLER(prc)
{
  static char const * const names[]           = {"prc", NULL};
  static sox_rate_t   const write_rates[]     = {8000, 0};
  static unsigned     const write_encodings[] = {
    SOX_ENCODING_ALAW, 8, 0,
    SOX_ENCODING_IMA_ADPCM, 4, 0,
    0};
  static sox_format_handler_t const handler = {
    "Psion Record; used in EPOC devices (Series 5, Revo and similar)",
    names, SOX_FILE_LIT_END | SOX_FILE_MONO,
    startread, read_samples, stopread,
    startwrite, write_samples, stopwrite,
    seek, write_encodings, write_rates
  };
  return &handler;
}