/* Pseudo /dev/sequencer of TiMidity
 *			by Masanao Izumo <mo@goice.co.jp>
 *
 * This code for TiMidity++ v2.0.1.
 *
 * ChangeLog:
 * 2000-07-14  LD_PRELOAD=timidity-dynamic-io.so playmidi -e hoge.mid
 * 1999-09-20  timidity_sync(): Wait until sync is completed.
 * 1999-04-27  SNDCTL_SEQ_CTRLRATE: Query TiMidity server with TIMEBASE.
 * 
 */


/*
 * Compile information
 *
 * gcc -DTIMIDITY_LOW_DELAY  -fPIC -c timidity-dynamic-io.c
 * gcc -shared timidity-dynamic-io.o -o timidity-dynamic-io.so
 *
 * Run
 * LD_PRELOAD=./timidity-dynamic-io.so playmidi hoge.mid
 */

/*#define	DEBUG*/


#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>		/* for gethostbyname */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdarg.h>
#include <stdlib.h>
#include <unistd.h>

/* Please modify include path if need */
#include <sys/soundcard.h>
#ifdef HAVE_SYS_AWE_VOICE_H
#include <sys/awe_voice.h>
#endif

#ifndef	SEQUENCER_DEV
#define SEQUENCER_DEV "/dev/sequencer"
#endif

#ifdef TIMIDITY_LOW_DELAY
#define BUF_LOW_SYNC	0.1
#define BUF_HIGH_SYNC	0.15
#else
#define BUF_LOW_SYNC	0.4
#define BUF_HIGH_SYNC	0.8
#endif /* TIMIDITY_ASYNC_MODE */

/* Default host & port */
#define TIMIDITY_HOST "127.0.0.1:7777"
#define TIMIDITY_CONTROL_PORT 7777

#ifndef INADDR_NONE
#define INADDR_NONE 0xffffffff	/* -1 return */
#endif /* INADDR_NONE */

struct fd_read_buffer
{
    char buff[BUFSIZ];
    /* count: beginning of read pointer
     * size:  end of read pointer
     * fd:    file descripter for input
     */
    int count, size, fd;
};

static struct fd_read_buffer control_fd;
static int fdgets(char *buff, size_t buff_size, struct fd_read_buffer *p);
static char *timidity_ctl_command(const char* fmt, ...);

static struct synth_info ext_synth_info[] =
{
    {
	"TiMidity server",	/* name */
	0,			/* device */
	SYNTH_TYPE_MIDI,	/* synth_type */
	SAMPLE_TYPE_BASIC,	/* synth_subtype */
	0,	/* perc_mode */
	36,	/* nr_voices */
	0,	/* nr_drums */
	128,	/* instr_bank_size */
	0	/* capabilities */
    },
#ifdef HAVE_SYS_AWE_VOICE_H
    {
	"TiMidity server",	/* name */
	1,			/* device */
	SYNTH_TYPE_SAMPLE,	/* synth_type */
	SAMPLE_TYPE_AWE32,	/* synth_subtype */
	0,	/* perc_mode */
	36,	/* nr_voices */
	0,	/* nr_drums */
	128,	/* instr_bank_size */
	0	/* capabilities */
    },
#endif
};
static int seqfd = -1, sync_mode = 0;

//SEQ_USE_EXTBUF();
SEQ_DEFINEBUF(128);
void seqbuf_dump(void)
{
    if(_seqbufptr)
	if(write(seqfd, _seqbuf, _seqbufptr) == -1)
	{
	    perror ("write " SEQUENCER_DEV);
	    exit(-1);
	}
    _seqbufptr = 0;
}
static void timidity_seqbuf_dump(void)
{
    if(_seqbufptr)
	if(write(seqfd, _seqbuf, _seqbufptr) == -1)
	{
	    perror ("write " SEQUENCER_DEV);
	    exit(-1);
	}
    _seqbufptr = 0;
}

static void timidity_meta_seq(int p1, int p2, int p3)
{
    _CHN_COMMON(0, 0xff, 0x7f, p1, p2, p3);
    SEQ_DUMPBUF();
}

static int timidity_sync(int centsec)
{
    char *res;
    int status;
    unsigned long sleep_usec;

    timidity_meta_seq(0x02, 0x00, centsec); /* Wait playout */

    /* Wait "301 Sync OK" */
    do
    {
	res = timidity_ctl_command(NULL);
	status = atoi(res);
	if(status != 301)
	    fprintf(stderr, "ERROR: SYNC: %s", res);
    } while(status && status != 301);
    if(status != 301)
	return -1; /* error */
    sleep_usec = (unsigned long)(atof(res + 4) * 1000000);
    if(sleep_usec > 0)
	usleep(sleep_usec);
    return 0;
}

static int timidity_eot(void)
{
    timidity_meta_seq(0x00, 0x00, 0); /* End of playing */
    return timidity_sync(0);
}

/* Return internet IP address in network byte order */
static unsigned int host_ip_address(const char* address)
{
    unsigned int addr;
    struct hostent *hp;

    if((addr = inet_addr(address)) != INADDR_NONE)
	return addr;

    if((hp = gethostbyname(address)) == NULL)
    {
	fprintf(stderr, "Unknown host name: %s\n", address);
	exit(1);
    }
    memcpy(&addr, hp->h_addr, hp->h_length);
    return addr;
}

static int connect_to_server(const char* hostname,
			     unsigned short tcp_port)
{
    int fd, len;
    struct sockaddr_in in;
    unsigned int addr;
    struct hostent *hp;

    if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
	perror("socket");
	exit(1);
    }

    memset(&in, 0, sizeof(in));
    in.sin_family = AF_INET;
    in.sin_port   = htons(tcp_port);
    addr = host_ip_address(hostname);
    memcpy(&in.sin_addr, &addr, 4);

    hp = gethostbyaddr((const char *)&in.sin_addr, 4, AF_INET);
    if(hp != NULL)
	fprintf(stderr, "Try to connect to %s (addr=%s, port=%d)\n",
		hp->h_name,
		inet_ntoa(in.sin_addr),
		tcp_port);
    else
	fprintf(stderr, "Try to connect to %s (port=%d)\n",
		inet_ntoa(in.sin_addr), tcp_port);
    alarm(2 * 60);
    if(connect(fd, (struct sockaddr *)&in, sizeof(in)) < 0)
    {
	perror("connect");
	return -1;
    }
    alarm(0);

    len = sizeof(in);
    if(getsockname(fd, (struct sockaddr *)&in, &len) < 0)
	perror("getsockname");
    return fd;
}

static int fdgets(char *buff, size_t buff_size, struct fd_read_buffer *p)
{
    int n, len, count, size, fd;
    char *buff_endp = buff + buff_size - 1, *pbuff, *beg;

    fd = p->fd;
    if(buff_size == 0)
	return 0;
    else if(buff_size == 1) /* buff == buff_endp */
    {
	*buff = '\0';
	return 0;
    }

    len = 0;
    count = p->count;
    size = p->size;
    pbuff = p->buff;
    beg = buff;
    do
    {
	if(count == size)
	{
	    if((n = read(fd, pbuff, BUFSIZ)) <= 0)
	    {
		*buff = '\0';
		if(n == 0)
		{
		    p->count = p->size = 0;
		    return buff - beg;
		}
		return -1; /* < 0 error */
	    }
	    count = p->count = 0;
	    size = p->size = n;
	}
	*buff++ = pbuff[count++];
    } while(*(buff - 1) != '\n' && buff != buff_endp);
    *buff = '\0';
    p->count = count;
    return buff - beg;
}

static char *timidity_ctl_command(const char* fmt, ...)
{
    static char buff[BUFSIZ];
    int status, len;
    va_list ap;

    if(fmt != NULL)
    {
	va_start(ap, fmt);
	vsprintf(buff, fmt, ap);
	va_end(ap);
	len = strlen(buff);
	if(len > 0 && buff[len - 1] != '\n')
	    buff[len++] = '\n';
	write(control_fd.fd, buff, len);
    }

    while(1)
    {
	if(fdgets(buff, sizeof(buff), &control_fd) <= 0)
	{
	    strcpy(buff, "Read error\n");
	    break;
	}
	status = atoi(buff);
	if(400 <= status && status <= 499) /* Error of data stream */
	{
	    fputs(buff, stderr);
	    continue;
	}
	break;
    }
    return buff;
}

#ifdef	__cplusplus
extern "C" {
#endif
int open(const char *path, int oflag, ...)
{
    char *res;
    int fd, i, data_port;
    char timidity_host[128];
    int   timidity_port;

    if(strcmp(path, SEQUENCER_DEV) != 0)
    {
	int ret;
	va_list ap;
	mode_t mode;

	va_start(ap, oflag);
	mode = va_arg(ap, mode_t);
	ret = syscall(SYS_open, path, oflag, mode);
	va_end(ap);
	return ret;
    }
#ifdef	DEBUG
    fprintf(stderr, "open\n");
#endif

    if(seqfd != -1)
	return seqfd;

    if((res = getenv("TIMIDITY_HOST")) == NULL)
	res = TIMIDITY_HOST;
    strncpy(timidity_host, res, sizeof(timidity_host));
    if((res = strrchr(timidity_host, ':')) != NULL)
    {
	*res++ = '\0';
	timidity_port = atoi(res);
    }
    else
	timidity_port = TIMIDITY_CONTROL_PORT;

    memset(&control_fd, 0, sizeof(control_fd));
    control_fd.fd = connect_to_server(timidity_host, timidity_port);
    res = timidity_ctl_command(NULL);
    if(atoi(res) != 220)
    {
	fprintf(stderr, "Can't connect timidity: (host=%s, port=%d)\n",
		timidity_host, timidity_port);
	return -1;
    }
    fputs(res, stdout);

    res = timidity_ctl_command("SETBUF %f %f",
			       BUF_LOW_SYNC, BUF_HIGH_SYNC);
    if(atoi(res) != 200)
	fprintf(stderr, "WARNING: %s", res);

    i = 1;
    if(*(char *)&i == 1)
	res = timidity_ctl_command("OPEN lsb");
    else
	res = timidity_ctl_command("OPEN msb");

    if(atoi(res) != 200)
    {
	fprintf(stderr, "Can't open timidity data connection: %s", res);
	syscall(SYS_close, control_fd.fd);
	return -1;
    }

    data_port = atoi(res + 4);
    fd = connect_to_server(timidity_host, data_port);
    res = timidity_ctl_command(NULL);
    if(atoi(res) != 200)
    {
	fprintf(stderr, "Can't connect timidity: %s\t(host=%s, port=%d)\n",
		res, timidity_host, data_port);
	syscall(SYS_close, control_fd.fd);
	return -1;
    }
    seqfd = fd;

    return fd;
}

int close(int fd)
{
    char *res;

    if(fd == seqfd)
    {
#ifdef	DEBUG
	fprintf(stderr, "close\n");
#endif
	timidity_eot();
	timidity_sync(0);
	do
	{
	    res = timidity_ctl_command("QUIT");
	} while(atoi(res) && atoi(res) != 302);
	syscall(SYS_close, control_fd.fd);
	seqfd = -1;
    }
    return syscall(SYS_close, fd);
}

static double get_time(void)
{
    struct timeval tv;
    struct timezone dmy;

#ifdef GETTIMEOFDAY_ONE_ARGUMENT
    gettimeofday(&tv);
#else
    gettimeofday(&tv, &dmy);
#endif

    return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0 ;
}

int ioctl(int fildes, int request, ...)
{
    void *arg;
    va_list ap;
    char *res;

    va_start(ap, request);
    arg = va_arg(ap, void *);
    va_end(ap);
    if(fildes != seqfd)
      return syscall(SYS_ioctl, fildes, request, arg);

    switch(request)
    {
      case SNDCTL_SEQ_RESET:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_SEQ_RESET\n");
#endif
	timidity_eot();
	timidity_meta_seq(0x01, 0x00, 0); /* TiMidity reset */
	return 0;

      case SNDCTL_SEQ_SYNC: {
	    static double last_time;
	    double t;

#ifdef	DEBUG
	    fprintf(stderr, "ioctl SNDCTL_SEQ_SYNC\n");
#endif
	    if(sync_mode)
		return 0;
	    t = get_time();
	    if(t - last_time < 0.1)
		return 0;
	    last_time = t;
	    sync_mode = 1;
	    timidity_sync((int)(BUF_HIGH_SYNC * 100));
	    sync_mode = 0;
	}
	return 0;

      case SNDCTL_SYNTH_INFO:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_SYNTH_INFO\n");
#endif
	memcpy(arg, &ext_synth_info[ ((struct synth_info *)arg)->device ],
	       sizeof(struct synth_info));
	return 0;

      case SNDCTL_SEQ_CTRLRATE:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_SEQ_CTRLRATE\n");
#endif
	res = timidity_ctl_command("TIMEBASE");
	if(atoi(res) != 200)
	    return -1;
	*(int *)arg = atoi(res + 4);
	return 0;

      case SNDCTL_SEQ_TESTMIDI:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_SEQ_TESTMIDI\n");
#endif
	return 0;

      case SNDCTL_SEQ_RESETSAMPLES:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_SEQ_RESETSAMPLES\n");
#endif
	return 0;

      case SNDCTL_SEQ_NRSYNTHS:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_SEQ_NRSYNTHS\n");
#endif
	*(int *)arg = sizeof(ext_synth_info)/sizeof(ext_synth_info[0]);
	return 0;

      case SNDCTL_SEQ_NRMIDIS:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_SEQ_NRMIDIS\n");
#endif
	*(int *)arg = 1;
	return 0;

      case SNDCTL_MIDI_INFO:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_MIDI_INFO\n");
#endif
	return -1;

      case SNDCTL_SYNTH_MEMAVL:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_SYNTH_MEMAVL\n");
#endif
	return -1;

      case SNDCTL_FM_4OP_ENABLE:
#ifdef	DEBUG
	fprintf(stderr, "ioctl SNDCTL_FM_40P_ENABLE\n");
#endif
	return -1;

      default:
#ifdef	DEBUG
	fprintf(stderr, "ioctl UNKNOWN : %08x\n", request);
#endif
	return -1;
    }
}

#ifdef	__cplusplus
}
#endif
