/*
 * Book386 OPLplayer v1.0  (oplplay.c)
 * AdLib / OPL2 / OPL3 (Yamaha YMF262) music player for Linux, written
 * for Slackware 4.0, gcc 2.7.2.3, glibc 2.0.x, i386
 *
 * Only files recognised as one of the formats below are loaded; anything
 * else is rejected with "unknown/unsupported format" and exit status 1.
 * Plays:
 *   .imf / .wlf   id Software Music Format (type 0 and type 1); has no
 *                 signature, so the extension is required and the data
 *                 is validated
 *   .rad          Reality AdLib Tracker module, version 1.0/1.1 (9 channels)
 *   .dro          DOSBox Raw OPL capture, v0.1 and v2.0
 *   .raw          Rdos RAW OPL capture ("RAWADATA")
 *   .mid / .midi  Standard MIDI File, format 0 and 1 (built-in 18-voice
 *                 OPL3 FM synth; optional IBK instrument banks)
 *
 * It programs the chip directly via I/O ports (default 0x388), so it must
 * be run as root and nothing else may be using the card at the same time.
 * Pure C89: no // comments, no declarations after statements, no <stdint.h>.
 *
 * Build:   gcc -O2 -Wall -o oplplay oplplay.c
 *          (-O2 is REQUIRED: inb()/outb() are inline assembler)
 *
 * Usage:   oplplay [-p port] [-r rate] [-o GENMIDI.OP2]
 *                  [-b melodic.ibk] [-d drums.ibk] [-f] [-l] [-R] [-n] file
 *    -p port   OPL base port in hex (default 388)
 *    -r rate   IMF tick rate in Hz (default 560; Duke II = 280, Duke 3D = 700)
 *    -o file   DMX GENMIDI.OP2 bank (DOOM etc): melodic + percussion,
 *              incl. double-voice instruments; -b/-d override it
 *    -b file   IBK bank for MIDI melodic programs 0..127
 *    -d file   IBK bank for MIDI percussion, indexed by MIDI note 0..127
 *    -g dB     MIDI boost 0-30 dB (default 0): lowers note attenuation and
 *              even eats into a patch's own output level; try -g 6
 *    -v pct    software volume 0-100 (default 100 = full). Only ever
 *              LOWERS the chip output; OPL cannot go louder than full.
 *    -q        quiet: no live status line
 *    -f        skip OPL detection
 *    -l        loop forever
 *    -R        use SCHED_FIFO real-time priority (can starve the machine!)
 *    -n        dry run: print register writes, do not touch hardware
 */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sched.h>
#include <sys/time.h>
#include <sys/types.h>
#if defined(__GLIBC__)
#include <sys/io.h>
#else
/* libc5: no <sys/io.h> or <sys/perm.h>; use the kernel's inline
   inb()/outb() (needs -O2) and declare the libc ioperm() wrapper. */
#include <asm/io.h>
extern int ioperm(unsigned long from, unsigned long num, int turn_on);
#endif

typedef unsigned char  u8;
typedef unsigned short u16;
typedef unsigned long  u32;

/* ------------------------------------------------------------------ */
/* globals                                                             */
/* ------------------------------------------------------------------ */

static int opl_port = 0x388;
static int opl3_on  = 0;
static int dry      = 0;
static int quiet    = 0;
static int use_rt   = 0;
static u8  shadow[2][256];
static int midi_boost = 0;  /* MIDI boost in 0.75 dB steps (-g) */
static int vol_att  = 0;   /* software attenuation in 0.75 dB steps */
static const int op_off[9] = { 0, 1, 2, 8, 9, 10, 16, 17, 18 };
static volatile int quit = 0;

#define PROG_TITLE "Book386 OPLplayer"
#define PROG_VER   "v1.0"
#define PROG_AUTHOR "astr0baby"
static int    show_status = 1;  /* live status line (-q turns it off) */
static int    st_midi = 0;      /* currently playing MIDI */
static int    st_rad  = 0;      /* currently playing RAD */
static double total_us = 0.0;   /* song length found by the pre-scan */
static double st_last = -1e9;   /* wall time of last status redraw */
static char   fmt_info[96] = "";

static struct timeval t_base;
static double clock_us = 0.0;   /* song time since start of playback */
static double t_play0  = 0.0;   /* wall time (us) at start of playback */

static void on_signal(int s)
{
    (void)s;
    quit = 1;
}

/* ------------------------------------------------------------------ */
/* timing                                                              */
/* ------------------------------------------------------------------ */

static double now_us(void)
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (double)(tv.tv_sec - t_base.tv_sec) * 1000000.0
         + (double)(tv.tv_usec - t_base.tv_usec);
}

static void wait_until(double t)
{
    double d;
    while (!quit) {
        d = t - now_us();
        if (d <= 0.0)
            break;
        /* 2.2 kernels have 10 ms timer granularity: sleep coarsely,
           then spin the last stretch for accuracy. */
        if (d > 30000.0)
            usleep((unsigned long)(d - 20000.0));
        else if (!use_rt)
            sched_yield();
    }
}

static void status_tick(void);

/* advance song clock by 'us' microseconds and wait for it */
static void advance(double us)
{
    double target, now;

    clock_us += us;
    if (dry)
        return;
    target = t_play0 + clock_us;
    now = now_us();
    if (now - target > 250000.0) {      /* fell far behind: resync */
        t_play0 += now - target;
        target = now;
    }
    while (!quit) {                     /* wait in slices, keep status alive */
        now = now_us();
        if (now >= target)
            break;
        status_tick();
        wait_until((target - now > 250000.0) ? now + 250000.0 : target);
    }
    status_tick();
}

/* ------------------------------------------------------------------ */
/* OPL register access                                                 */
/* ------------------------------------------------------------------ */

static void opl_raw(int bank, int reg, int val)
{
    int p, i;

    if (dry) {
        if (!quiet)
            printf("%9.1f ms  bank %d  reg %02X = %02X\n",
                   clock_us / 1000.0, bank, reg, val);
        return;
    }
    p = opl_port + (bank ? 2 : 0);
    outb((unsigned char)reg, p);
    for (i = 0; i < 6; i++)
        inb(opl_port);                  /* ~3.3 us */
    outb((unsigned char)val, p + 1);
    for (i = 0; i < 35; i++)
        inb(opl_port);                  /* ~23 us */
}

static void refresh_stereo(void)
{
    int b, c, v;
    for (b = 0; b < 2; b++)
        for (c = 0; c < 9; c++) {
            v = shadow[b][0xC0 + c];
            if ((v & 0x30) == 0) {
                v |= 0x30;
                shadow[b][0xC0 + c] = (u8)v;
                opl_raw(b, 0xC0 + c, v);
            }
        }
}

/* Software volume: add attenuation to total-level registers (0x40-0x55).
   Carriers are always scaled; modulators only where they are audible
   (additive connection, or rhythm-mode drum operators). */
static int scale_tl(int bank, int reg, int val)
{
    static const signed char op_ch[22] = {
         0, 1, 2, 0, 1, 2,-1,-1, 3, 4, 5, 3, 4, 5,-1,-1, 6, 7, 8, 6, 7, 8
    };
    int off, ch, car, tl, rhythm;

    if (vol_att == 0 || reg < 0x40 || reg > 0x55)
        return val;
    off = reg - 0x40;
    ch = op_ch[off];
    if (ch < 0)
        return val;
    car = (off % 8) >= 3;
    rhythm = (bank == 0 && (shadow[0][0xBD] & 0x20) && ch >= 6);
    if (!(car || (shadow[bank][0xC0 + ch] & 1) || (rhythm && ch >= 7)))
        return val;
    tl = (val & 0x3F) + vol_att;
    if (tl > 63)
        tl = 63;
    return (val & 0xC0) | tl;
}

static void reemit_tl(int bank, int ch)
{
    int mo = op_off[ch], co = op_off[ch] + 3;
    opl_raw(bank, 0x40 + mo, scale_tl(bank, 0x40 + mo, shadow[bank][0x40 + mo]));
    opl_raw(bank, 0x40 + co, scale_tl(bank, 0x40 + co, shadow[bank][0x40 + co]));
}

static void opl_write(int bank, int reg, int val)
{
    reg &= 0xFF;
    val &= 0xFF;

    if (bank == 1 && reg == 0x05) {
        opl3_on = val & 1;
        shadow[1][5] = (u8)val;
        opl_raw(1, 5, val);
        if (opl3_on)
            refresh_stereo();
        return;
    }
    if (bank == 1 && !opl3_on)          /* dual-OPL2 style file */
        opl_write(1, 5, 1);

    /* OPL2-era data never sets the L/R output bits; in OPL3 mode that
       would be silence, so force both outputs on in that case. */
    if (reg >= 0xC0 && reg <= 0xC8 && opl3_on && (val & 0x30) == 0)
        val |= 0x30;

    shadow[bank][reg] = (u8)val;
    opl_raw(bank, reg, scale_tl(bank, reg, val));
    if (vol_att) {
        if (reg >= 0xC0 && reg <= 0xC8)         /* connection changed */
            reemit_tl(bank, reg - 0xC0);
        else if (bank == 0 && reg == 0xBD) {    /* rhythm mode toggled */
            reemit_tl(0, 6);
            reemit_tl(0, 7);
            reemit_tl(0, 8);
        }
    }
}

static void opl_reset(void)
{
    int b, r;

    quiet = 1;
    opl3_on = 0;
    for (b = 0; b < 2; b++)
        for (r = 0x20; r <= 0xF5; r++) {
            shadow[b][r] = 0;
            opl_raw(b, r, 0);
        }
    opl_raw(1, 0x04, 0);
    opl_raw(1, 0x05, 0);
    opl_raw(0, 0x01, 0x20);             /* waveform select enable */
    memset(shadow, 0, sizeof(shadow));
    shadow[0][1] = 0x20;
    quiet = 0;
}

static int opl_detect(void)
{
    int s1, s2;

    opl_raw(0, 0x04, 0x60);
    opl_raw(0, 0x04, 0x80);
    s1 = inb(opl_port) & 0xE0;
    opl_raw(0, 0x02, 0xFF);
    opl_raw(0, 0x04, 0x21);
    usleep(20000);
    s2 = inb(opl_port) & 0xE0;
    opl_raw(0, 0x04, 0x60);
    opl_raw(0, 0x04, 0x80);
    return (s1 == 0x00 && s2 == 0xC0);
}

/* ------------------------------------------------------------------ */
/* little helpers                                                      */
/* ------------------------------------------------------------------ */

static u32 rd16(const u8 *p) { return (u32)p[0] | ((u32)p[1] << 8); }
static u32 rd32(const u8 *p)
{
    return (u32)p[0] | ((u32)p[1] << 8) | ((u32)p[2] << 16) | ((u32)p[3] << 24);
}
static u32 be16(const u8 *p) { return ((u32)p[0] << 8) | (u32)p[1]; }
static u32 be32(const u8 *p)
{
    return ((u32)p[0] << 24) | ((u32)p[1] << 16) | ((u32)p[2] << 8) | (u32)p[3];
}

/* ------------------------------------------------------------------ */
/* IMF                                                                 */
/* ------------------------------------------------------------------ */

static void play_imf(const u8 *b, long len, double rate)
{
    long pos, end;
    u32 first;

    if (len < 4)
        return;
    first = rd16(b);
    if (first == 0) {
        pos = 0;                        /* type 0: raw stream */
        end = len;
    } else {
        pos = 2;                        /* type 1: length-prefixed */
        end = 2 + (long)first;
    }
    sprintf(fmt_info, "id Software IMF type %d, %.0f Hz", pos ? 1 : 0, rate);
    while (!quit && pos + 4 <= end) {
        opl_write(0, b[pos], b[pos + 1]);
        advance((double)rd16(b + pos + 2) * 1000000.0 / rate);
        pos += 4;
    }
}

/* ------------------------------------------------------------------ */
/* DOSBox DRO                                                          */
/* ------------------------------------------------------------------ */

static void play_dro_v1(const u8 *b, long len)
{
    long pos;
    int bank = 0, reg, val;
    u32 nbytes;

    if (len < 0x18)
        return;
    nbytes = rd32(b + 0x10);
    if ((long)nbytes == len - 0x15)     /* 1-byte hardware type */
        pos = 0x15;
    else
        pos = 0x18;                     /* 4-byte hardware type */

    while (!quit && pos < len) {
        int c = b[pos++];
        switch (c) {
        case 0:
            if (pos >= len) return;
            advance((double)(b[pos++] + 1) * 1000.0);
            break;
        case 1:
            if (pos + 2 > len) return;
            advance((double)(rd16(b + pos) + 1) * 1000.0);
            pos += 2;
            break;
        case 2: bank = 0; break;
        case 3: bank = 1; break;
        case 4:
            if (pos + 2 > len) return;
            reg = b[pos]; val = b[pos + 1]; pos += 2;
            opl_write(bank, reg, val);
            break;
        default:
            if (pos >= len) return;
            val = b[pos++];
            opl_write(bank, c, val);
            break;
        }
    }
}

static void play_dro_v2(const u8 *b, long len)
{
    long pos, end;
    u32 npairs, i;
    int shortc, longc, mapn, idx, val;

    if (len < 26)
        return;
    npairs = rd32(b + 12);
    /* b[20]=hardware, b[21]=format, b[22]=compression */
    shortc = b[23];
    longc  = b[24];
    mapn   = b[25];
    if (26 + mapn > len)
        return;
    pos = 26 + mapn;
    end = pos + (long)npairs * 2;
    if (end > len)
        end = len;

    for (i = 0; !quit && pos + 2 <= end; i++, pos += 2) {
        idx = b[pos];
        val = b[pos + 1];
        if (idx == shortc)
            advance((double)(val + 1) * 1000.0);
        else if (idx == longc)
            advance((double)((val + 1) << 8) * 1000.0);
        else if ((idx & 0x7F) < mapn)
            opl_write((idx & 0x80) ? 1 : 0, b[26 + (idx & 0x7F)], val);
    }
}

static void play_dro(const u8 *b, long len)
{
    u32 maj, min;

    if (len < 12)
        return;
    maj = rd16(b + 8);
    min = rd16(b + 10);
    if (maj == 2) {
        strcpy(fmt_info, "DOSBox Raw OPL (DRO) v2.0");
        play_dro_v2(b, len);
    } else if (maj == 0 && min == 1) {
        strcpy(fmt_info, "DOSBox Raw OPL (DRO) v0.1");
        play_dro_v1(b, len);
    } else
        fprintf(stderr, "unsupported DRO version %lu.%lu\n",
                (unsigned long)maj, (unsigned long)min);
}

/* ------------------------------------------------------------------ */
/* Rdos RAW                                                            */
/* ------------------------------------------------------------------ */

static void play_raw(const u8 *b, long len)
{
    long pos;
    u32 clock;
    int bank = 0, data, cmd;

    if (len < 10)
        return;
    clock = rd16(b + 8);
    if (clock == 0)
        clock = 0x10000;
    strcpy(fmt_info, "Rdos RAW OPL capture");
    pos = 10;
    while (!quit && pos + 2 <= len) {
        data = b[pos];
        cmd  = b[pos + 1];
        pos += 2;
        if (cmd == 0x00) {
            advance((double)data * (double)clock * 1000000.0 / 1193180.0);
        } else if (cmd == 0x02) {
            if (data == 0) {
                if (pos + 2 > len) return;
                clock = rd16(b + pos);
                if (clock == 0)
                    clock = 0x10000;
                pos += 2;
            } else {
                bank = (data - 1) ? 1 : 0;
            }
        } else if (cmd == 0xFF && data == 0xFF) {
            break;
        } else {
            opl_write(bank, cmd, data);
        }
    }
}

/* ------------------------------------------------------------------ */
/* Reality AdLib Tracker (RAD) v1.0 module player                      */
/* Ported from Shayde/REALITY's PLAYER.ASM v1.1a. 9 OPL2 channels,     */
/* 64-line patterns, ticked at 50 Hz (18.2 Hz for "slow-timer" tunes). */
/* ------------------------------------------------------------------ */

#define RAD_FSTART 0x156
#define RAD_FRANGE 0x158                /* 0x2AE - 0x156 */

static const int rad_notefreq[12] = {   /* notes 1..12 = C# .. C(next oct) */
    0x16B, 0x181, 0x198, 0x1B0, 0x1CA, 0x1E5,
    0x202, 0x220, 0x241, 0x263, 0x287, 0x2AE
};
static const int rad_choff[9] = { 0x20, 0x21, 0x22, 0x28, 0x29, 0x2A, 0x30, 0x31, 0x32 };

static const u8 *rd_b;
static long rd_len;
static long rd_inst[31];                /* file offset of 11-byte instrument */
static long rd_orderlist, rd_patlist, rd_patpos;
static int  rd_nord, rd_orderpos, rd_speed, rd_speedcnt, rd_line, rd_ended;
static u8   rd_old43[9], rd_oldA0[9], rd_oldB0[9], rd_tsspeed[9];
static u8   rd_tone[9], rd_port[9], rd_vol[9];
static unsigned rd_tsfreq[9];

static void rad_setvol(int ch, int vol) /* vol: 0..255 byte, >=64 -> 63 */
{
    if (vol >= 64)
        vol = 63;
    rd_old43[ch] = (u8)((rd_old43[ch] & 0xC0) | (vol ^ 0x3F));
    opl_write(0, rad_choff[ch] + 0x23, rd_old43[ch]);
}

/* instrument: 0 car20 1 mod20 2 car40 3 mod40 4 car60 5 mod60 6 car80
   7 mod80 8 C0 9 carE0 10 modE0 */
static void rad_loadinst(int ch, int inst)
{
    long p;
    const u8 *d;
    int off;

    if (inst < 1 || inst > 31)
        return;
    p = rd_inst[inst - 1];
    if (!p)
        return;
    d = rd_b + p;
    off = rad_choff[ch] - 0x20;
    rd_old43[ch] = d[2];
    opl_write(0, 0x20 + off, d[1]);
    opl_write(0, 0x23 + off, d[0]);
    opl_write(0, 0x40 + off, d[3]);
    opl_write(0, 0x43 + off, d[2]);
    opl_write(0, 0x60 + off, d[5]);
    opl_write(0, 0x63 + off, d[4]);
    opl_write(0, 0x80 + off, d[7]);
    opl_write(0, 0x83 + off, d[6]);
    opl_write(0, 0xE0 + off, d[10]);
    opl_write(0, 0xE3 + off, d[9]);
    opl_write(0, 0xC0 + ch, d[8]);
}

static unsigned rad_getfreq(int ch)
{
    unsigned cx, oct;

    cx = (unsigned)rd_oldA0[ch] | ((unsigned)(rd_oldB0[ch] & 3) << 8);
    oct = (unsigned)((rd_oldB0[ch] >> 2) & 7);
    cx = (cx - RAD_FSTART) & 0xFFFF;
    return (oct * RAD_FRANGE + cx) & 0xFFFF;
}

static void rad_setfreq(int ch, unsigned ax)
{
    unsigned oct, fr;
    int b0;

    ax &= 0xFFFF;
    oct = ax / RAD_FRANGE;
    fr = ax % RAD_FRANGE + RAD_FSTART;
    b0 = (rd_oldB0[ch] & 0xE0) | (int)((oct << 2) & 0xFF) | (int)(fr >> 8);
    rd_oldB0[ch] = (u8)b0;
    opl_write(0, 0xB0 + ch, b0);
    rd_oldA0[ch] = (u8)(fr & 0xFF);
    opl_write(0, 0xA0 + ch, (int)(fr & 0xFF));
}

/* Returns the line to jump to (0..63) or -1. */
static int rad_playnote(int ch, int nb, int ib, int param)
{
    int cmd = ib & 15, note, inst, f, b0;

    if (nb != 0) {
        if (cmd == 3) {                 /* tone slide: note is the target */
            note = (nb & 15) - 1;
            if (note >= 0 && note < 12) {
                rd_tsfreq[ch] = (unsigned)(((nb >> 4) & 7) * RAD_FRANGE
                                + rad_notefreq[note] - RAD_FSTART) & 0xFFFF;
                rd_tone[ch] = 1;
                if (param)
                    rd_tsspeed[ch] = (u8)param;
            }
            return -1;
        }
        rd_oldB0[ch] &= (u8)~0x20;      /* key off previous note */
        opl_write(0, 0xB0 + ch, rd_oldB0[ch]);
        inst = ((nb & 0x80) ? 16 : 0) | (ib >> 4);
        if (inst)
            rad_loadinst(ch, inst);
        note = nb & 15;
        if (note >= 1 && note <= 12) {
            f = rad_notefreq[note - 1];
            b0 = 0x20 | (((nb >> 4) & 7) << 2) | (f >> 8);
            rd_oldB0[ch] = (u8)b0;
            rd_oldA0[ch] = (u8)(f & 0xFF);
            opl_write(0, 0xA0 + ch, f & 0xFF);
            opl_write(0, 0xB0 + ch, b0);
        }
    }

    switch (cmd) {
    case 1:                             /* portamento up */
        rd_port[ch] = (u8)param;
        break;
    case 2:                             /* portamento down */
        rd_port[ch] = (u8)(-param);
        break;
    case 3:                             /* tone slide, no note */
        if (param)
            rd_tsspeed[ch] = (u8)param;
        rd_tone[ch] = 1;
        break;
    case 5:                             /* tone slide + volume slide */
    case 10:                            /* volume slide: <50 down, >50 up */
        rd_vol[ch] = (u8)((param >= 50) ? -(param - 50) : param);
        if (cmd == 5)
            rd_tone[ch] = 1;
        break;
    case 12:                            /* set volume */
        rad_setvol(ch, param);
        break;
    case 13:                            /* jump to line in next pattern */
        if (param < 64)
            return param;
        break;
    case 15:                            /* set speed */
        rd_speed = param;
        break;
    default:
        break;
    }
    return -1;
}

static void rad_update(void)
{
    int ch, vol, spd, s;
    unsigned ax, cx;

    for (ch = 0; ch < 9; ch++) {
        if (rd_port[ch]) {
            ax = rad_getfreq(ch);
            ax = (ax + (unsigned)(int)(signed char)rd_port[ch]) & 0xFFFF;
            rad_setfreq(ch, ax);
        }
        s = (signed char)rd_vol[ch];
        if (s != 0) {
            vol = (rd_old43[ch] & 0x3F) ^ 0x3F;
            vol -= s;
            if (vol < 0)
                vol = 0;
            if (vol > 63)
                vol = 63;
            rad_setvol(ch, vol);
        }
        if (rd_tone[ch]) {
            spd = (signed char)rd_tsspeed[ch];
            ax = rad_getfreq(ch);
            cx = rd_tsfreq[ch];
            if (ax < cx) {
                ax = (ax + (unsigned)spd) & 0xFFFF;
                if (ax >= cx) {
                    ax = cx;
                    rd_tone[ch] = 0;
                }
            } else if (ax > cx) {
                ax = (ax - (unsigned)spd) & 0xFFFF;
                if (ax <= cx) {
                    ax = cx;
                    rd_tone[ch] = 0;
                }
            } else {
                rd_tone[ch] = 0;
            }
            rad_setfreq(ch, ax);
        }
    }
}

/* Move to order position 'pos', following jump markers (bit 7).  Running
   off the end or jumping backwards = the tune would loop: flag it. */
static void rad_goto_order(int pos)
{
    int pat = 0, guard = 0;

    for (;;) {
        if (pos < 0 || pos >= rd_nord) {
            pos = 0;
            rd_ended = 1;
        }
        pat = rd_b[rd_orderlist + pos];
        if (!(pat & 0x80))
            break;
        if ((pat & 0x7F) <= pos)
            rd_ended = 1;
        pos = pat & 0x7F;
        if (++guard > 300) {
            rd_ended = 1;
            pat = 0;
            pos = 0;
            break;
        }
    }
    rd_orderpos = pos;
    rd_patpos = (pat < 32) ? (long)rd16(rd_b + rd_patlist + pat * 2) : 0L;
}

/* One timer tick (PlayMusic). */
static void rad_tick(void)
{
    int i, ch, nb, ib, par, last, lastline, id, jump = -1, j, bad;
    long p;

    if (rd_speedcnt != 0) {
        rd_speedcnt--;
        rad_update();
        return;
    }
    for (i = 0; i < 9; i++) {
        rd_port[i] = 0;
        rd_vol[i] = 0;
        rd_tone[i] = 0;
    }

    p = rd_patpos;
    if (p > 0 && p < rd_len && (rd_b[p] & 0x7F) == rd_line) {
        lastline = rd_b[p] & 0x80;
        p++;
        for (;;) {
            if (p + 3 > rd_len) {
                rd_patpos = 0;
                break;
            }
            ch = rd_b[p] & 0x7F;
            last = rd_b[p] & 0x80;
            nb = rd_b[p + 1];
            ib = rd_b[p + 2];
            p += 3;
            par = 0;
            if (ib & 15) {
                if (p >= rd_len) {
                    rd_patpos = 0;
                    break;
                }
                par = rd_b[p++];
            }
            if (ch < 9) {
                j = rad_playnote(ch, nb, ib, par);
                if (j >= 0) {
                    jump = j;
                    break;
                }
            }
            if (last) {
                rd_patpos = lastline ? 0L : p;
                break;
            }
        }
    }

    if (jump >= 0) {                    /* Dxx: line 'jump' of next pattern */
        rd_speedcnt = rd_speed & 0xFF;
        rd_line = jump;
        rad_goto_order(rd_orderpos + 1);
        p = rd_patpos;
        if (p != 0) {
            bad = 0;
            for (;;) {                  /* find first line >= target */
                if (p >= rd_len) { bad = 1; break; }
                id = rd_b[p] & 0x7F;
                if (id >= jump)
                    break;
                if (rd_b[p] & 0x80) { bad = 1; break; }
                p++;
                for (;;) {              /* skip this line's channel data */
                    if (p + 3 > rd_len) { bad = 1; break; }
                    last = rd_b[p] & 0x80;
                    par = rd_b[p + 2] & 15;
                    p += 3;
                    if (par)
                        p++;
                    if (last)
                        break;
                }
                if (bad)
                    break;
            }
            rd_patpos = bad ? 0L : p;
        }
        rad_update();
        return;
    }

    rd_speedcnt = (rd_speed - 1) & 0xFF;        /* after effects: new speed */
    rd_line++;
    if (rd_line >= 64) {
        rd_line = 0;
        rad_goto_order(rd_orderpos + 1);
    }
    rad_update();
}

static void play_rad(const u8 *b, long len)
{
    long pos;
    int flags, n, i, slow;
    double tick;
    unsigned long ticks = 0;

    rd_b = b;
    rd_len = len;
    flags = b[17];
    slow = (flags & 0x40) != 0;
    rd_speed = flags & 0x1F;
    pos = 18;
    if (flags & 0x80) {                 /* skip description */
        while (pos < len && b[pos])
            pos++;
        pos++;
    }
    for (i = 0; i < 31; i++)
        rd_inst[i] = 0;
    while (pos < len) {
        n = b[pos++];
        if (n == 0)
            break;
        if (n <= 31)
            rd_inst[n - 1] = pos;
        pos += 11;
    }
    rd_nord = (pos < len) ? b[pos++] : 0;
    rd_orderlist = pos;
    rd_patlist = pos + rd_nord;
    if (rd_nord == 0 || rd_patlist + 64 > len)
        return;

    memset(rd_old43, 0, sizeof(rd_old43));
    memset(rd_oldA0, 0, sizeof(rd_oldA0));
    memset(rd_oldB0, 0, sizeof(rd_oldB0));
    memset(rd_tsspeed, 1, sizeof(rd_tsspeed));
    memset(rd_tone, 0, sizeof(rd_tone));
    memset(rd_port, 0, sizeof(rd_port));
    memset(rd_vol, 0, sizeof(rd_vol));
    for (i = 0; i < 9; i++)
        rd_tsfreq[i] = 0;
    rd_speedcnt = 0;
    rd_line = 0;
    rd_ended = 0;
    rd_patpos = 0;
    rad_goto_order(0);

    sprintf(fmt_info, "Reality AdLib Tracker v1.0, %d orders, %s timer",
            rd_nord, slow ? "18.2 Hz" : "50 Hz");
    tick = slow ? 1000000.0 * 65535.0 / 1193182.0
                : 1000000.0 * 23863.0 / 1193182.0;

    st_rad = 1;
    /* once the tune wraps, let the last line finish, then stop */
    while (!quit && !(rd_ended && rd_speedcnt == 0) && ticks < 3000000UL) {
        rad_tick();
        advance(tick);
        ticks++;
    }
    if (!quit)
        advance(800000.0);
    st_rad = 0;
}

/* ------------------------------------------------------------------ */
/* MIDI synth on 18 two-operator OPL3 voices                           */
/* ------------------------------------------------------------------ */

/* d[]: mod20 car20 mod40 car40 mod60 car60 mod80 car80 modE0 carE0 C0 */
typedef struct {
    u8 d[11];
    u8 fixed;       /* if non-zero: always play at this MIDI note (drums) */
    signed char xpose;      /* semitone offset for voice 1 (OP2) */
    u8 has2;                /* OP2 double-voice instrument */
    u8 d2[11];              /* second voice data */
    signed char xpose2;
} patch_t;

#define NVOICES 18
#define DRUM_IBK_NOTE 48

typedef struct {
    int state;              /* 0 free, 1 key on, 2 released */
    int ch;
    int note;
    int vel;
    int sus;
    int which;              /* 0 = first voice of patch, 1 = second */
    unsigned long age;
    double off_us;
    const patch_t *p;
} voice_t;

typedef struct {
    int prog, vol, expr, pan, bend, sustain;
} mchan_t;

static const int fnum_tab[12] = {
    0x157, 0x16B, 0x181, 0x198, 0x1B0, 0x1CA,
    0x1E5, 0x202, 0x220, 0x241, 0x263, 0x287
};

static patch_t melodic[128];
static patch_t drums[128];
static voice_t vo[NVOICES];
static mchan_t mch[16];
static unsigned long age_ctr;

/* ---- built-in starter instruments, one per GM family of 8 ---- */
static const u8 fam[16][11] = {
    /* 0 piano     */ {0x01,0x01,0x1F,0x00,0xF3,0xF4,0x54,0x36,0x00,0x00,0x06},
    /* 1 chrom prc */ {0x06,0x01,0x18,0x00,0xF6,0xF5,0x37,0x27,0x00,0x00,0x0A},
    /* 2 organ     */ {0x32,0x21,0x22,0x00,0xF0,0xF0,0x0F,0x0F,0x00,0x00,0x04},
    /* 3 guitar    */ {0x11,0x01,0x1A,0x00,0xF5,0xF4,0x66,0x56,0x00,0x00,0x08},
    /* 4 bass      */ {0x01,0x01,0x16,0x00,0xF4,0xF4,0x55,0x55,0x00,0x00,0x0A},
    /* 5 strings   */ {0x61,0x61,0x2A,0x00,0x63,0x63,0x08,0x08,0x00,0x00,0x02},
    /* 6 ensemble  */ {0x61,0x21,0x30,0x00,0x53,0x53,0x08,0x08,0x00,0x00,0x04},
    /* 7 brass     */ {0x21,0x21,0x1A,0x00,0x82,0x82,0x16,0x16,0x00,0x00,0x06},
    /* 8 reed      */ {0x21,0x21,0x24,0x00,0xA2,0xA2,0x18,0x18,0x00,0x00,0x08},
    /* 9 pipe      */ {0x21,0x21,0x3C,0x00,0x84,0x84,0x16,0x16,0x00,0x00,0x00},
    /*10 syn lead  */ {0x22,0x21,0x0E,0x00,0xF2,0xF2,0x07,0x07,0x02,0x00,0x0C},
    /*11 syn pad   */ {0xA1,0x61,0x28,0x00,0x43,0x43,0x08,0x08,0x00,0x00,0x04},
    /*12 syn fx    */ {0x23,0x21,0x12,0x00,0xA3,0x93,0x35,0x25,0x00,0x00,0x0E},
    /*13 ethnic    */ {0x11,0x01,0x1C,0x00,0xF4,0xF3,0x65,0x65,0x00,0x00,0x08},
    /*14 percussive*/ {0x04,0x01,0x12,0x00,0xF8,0xF7,0x75,0x66,0x00,0x00,0x08},
    /*15 sfx       */ {0x0E,0x01,0x08,0x00,0xA2,0x82,0x44,0x44,0x00,0x00,0x0E}
};

/* ---- built-in drum kit approximations ---- */
static const u8 drum_tab[7][11] = {
    /*0 kick   */ {0x01,0x01,0x0B,0x00,0xF8,0xD9,0x67,0x66,0x00,0x00,0x0E},
    /*1 snare  */ {0x0F,0x01,0x00,0x00,0xF9,0xF8,0x66,0x56,0x00,0x00,0x0E},
    /*2 tom    */ {0x04,0x01,0x10,0x00,0xF8,0xF7,0x75,0x66,0x00,0x00,0x08},
    /*3 c.hat  */ {0x0F,0x0F,0x00,0x00,0xF9,0xFA,0xF8,0xF8,0x00,0x00,0x0F},
    /*4 o.hat  */ {0x0F,0x0F,0x00,0x00,0xF4,0xF4,0xA4,0xA4,0x00,0x00,0x0F},
    /*5 cymbal */ {0x0F,0x0F,0x00,0x00,0xF3,0xF3,0x73,0x73,0x00,0x00,0x0F},
    /*6 generic*/ {0x01,0x01,0x08,0x00,0xF9,0xF8,0x66,0x66,0x00,0x00,0x08}
};
static const u8 drum_note[7] = { 36, 60, 0, 84, 84, 72, 60 };

static int drum_class(int n)
{
    switch (n) {
    case 35: case 36: return 0;
    case 37: case 38: case 39: case 40: return 1;
    case 41: case 43: case 45: case 47: case 48: case 50: return 2;
    case 42: case 44: return 3;
    case 46: return 4;
    case 49: case 51: case 52: case 55: case 57: case 59: return 5;
    default: return 6;
    }
}

static void init_banks(void)
{
    int i, k;
    for (i = 0; i < 128; i++) {
        memset(&melodic[i], 0, sizeof(patch_t));
        memset(&drums[i], 0, sizeof(patch_t));
        memcpy(melodic[i].d, fam[i / 8], 11);
        k = drum_class(i);
        memcpy(drums[i].d, drum_tab[k], 11);
        drums[i].fixed = drum_note[k];
    }
}

/* IBK = "IBK\x1A" + 128 * 16-byte instruments (+ names, ignored) */
static int load_ibk(const char *fn, patch_t *dst, int is_drum)
{
    FILE *f;
    u8 hdr[4], rec[16];
    int i;

    f = fopen(fn, "rb");
    if (!f) {
        perror(fn);
        return -1;
    }
    if (fread(hdr, 1, 4, f) != 4 || memcmp(hdr, "IBK\x1A", 4) != 0) {
        fprintf(stderr, "%s: not an IBK file\n", fn);
        fclose(f);
        return -1;
    }
    for (i = 0; i < 128; i++) {
        if (fread(rec, 1, 16, f) != 16)
            break;
        memset(&dst[i], 0, sizeof(patch_t));
        memcpy(dst[i].d, rec, 11);
        dst[i].d[10] &= 0x0F;
        dst[i].fixed = is_drum ? DRUM_IBK_NOTE : 0;
    }
    fclose(f);
    return 0;
}

/* Convert one 16-byte OP2 voice into our register layout.
   OP2 voice: mod{trem,ad,sr,wave,scale,level} fb car{...6...} unused i16 base */
static void op2_voice(const u8 *v, u8 *d, signed char *xp)
{
    int base = (int)(short)(v[14] | (v[15] << 8));
    d[0]  = v[0];                               /* mod 0x20 */
    d[1]  = v[7];                               /* car 0x20 */
    d[2]  = (u8)((v[4] & 0xC0) | (v[5] & 0x3F));/* mod 0x40 */
    d[3]  = (u8)((v[11] & 0xC0) | (v[12] & 0x3F));
    d[4]  = v[1];                               /* mod 0x60 */
    d[5]  = v[8];                               /* car 0x60 */
    d[6]  = v[2];                               /* mod 0x80 */
    d[7]  = v[9];                               /* car 0x80 */
    d[8]  = v[3];                               /* mod 0xE0 */
    d[9]  = v[10];                              /* car 0xE0 */
    d[10] = v[6] & 0x0F;                        /* feedback/connection */
    if (base < -127) base = -127;
    if (base > 127) base = 127;
    *xp = (signed char)base;
}

/* GENMIDI.OP2 = "#OPL_II#" + 175 * 36-byte instruments (+ names).
   0..127 melodic, 128..174 percussion for MIDI notes 35..81. */
static int load_op2(const char *fn)
{
    FILE *f;
    u8 hdr[8], rec[36];
    patch_t *p;
    int i, flags, fixednote;

    f = fopen(fn, "rb");
    if (!f) {
        perror(fn);
        return -1;
    }
    if (fread(hdr, 1, 8, f) != 8 || memcmp(hdr, "#OPL_II#", 8) != 0) {
        fprintf(stderr, "%s: not an OP2 (GENMIDI) file\n", fn);
        fclose(f);
        return -1;
    }
    for (i = 0; i < 175; i++) {
        if (fread(rec, 1, 36, f) != 36)
            break;
        flags = rec[0] | (rec[1] << 8);
        fixednote = rec[3];
        p = (i < 128) ? &melodic[i] : &drums[35 + (i - 128)];
        memset(p, 0, sizeof(patch_t));
        op2_voice(rec + 4, p->d, &p->xpose);
        if (flags & 0x04) {
            p->has2 = 1;
            op2_voice(rec + 20, p->d2, &p->xpose2);
        }
        if (flags & 0x01) {
            p->fixed = (u8)(fixednote ? fixednote : 1);
            p->xpose = 0;
            p->xpose2 = 0;
        } else if (i >= 128) {
            p->fixed = 0;
        }
    }
    fclose(f);
    return 0;
}

static int pan_bits(int ch)
{
    int p = mch[ch].pan;
    if (p < 43) return 0x10;
    if (p > 84) return 0x20;
    return 0x30;
}

static const u8 *vdata(int v)
{
    return vo[v].which ? vo[v].p->d2 : vo[v].p->d;
}

static int vxpose(int v)
{
    return vo[v].which ? vo[v].p->xpose2 : vo[v].p->xpose;
}

static int isqrt(int n)
{
    int r = 0;
    while ((r + 1) * (r + 1) <= n)
        r++;
    return r;
}

/* Attenuation in 0.75 dB steps (0..40 = 0..30 dB).  A square-root curve
   keeps normal velocities close to full level. */
static int voice_att(int v)
{
    int ch = vo[v].ch, vol;
    vol = vo[v].vel;
    vol = vol * mch[ch].vol / 127;
    vol = vol * mch[ch].expr / 127;
    return (127 - isqrt(vol * 127)) * 40 / 127;
}

static void voice_set_vol(int v)
{
    const u8 *d = vdata(v);
    int bank = v / 9, c = v % 9;
    int mo = op_off[c], co = op_off[c] + 3;
    int att = voice_att(v), t;

    t = (d[3] & 0x3F) + att - midi_boost;
    if (t > 63) t = 63;
    if (t < 0) t = 0;
    opl_write(bank, 0x40 + co, (d[3] & 0xC0) | t);
    if (d[10] & 1) {                    /* additive: modulator is audible */
        t = (d[2] & 0x3F) + att - midi_boost;
        if (t > 63) t = 63;
        if (t < 0) t = 0;
        opl_write(bank, 0x40 + mo, (d[2] & 0xC0) | t);
    }
}

static void voice_set_freq(int v, int keyon)
{
    int bank = v / 9, c = v % 9;
    int n, total, n0, fr, blk, idx, f0, f1, fn;

    n = vo[v].p->fixed ? vo[v].p->fixed : vo[v].note + vxpose(v);
    total = n * 256 + (mch[vo[v].ch].bend - 8192) / 16;
    if (total < 12 * 256) total = 12 * 256;
    if (total > 108 * 256 - 1) total = 108 * 256 - 1;
    n0 = total >> 8;
    fr = total & 255;
    blk = n0 / 12 - 1;
    idx = n0 % 12;
    f0 = fnum_tab[idx];
    f1 = (idx == 11) ? fnum_tab[0] * 2 : fnum_tab[idx + 1];
    fn = f0 + (f1 - f0) * fr / 256;
    if (fn > 1023) fn = 1023;

    opl_write(bank, 0xA0 + c, fn & 0xFF);
    opl_write(bank, 0xB0 + c, (keyon ? 0x20 : 0) | (blk << 2) | (fn >> 8));
}

static void voice_release(int v)
{
    int bank = v / 9, c = v % 9;
    opl_write(bank, 0xB0 + c, shadow[bank][0xB0 + c] & ~0x20);
    vo[v].state = 2;
    vo[v].sus = 0;
    vo[v].off_us = clock_us;
}

static int alloc_voice(void)
{
    int v, best = -1;
    double bt = 1e30;
    unsigned long ba = ~0UL;

    for (v = 0; v < NVOICES; v++)
        if (vo[v].state == 0)
            return v;
    for (v = 0; v < NVOICES; v++)
        if (vo[v].state == 2 && vo[v].off_us < bt) {
            bt = vo[v].off_us;
            best = v;
        }
    if (best >= 0)
        return best;
    for (v = 0; v < NVOICES; v++)
        if (vo[v].age < ba) {
            ba = vo[v].age;
            best = v;
        }
    return best;
}

static void start_voice(int v, int ch, int note, int vel,
                        const patch_t *p, int which, unsigned long age)
{
    int bank, c, mo, co;
    const u8 *d;

    bank = v / 9;
    c = v % 9;
    mo = op_off[c];
    co = mo + 3;

    vo[v].state = 1;
    vo[v].ch = ch;
    vo[v].note = note;
    vo[v].vel = vel;
    vo[v].sus = 0;
    vo[v].which = which;
    vo[v].age = age;
    vo[v].p = p;
    d = vdata(v);

    opl_write(bank, 0xB0 + c, 0);       /* key off to retrigger envelope */
    opl_write(bank, 0x20 + mo, d[0]);
    opl_write(bank, 0x20 + co, d[1]);
    opl_write(bank, 0x40 + mo, d[2]);
    opl_write(bank, 0x60 + mo, d[4]);
    opl_write(bank, 0x60 + co, d[5]);
    opl_write(bank, 0x80 + mo, d[6]);
    opl_write(bank, 0x80 + co, d[7]);
    opl_write(bank, 0xE0 + mo, d[8]);
    opl_write(bank, 0xE0 + co, d[9]);
    opl_write(bank, 0xC0 + c, (d[10] & 0x0F) | pan_bits(ch));
    voice_set_vol(v);
    voice_set_freq(v, 1);
}

static void note_on(int ch, int note, int vel)
{
    int v;
    const patch_t *p;
    unsigned long age;

    p = (ch == 9) ? &drums[note & 127] : &melodic[mch[ch].prog & 127];
    age = ++age_ctr;
    v = alloc_voice();
    if (v < 0)
        return;
    start_voice(v, ch, note, vel, p, 0, age);
    if (p->has2) {
        v = alloc_voice();
        if (v >= 0)
            start_voice(v, ch, note, vel, p, 1, age);
    }
}

static void note_off(int ch, int note)
{
    int v, found = -1;
    unsigned long grp = 0;

    for (v = 0; v < NVOICES; v++)
        if (vo[v].state == 1 && !vo[v].sus && vo[v].ch == ch
            && vo[v].note == note) {
            found = v;
            grp = vo[v].age;
            break;
        }
    if (found < 0)
        return;
    for (v = 0; v < NVOICES; v++)       /* both halves of a double voice */
        if (vo[v].state == 1 && vo[v].ch == ch && vo[v].note == note
            && vo[v].age == grp) {
            if (mch[ch].sustain)
                vo[v].sus = 1;
            else
                voice_release(v);
        }
}

static void chan_notes_off(int ch)
{
    int v;
    for (v = 0; v < NVOICES; v++)
        if (vo[v].state == 1 && vo[v].ch == ch)
            voice_release(v);
}

static void midi_cc(int ch, int cc, int val)
{
    int v;

    switch (cc) {
    case 7:
        mch[ch].vol = val;
        for (v = 0; v < NVOICES; v++)
            if (vo[v].state == 1 && vo[v].ch == ch)
                voice_set_vol(v);
        break;
    case 11:
        mch[ch].expr = val;
        for (v = 0; v < NVOICES; v++)
            if (vo[v].state == 1 && vo[v].ch == ch)
                voice_set_vol(v);
        break;
    case 10:
        mch[ch].pan = val;
        for (v = 0; v < NVOICES; v++)
            if (vo[v].state == 1 && vo[v].ch == ch)
                opl_write(v / 9, 0xC0 + v % 9,
                          (shadow[v / 9][0xC0 + v % 9] & 0x0F) | pan_bits(ch));
        break;
    case 64:
        mch[ch].sustain = (val >= 64);
        if (val < 64)
            for (v = 0; v < NVOICES; v++)
                if (vo[v].state == 1 && vo[v].ch == ch && vo[v].sus)
                    voice_release(v);
        break;
    case 120: case 123:
        chan_notes_off(ch);
        break;
    case 121:
        mch[ch].vol = 100; mch[ch].expr = 127; mch[ch].pan = 64;
        mch[ch].bend = 8192; mch[ch].sustain = 0;
        break;
    default:
        break;
    }
}

static void midi_init(void)
{
    int i;
    memset(vo, 0, sizeof(vo));
    for (i = 0; i < 16; i++) {
        mch[i].prog = 0;
        mch[i].vol = 100;
        mch[i].expr = 127;
        mch[i].pan = 64;
        mch[i].bend = 8192;
        mch[i].sustain = 0;
    }
    age_ctr = 0;
    opl_write(1, 0x05, 1);              /* OPL3 mode: 18 voices */
    opl_write(0, 0x01, 0x20);
    opl_write(0, 0xBD, 0);              /* melodic mode, no depth bits */
}

/* ---- Standard MIDI File reader ---- */

#define MAXTRK 64

typedef struct {
    const u8 *p, *end;
    unsigned long tick;
    int status;
    int done;
} trk_t;

static unsigned long tempo = 500000;
static unsigned long smf_div = 96;

static double us_per_tick(void)
{
    if (smf_div & 0x8000UL) {           /* SMPTE */
        int fps = 256 - (int)((smf_div >> 8) & 0xFF);
        int tpf = (int)(smf_div & 0xFF);
        if (fps <= 0 || tpf <= 0)
            return 10000.0;
        return 1000000.0 / ((double)fps * (double)tpf);
    }
    if (smf_div == 0)
        return 1000.0;
    return (double)tempo / (double)smf_div;
}

static unsigned long rd_var(trk_t *t)
{
    unsigned long v = 0;
    int i, c;
    for (i = 0; i < 4 && t->p < t->end; i++) {
        c = *t->p++;
        v = (v << 7) | (unsigned long)(c & 0x7F);
        if (!(c & 0x80))
            break;
    }
    return v;
}

static void do_event(trk_t *t)
{
    int b, st, d1, d2, ch, type;
    unsigned long len;

    if (t->p >= t->end) {
        t->done = 1;
        return;
    }
    b = *t->p;
    if (b >= 0x80) {
        t->p++;
        st = b;
        t->status = (b < 0xF0) ? b : 0;
    } else {
        st = t->status;                 /* running status */
    }
    if (st == 0) {
        t->done = 1;
        return;
    }

    if (st == 0xFF) {
        if (t->p >= t->end) { t->done = 1; return; }
        type = *t->p++;
        len = rd_var(t);
        if (len > (unsigned long)(t->end - t->p))
            len = (unsigned long)(t->end - t->p);
        if (type == 0x51 && len >= 3)
            tempo = ((unsigned long)t->p[0] << 16) | ((unsigned long)t->p[1] << 8)
                  | (unsigned long)t->p[2];
        else if (type == 0x2F)
            t->done = 1;
        t->p += len;
    } else if (st == 0xF0 || st == 0xF7) {
        len = rd_var(t);
        if (len > (unsigned long)(t->end - t->p))
            len = (unsigned long)(t->end - t->p);
        t->p += len;
    } else {
        ch = st & 0x0F;
        if ((st & 0xF0) == 0xC0 || (st & 0xF0) == 0xD0) {
            if (t->p + 1 > t->end) { t->done = 1; return; }
            d1 = *t->p++ & 0x7F;
            d2 = 0;
        } else {
            if (t->p + 2 > t->end) { t->done = 1; return; }
            d1 = *t->p++ & 0x7F;
            d2 = *t->p++ & 0x7F;
        }
        switch (st & 0xF0) {
        case 0x80:
            note_off(ch, d1);
            break;
        case 0x90:
            if (d2 == 0)
                note_off(ch, d1);
            else
                note_on(ch, d1, d2);
            break;
        case 0xB0:
            midi_cc(ch, d1, d2);
            break;
        case 0xC0:
            mch[ch].prog = d1;
            break;
        case 0xE0: {
            int v;
            mch[ch].bend = d1 | (d2 << 7);
            for (v = 0; v < NVOICES; v++)
                if (vo[v].state == 1 && vo[v].ch == ch)
                    voice_set_freq(v, 1);
            break;
        }
        default:
            break;
        }
    }
    if (!t->done) {
        if (t->p >= t->end)
            t->done = 1;
        else
            t->tick += rd_var(t);
    }
}

static void play_midi(const u8 *b, long len)
{
    trk_t tr[MAXTRK];
    int nt, ntrk, i, best;
    long pos;
    u32 hlen, tlen;
    unsigned long cur_tick, dt;

    if (len < 14 || memcmp(b, "MThd", 4) != 0)
        return;
    hlen = be32(b + 4);
    ntrk = (int)be16(b + 10);
    smf_div = be16(b + 12);
    tempo = 500000;
    pos = 8 + (long)hlen;
    nt = 0;
    while (nt < ntrk && nt < MAXTRK && pos + 8 <= len) {
        tlen = be32(b + pos + 4);
        if (memcmp(b + pos, "MTrk", 4) == 0) {
            tr[nt].p = b + pos + 8;
            tr[nt].end = tr[nt].p + tlen;
            if (tr[nt].end > b + len)
                tr[nt].end = b + len;
            tr[nt].status = 0;
            tr[nt].done = 0;
            tr[nt].tick = rd_var(&tr[nt]);
            nt++;
        }
        pos += 8 + (long)tlen;
    }

    sprintf(fmt_info, "MIDI format %lu, %d tracks, %lu ticks/qn",
            (unsigned long)be16(b + 8), nt, smf_div);
    midi_init();
    cur_tick = 0;
    while (!quit) {
        best = -1;
        for (i = 0; i < nt; i++)
            if (!tr[i].done && (best < 0 || tr[i].tick < tr[best].tick))
                best = i;
        if (best < 0)
            break;
        dt = tr[best].tick - cur_tick;
        if (dt) {
            advance((double)dt * us_per_tick());
            cur_tick = tr[best].tick;
        }
        do_event(&tr[best]);
    }
    if (!quit)
        advance(1200000.0);             /* let the last notes ring out */
}

/* ------------------------------------------------------------------ */
/* main                                                                */
/* ------------------------------------------------------------------ */

static void status_tick(void)
{
    double now, el;
    int w, i, v, n, es, ts;
    char bar[21];

    if (!show_status || dry)
        return;
    now = now_us();
    if (now - st_last < 250000.0)
        return;
    st_last = now;
    el = now - t_play0;
    if (el < 0.0)
        el = 0.0;
    if (total_us > 0.0 && el > total_us)
        el = total_us;
    w = (total_us > 0.0) ? (int)(el * 20.0 / total_us) : 0;
    if (w > 20) w = 20;
    for (i = 0; i < 20; i++)
        bar[i] = (i < w) ? '#' : '-';
    bar[20] = 0;
    es = (int)(el / 1000000.0);
    ts = (int)(total_us / 1000000.0);
    printf("\r %02d:%02d / %02d:%02d [%s]", es / 60, es % 60, ts / 60, ts % 60, bar);
    if (st_midi) {
        n = 0;
        for (v = 0; v < NVOICES; v++)
            if (vo[v].state == 1)
                n++;
        printf("  Voices %2d/%d  %3lu BPM", n, NVOICES,
               tempo ? 60000000UL / tempo : 0UL);
    } else if (st_rad) {
        printf("  Order %2d/%d  Line %2d", rd_orderpos + 1, rd_nord, rd_line);
    }
    printf("   ");
    fflush(stdout);
}

enum { FMT_NONE = 0, FMT_MIDI, FMT_DRO, FMT_RAW, FMT_IMF, FMT_RAD };
static char fmt_reason[96] = "";

static int has_ext(const char *fn, const char *ext)     /* ext incl. dot */
{
    size_t n = strlen(fn), e = strlen(ext), i;
    char c;

    if (n < e)
        return 0;
    for (i = 0; i < e; i++) {
        c = fn[n - e + i];
        if (c >= 'A' && c <= 'Z')
            c = (char)(c - 'A' + 'a');
        if (c != ext[i])
            return 0;
    }
    return 1;
}

static int match_nocase(const u8 *b, const char *str, int n)
{
    int i, c, d;

    for (i = 0; i < n; i++) {
        c = b[i];
        d = (unsigned char)str[i];
        if (c >= 'a' && c <= 'z')
            c -= 'a' - 'A';
        if (c != d)
            return 0;
    }
    return 1;
}

/* Structural check of a RAD v1.0 module (mirrors play_rad's parsing). */
static int check_rad(const u8 *b, long len)
{
    long pos, patlist, off;
    int n, nord, i, o;

    if (b[16] != 0x10) {
        sprintf(fmt_reason, "unsupported RAD version %d.%d",
                b[16] >> 4, b[16] & 15);
        return 0;
    }
    pos = 18;
    if (b[17] & 0x80) {                 /* description, NUL terminated */
        while (pos < len && b[pos])
            pos++;
        if (pos >= len) {
            strcpy(fmt_reason, "truncated RAD description");
            return 0;
        }
        pos++;
    }
    for (;;) {                          /* instruments */
        if (pos >= len) {
            strcpy(fmt_reason, "truncated RAD instrument list");
            return 0;
        }
        n = b[pos++];
        if (n == 0)
            break;
        if (n > 31 || pos + 11 > len) {
            strcpy(fmt_reason, "corrupt RAD instrument data");
            return 0;
        }
        pos += 11;
    }
    if (pos >= len) {
        strcpy(fmt_reason, "truncated RAD order list");
        return 0;
    }
    nord = b[pos++];
    if (nord == 0) {
        strcpy(fmt_reason, "RAD module has an empty order list");
        return 0;
    }
    patlist = pos + nord;
    if (patlist + 64 > len) {
        strcpy(fmt_reason, "truncated RAD pattern table");
        return 0;
    }
    for (i = 0; i < nord; i++) {
        o = b[pos + i];
        if ((o & 0x80) ? ((o & 0x7F) >= nord) : (o >= 32)) {
            strcpy(fmt_reason, "corrupt RAD order list");
            return 0;
        }
    }
    for (i = 0; i < 32; i++) {
        off = (long)rd16(b + patlist + 2 * i);
        if (off != 0 && (off < patlist + 64 || off >= len)) {
            strcpy(fmt_reason, "corrupt RAD pattern offset");
            return 0;
        }
    }
    return 1;
}

/* Decide what a file is.  Only formats we can really play are accepted;
   anything else returns FMT_NONE (fmt_reason may hold a detail). */
static int detect_format(const u8 *b, long len, const char *fn)
{
    long pos, i, end;
    u32 hlen, tlen, first, maj, min;
    int found;

    fmt_reason[0] = 0;

    if (len >= 4 && memcmp(b, "MThd", 4) == 0) {
        if (len < 14) {
            strcpy(fmt_reason, "truncated MIDI header");
            return FMT_NONE;
        }
        hlen = be32(b + 4);
        if (hlen < 6 || (long)hlen > len - 8 || be16(b + 12) == 0) {
            strcpy(fmt_reason, "corrupt MIDI header");
            return FMT_NONE;
        }
        if (be16(b + 8) > 1) {
            strcpy(fmt_reason, "MIDI format 2 is not supported");
            return FMT_NONE;
        }
        pos = 8 + (long)hlen;
        found = 0;
        while (pos + 8 <= len) {
            tlen = be32(b + pos + 4);
            if (memcmp(b + pos, "MTrk", 4) == 0)
                found++;
            if (tlen > (u32)(len - pos - 8))
                break;
            pos += 8 + (long)tlen;
        }
        if (!found) {
            strcpy(fmt_reason, "MIDI file contains no tracks");
            return FMT_NONE;
        }
        return FMT_MIDI;
    }

    if (len >= 8 && memcmp(b, "DBRAWOPL", 8) == 0) {
        if (len < 12) {
            strcpy(fmt_reason, "truncated DRO header");
            return FMT_NONE;
        }
        maj = rd16(b + 8);
        min = rd16(b + 10);
        if (maj == 2 && min == 0) {
            if (len < 26 || 26 + (long)b[25] > len) {
                strcpy(fmt_reason, "truncated DRO v2 header");
                return FMT_NONE;
            }
            if (b[20] > 2 || b[21] != 0 || b[22] != 0) {
                strcpy(fmt_reason, "DRO v2 with unsupported layout or compression");
                return FMT_NONE;
            }
            return FMT_DRO;
        }
        if (maj == 0 && min == 1) {
            if (len < 0x18) {
                strcpy(fmt_reason, "truncated DRO v0.1 header");
                return FMT_NONE;
            }
            return FMT_DRO;
        }
        sprintf(fmt_reason, "unsupported DRO version %lu.%lu",
                (unsigned long)maj, (unsigned long)min);
        return FMT_NONE;
    }

    if (len >= 16 && match_nocase(b, "RAD BY REALITY!!", 16)) {
        if (len < 19) {
            strcpy(fmt_reason, "truncated RAD header");
            return FMT_NONE;
        }
        return check_rad(b, len) ? FMT_RAD : FMT_NONE;
    }

    if (len >= 8 && memcmp(b, "RAWADATA", 8) == 0) {
        if (len < 10) {
            strcpy(fmt_reason, "truncated RAW header");
            return FMT_NONE;
        }
        return FMT_RAW;
    }

    /* IMF has no signature: require the extension AND a sane structure */
    if (has_ext(fn, ".imf") || has_ext(fn, ".wlf")) {
        if (len < 4) {
            strcpy(fmt_reason, "IMF file too short");
            return FMT_NONE;
        }
        first = rd16(b);
        if (first == 0) {
            pos = 0;
            end = len;
        } else if ((long)first + 2 <= len) {
            pos = 2;
            end = 2 + (long)first;
        } else {
            strcpy(fmt_reason, "IMF length field exceeds file size");
            return FMT_NONE;
        }
        if (end - pos < 4) {
            strcpy(fmt_reason, "IMF file contains no data");
            return FMT_NONE;
        }
        for (i = pos; i + 4 <= end; i += 4)
            if (b[i] > 0xF5) {
                sprintf(fmt_reason, "not a valid IMF stream (bad register at offset %ld)", i);
                return FMT_NONE;
            }
        return FMT_IMF;
    }
    return FMT_NONE;
}

static void play_any(int type, const u8 *buf, long len, double imf_rate)
{
    switch (type) {
    case FMT_MIDI:
        st_midi = 1;
        play_midi(buf, len);
        st_midi = 0;
        break;
    case FMT_DRO:
        play_dro(buf, len);
        break;
    case FMT_RAW:
        play_raw(buf, len);
        break;
    case FMT_IMF:
        play_imf(buf, len, imf_rate);
        break;
    case FMT_RAD:
        play_rad(buf, len);
        break;
    default:
        break;
    }
}

/* Run the player silently and without waiting to learn the song length. */
static double scan_length(int type, const u8 *buf, long len, double imf_rate)
{
    int sd = dry, sq = quiet;
    double t;

    dry = 1;
    quiet = 1;
    clock_us = 0.0;
    play_any(type, buf, len, imf_rate);
    t = clock_us;
    dry = sd;
    quiet = sq;
    clock_us = 0.0;
    memset(shadow, 0, sizeof(shadow));
    opl3_on = 0;
    return t;
}

/* ---- startup banner (plain ASCII art, no escape sequences) ---- */

static const char *art_rows[5] = {
    "######   ######   ######  ##   ## ######   #####   ######",
    "##   ## ##    ## ##    ## ##  ##       ## ##   ## ##",
    "######  ##    ## ##    ## #####    #####   #####  #######",
    "##   ## ##    ## ##    ## ##  ##       ## ##   ## ##    ##",
    "######   ######   ######  ##   ## ######   #####   ######"
};

static int art_cell(int r, int c)
{
    if (r < 0 || r >= 5 || c < 0)
        return 0;
    return c < (int)strlen(art_rows[r]) && art_rows[r][c] == '#';
}

#define BH 8
#define BW 72
static unsigned char outside[BH][BW];   /* grid cell (r+1,c+1) reachable from
                                           outside the lettering */

/* Flood-fill the empty space around the letters so the drop shadow is only
   drawn outside them and the holes of B, O, 6, 8 stay clean. */
static void mark_outside(void)
{
    static int sr[BH * BW], sc[BH * BW];
    static const int dr[4] = { 1, -1, 0, 0 }, dc[4] = { 0, 0, 1, -1 };
    int n, r, c, rr, cc, k;

    memset(outside, 0, sizeof(outside));
    outside[0][0] = 1;
    sr[0] = 0;
    sc[0] = 0;
    n = 1;
    while (n > 0) {
        n--;
        r = sr[n];
        c = sc[n];
        for (k = 0; k < 4; k++) {
            rr = r + dr[k];
            cc = c + dc[k];
            if (rr < 0 || rr >= BH || cc < 0 || cc >= BW)
                continue;
            if (outside[rr][cc] || art_cell(rr - 1, cc - 1))
                continue;
            outside[rr][cc] = 1;
            sr[n] = rr;
            sc[n] = cc;
            n++;
        }
    }
}

static int art_shadow(int r, int c)
{
    return !art_cell(r, c) && art_cell(r - 1, c - 1) && outside[r + 1][c + 1];
}

/* On a colour terminal: blue-to-white gradient with a dark drop shadow and
   a BitchX-style bracketed info line.  Only 7-bit ASCII characters and
   standard SGR colour codes (ESC [ .. m) are used, so it works on the plain
   Linux console.  Otherwise (pipe, TERM=dumb, NO_COLOR) it is plain text. */
static void print_banner(FILE *f)
{
    static const char *row_col[6] = {
        "0;34", "1;34", "0;36", "1;36", "1;37", "1;30"
    };
    const char *term, *cur, *style;
    int r, c, end, color;
    char ch;

    term = getenv("TERM");
    color = isatty(fileno(f)) && term && strcmp(term, "dumb") != 0
            && getenv("NO_COLOR") == NULL;

    fputc('\n', f);
    if (!color) {
        for (r = 0; r < 5; r++)
            fprintf(f, "%s\n", art_rows[r]);
        fprintf(f, "  %s\n\n  %s %s\n\n", PROG_AUTHOR, PROG_TITLE, PROG_VER);
        return;
    }

    mark_outside();
    for (r = 0; r < 6; r++) {
        end = 0;                        /* last used column + 1 */
        for (c = 0; c < 70; c++)
            if (art_cell(r, c) || art_shadow(r, c))
                end = c + 1;
        cur = NULL;
        for (c = 0; c < end; c++) {
            ch = ' ';
            style = NULL;
            if (art_cell(r, c)) {
                ch = '#';
                style = row_col[r];
            } else if (art_shadow(r, c)) {
                ch = ':';
                style = "1;30";
            }
            if (style != cur) {
                if (style)
                    fprintf(f, "\033[%sm", style);
                else
                    fputs("\033[0m", f);
                cur = style;
            }
            fputc(ch, f);
        }
        fputs("\033[0m\n", f);
    }
    fprintf(f, "\n \033[0;34m[\033[1;37m %s \033[0;34m]-[\033[1;36m %s %s \033[0;34m]"
               "-[\033[0;36m YMF262 / OPL3 \033[0;34m]\033[0m\n\n",
            PROG_AUTHOR, PROG_TITLE, PROG_VER);
}

static void usage(int code)
{
    FILE *f = code ? stderr : stdout;

    print_banner(f);
    fputs("Plays AdLib / OPL2 / OPL3 music directly on the sound chip.\n"
          "\n"
          "usage: oplplay [options] file\n"
          "\n"
          "formats:  .imf .wlf (id Software IMF)   .dro (DOSBox raw OPL)\n"
          "          .raw (Rdos raw OPL)           .rad (Reality AdLib Tracker)\n"
          "          .mid (Standard MIDI file)\n"
          "\n", f);
    fputs("options:\n"
          "  -p port   OPL base port in hex             (default 388)\n"
          "  -r rate   IMF tick rate in Hz              (default 560; 280, 700)\n"
          "  -o file   GENMIDI.OP2 bank for MIDI        (melodic + drums)\n"
          "  -b file   IBK bank, MIDI melodic programs\n"
          "  -d file   IBK bank, MIDI percussion\n"
          "  -g dB     MIDI boost, 0-30 dB              (default 0)\n"
          "  -v pct    software volume, 0-100           (default 100)\n", f);
    fputs("  -l        loop forever\n"
          "  -q        no live status line\n"
          "  -f        skip OPL chip detection\n"
          "  -R        real-time priority (can starve the machine)\n"
          "  -n        dry run: print register writes, do not touch hardware\n"
          "  -h        show this help\n"
          "\n", f);
    fputs("Needs root (direct port access).  Examples:\n"
          "  oplplay song.imf\n"
          "  oplplay -o GENMIDI.OP2 -g 6 song.mid\n", f);
    exit(code);
}

static u8 *slurp(const char *fn, long *len)
{
    FILE *f;
    u8 *buf;
    long n;

    f = fopen(fn, "rb");
    if (!f) {
        perror(fn);
        return NULL;
    }
    fseek(f, 0, SEEK_END);
    n = ftell(f);
    fseek(f, 0, SEEK_SET);
    if (n <= 0) {
        fprintf(stderr, "%s: unknown/unsupported format (empty file)\n", fn);
        fclose(f);
        return NULL;
    }
    buf = (u8 *)malloc((size_t)n);
    if (!buf || fread(buf, 1, (size_t)n, f) != (size_t)n) {
        fprintf(stderr, "%s: read error\n", fn);
        free(buf);
        fclose(f);
        return NULL;
    }
    fclose(f);
    *len = n;
    return buf;
}

int main(int argc, char **argv)
{
    extern char *optarg;
    extern int optind;
    int c, nodetect = 0, loop = 0, type;
    double imf_rate = 560.0;
    int sw_vol = 100, boost_db = 0;
    const char *melfile = NULL, *drumfile = NULL, *op2file = NULL;
    u8 *buf;
    long len = 0;
    struct sched_param sp;

    while ((c = getopt(argc, argv, "p:r:b:d:o:v:g:flRnqh")) != -1) {
        switch (c) {
        case 'p': opl_port = (int)strtol(optarg, NULL, 16); break;
        case 'r': imf_rate = atof(optarg); break;
        case 'b': melfile = optarg; break;
        case 'd': drumfile = optarg; break;
        case 'o': op2file = optarg; break;
        case 'v': sw_vol = atoi(optarg); break;
        case 'g': boost_db = atoi(optarg); break;
        case 'f': nodetect = 1; break;
        case 'l': loop = 1; break;
        case 'R': use_rt = 1; break;
        case 'n': dry = 1; break;
        case 'q': show_status = 0; break;
        case 'h': usage(0); break;
        default:  usage(1);
        }
    }
    if (optind >= argc || imf_rate <= 0.0)
        usage(1);

    print_banner(stdout);

    buf = slurp(argv[optind], &len);
    if (!buf)
        return 1;
    type = detect_format(buf, len, argv[optind]);
    if (type == FMT_NONE) {
        fprintf(stderr, "%s: unknown/unsupported format", argv[optind]);
        if (fmt_reason[0])
            fprintf(stderr, " (%s)", fmt_reason);
        fprintf(stderr, "\n");
        free(buf);
        return 1;
    }
    printf("Chip   : Yamaha YMF262 (OPL3) at port 0x%X\n", opl_port);

    if (sw_vol < 0) sw_vol = 0;
    if (sw_vol > 100) sw_vol = 100;
    vol_att = (100 - sw_vol) * 48 / 100;        /* 0 .. 36 dB */
    if (boost_db < 0) boost_db = 0;
    if (boost_db > 30) boost_db = 30;
    midi_boost = boost_db * 4 / 3;              /* dB -> 0.75 dB steps */

    init_banks();
    if (op2file && load_op2(op2file) < 0)
        return 1;
    if (melfile && load_ibk(melfile, melodic, 0) < 0)
        return 1;
    if (drumfile && load_ibk(drumfile, drums, 1) < 0)
        return 1;

    total_us = scan_length(type, buf, len, imf_rate);
    {
        int ts = (int)(total_us / 1000000.0);
        printf("File   : %s (%ld bytes)\n", argv[optind], len);
        printf("Format : %s\n", fmt_info);
        printf("Length : %02d:%02d\n", ts / 60, ts % 60);
        if (fmt_info[0] == 'M') {
            printf("Bank   : ");
            if (op2file) printf("OP2 %s ", op2file);
            if (melfile) printf("melodic IBK %s ", melfile);
            if (drumfile) printf("drum IBK %s ", drumfile);
            if (!op2file && !melfile && !drumfile) printf("built-in starter set");
            printf("\n");
        }
        if (fmt_info[0] == 'M')
            printf("Level  : software %d%%, MIDI boost %d dB\n", sw_vol, boost_db);
        else
            printf("Level  : software %d%%\n", sw_vol);
    }

    gettimeofday(&t_base, NULL);
    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    if (!dry) {
        if (ioperm(opl_port, 4, 1) != 0) {
            perror("ioperm (are you root?)");
            return 1;
        }
        if (!nodetect && !opl_detect()) {
            fprintf(stderr, "no OPL chip found at port 0x%X (use -f to force)\n",
                    opl_port);
            ioperm(opl_port, 4, 0);
            return 1;
        }
        if (use_rt) {
            sp.sched_priority = 20;
            if (sched_setscheduler(0, SCHED_FIFO, &sp) != 0)
                perror("sched_setscheduler");
        }
        if (!nodetect)
            printf("Detect : OPL chip found\n");
    }
    if (!dry)
        printf("Playing: Ctrl-C to stop%s\n\n", loop ? " (looping)" : "");

    do {
        opl_reset();
        clock_us = 0.0;
        st_last = -1e9;
        t_play0 = now_us();
        play_any(type, buf, len, imf_rate);
    } while (loop && !quit);

    if (!dry && show_status)
        printf("\n%s\n", quit ? "Stopped." : "Done.");
    opl_reset();                        /* silence everything */
    if (!dry)
        ioperm(opl_port, 4, 0);
    free(buf);
    return 0;
}
