/*
 * Copyrighted as an unpublished work.
 * (c) Copyright 1991 Brian Smith
 * All rights reserved.
 *
 * Read the LICENSE file for details on distribution and use.
 *
 */

#include <sys/fcntl.h>
#include <sys/unistd.h>
#include <sys/sb.h>
#include <sys/time.h>
#include <stdio.h>

#define lobyte(X)   (((unsigned char *)&X)[0])
#define hibyte(X)   (((unsigned char *)&X)[1])

/* Globals */
int fm_herz;        /* clock ticks per second */
int tempo;          /* clock ticks per quarter note */
int fm_fd;
int note_on[16];
char **instrument_table;
int note_table[12] = {
    343,
    363,
    385,
    408,
    432,
    458,
    485,
    514,
    544,
    577,
    611,
    647
    };


int main(argc, argv)
int argc;
char **argv;
{
    int cmf_fd;

    if (argc != 2)
    {
        printf("usage: %s <cmf file>\n", argv[0]);
        exit(-1);
    }

    /* open cmf file */
    cmf_fd = open(argv[1], O_RDONLY);
    if (cmf_fd == -1)
    {
        printf("usage: %s <cmf file>\n", argv[0]);
        exit(-1);
    }

    /* verify that file is a cmf file */
    if (!verify_cmf(cmf_fd))
    {
        printf("file was not a cmf file\n");
        printf("usage: %s <cmf file>\n", argv[0]);
        exit(-1);
    }

    /* read and set instruments from cmf file */
    get_instruments(cmf_fd);

    /* get timing */
    set_timing(cmf_fd);

    /* open soundblaster fm chips */
    fm_fd = open("/dev/sbfm", O_WRONLY);
    if (fm_fd == -1)
    {
        perror("opening fm chips");
        exit(-1);
    }

    /* play song */
    play_song(cmf_fd);

    return(0);
}


/* check for "CTMF" in first four bytes of file */
int verify_cmf(fd)
int fd;
{
    char idbuf[5];

    /* get id */
    lseek(fd, 0, SEEK_SET);
    if (read(fd, idbuf, 4) != 4)
        return(FALSE);
    
    /* compare to standard id */
    idbuf[4] = (char)0;
    if (strcmp(idbuf, "CTMF") != 0)
        return(FALSE);
    
    return(TRUE);
}

int get_instruments(fd)
int fd;
{
    int offset;
    int num_instruments;
    int i;
    int rc;
    int fnum, block, note;
    unsigned char tmp_byte;
    sb_fm_character note_character;

    /* get offset of instrument block */
    offset = 0;
    lseek(fd, 0x06, SEEK_SET);
    read(fd, &tmp_byte, 1);
    lobyte(offset) = tmp_byte;
    read(fd, &tmp_byte, 1);
    hibyte(offset) = tmp_byte;

    /* get number of instruments */
    num_instruments = 0;
    lseek(fd, 0x24, SEEK_SET);
    read(fd, &tmp_byte, 1);
    lobyte(num_instruments) = tmp_byte;
    read(fd, &tmp_byte, 1);
    hibyte(num_instruments) = tmp_byte;

    /* allocate space */
    instrument_table = (char **)malloc(sizeof(int *) * num_instruments);

    /* read each instrument */
    lseek(fd, offset, SEEK_SET);
    for (i=0; i< num_instruments; i++)
    {
        /* allocate space */
        instrument_table[i] = (char *)malloc(16);

        /* set instrument characteristics */
        read(fd, instrument_table[i], 16);
    }

    return(0);
}


/*
 * get and set timing parameters
 */
int set_timing(fd)
int fd;
{
    unsigned char tmp_byte;

    /* get tempo */
    tempo = 0;
    lseek(fd, 0x0C, SEEK_SET);
    read(fd, &tmp_byte, 1);
    tempo = (unsigned int)tmp_byte;
    read(fd, &tmp_byte, 1);
    tempo += (unsigned int)tmp_byte << 8;

    /* get herz of timing clock */
    fm_herz = 0;
    lseek(fd, 0x0C, SEEK_SET);
    read(fd, &tmp_byte, 1);
    fm_herz = (unsigned int)tmp_byte;
    read(fd, &tmp_byte, 1);
    fm_herz += (unsigned int)tmp_byte << 8;
    
    return(0);
}


/*
 * seek to the midi stream and handle midi events for the song
 */
int play_song(fd)
int fd;
{
    int offset;
    unsigned char tmp_byte;
    int delta;

    /* get offset of music stream */
    lseek(fd, 8, SEEK_SET);
    read(fd, &tmp_byte, 1);
    offset = (unsigned int)tmp_byte;
    read(fd, &tmp_byte, 1);
    offset += (unsigned int)tmp_byte << 8;
    lseek(fd, offset, SEEK_SET);

    /* process till EOF */
    while(1)
    {
        /* get delta time */
        delta = ReadVarLen(fd);
        if (delta == -1)
            break;

        /* wait delta */
        if (delta > 0)
            high_res_sleep((double)delta/(double)fm_herz);

        /* process midi event */
        process_event(fd, delta);
    }


    return(0);
}


/*
 * read a variable length scalar in MIDI format
 */
int ReadVarLen(fd)
int fd;
{
    int value;
    unsigned char tmp_byte;

    if (read(fd, &tmp_byte, 1) == 0)
        return(-1);
    value = (int)tmp_byte;
    if (tmp_byte & 0x80)
    {
        value &= 0x7F;
        do
        {
            if (read(fd, &tmp_byte, 1) == 0)
                return(-1);
            value = (value << 7) + (tmp_byte & 0x7F);
        } while (tmp_byte & 0x80);
    }

    return(value);
}


/*
 * process a midi event
 */
int process_event(fd, delta)
int fd;
{
    int rc;
    unsigned char tmp_byte;
    static int status = -1;
    sb_fm_note note;

    /* get status byte */
    read(fd, &tmp_byte, 1);
    if (tmp_byte & 0x80)
    {
        status = (unsigned int)tmp_byte;
    }
    else
    {
        /* running status, so back up one */
        if (status == -1)
        {
            printf("ERROR in cmf file. Running status at beginning of file\n");
            exit(-1);
        }
        lseek(fd, -1, SEEK_CUR);
    }
    
    /* switch different events */
    switch (status & 0xF0)
    {
        case 0x80:
            /* turn note off */
            ioctl(fm_fd, FM_IOCTL_NOTE_OFF, status & 0x0F);
            note_on[status&0x0F] = 0;
            /* waste two bytes */
            read(fd, &tmp_byte, 1);
            read(fd, &tmp_byte, 1);
            break;
        case 0x90:
            /* get note */
            read(fd, &tmp_byte, 1);
            /* determine note */
            note_num(note) = status & 0x0F;
            fnum_low(note) = note_table[tmp_byte%12] & 0xFF;
            fnum_low(note) = note_table[tmp_byte%12] & 0xFF;
            keyon_blk_fnum(note) = 0;
            keyon_blk_fnum(note) |= 1<<5;
            keyon_blk_fnum(note) |= ((tmp_byte/12) & 7) << 2;
            keyon_blk_fnum(note) |= (note_table[tmp_byte%12] & 0x3FF) >> 8;

            /* turn note on */
            if (note_on[status&0x0F])
                ioctl(fm_fd, FM_IOCTL_NOTE_OFF, status&0x0F);
            ioctl(fm_fd, FM_IOCTL_NOTE_ON, note);
            note_on[status&0x0F] = 1;

            /* waste a bytes */
            read(fd, &tmp_byte, 1);
            break;
        case 0xA0:
            printf("polyphonic key pressure: not handled\n");
            /* waste two bytes */
            read(fd, &tmp_byte, 1);
            read(fd, &tmp_byte, 1);
            break;
        case 0xB0:
            printf("control change: not handled\n");
            /* waste two bytes */
            read(fd, &tmp_byte, 1);
            read(fd, &tmp_byte, 1);
            break;
        case 0xC0:
            /* change the instrument on a channel */
            read(fd, &tmp_byte, 1);
            load_instrument(status&0x0F, tmp_byte & 0x0F);
            break;
        case 0xD0:
            printf("Channel Pressure: not handled\n");
            /* waste a byte */
            read(fd, &tmp_byte, 1);
            break;
        case 0xE0:
            printf("Pitch Wheel Change: not handled\n");
            /* waste two bytes */
            read(fd, &tmp_byte, 1);
            read(fd, &tmp_byte, 1);
            break;
        case 0xF0:
            printf("System Exclusive: not handled\n");
            /* waste two bytes */
            read(fd, &tmp_byte, 1);
            read(fd, &tmp_byte, 1);
            break;
        default:
            printf("internal program error\n");
            /* waste two bytes */
            read(fd, &tmp_byte, 1);
            read(fd, &tmp_byte, 1);
            break;
    }


    return(0);
}


/*
 * load an instrument from the instrument table into the SoundBlaster
 */
int load_instrument(channel, instrument)
{
    int rc;
    sb_fm_character note_character;

    /* error check! */
    if ((channel <0) || (channel >= 9))
        return;

    /* abort instrument if being loaded */
    if (note_on[channel])
        ioctl(fm_fd, FM_IOCTL_NOTE_OFF, channel);

    /* set instrument characteristics */
    note_character.voice_num = channel;
    memcpy(note_character.data, instrument_table[instrument], 16);
    rc = ioctl(fm_fd, FM_IOCTL_SET_VOICE, (int)&note_character);
    if (rc == -1)
    {
        perror("fm chips voice set");
        exit(-1);
    }

    return(0);
}


/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Higher-resolution sleep
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
int high_res_sleep(seconds)
    double      seconds;
{
    int         fds = 0;
    struct timeval timeout;

    timeout.tv_sec = seconds;
    timeout.tv_usec = (seconds - timeout.tv_sec) * 1000000.0;
    select(0, &fds, &fds, &fds, &timeout);
}

