/* libSoX direct to Sun Audio Driver
 *
 * Added by Chris Bagwell (cbagwell@sprynet.com) on 2/26/96
 * Based on oss handler.
 *
 * Cleaned up changes of format somewhat in sunstartwrite on 03/31/98
 *
 */

/*
 * Copyright 1997 Chris Bagwell And Sundry Contributors
 * This source code is freely redistributable and may be used for
 * any purpose.  This copyright notice must be maintained.
 * Rick Richardson, Lance Norskog And Sundry Contributors are not
 * responsible for the consequences of using this software.
 */

#include "sox_i.h"
#include "g711.h"

#include <sys/ioctl.h>
#ifdef HAVE_SUN_AUDIOIO_H
  #include <sun/audioio.h>
#else
  typedef unsigned int u_int;
  typedef unsigned char u_char;
  #include <sys/audioio.h>
#endif
#if HAVE_STROPTS_H
#include <stropts.h>
#endif
#include <fcntl.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

typedef struct
{
    char* pOutput;
    unsigned cOutput;
    int device;
    unsigned sample_shift;
} priv_t;

/*
 * Do anything required before you start reading samples.
 * Read file header.
 *      Find out sampling rate,
 *      size and encoding of samples,
 *      mono/stereo/quad.
 */
static int sunstartread(sox_format_t * ft)
{
    char const* szDevname;
    priv_t* pPriv = (priv_t*)ft->priv;
 
    size_t samplesize, encoding;
    audio_info_t audio_if;
#ifdef __SVR4
    audio_device_t audio_dev;
#endif
    char simple_hw=0;

    lsx_set_signal_defaults(ft);

    if (ft->filename == 0 || ft->filename[0] == 0 || !strcasecmp("default", ft->filename)) {
        szDevname = "/dev/audio";
    } else {
        szDevname = ft->filename;
    }

    pPriv->device = open(szDevname, O_RDONLY);
    if (pPriv->device < 0) {
        lsx_fail_errno(ft, errno, "open failed for device %s", szDevname);
        return SOX_EOF;
    }

    if (ft->encoding.encoding == SOX_ENCODING_UNKNOWN) ft->encoding.encoding = SOX_ENCODING_ULAW;

#ifdef __SVR4
    /* Read in old values, change to what we need and then send back */
    if (ioctl(pPriv->device, AUDIO_GETDEV, &audio_dev) < 0) {
        lsx_fail_errno(ft,errno,"unable to get information for device %s", szDevname);
        return(SOX_EOF);
    }
    lsx_report("hardware detected:  %s",audio_dev.name);
    if (strcmp("SUNW,am79c30",audio_dev.name) == 0)
    {
        simple_hw = 1;
    }
#endif

    /* If simple hardware detected in force data to ulaw. */
    if (simple_hw)
    {
        if (ft->encoding.bits_per_sample == 8)
        {
            if (ft->encoding.encoding != SOX_ENCODING_ULAW &&
                ft->encoding.encoding != SOX_ENCODING_ALAW)
            {
                lsx_warn("detected simple hardware; forcing output to ULAW");
                ft->encoding.encoding = SOX_ENCODING_ULAW;
            }
        }
        else if (ft->encoding.bits_per_sample == 16)
        {
            lsx_warn("detected simple hardware; forcing output to ULAW");
            ft->encoding.bits_per_sample = 8;
            ft->encoding.encoding = SOX_ENCODING_ULAW;
        }
    }

    if (ft->encoding.bits_per_sample == 8) {
        samplesize = 8;
        pPriv->sample_shift = 0;
        if (ft->encoding.encoding != SOX_ENCODING_ULAW &&
            ft->encoding.encoding != SOX_ENCODING_ALAW &&
            ft->encoding.encoding != SOX_ENCODING_SIGN2) {
            lsx_fail_errno(ft,SOX_EFMT,"driver only supports ULAW, ALAW, and signed linear for bytes");
                return (SOX_EOF);
        }
        if ((ft->encoding.encoding == SOX_ENCODING_ULAW ||
             ft->encoding.encoding == SOX_ENCODING_ALAW) &&
            ft->signal.channels == 2)
        {
            lsx_warn("only support mono for ULAW and ALAW data; forcing to mono");
            ft->signal.channels = 1;
        }
    }
    else if (ft->encoding.bits_per_sample == 16) {
        samplesize = 16;
        pPriv->sample_shift = 1;
        if (ft->encoding.encoding != SOX_ENCODING_SIGN2) {
            lsx_fail_errno(ft,SOX_EFMT,"driver only supports signed linear for words");
            return(SOX_EOF);
        }
    }
    else {
        lsx_fail_errno(ft,SOX_EFMT,"driver only supports bytes and words");
        return(SOX_EOF);
    }

    if (ft->signal.channels == 0)
        ft->signal.channels = 1;
    else if (ft->signal.channels > 1) {
        lsx_warn("some Sun audio devices can not play stereo at all");
        lsx_warn("or sometimes only with signed words. If the sound");
        lsx_warn("seems sluggish, try forcing the output to signed words");
        lsx_warn("or use the avg filter to reduce the number of channels.");
        ft->signal.channels = 2;
    }

    /* Read in old values, change to what we need and then send back */
    if (ioctl(pPriv->device, AUDIO_GETINFO, &audio_if) < 0) {
        lsx_fail_errno(ft,errno,"unable to initialize %s", szDevname);
        return(SOX_EOF);
    }
    audio_if.record.precision = samplesize;
    audio_if.record.channels = ft->signal.channels;
    audio_if.record.sample_rate = ft->signal.rate;
    if (ft->encoding.encoding == SOX_ENCODING_ULAW)
        encoding = AUDIO_ENCODING_ULAW;
    else if (ft->encoding.encoding == SOX_ENCODING_ALAW)
        encoding = AUDIO_ENCODING_ALAW;
    else
        encoding = AUDIO_ENCODING_LINEAR;
    audio_if.record.encoding = encoding;

    ioctl(pPriv->device, AUDIO_SETINFO, &audio_if);
    if (audio_if.record.precision != samplesize) {
        lsx_fail_errno(ft,errno,"unable to initialize sample size for %s", szDevname);
        return(SOX_EOF);
    }
    if (audio_if.record.channels != ft->signal.channels) {
        lsx_fail_errno(ft,errno,"unable to initialize number of channels for %s", szDevname);
        return(SOX_EOF);
    }
    if (audio_if.record.sample_rate != ft->signal.rate) {
        lsx_fail_errno(ft,errno,"unable to initialize rate for %s", szDevname);
        return(SOX_EOF);
    }
    if (audio_if.record.encoding != encoding) {
        lsx_fail_errno(ft,errno,"unable to initialize encoding for %s", szDevname);
        return(SOX_EOF);
    }
    /* Flush any data in the buffers - its probably in the wrong format */
#if defined(__NetBSD__) || defined(__OpenBSD__)
    ioctl(pPriv->device, AUDIO_FLUSH);
#elif defined __GLIBC__
    ioctl(pPriv->device, (unsigned long int)I_FLUSH, FLUSHR);
#else
    ioctl(pPriv->device, I_FLUSH, FLUSHR);
#endif

    pPriv->cOutput = 0;
    pPriv->pOutput = NULL;

    return (SOX_SUCCESS);
}

static int sunstartwrite(sox_format_t * ft)
{
    size_t samplesize, encoding;
    audio_info_t audio_if;
#ifdef __SVR4
    audio_device_t audio_dev;
#endif
    char simple_hw=0;
    char const* szDevname;
    priv_t* pPriv = (priv_t*)ft->priv;
 
    if (ft->filename == 0 || ft->filename[0] == 0 || !strcasecmp("default", ft->filename)) {
        szDevname = "/dev/audio";
    } else {
        szDevname = ft->filename;
    }

    pPriv->device = open(szDevname, O_WRONLY);
    if (pPriv->device < 0) {
        lsx_fail_errno(ft, errno, "open failed for device: %s", szDevname);
        return SOX_EOF;
    }

#ifdef __SVR4
    /* Read in old values, change to what we need and then send back */
    if (ioctl(pPriv->device, AUDIO_GETDEV, &audio_dev) < 0) {
        lsx_fail_errno(ft,errno,"unable to get device information");
        return(SOX_EOF);
    }
    lsx_report("hardware detected:  %s",audio_dev.name);
    if (strcmp("SUNW,am79c30",audio_dev.name) == 0)
    {
        simple_hw = 1;
    }
#endif

    if (simple_hw)
    {
        if (ft->encoding.bits_per_sample == 8)
        {
            if (ft->encoding.encoding != SOX_ENCODING_ULAW &&
                ft->encoding.encoding != SOX_ENCODING_ALAW)
            {
                lsx_warn("detected simple hardware; forcing output to ULAW");
                ft->encoding.encoding = SOX_ENCODING_ULAW;
            }
        }
        else if (ft->encoding.bits_per_sample == 16)
        {
            lsx_warn("detected simple hardware; forcing output to ULAW");
            ft->encoding.bits_per_sample = 8;
            ft->encoding.encoding = SOX_ENCODING_ULAW;
        }
    }

    if (ft->encoding.bits_per_sample == 8)
    {
        samplesize = 8;
        pPriv->sample_shift = 0;
        if (ft->encoding.encoding == SOX_ENCODING_UNKNOWN)
            ft->encoding.encoding = SOX_ENCODING_ULAW;
        else if (ft->encoding.encoding != SOX_ENCODING_ULAW &&
            ft->encoding.encoding != SOX_ENCODING_ALAW &&
            ft->encoding.encoding != SOX_ENCODING_SIGN2) {
            lsx_report("Sun audio driver only supports ULAW, ALAW, and Signed Linear for bytes");
            lsx_report("forcing to ULAW");
            ft->encoding.encoding = SOX_ENCODING_ULAW;
        }
        if ((ft->encoding.encoding == SOX_ENCODING_ULAW ||
             ft->encoding.encoding == SOX_ENCODING_ALAW) &&
            ft->signal.channels == 2)
        {
            lsx_warn("only mono is supported for ULAW and ALAW data; forcing to mono");
            ft->signal.channels = 1;
        }

    }
    else if (ft->encoding.bits_per_sample == 16) {
        samplesize = 16;
        pPriv->sample_shift = 1;
        if (ft->encoding.encoding == SOX_ENCODING_UNKNOWN)
            ft->encoding.encoding = SOX_ENCODING_SIGN2;
        else if (ft->encoding.encoding != SOX_ENCODING_SIGN2) {
            lsx_report("only signed linear is supported for words; forcing to signed linear");
            ft->encoding.encoding = SOX_ENCODING_SIGN2;
        }
    }
    else {
        lsx_report("only bytes and words are supported");
        ft->encoding.bits_per_sample = 16;
        ft->encoding.encoding = SOX_ENCODING_SIGN2;
        samplesize = 16;
        pPriv->sample_shift = 1;
    }

    if (ft->signal.channels > 1) ft->signal.channels = 2;

    /* Read in old values, change to what we need and then send back */
    if (ioctl(pPriv->device, AUDIO_GETINFO, &audio_if) < 0) {
        lsx_fail_errno(ft,errno,"unable to initialize /dev/audio");
        return(SOX_EOF);
    }
    audio_if.play.precision = samplesize;
    audio_if.play.channels = ft->signal.channels;
    audio_if.play.sample_rate = ft->signal.rate;
    if (ft->encoding.encoding == SOX_ENCODING_ULAW)
        encoding = AUDIO_ENCODING_ULAW;
    else if (ft->encoding.encoding == SOX_ENCODING_ALAW)
        encoding = AUDIO_ENCODING_ALAW;
    else
        encoding = AUDIO_ENCODING_LINEAR;
    audio_if.play.encoding = encoding;

    ioctl(pPriv->device, AUDIO_SETINFO, &audio_if);
    if (audio_if.play.precision != samplesize) {
        lsx_fail_errno(ft,errno,"unable to initialize sample size for /dev/audio");
        return(SOX_EOF);
    }
    if (audio_if.play.channels != ft->signal.channels) {
        lsx_fail_errno(ft,errno,"unable to initialize number of channels for /dev/audio");
        return(SOX_EOF);
    }
    if (audio_if.play.sample_rate != ft->signal.rate) {
        lsx_fail_errno(ft,errno,"unable to initialize rate for /dev/audio");
        return(SOX_EOF);
    }
    if (audio_if.play.encoding != encoding) {
        lsx_fail_errno(ft,errno,"unable to initialize encoding for /dev/audio");
        return(SOX_EOF);
    }

    pPriv->cOutput = sox_globals.bufsiz >> pPriv->sample_shift;
    pPriv->pOutput = lsx_malloc((size_t)pPriv->cOutput << pPriv->sample_shift);

    ft->fp = &pPriv->device;

    return (SOX_SUCCESS);
}

static int sunstop(sox_format_t* ft)
{
    priv_t* pPriv = (priv_t*)ft->priv;
    if (pPriv->device >= 0) {
        close(pPriv->device);
    }
    if (pPriv->pOutput) {
        free(pPriv->pOutput);
    }
    ft->fp = NULL;
    return SOX_SUCCESS;
}

typedef sox_uint16_t sox_uint14_t;
typedef sox_uint16_t sox_uint13_t;
typedef sox_int16_t sox_int14_t;
typedef sox_int16_t sox_int13_t;
#define SOX_ULAW_BYTE_TO_SAMPLE(d,clips)   SOX_SIGNED_16BIT_TO_SAMPLE(sox_ulaw2linear16(d),clips)
#define SOX_ALAW_BYTE_TO_SAMPLE(d,clips)   SOX_SIGNED_16BIT_TO_SAMPLE(sox_alaw2linear16(d),clips)
#define SOX_SAMPLE_TO_ULAW_BYTE(d,c) sox_14linear2ulaw(SOX_SAMPLE_TO_UNSIGNED(14,d,c) - 0x2000)
#define SOX_SAMPLE_TO_ALAW_BYTE(d,c) sox_13linear2alaw(SOX_SAMPLE_TO_UNSIGNED(13,d,c) - 0x1000)

static size_t sunread(sox_format_t* ft, sox_sample_t* pOutput, size_t cOutput)
{
    priv_t* pPriv = (priv_t*)ft->priv;
    char* pbOutput = (char*)pOutput;
    size_t cbOutputLeft = cOutput << pPriv->sample_shift;
    size_t i, cRead;
    int cbRead;
    SOX_SAMPLE_LOCALS;
    LSX_USE_VAR(sox_macro_temp_double);

    while (cbOutputLeft) {
        cbRead = read(pPriv->device, pbOutput, cbOutputLeft);
        if (cbRead <= 0) {
            if (cbRead < 0) {
                lsx_fail_errno(ft, errno, "error reading from device");
                return 0;
            }
            break;
        }
        cbOutputLeft -= cbRead;
        pbOutput += cbRead;
    }

    /* Convert in-place (backwards) */
    cRead = cOutput - (cbOutputLeft >> pPriv->sample_shift);
    switch (pPriv->sample_shift)
    {
    case 0:
        switch (ft->encoding.encoding)
        {
        case SOX_ENCODING_SIGN2:
            for (i = cRead; i != 0; i--) {
                pOutput[i - 1] = SOX_UNSIGNED_8BIT_TO_SAMPLE(
                ((sox_uint8_t*)pOutput)[i - 1],
                dummy);
            }
            break;
        case SOX_ENCODING_ULAW:
            for (i = cRead; i != 0; i--) {
                pOutput[i - 1] = SOX_ULAW_BYTE_TO_SAMPLE(
                ((sox_uint8_t*)pOutput)[i - 1],
                dummy);
            }
            break;
        case SOX_ENCODING_ALAW:
            for (i = cRead; i != 0; i--) {
                pOutput[i - 1] = SOX_ALAW_BYTE_TO_SAMPLE(
                ((sox_uint8_t*)pOutput)[i - 1],
                dummy);
            }
            break;
        default:
            return 0;
        }
        break;
    case 1:
        for (i = cRead; i != 0; i--) {
            pOutput[i - 1] = SOX_SIGNED_16BIT_TO_SAMPLE(
            ((sox_int16_t*)pOutput)[i - 1],
            dummy);
        }
        break;
    }

    return cRead;
}

static size_t sunwrite(
    sox_format_t* ft,
    const sox_sample_t* pInput,
    size_t cInput)
{
    priv_t* pPriv = (priv_t*)ft->priv;
    size_t cInputRemaining = cInput;
    unsigned cClips = 0;
    SOX_SAMPLE_LOCALS;

    while (cInputRemaining) {
        size_t cStride;
        size_t i;
        size_t cbStride;
        int cbWritten;

        cStride = cInputRemaining;
        if (cStride > pPriv->cOutput) {
            cStride = pPriv->cOutput;
        }

        switch (pPriv->sample_shift)
        {
        case 0:
            switch (ft->encoding.encoding)
            {
            case SOX_ENCODING_SIGN2:
                for (i = 0; i != cStride; i++) {
                    ((sox_uint8_t*)pPriv->pOutput)[i] =
                        SOX_SAMPLE_TO_UNSIGNED_8BIT(pInput[i], cClips);
                }
                break;
            case SOX_ENCODING_ULAW:
                for (i = 0; i != cStride; i++) {
                    ((sox_uint8_t*)pPriv->pOutput)[i] =
                        SOX_SAMPLE_TO_ULAW_BYTE(pInput[i], cClips);
                }
                break;
            case SOX_ENCODING_ALAW:
                for (i = 0; i != cStride; i++) {
                    ((sox_uint8_t*)pPriv->pOutput)[i] =
                        SOX_SAMPLE_TO_ALAW_BYTE(pInput[i], cClips);
                }
                break;
            default:
                return 0;
            }
            break;
        case 1:
            for (i = 0; i != cStride; i++) {
                ((sox_int16_t*)pPriv->pOutput)[i] =
                    SOX_SAMPLE_TO_SIGNED_16BIT(pInput[i], cClips);
            }
            break;
        }

        cbStride = cStride << pPriv->sample_shift;
        i = 0;
        do {
            cbWritten = write(pPriv->device, &pPriv->pOutput[i], cbStride - i);
            i += cbWritten;
            if (cbWritten <= 0) {
                lsx_fail_errno(ft, errno, "error writing to device");
                return 0;
            }
        } while (i != cbStride);

        cInputRemaining -= cStride;
        pInput += cStride;
    }

    return cInput;
}

LSX_FORMAT_HANDLER(sunau)
{
  static char const * const names[] = {"sunau", NULL};
  static unsigned const write_encodings[] = {
    SOX_ENCODING_ULAW, 8, 0,
    SOX_ENCODING_ALAW, 8, 0,
    SOX_ENCODING_SIGN2, 8, 16, 0,
    0};
  static sox_format_handler_t const handler = {SOX_LIB_VERSION_CODE,
    "Sun audio device driver",
    names, SOX_FILE_DEVICE | SOX_FILE_NOSTDIO,
    sunstartread, sunread, sunstop,
    sunstartwrite, sunwrite, sunstop,
    NULL, write_encodings, NULL, sizeof(priv_t)
  };
  return &handler;
}
