/*
 * playsnd.oss -- replacement for playsnd.m for Linux using OSS API
 * Copyright (c) 1997 Kristopher D. Giesing
 */

/*
 * Based on Open Sound System specifications
 * Reference: see Open Sound System Programmer's Guide on WWW at
 * http://www.opensound.com/pguide/index.html
 */

/* Includes: */
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>          /* Needed for signal()                 */
#include <string.h>          /* Needed for memcpy()                 */
#include <sys/ioctl.h>
#include <sys/stat.h>        /* needed for open()                   */
#include <sys/soundcard.h>   /* OSS Audio functions                 */
#include "mex.h"             /* Needed for MATLAB gateway functions */

#define NORMALLY 0

static int audio_fd;
static void (*mwCtrlC)(int);

void expire(
    char *message    /* Error message; NULL is normal exit */
    )
{
    signal(SIGINT, mwCtrlC);
    if (audio_fd != 0) {
        if (close(audio_fd) == -1) {
            mexErrMsgTxt("Unable to close /dev/dsp");
        } else {
            if (message != NORMALLY) {
                mexErrMsgTxt(message);
            }
        }
    }

    return;
}

void sndCtrlC(int input)
{
    expire("Interrupted.");
}

int sndInitDevice(char *device)
{
    /* Leave gracefully if this machine has no audio capabilities */
    if ((audio_fd = open(device, O_WRONLY, 0)) == -1)
        mexErrMsgTxt("Could not open audio device");

    /*
     * Now that we've opened the file, we need to swap in our
     * own signal handler
     */
    mwCtrlC = signal(SIGINT, sndCtrlC);

    return;
}

int sndGetStereoMode(int ncols, double **pdata, int *pnum_samples)
{
    int stereo = 0;

    if (ncols == 2) {
        stereo = 1;
        if (ioctl(audio_fd, SNDCTL_DSP_STEREO, &stereo)==-1)
            expire("Unable to query /dev/dsp");
        if (stereo != 1) {
            mexWarnMsgTxt("Device does not support stereo sound; using mono");
            *pnum_samples = (*pnum_samples)/2; /* Drop last half of signal */
        }
    } else {
        if (ioctl(audio_fd, SNDCTL_DSP_STEREO, &stereo) == -1)
            expire("Unable to query /dev/dsp");
        if (stereo != 0) {
            double *temp;

            mexWarnMsgTxt("Device does not support mono sound.");

            /*
             * Attempt to simulate mono by duplicating the signal
             */
            temp = mxMalloc((*pnum_samples)*2*sizeof(double));
            memcpy(temp, *pdata, (*pnum_samples)*sizeof(double));
            memcpy(temp+*pnum_samples, *pdata, *pnum_samples*sizeof(double));
            *pdata = temp;

            *pnum_samples = (*pnum_samples)*2;
        }
    }

    return(stereo);
}

int sndGetFormat(
    int bits
    )
{
    int format;

    if (ioctl(audio_fd, SNDCTL_DSP_GETFMTS, &format) == -1)
        expire("Unable to query /dev/dsp");

    if ((format & AFMT_S16_LE) && (bits == 0 || bits == 16)) {
        format = AFMT_S16_LE;    
    } else if ((format & AFMT_U16_LE) && (bits == 0 || bits == 16)) {
        format = AFMT_U16_LE;
    } else if ((format & AFMT_U8) && (bits == 0 || bits == 8)) {
        format = AFMT_U8;
    } else if ((format & AFMT_S8) && (bits == 0 || bits == 8)) {    
        format = AFMT_S8;
    } else {
        expire("Unable to find a suitable audio format");
    }
    return(format);

}

unsigned char *sndConvertData(
    double *data,
    int num_samples,
    int format,
    int stereo,
    int *buffer_size
    )
{
    int rcoffset = num_samples/2; /* Start of right channel data */
    int ssize;                    /* Sample size in bytes */
    int scale;                    /* Scale to max. dynamic range */
    int offset;                   /* Offset for unsigned data */
    unsigned char *devbuf;
    int i,p,datum;

    switch (format) {
    case AFMT_S16_LE:
        ssize = 2;
        scale = 32767;
        offset = 0;
        break;
    case AFMT_U16_LE:
        ssize = 2;
        scale = 32767;
        offset = 32768;
        break;
    case AFMT_U8:
        ssize = 1;
        scale = 127;
        offset = 128;
        break;
    case AFMT_S8:
        ssize = 1;
        scale = 127;
        offset = 0;
        break;
    }

    *buffer_size = num_samples*ssize*sizeof(char);
    devbuf = mxMalloc(*buffer_size);

    if (ssize == 1) {
        /* Eight bit data */
        for (i=0,p=0;p<*buffer_size;i++) {
            devbuf[p++] = (unsigned char)(data[i]*127 + offset);
            if (stereo) {
                devbuf[p++] = (unsigned char)(data[rcoffset+i]*127 + offset);
            }
        }

    } else {
        /* Sixteen bit data */
        for (i=0,p=0;p<*buffer_size;i++) {
            datum = (int)(data[i]*scale + offset);
            /* first send the low byte then the high byte */
            devbuf[p++] = (unsigned char)( datum       & 0xff);
            devbuf[p++] = (unsigned char)((datum >> 8) & 0xff);
            if (stereo) {
                datum = (int)(data[rcoffset+i]*scale + offset);
                /* first send the low byte then the high byte */
                devbuf[p++] = (unsigned char)( datum       & 0xff); 
                devbuf[p++] = (unsigned char)((datum >> 8) & 0xff);
            }
        }
    }

    return(devbuf);
}

void sndPlay(
    double *data,
    int num_samples,
    int sampling_rate,
    int stereo,
    int bits
    )
{
    int format;
    unsigned char *devbuf;
    int i, datum, len, bufsize, p=0;

    format = sndGetFormat(bits);

    /* Set sample format */
    if (ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format)==-1)
        expire("Error writing format to /dev/dsp");

    /* Convert audio data */
    devbuf = sndConvertData(data, num_samples, format, stereo, &bufsize);

    /* Set sample rate */
    if (ioctl(audio_fd,SNDCTL_DSP_SPEED, &sampling_rate)==-1)
        expire("Error writing format to /dev/dsp");

    /* Write to output device */
    if ((len = write(audio_fd, devbuf, bufsize)) == -1)  
        expire("Error writing data to /dev/dsp");

}

void mexFunction(
    int            nlhs,
    mxArray       *plhs[],
    int            nrhs,
    const mxArray *prhs[]
    )
{
    double *data;
    int num_samples, sample_rate, stereo, bits;

    audio_fd = 0;

    /*
     * This function returns no outputs
     */
    if (nlhs != 0)
        mexErrMsgTxt("Too many output arguments");

    /* Check input arguments */
    switch (nrhs) {
    case 0:
        /*
         * This return here takes us back to matlab prompt.  The reason
         * why we do this is because playsnd with no args on sun returns
         * silently without playing anything
         */
        return;
        break;
    case 1:
        sample_rate = 8192;  /* Use 8192 Hz by default */
        bits = 0;            /* determine best sample resolution */
        break;
    case 2:
        sample_rate = (int)mxGetScalar(prhs[1]);
        bits = 0;            /* determine best sample resolution */
        break;
    case 3:
        sample_rate = (int)mxGetScalar(prhs[1]);
        bits = (int)mxGetScalar(prhs[2]);
        break;
    default: expire("Too many input arguments.");
        break;
    }
  
    sndInitDevice("/dev/dsp");

    num_samples = mxGetM(prhs[0])*mxGetN(prhs[0]);
  
    stereo = sndGetStereoMode(mxGetN(prhs[0]), &data, &num_samples);

    data = mxGetPr(prhs[0]);

    sndPlay(data, num_samples, sample_rate, stereo, bits);

    /*
     * Exit normallly
     */
    expire(NORMALLY);

    return;
}
