/* FluidSynth - A Software Synthesizer
 *
 * Copyright (C) 2003  Peter Hanappe and others.
 *
 * 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.1 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, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA
 */

/* fluid_alsa.c
 *
 * Driver for the Advanced Linux Sound Architecture
 *
 */

#include "fluid_synth.h"
#include "fluid_midi.h"
#include "fluid_adriver.h"
#include "fluid_mdriver.h"
#include "fluid_settings.h"

#if ALSA_SUPPORT

#define ALSA_PCM_NEW_HW_PARAMS_API
#include <alsa/asoundlib.h>
#include <sys/poll.h>

#include "fluid_lash.h"

#define FLUID_ALSA_DEFAULT_MIDI_DEVICE  "default"
#define FLUID_ALSA_DEFAULT_SEQ_DEVICE   "default"

#define BUFFER_LENGTH 512

/** fluid_alsa_audio_driver_t
 *
 * This structure should not be accessed directly. Use audio port
 * functions instead.
 */
typedef struct
{
    fluid_audio_driver_t driver;
    snd_pcm_t *pcm;
    fluid_audio_func_t callback;
    void *data;
    int buffer_size;
    fluid_thread_t *thread;
    int cont;
} fluid_alsa_audio_driver_t;


static fluid_thread_return_t fluid_alsa_audio_run_float(void *d);
static fluid_thread_return_t fluid_alsa_audio_run_s16(void *d);


typedef struct
{
    char *name;
    snd_pcm_format_t format;
    snd_pcm_access_t access;
    fluid_thread_func_t run;
} fluid_alsa_formats_t;

static const fluid_alsa_formats_t fluid_alsa_formats[] =
{
    {
        "s16, rw, interleaved",
        SND_PCM_FORMAT_S16,
        SND_PCM_ACCESS_RW_INTERLEAVED,
        fluid_alsa_audio_run_s16
    },
    {
        "float, rw, non interleaved",
        SND_PCM_FORMAT_FLOAT,
        SND_PCM_ACCESS_RW_NONINTERLEAVED,
        fluid_alsa_audio_run_float
    },
    { NULL, 0, 0, NULL }
};



/*
 * fluid_alsa_rawmidi_driver_t
 *
 */
typedef struct
{
    fluid_midi_driver_t driver;
    snd_rawmidi_t *rawmidi_in;
    struct pollfd *pfd;
    int npfd;
    fluid_thread_t *thread;
    fluid_atomic_int_t should_quit;
    unsigned char buffer[BUFFER_LENGTH];
    fluid_midi_parser_t *parser;
} fluid_alsa_rawmidi_driver_t;


static fluid_thread_return_t fluid_alsa_midi_run(void *d);


/*
 * fluid_alsa_seq_driver_t
 *
 */
typedef struct
{
    fluid_midi_driver_t driver;
    snd_seq_t *seq_handle;
    struct pollfd *pfd;
    int npfd;
    fluid_thread_t *thread;
    fluid_atomic_int_t should_quit;
    int port_count;
    int autoconn_inputs;
    snd_seq_addr_t autoconn_dest;
} fluid_alsa_seq_driver_t;

static fluid_thread_return_t fluid_alsa_seq_run(void *d);

/**************************************************************
 *
 *        Alsa audio driver
 *
 */

void fluid_alsa_audio_driver_settings(fluid_settings_t *settings)
{
    fluid_settings_register_str(settings, "audio.alsa.device", "default", 0);
}


fluid_audio_driver_t *
new_fluid_alsa_audio_driver(fluid_settings_t *settings,
                            fluid_synth_t *synth)
{
    return new_fluid_alsa_audio_driver2(settings, NULL, synth);
}

fluid_audio_driver_t *
new_fluid_alsa_audio_driver2(fluid_settings_t *settings,
                             fluid_audio_func_t func, void *data)
{
    fluid_alsa_audio_driver_t *dev;
    double sample_rate;
    int periods, period_size;
    char *device = NULL;
    int realtime_prio = 0;
    int i, err, dir = 0;
    snd_pcm_hw_params_t *hwparams;
    snd_pcm_sw_params_t *swparams = NULL;
    snd_pcm_uframes_t uframes;
    unsigned int tmp;

    dev = FLUID_NEW(fluid_alsa_audio_driver_t);

    if(dev == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Out of memory");
        return NULL;
    }

    FLUID_MEMSET(dev, 0, sizeof(fluid_alsa_audio_driver_t));

    fluid_settings_getint(settings, "audio.periods", &periods);
    fluid_settings_getint(settings, "audio.period-size", &period_size);
    fluid_settings_getnum(settings, "synth.sample-rate", &sample_rate);
    fluid_settings_dupstr(settings, "audio.alsa.device", &device);   /* ++ dup device name */
    fluid_settings_getint(settings, "audio.realtime-prio", &realtime_prio);

    dev->data = data;
    dev->callback = func;
    dev->cont = 1;
    dev->buffer_size = period_size;

    /* Open the PCM device */
    if((err = snd_pcm_open(&dev->pcm, device ? device : "default",
                           SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) != 0)
    {
        if(err == -EBUSY)
        {
            FLUID_LOG(FLUID_ERR, "The \"%s\" audio device is used by another application",
                      device ? device : "default");
            goto error_recovery;
        }
        else
        {
            FLUID_LOG(FLUID_ERR, "Failed to open the \"%s\" audio device",
                      device ? device : "default");
            goto error_recovery;
        }
    }

    snd_pcm_hw_params_alloca(&hwparams);
    snd_pcm_sw_params_alloca(&swparams);

    /* Set hardware parameters. We continue trying access methods and
       sample formats until we have one that works. For example, if
       memory mapped access fails we try regular IO methods. (not
       finished, yet). */

    for(i = 0; fluid_alsa_formats[i].name != NULL; i++)
    {

        snd_pcm_hw_params_any(dev->pcm, hwparams);

        if(snd_pcm_hw_params_set_access(dev->pcm, hwparams, fluid_alsa_formats[i].access) < 0)
        {
            FLUID_LOG(FLUID_DBG, "snd_pcm_hw_params_set_access() failed with audio format '%s'", fluid_alsa_formats[i].name);
            continue;
        }

        if(snd_pcm_hw_params_set_format(dev->pcm, hwparams, fluid_alsa_formats[i].format) < 0)
        {
            FLUID_LOG(FLUID_DBG, "snd_pcm_hw_params_set_format() failed with audio format '%s'", fluid_alsa_formats[i].name);
            continue;
        }

        if((err = snd_pcm_hw_params_set_channels(dev->pcm, hwparams, 2)) < 0)
        {
            FLUID_LOG(FLUID_ERR, "Failed to set the channels: %s",
                      snd_strerror(err));
            goto error_recovery;
        }

        tmp = (unsigned int) sample_rate;

        if((err = snd_pcm_hw_params_set_rate_near(dev->pcm, hwparams, &tmp, NULL)) < 0)
        {
            FLUID_LOG(FLUID_ERR, "Failed to set the sample rate: %s",
                      snd_strerror(err));
            goto error_recovery;
        }

        if(tmp != sample_rate)
        {
            /* There's currently no way to change the sampling rate of the
            synthesizer after it's been created. */
            FLUID_LOG(FLUID_WARN, "Requested sample rate of %u, got %u instead, "
                      "synthesizer likely out of tune!", (unsigned int) sample_rate, tmp);
        }

        uframes = period_size;

        if(snd_pcm_hw_params_set_period_size_near(dev->pcm, hwparams, &uframes, &dir) < 0)
        {
            FLUID_LOG(FLUID_ERR, "Failed to set the period size");
            goto error_recovery;
        }

        if(uframes != (unsigned long) period_size)
        {
            FLUID_LOG(FLUID_WARN, "Requested a period size of %d, got %d instead",
                      period_size, (int) uframes);
            dev->buffer_size = (int) uframes;
            period_size = uframes;	/* period size is used below, so set it to the real value */
        }

        tmp = periods;

        if(snd_pcm_hw_params_set_periods_near(dev->pcm, hwparams, &tmp, &dir) < 0)
        {
            FLUID_LOG(FLUID_ERR, "Failed to set the number of periods");
            goto error_recovery;
        }

        if(tmp != (unsigned int) periods)
        {
            FLUID_LOG(FLUID_WARN, "Requested %d periods, got %d instead",
                      periods, (int) tmp);
        }

        if(snd_pcm_hw_params(dev->pcm, hwparams) < 0)
        {
            FLUID_LOG(FLUID_WARN, "Audio device hardware configuration failed");
            continue;
        }

        break;
    }

    if(fluid_alsa_formats[i].name == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Failed to find an audio format supported by alsa");
        goto error_recovery;
    }

    /* Set the software params */
    snd_pcm_sw_params_current(dev->pcm, swparams);

    if(snd_pcm_sw_params_set_start_threshold(dev->pcm, swparams, period_size) != 0)
    {
        FLUID_LOG(FLUID_ERR, "Failed to set start threshold.");
    }

    if(snd_pcm_sw_params_set_avail_min(dev->pcm, swparams, period_size) != 0)
    {
        FLUID_LOG(FLUID_ERR, "Software setup for minimum available frames failed.");
    }

    if(snd_pcm_sw_params(dev->pcm, swparams) != 0)
    {
        FLUID_LOG(FLUID_ERR, "Software setup failed.");
    }

    if(snd_pcm_nonblock(dev->pcm, 0) != 0)
    {
        FLUID_LOG(FLUID_ERR, "Failed to set the audio device to blocking mode");
        goto error_recovery;
    }

    /* Create the audio thread */
    dev->thread = new_fluid_thread("alsa-audio", fluid_alsa_formats[i].run, dev, realtime_prio, FALSE);

    if(!dev->thread)
    {
        FLUID_LOG(FLUID_ERR, "Failed to start the alsa-audio thread.");
        goto error_recovery;
    }

    if(device)
    {
        FLUID_FREE(device);    /* -- free device name */
    }

    return (fluid_audio_driver_t *) dev;

error_recovery:

    if(device)
    {
        FLUID_FREE(device);    /* -- free device name */
    }

    delete_fluid_alsa_audio_driver((fluid_audio_driver_t *) dev);
    return NULL;
}

void delete_fluid_alsa_audio_driver(fluid_audio_driver_t *p)
{
    fluid_alsa_audio_driver_t *dev = (fluid_alsa_audio_driver_t *) p;
    fluid_return_if_fail(dev != NULL);

    dev->cont = 0;

    if(dev->thread)
    {
        fluid_thread_join(dev->thread);
        delete_fluid_thread(dev->thread);
    }

    if(dev->pcm)
    {
        snd_pcm_close(dev->pcm);
    }

    FLUID_FREE(dev);
}

/* handle error after an ALSA write call */
static int fluid_alsa_handle_write_error(snd_pcm_t *pcm, int errval)
{
    switch(errval)
    {
    case -EAGAIN:
        snd_pcm_wait(pcm, 1);
        break;
// on some BSD variants ESTRPIPE is defined as EPIPE.
// not sure why, maybe because this version of alsa doesn't support
// suspending pcm streams. anyway, since EPIPE seems to be more
// likely than ESTRPIPE, so ifdef it out in case.
#if ESTRPIPE == EPIPE
#warning "ESTRPIPE defined as EPIPE. This may cause trouble with ALSA playback."
#else

    case -ESTRPIPE:
        if(snd_pcm_resume(pcm) != 0)
        {
            FLUID_LOG(FLUID_ERR, "Failed to resume the audio device");
            return FLUID_FAILED;
        }

#endif

    /* fall through ... */
    /* ... since the stream got resumed, but still has to be prepared */
    case -EPIPE:
    case -EBADFD:
        if(snd_pcm_prepare(pcm) != 0)
        {
            FLUID_LOG(FLUID_ERR, "Failed to prepare the audio device");
            return FLUID_FAILED;
        }

        break;

    default:
        FLUID_LOG(FLUID_ERR, "The audio device error: %s", snd_strerror(errval));
        return FLUID_FAILED;
    }

    return FLUID_OK;
}

static fluid_thread_return_t fluid_alsa_audio_run_float(void *d)
{
    fluid_alsa_audio_driver_t *dev = (fluid_alsa_audio_driver_t *) d;
    fluid_synth_t *synth = (fluid_synth_t *)(dev->data);
    float *left;
    float *right;
    float *handle[2];
    int n, buffer_size, offset;

    buffer_size = dev->buffer_size;

    left = FLUID_ARRAY(float, buffer_size);
    right = FLUID_ARRAY(float, buffer_size);

    if((left == NULL) || (right == NULL))
    {
        FLUID_LOG(FLUID_ERR, "Out of memory.");
        goto error_recovery;
    }

    if(snd_pcm_prepare(dev->pcm) != 0)
    {
        FLUID_LOG(FLUID_ERR, "Failed to prepare the audio device");
        goto error_recovery;
    }

    /* use separate loops depending on if callback supplied or not (overkill?) */
    if(dev->callback)
    {
        while(dev->cont)
        {
            FLUID_MEMSET(left, 0, buffer_size * sizeof(*left));
            FLUID_MEMSET(right, 0, buffer_size * sizeof(*right));

            handle[0] = left;
            handle[1] = right;

            (*dev->callback)(synth, buffer_size, 0, NULL, 2, handle);

            offset = 0;

            while(offset < buffer_size)
            {
                handle[0] = left + offset;
                handle[1] = right + offset;

                n = snd_pcm_writen(dev->pcm, (void *)handle, buffer_size - offset);

                if(n < 0)	/* error occurred? */
                {
                    if(fluid_alsa_handle_write_error(dev->pcm, n) != FLUID_OK)
                    {
                        goto error_recovery;
                    }
                }
                else
                {
                    offset += n;    /* no error occurred */
                }
            }	/* while (offset < buffer_size) */
        }	/* while (dev->cont) */
    }
    else	/* no user audio callback (faster) */
    {
        while(dev->cont)
        {
            fluid_synth_write_float(dev->data, buffer_size, left, 0, 1, right, 0, 1);

            offset = 0;

            while(offset < buffer_size)
            {
                handle[0] = left + offset;
                handle[1] = right + offset;

                n = snd_pcm_writen(dev->pcm, (void *)handle, buffer_size - offset);

                if(n < 0)	/* error occurred? */
                {
                    if(fluid_alsa_handle_write_error(dev->pcm, n) != FLUID_OK)
                    {
                        goto error_recovery;
                    }
                }
                else
                {
                    offset += n;    /* no error occurred */
                }
            }	/* while (offset < buffer_size) */
        }	/* while (dev->cont) */
    }

error_recovery:

    FLUID_FREE(left);
    FLUID_FREE(right);

    return FLUID_THREAD_RETURN_VALUE;
}

static fluid_thread_return_t fluid_alsa_audio_run_s16(void *d)
{
    fluid_alsa_audio_driver_t *dev = (fluid_alsa_audio_driver_t *) d;
    float *left;
    float *right;
    short *buf;
    float *handle[2];
    int n, buffer_size, offset;

    buffer_size = dev->buffer_size;

    left = FLUID_ARRAY(float, buffer_size);
    right = FLUID_ARRAY(float, buffer_size);
    buf = FLUID_ARRAY(short, 2 * buffer_size);

    if((left == NULL) || (right == NULL) || (buf == NULL))
    {
        FLUID_LOG(FLUID_ERR, "Out of memory.");
        goto error_recovery;
    }

    handle[0] = left;
    handle[1] = right;

    if(snd_pcm_prepare(dev->pcm) != 0)
    {
        FLUID_LOG(FLUID_ERR, "Failed to prepare the audio device");
        goto error_recovery;
    }

    /* use separate loops depending on if callback supplied or not */
    if(dev->callback)
    {
        int dither_index = 0;

        while(dev->cont)
        {
            FLUID_MEMSET(left, 0, buffer_size * sizeof(*left));
            FLUID_MEMSET(right, 0, buffer_size * sizeof(*right));
            
            (*dev->callback)(dev->data, buffer_size, 0, NULL, 2, handle);

            /* convert floating point data to 16 bit (with dithering) */
            fluid_synth_dither_s16(&dither_index, buffer_size, left, right,
                                   buf, 0, 2, buf, 1, 2);
            offset = 0;

            while(offset < buffer_size)
            {
                n = snd_pcm_writei(dev->pcm, (void *)(buf + 2 * offset),
                                   buffer_size - offset);

                if(n < 0)	/* error occurred? */
                {
                    if(fluid_alsa_handle_write_error(dev->pcm, n) != FLUID_OK)
                    {
                        goto error_recovery;
                    }
                }
                else
                {
                    offset += n;    /* no error occurred */
                }
            }	/* while (offset < buffer_size) */
        }	/* while (dev->cont) */
    }
    else	/* no user audio callback, dev->data is the synth instance */
    {
        fluid_synth_t *synth = (fluid_synth_t *)(dev->data);

        while(dev->cont)
        {
            fluid_synth_write_s16(synth, buffer_size, buf, 0, 2, buf, 1, 2);

            offset = 0;

            while(offset < buffer_size)
            {
                n = snd_pcm_writei(dev->pcm, (void *)(buf + 2 * offset),
                                   buffer_size - offset);

                if(n < 0)	/* error occurred? */
                {
                    if(fluid_alsa_handle_write_error(dev->pcm, n) != FLUID_OK)
                    {
                        goto error_recovery;
                    }
                }
                else
                {
                    offset += n;    /* no error occurred */
                }
            }	/* while (offset < buffer_size) */
        }	/* while (dev->cont) */
    }

error_recovery:

    FLUID_FREE(left);
    FLUID_FREE(right);
    FLUID_FREE(buf);

    return FLUID_THREAD_RETURN_VALUE;
}


/**************************************************************
 *
 *        Alsa MIDI driver
 *
 */


void fluid_alsa_rawmidi_driver_settings(fluid_settings_t *settings)
{
    fluid_settings_register_str(settings, "midi.alsa.device", "default", 0);
}

/*
 * new_fluid_alsa_rawmidi_driver
 */
fluid_midi_driver_t *
new_fluid_alsa_rawmidi_driver(fluid_settings_t *settings,
                              handle_midi_event_func_t handler,
                              void *data)
{
    int i, err;
    fluid_alsa_rawmidi_driver_t *dev;
    int realtime_prio = 0;
    int count;
    struct pollfd *pfd = NULL;
    char *device = NULL;

    /* not much use doing anything */
    if(handler == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Invalid argument");
        return NULL;
    }

    /* allocate the device */
    dev = FLUID_NEW(fluid_alsa_rawmidi_driver_t);

    if(dev == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Out of memory");
        return NULL;
    }

    FLUID_MEMSET(dev, 0, sizeof(fluid_alsa_rawmidi_driver_t));

    dev->driver.handler = handler;
    dev->driver.data = data;

    /* allocate one event to store the input data */
    dev->parser = new_fluid_midi_parser();

    if(dev->parser == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Out of memory");
        goto error_recovery;
    }

    fluid_settings_getint(settings, "midi.realtime-prio", &realtime_prio);

    /* get the device name. if none is specified, use the default device. */
    fluid_settings_dupstr(settings, "midi.alsa.device", &device);         /* ++ alloc device name */

    /* open the hardware device. only use midi in. */
    if((err = snd_rawmidi_open(&dev->rawmidi_in, NULL, device ? device : "default",
                               SND_RAWMIDI_NONBLOCK)) < 0)
    {
        FLUID_LOG(FLUID_ERR, "Error opening ALSA raw MIDI port: %s", snd_strerror(err));
        goto error_recovery;
    }

    snd_rawmidi_nonblock(dev->rawmidi_in, 1);

    /* get # of MIDI file descriptors */
    count = snd_rawmidi_poll_descriptors_count(dev->rawmidi_in);

    if(count > 0)  		/* make sure there are some */
    {
        pfd = FLUID_MALLOC(sizeof(struct pollfd) * count);
        dev->pfd = FLUID_MALLOC(sizeof(struct pollfd) * count);
        /* grab file descriptor POLL info structures */
        count = snd_rawmidi_poll_descriptors(dev->rawmidi_in, pfd, count);
    }

    /* copy the input FDs */
    for(i = 0; i < count; i++)  		/* loop over file descriptors */
    {
        if(pfd[i].events & POLLIN)    /* use only the input FDs */
        {
            dev->pfd[dev->npfd].fd = pfd[i].fd;
            dev->pfd[dev->npfd].events = POLLIN;
            dev->pfd[dev->npfd].revents = 0;
            dev->npfd++;
        }
    }

    FLUID_FREE(pfd);

    fluid_atomic_int_set(&dev->should_quit, 0);

    /* create the MIDI thread */
    dev->thread = new_fluid_thread("alsa-midi-raw", fluid_alsa_midi_run, dev, realtime_prio, FALSE);

    if(!dev->thread)
    {
        goto error_recovery;
    }

    if(device)
    {
        FLUID_FREE(device);    /* -- free device name */
    }

    return (fluid_midi_driver_t *) dev;

error_recovery:

    if(device)
    {
        FLUID_FREE(device);    /* -- free device name */
    }

    delete_fluid_alsa_rawmidi_driver((fluid_midi_driver_t *) dev);
    return NULL;

}

/*
 * delete_fluid_alsa_rawmidi_driver
 */
void
delete_fluid_alsa_rawmidi_driver(fluid_midi_driver_t *p)
{
    fluid_alsa_rawmidi_driver_t *dev = (fluid_alsa_rawmidi_driver_t *) p;
    fluid_return_if_fail(dev != NULL);

    /* cancel the thread and wait for it before cleaning up */
    fluid_atomic_int_set(&dev->should_quit, 1);

    if(dev->thread)
    {
        fluid_thread_join(dev->thread);
        delete_fluid_thread(dev->thread);
    }

    if(dev->rawmidi_in)
    {
        snd_rawmidi_close(dev->rawmidi_in);
    }

    if(dev->parser != NULL)
    {
        delete_fluid_midi_parser(dev->parser);
    }

    FLUID_FREE(dev);
}

/*
 * fluid_alsa_midi_run
 */
fluid_thread_return_t
fluid_alsa_midi_run(void *d)
{
    fluid_midi_event_t *evt;
    fluid_alsa_rawmidi_driver_t *dev = (fluid_alsa_rawmidi_driver_t *) d;
    int n, i;

    /* go into a loop until someone tells us to stop */
    while(!fluid_atomic_int_get(&dev->should_quit))
    {

        /* is there something to read? */
        n = poll(dev->pfd, dev->npfd, 100); /* use a 100 milliseconds timeout */

        if(n < 0)
        {
            perror("poll");
        }
        else if(n > 0)
        {

            /* read new data */
            n = snd_rawmidi_read(dev->rawmidi_in, dev->buffer, BUFFER_LENGTH);

            if((n < 0) && (n != -EAGAIN))
            {
                FLUID_LOG(FLUID_ERR, "Failed to read the midi input");
                fluid_atomic_int_set(&dev->should_quit, 1);
            }

            /* let the parser convert the data into events */
            for(i = 0; i < n; i++)
            {
                evt = fluid_midi_parser_parse(dev->parser, dev->buffer[i]);

                if(evt != NULL)
                {
                    (*dev->driver.handler)(dev->driver.data, evt);
                }
            }
        }
    }

    return FLUID_THREAD_RETURN_VALUE;
}

/**************************************************************
 *
 *        Alsa sequencer
 *
 */


void fluid_alsa_seq_driver_settings(fluid_settings_t *settings)
{
    fluid_settings_register_str(settings, "midi.alsa_seq.device", "default", 0);
    fluid_settings_register_str(settings, "midi.alsa_seq.id", "pid", 0);
}


static char *fluid_alsa_seq_full_id(char *id, char *buf, int len)
{
    if(id != NULL)
    {
        if(FLUID_STRCMP(id, "pid") == 0)
        {
            FLUID_SNPRINTF(buf, len, "FLUID Synth (%d)", getpid());
        }
        else
        {
            FLUID_SNPRINTF(buf, len, "FLUID Synth (%s)", id);
        }
    }
    else
    {
        FLUID_SNPRINTF(buf, len, "FLUID Synth");
    }

    return buf;
}

static char *fluid_alsa_seq_full_name(char *id, int port, char *buf, int len)
{
    if(id != NULL)
    {
        if(FLUID_STRCMP(id, "pid") == 0)
        {
            FLUID_SNPRINTF(buf, len, "Synth input port (%d:%d)", getpid(), port);
        }
        else
        {
            FLUID_SNPRINTF(buf, len, "Synth input port (%s:%d)", id, port);
        }
    }
    else
    {
        FLUID_SNPRINTF(buf, len, "Synth input port");
    }

    return buf;
}

// Connect a single port_info to autoconnect_dest if it has right type/capabilities
static void fluid_alsa_seq_autoconnect_port_info(fluid_alsa_seq_driver_t *dev, snd_seq_port_info_t *pinfo)
{
    snd_seq_port_subscribe_t *subs;
    snd_seq_t *seq = dev->seq_handle;
    const unsigned int needed_type = SND_SEQ_PORT_TYPE_MIDI_GENERIC;
    const unsigned int needed_cap = SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ;
    const snd_seq_addr_t *sender = snd_seq_port_info_get_addr(pinfo);
    const char *pname = snd_seq_port_info_get_name(pinfo);

    if((snd_seq_port_info_get_type(pinfo) & needed_type) != needed_type)
    {
        return;
    }

    if((snd_seq_port_info_get_capability(pinfo) & needed_cap) != needed_cap)
    {
        return;
    }

    snd_seq_port_subscribe_alloca(&subs);
    snd_seq_port_subscribe_set_sender(subs, sender);
    snd_seq_port_subscribe_set_dest(subs, &dev->autoconn_dest);

    if(snd_seq_get_port_subscription(seq, subs) == 0)
    {
        FLUID_LOG(FLUID_WARN, "Connection %s is already subscribed", pname);
        return;
    }

    if(snd_seq_subscribe_port(seq, subs) < 0)
    {
        FLUID_LOG(FLUID_ERR, "Connection of %s failed (%s)", pname, snd_strerror(errno));
        return;
    }

    FLUID_LOG(FLUID_INFO, "Connection of %s succeeded", pname);
}

// Autoconnect a single client port (by id) to autoconnect_dest if it has right type/capabilities
static void fluid_alsa_seq_autoconnect_port(fluid_alsa_seq_driver_t *dev, int client_id, int port_id)
{
    int err;
    snd_seq_t *seq = dev->seq_handle;
    snd_seq_port_info_t *pinfo;

    snd_seq_port_info_alloca(&pinfo);

    if((err = snd_seq_get_any_port_info(seq, client_id, port_id, pinfo)) < 0)
    {
        FLUID_LOG(FLUID_ERR, "snd_seq_get_any_port_info() failed: %s", snd_strerror(err));
        return;
    }

    fluid_alsa_seq_autoconnect_port_info(dev, pinfo);
}

// Connect available ALSA MIDI inputs to the provided port_info
static void fluid_alsa_seq_autoconnect(fluid_alsa_seq_driver_t *dev, const snd_seq_port_info_t *dest_pinfo)
{
    int err;
    snd_seq_t *seq = dev->seq_handle;
    snd_seq_client_info_t *cinfo;
    snd_seq_port_info_t *pinfo;

    // subscribe to future new clients/ports showing up
    if((err = snd_seq_connect_from(seq, snd_seq_port_info_get_port(dest_pinfo),
                                   SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE)) < 0)
    {
        FLUID_LOG(FLUID_ERR, "snd_seq_connect_from() failed: %s", snd_strerror(err));
    }

    snd_seq_client_info_alloca(&cinfo);
    snd_seq_port_info_alloca(&pinfo);

    dev->autoconn_dest = *snd_seq_port_info_get_addr(dest_pinfo);

    snd_seq_client_info_set_client(cinfo, -1);

    while(snd_seq_query_next_client(seq, cinfo) >= 0)
    {
        snd_seq_port_info_set_client(pinfo, snd_seq_client_info_get_client(cinfo));
        snd_seq_port_info_set_port(pinfo, -1);

        while(snd_seq_query_next_port(seq, pinfo) >= 0)
        {
            fluid_alsa_seq_autoconnect_port_info(dev, pinfo);
        }
    }
}

/*
 * new_fluid_alsa_seq_driver
 */
fluid_midi_driver_t *
new_fluid_alsa_seq_driver(fluid_settings_t *settings,
                          handle_midi_event_func_t handler, void *data)
{
    int i, err;
    fluid_alsa_seq_driver_t *dev;
    int realtime_prio = 0;
    int count;
    struct pollfd *pfd = NULL;
    char *device = NULL;
    char *id = NULL;
    char *portname = NULL;
    char full_id[64];
    char full_name[64];
    snd_seq_port_info_t *port_info = NULL;
    int midi_channels;

    /* not much use doing anything */
    if(handler == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Invalid argument");
        return NULL;
    }

    /* allocate the device */
    dev = FLUID_NEW(fluid_alsa_seq_driver_t);

    if(dev == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Out of memory");
        return NULL;
    }

    FLUID_MEMSET(dev, 0, sizeof(fluid_alsa_seq_driver_t));
    dev->driver.data = data;
    dev->driver.handler = handler;

    fluid_settings_getint(settings, "midi.realtime-prio", &realtime_prio);

    /* get the device name. if none is specified, use the default device. */
    if(fluid_settings_dupstr(settings, "midi.alsa_seq.device", &device) != FLUID_OK)     /* ++ alloc device name */
    {
        goto error_recovery;
    }

    if(fluid_settings_dupstr(settings, "midi.alsa_seq.id", &id) != FLUID_OK)     /* ++ alloc id string */
    {
        goto error_recovery;
    }

    if(id == NULL)
    {
        id = FLUID_MALLOC(32);

        if(!id)
        {
            FLUID_LOG(FLUID_ERR, "Out of memory");
            goto error_recovery;
        }

        sprintf(id, "%d", getpid());
    }

    /* get the midi portname */
    fluid_settings_dupstr(settings, "midi.portname", &portname);

    if(portname && FLUID_STRLEN(portname) == 0)
    {
        FLUID_FREE(portname);       /* -- free port name */
        portname = NULL;
    }

    /* open the sequencer INPUT only */
    err = snd_seq_open(&dev->seq_handle, device ? device : "default", SND_SEQ_OPEN_INPUT, 0);

    if(err < 0)
    {
        FLUID_LOG(FLUID_ERR, "Error opening ALSA sequencer");
        goto error_recovery;
    }

    snd_seq_nonblock(dev->seq_handle, 1);

    /* get # of MIDI file descriptors */
    count = snd_seq_poll_descriptors_count(dev->seq_handle, POLLIN);

    if(count > 0)  		/* make sure there are some */
    {
        pfd = FLUID_MALLOC(sizeof(struct pollfd) * count);
        dev->pfd = FLUID_MALLOC(sizeof(struct pollfd) * count);
        /* grab file descriptor POLL info structures */
        count = snd_seq_poll_descriptors(dev->seq_handle, pfd, count, POLLIN);
    }

    /* copy the input FDs */
    for(i = 0; i < count; i++)  		/* loop over file descriptors */
    {
        if(pfd[i].events & POLLIN)    /* use only the input FDs */
        {
            dev->pfd[dev->npfd].fd = pfd[i].fd;
            dev->pfd[dev->npfd].events = POLLIN;
            dev->pfd[dev->npfd].revents = 0;
            dev->npfd++;
        }
    }

    FLUID_FREE(pfd);

    /* set the client name */
    if(!portname)
    {
        snd_seq_set_client_name(dev->seq_handle, fluid_alsa_seq_full_id(id, full_id, 64));
    }
    else
    {
        snd_seq_set_client_name(dev->seq_handle, portname);
    }


    /* create the ports */
    snd_seq_port_info_alloca(&port_info);
    FLUID_MEMSET(port_info, 0, snd_seq_port_info_sizeof());

    fluid_settings_getint(settings, "synth.midi-channels", &midi_channels);
    dev->port_count = midi_channels / 16;

    snd_seq_port_info_set_capability(port_info,
                                     SND_SEQ_PORT_CAP_WRITE |
                                     SND_SEQ_PORT_CAP_SUBS_WRITE);
    snd_seq_port_info_set_type(port_info,
                               SND_SEQ_PORT_TYPE_MIDI_GM      |
                               SND_SEQ_PORT_TYPE_SYNTHESIZER  |
                               SND_SEQ_PORT_TYPE_APPLICATION  |
                               SND_SEQ_PORT_TYPE_MIDI_GENERIC);
    snd_seq_port_info_set_midi_channels(port_info, 16);
    snd_seq_port_info_set_port_specified(port_info, 1);

    for(i = 0; i < dev->port_count; i++)
    {

        if(!portname)
        {
            snd_seq_port_info_set_name(port_info, fluid_alsa_seq_full_name(id, i, full_name, 64));
        }
        else
        {
            snd_seq_port_info_set_name(port_info, portname);
        }

        snd_seq_port_info_set_port(port_info, i);

        err = snd_seq_create_port(dev->seq_handle, port_info);

        if(err  < 0)
        {
            FLUID_LOG(FLUID_ERR, "Error creating ALSA sequencer port");
            goto error_recovery;
        }
    }

    fluid_settings_getint(settings, "midi.autoconnect", &dev->autoconn_inputs);

    if(dev->autoconn_inputs)
    {
        fluid_alsa_seq_autoconnect(dev, port_info);
    }

    /* tell the lash server our client id */
#ifdef HAVE_LASH
    {
        int enable_lash = 0;
        fluid_settings_getint(settings, "lash.enable", &enable_lash);

        if(enable_lash)
        {
            fluid_lash_alsa_client_id(fluid_lash_client, snd_seq_client_id(dev->seq_handle));
        }
    }
#endif /* HAVE_LASH */

    fluid_atomic_int_set(&dev->should_quit, 0);

    /* create the MIDI thread */
    dev->thread = new_fluid_thread("alsa-midi-seq", fluid_alsa_seq_run, dev, realtime_prio, FALSE);

    if(portname)
    {
        FLUID_FREE(portname);
    }

    if(id)
    {
        FLUID_FREE(id);
    }

    if(device)
    {
        FLUID_FREE(device);
    }

    return (fluid_midi_driver_t *) dev;

error_recovery:

    if(portname)
    {
        FLUID_FREE(portname);
    }

    if(id)
    {
        FLUID_FREE(id);
    }

    if(device)
    {
        FLUID_FREE(device);
    }

    delete_fluid_alsa_seq_driver((fluid_midi_driver_t *) dev);

    return NULL;
}

/*
 * delete_fluid_alsa_seq_driver
 */
void
delete_fluid_alsa_seq_driver(fluid_midi_driver_t *p)
{
    fluid_alsa_seq_driver_t *dev = (fluid_alsa_seq_driver_t *) p;
    fluid_return_if_fail(dev != NULL);

    /* cancel the thread and wait for it before cleaning up */
    fluid_atomic_int_set(&dev->should_quit, 1);

    if(dev->thread)
    {
        fluid_thread_join(dev->thread);
        delete_fluid_thread(dev->thread);
    }

    if(dev->seq_handle)
    {
        snd_seq_close(dev->seq_handle);
    }

    if(dev->pfd)
    {
        FLUID_FREE(dev->pfd);
    }

    FLUID_FREE(dev);
}

/*
 * fluid_alsa_seq_run
 */
fluid_thread_return_t
fluid_alsa_seq_run(void *d)
{
    int n, ev;
    snd_seq_event_t *seq_ev;
    fluid_midi_event_t evt;
    fluid_alsa_seq_driver_t *dev = (fluid_alsa_seq_driver_t *) d;

    /* go into a loop until someone tells us to stop */
    while(!fluid_atomic_int_get(&dev->should_quit))
    {

        /* is there something to read? */
        n = poll(dev->pfd, dev->npfd, 100); /* use a 100 milliseconds timeout */

        if(n < 0)
        {
            perror("poll");
        }
        else if(n > 0)           /* check for pending events */
        {
            do
            {
                ev = snd_seq_event_input(dev->seq_handle, &seq_ev);	/* read the events */

                if(ev == -EAGAIN)
                {
                    break;
                }

                /* Negative value indicates an error, ignore interrupted system call
                 * (-EPERM) and input event buffer overrun (-ENOSPC) */
                if(ev < 0)
                {
                    /* FIXME - report buffer overrun? */
                    if(ev != -EPERM && ev != -ENOSPC)
                    {
                        FLUID_LOG(FLUID_ERR, "Error while reading ALSA sequencer (code=%d)", ev);
                        fluid_atomic_int_set(&dev->should_quit, 1);
                    }

                    break;
                }

                switch(seq_ev->type)
                {
                case SND_SEQ_EVENT_NOTEON:
                    evt.type = NOTE_ON;
                    evt.channel = seq_ev->dest.port * 16 + seq_ev->data.note.channel;
                    evt.param1 = seq_ev->data.note.note;
                    evt.param2 = seq_ev->data.note.velocity;
                    break;

                case SND_SEQ_EVENT_NOTEOFF:
                    evt.type = NOTE_OFF;
                    evt.channel = seq_ev->dest.port * 16 + seq_ev->data.note.channel;
                    evt.param1 = seq_ev->data.note.note;
                    evt.param2 = seq_ev->data.note.velocity;
                    break;

                case SND_SEQ_EVENT_KEYPRESS:
                    evt.type = KEY_PRESSURE;
                    evt.channel = seq_ev->dest.port * 16 + seq_ev->data.note.channel;
                    evt.param1 = seq_ev->data.note.note;
                    evt.param2 = seq_ev->data.note.velocity;
                    break;

                case SND_SEQ_EVENT_CONTROLLER:
                    evt.type = CONTROL_CHANGE;
                    evt.channel = seq_ev->dest.port * 16 + seq_ev->data.control.channel;
                    evt.param1 = seq_ev->data.control.param;
                    evt.param2 = seq_ev->data.control.value;
                    break;

                case SND_SEQ_EVENT_PITCHBEND:
                    evt.type = PITCH_BEND;
                    evt.channel = seq_ev->dest.port * 16 + seq_ev->data.control.channel;

                    /* ALSA pitch bend is -8192 - 8191, we adjust it here */
                    evt.param1 = seq_ev->data.control.value + 8192;
                    break;

                case SND_SEQ_EVENT_PGMCHANGE:
                    evt.type = PROGRAM_CHANGE;
                    evt.channel = seq_ev->dest.port * 16 + seq_ev->data.control.channel;
                    evt.param1 = seq_ev->data.control.value;
                    break;

                case SND_SEQ_EVENT_CHANPRESS:
                    evt.type = CHANNEL_PRESSURE;
                    evt.channel = seq_ev->dest.port * 16 + seq_ev->data.control.channel;
                    evt.param1 = seq_ev->data.control.value;
                    break;

                case SND_SEQ_EVENT_SYSEX:
                    if(seq_ev->data.ext.len < 2)
                    {
                        continue;
                    }

                    fluid_midi_event_set_sysex(&evt, (char *)(seq_ev->data.ext.ptr) + 1,
                                               seq_ev->data.ext.len - 2, FALSE);
                    break;

                case SND_SEQ_EVENT_PORT_START:
                {
                    if(dev->autoconn_inputs)
                    {
                        fluid_alsa_seq_autoconnect_port(dev, seq_ev->data.addr.client, seq_ev->data.addr.port);
                    }
                }
                break;

                default:
                    continue;		/* unhandled event, next loop iteration */
                }

                /* send the events to the next link in the chain */
                (*dev->driver.handler)(dev->driver.data, &evt);
            }
            while(ev > 0);
        }	/* if poll() > 0 */
    }	/* while (!dev->should_quit) */

    return FLUID_THREAD_RETURN_VALUE;
}

#endif /* #if ALSA_SUPPORT */
