// DOS audio HAL: Sound Blaster Pro / SB16 8-bit mono playback driven
// by 8237 auto-init DMA, fed by libxmp-lite's mixer.
//
// Pipeline:
//   * jlpAudioInit detects the card via DSP reset + version, allocates
//     a 4 KB conventional-memory DMA buffer (aligned to not cross a
//     64 KB boundary), installs an IRQ handler on the SB's IRQ line,
//     creates an xmp_context, and arms auto-init DMA at 22050 Hz mono
//     8-bit. The DMA buffer is split into two halves; the SB IRQ fires
//     at the end of each half and our handler refills the *other* half
//     by calling xmp_play_buffer with libxmp-lite's mixer output.
//   * jlpAudioPlayMod loads a .MOD blob through xmp_load_module and
//     starts the player; subsequent IRQs auto-pump the audio.
//   * jlpAudioPlaySfx routes through libxmp-lite's SMIX overlay so SFX
//     mix on top of the music using xmp_smix_play_sample.
//
// Defaults match DOSBox-Staging's emulated SB Pro: base $220, IRQ 7,
// 8-bit DMA channel 1. BLASTER environment variable overrides each.
//
// Because DJGPP runs in protected mode with paging, the IRQ handler
// code and any data it touches (DMA selector, libxmp context pointer,
// the DMA buffer) must be locked or a page fault at interrupt time
// will hang the machine.

#include <ctype.h>
#include <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/farptr.h>
#include <sys/movedata.h>
#include <sys/nearptr.h>

#include <libxmp-lite/xmp.h>

#include "port.h"
#include "audioSfxMixInternal.h"
#include "joey/audio.h"

// ----- SB I/O port offsets from base -----

#define SB_RESET_OFFSET       0x06
#define SB_READ_OFFSET        0x0A
#define SB_WRITE_OFFSET       0x0C
#define SB_DATA_AVAIL_OFFSET  0x0E

// ----- DSP commands -----

#define DSP_GET_VERSION       0xE1
#define DSP_SPEAKER_ON        0xD1
#define DSP_SPEAKER_OFF       0xD3
#define DSP_TIME_CONST        0x40
#define DSP_BLOCK_SIZE        0x48
#define DSP_OUT_8_AUTO        0x1C
#define DSP_HALT_DMA8         0xD0
#define DSP_EXIT_AUTO         0xDA

#define DSP_RESET_RETRIES     1024
#define DSP_READY_BYTE        0xAA

// ----- 8237 DMA controller (channel 1) -----

#define DMA_MASK              0x0A
#define DMA_MODE              0x0B
#define DMA_FLIPFLOP          0x0C
#define DMA1_ADDR             0x02
#define DMA1_COUNT            0x03
#define DMA1_PAGE             0x83

// Channel-1 mode byte: single transfer (01), incrementing (0),
// auto-init (1), read transfer (10), channel 1 (01) = 01011001 = 0x59.
#define DMA_MODE_OUT_AI_CH1   0x59
#define DMA_MASK_DISABLE_CH1  0x05    // bit 2 = mask, channel 1
#define DMA_MASK_ENABLE_CH1   0x01

// ----- PIC -----

#define PIC1_CMD              0x20
#define PIC1_IMR              0x21
#define PIC2_CMD              0xA0
#define PIC2_IMR              0xA1
#define PIC_EOI               0x20
#define PIC_CASCADE_IRQ       2

// ----- Defaults -----

#define DEFAULT_SB_BASE       0x220
#define DEFAULT_SB_IRQ        7
#define DEFAULT_SB_DMA8       1

// ----- Mix configuration -----

// MIX_RATE is the nominal target; the SB's 8-bit time-constant DAC can
// only clock at 1000000/(256-tc) Hz, which is not exactly 22050. The
// SB hardware rate is the fixed one, so the effective rate derived from
// the same time constant (see gEffRate, set in jlpAudioInit) is the
// single source of truth handed to libxmp and the SFX resampler.
#define MIX_RATE              22050
#define MIX_DMA_BUFFER        4096        // total; two 2048-byte halves
#define MIX_HALF              (MIX_DMA_BUFFER / 2)
#define SB_DSP_CLOCK_HZ       1000000u    // 8-bit DSP time-constant base

#define ISR_LOCK_SIZE         4096

// ----- Module state -----

static uint16_t gSbBase     = DEFAULT_SB_BASE;
static uint8_t  gSbIrq      = DEFAULT_SB_IRQ;
static uint8_t  gSbDma8     = DEFAULT_SB_DMA8;
static uint8_t  gDspMajor   = 0;
static uint8_t  gDspMinor   = 0;
static bool     gDspPresent = false;

// Effective SB playback rate derived from the DSP time constant. Set
// once in jlpAudioInit and used as the single rate source of truth for
// libxmp's mixer and the SFX resampler, so they match what the DAC
// actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting.
static uint16_t gEffRate    = MIX_RATE;

// Conventional-memory DMA buffer.
static int      gDmaSelector = 0;
static uint32_t gDmaLinear   = 0;
static uint8_t *gDmaBuf      = NULL;

// PIC vector for the SB IRQ.
static _go32_dpmi_seginfo gOldIrqVec;
static _go32_dpmi_seginfo gNewIrqVec;
static bool               gIrqHooked = false;

// libxmp state.
static xmp_context gXmpCtx       = NULL;
static bool        gXmpStarted   = false;
static bool        gXmpLoaded    = false;
static volatile int gXmpLoopCount = 0;   // last arg to xmp_play_buffer

// SFX overlay: libxmp-lite's SMIX takes a file path (not an in-memory
// buffer), so we run a one-shot sample mixer (shared with the ST HAL,
// see src/core/audioSfxMix.c) on top of the MOD output. Mix happens in
// refillHalf after libxmp has filled the half-buffer.
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];

// ISR-side state. volatile so the main loop sees IRQ updates promptly.
// gNextHalf flips between 0 and 1 each IRQ; gNeedRefill[i] is raised
// in the IRQ handler and lowered in jlpAudioFrameTick after the
// half has been refilled. We deliberately do NOT call libxmp-lite
// from interrupt context -- the mixer allocates internally and is
// not reentrancy-safe -- so all work that touches the xmp context
// happens in the main thread.
static volatile uint8_t gNextHalf       = 0;
static volatile bool    gNeedRefill[2]  = { false, false };

// ----- Prototypes -----

static uint8_t adlibFreqToFnum(uint16_t freqHz, uint16_t *fnumOut);
static void adlibInit(void);
static void adlibNoiseInit(void);
static void adlibWrite(uint8_t reg, uint8_t val);
static bool allocDmaBuffer(void);
static bool dspReset(void);
static int  dspRead(void);
static uint8_t dspTimeConst(uint16_t rate);
static void dspWrite(uint8_t value);
static void freeDmaBuffer(void);
static void haltDspDma(void);
static void parseBlasterEnv(void);
static void picIrqPortBit(uint8_t irq, uint16_t *port, uint8_t *bit);
static void picMaskIrq(uint8_t irq);
static void picUnmaskIrq(uint8_t irq);
static int  picVectorForIrq(uint8_t irq);
static void programDmaController(void);
static void programDspAutoInit(void);
static void refillHalf(uint8_t halfIdx);
static void sbIsr(void);

// ----- Internal helpers -----

static void parseBlasterEnv(void) {
    const char *blaster;
    const char *p;
    char        ch;
    long        val;
    char       *end;

    blaster = getenv("BLASTER");
    if (blaster == NULL || *blaster == '\0') {
        return;
    }
    for (p = blaster; *p != '\0';) {
        while (*p != '\0' && isspace((unsigned char)*p)) {
            p++;
        }
        if (*p == '\0') {
            break;
        }
        ch = (char)toupper((unsigned char)*p);
        p++;
        val = strtol(p, &end, ch == 'A' ? 16 : 10);
        if (end == p) {
            while (*p != '\0' && !isspace((unsigned char)*p)) {
                p++;
            }
            continue;
        }
        p = end;
        switch (ch) {
            case 'A': gSbBase = (uint16_t)val; break;
            case 'I': gSbIrq  = (uint8_t)val;  break;
            case 'D': gSbDma8 = (uint8_t)val;  break;
            default: break;
        }
    }
}


static void dspWrite(uint8_t value) {
    int i;

    for (i = 0; i < 65536; i++) {
        if ((inportb(gSbBase + SB_WRITE_OFFSET) & 0x80) == 0) {
            break;
        }
    }
    outportb(gSbBase + SB_WRITE_OFFSET, value);
}


static int dspRead(void) {
    int i;

    for (i = 0; i < 65536; i++) {
        if ((inportb(gSbBase + SB_DATA_AVAIL_OFFSET) & 0x80) != 0) {
            return inportb(gSbBase + SB_READ_OFFSET);
        }
    }
    return -1;
}


static bool dspReset(void) {
    int      retry;
    int      byte;
    uint16_t port;
    int      i;

    port = (uint16_t)(gSbBase + SB_RESET_OFFSET);
    outportb(port, 1);
    for (i = 0; i < 8; i++) {
        (void)inportb(port);
    }
    outportb(port, 0);

    for (retry = 0; retry < DSP_RESET_RETRIES; retry++) {
        byte = dspRead();
        if (byte == DSP_READY_BYTE) {
            return true;
        }
    }
    return false;
}


// PIC1 (master) handles IRQ 0-7 at INT $08-$0F. PIC2 (slave) handles
// IRQ 8-15 at INT $70-$77. Map an IRQ number to its PM interrupt
// vector. SB's typical IRQs are 5 or 7 (PIC1) or 10 (PIC2).
static int picVectorForIrq(uint8_t irq) {
    if (irq < 8) {
        return 0x08 + irq;
    }
    return 0x70 + (irq - 8);
}


// Allocate a DMA buffer in conventional memory that does not cross a
// 64 KB physical-page boundary -- the 8237 cannot DMA across that
// boundary on 8-bit channels. We over-allocate to 2x size and use the
// lower-aligned half so we always have room.
static bool allocDmaBuffer(void) {
    int      sel;
    int      seg;
    uint32_t linear;
    uint32_t aligned;
    int      paragraphs;

    paragraphs = ((MIX_DMA_BUFFER * 2) + 15) >> 4;
    seg = __dpmi_allocate_dos_memory(paragraphs, &sel);
    if (seg < 0) {
        return false;
    }
    linear = (uint32_t)seg << 4;

    // Round linear up to a starting offset that gives us
    // MIX_DMA_BUFFER without crossing the next 64 KB boundary.
    if (((linear & 0xFFFF) + MIX_DMA_BUFFER) > 0x10000) {
        aligned = (linear + 0xFFFFu) & ~0xFFFFu;
    } else {
        aligned = linear;
    }

    gDmaSelector = sel;
    gDmaLinear   = aligned;
    gDmaBuf      = (uint8_t *)(__djgpp_conventional_base + aligned);
    memset(gDmaBuf, 0x80, MIX_DMA_BUFFER);   // 8-bit unsigned silence
    return true;
}


static void freeDmaBuffer(void) {
    if (gDmaSelector != 0) {
        __dpmi_free_dos_memory(gDmaSelector);
        gDmaSelector = 0;
        gDmaLinear   = 0;
        gDmaBuf      = NULL;
    }
}


static void programDmaController(void) {
    uint8_t  page;
    uint16_t offset;
    uint16_t count;

    page   = (uint8_t)((gDmaLinear >> 16) & 0xFF);
    offset = (uint16_t)(gDmaLinear & 0xFFFF);
    count  = (uint16_t)(MIX_DMA_BUFFER - 1);

    outportb(DMA_MASK,     DMA_MASK_DISABLE_CH1);
    outportb(DMA_FLIPFLOP, 0);
    outportb(DMA_MODE,     DMA_MODE_OUT_AI_CH1);
    outportb(DMA1_ADDR,    (uint8_t)(offset & 0xFF));
    outportb(DMA1_ADDR,    (uint8_t)((offset >> 8) & 0xFF));
    outportb(DMA1_PAGE,    page);
    outportb(DMA1_COUNT,   (uint8_t)(count & 0xFF));
    outportb(DMA1_COUNT,   (uint8_t)((count >> 8) & 0xFF));
    outportb(DMA_MASK,     DMA_MASK_ENABLE_CH1);
}


// 8-bit DSP time constant: tc = 256 - SB_DSP_CLOCK_HZ / rate. SB plays
// at SB_DSP_CLOCK_HZ / (256 - tc) Hz, so we round to the nearest
// representable rate. For 22050 Hz: tc = 256 - 45 = 211.
static uint8_t dspTimeConst(uint16_t rate) {
    uint32_t tc;

    tc = 256u - (SB_DSP_CLOCK_HZ / rate);
    if (tc < 4) {
        tc = 4;
    }
    if (tc > 255) {
        tc = 255;
    }
    return (uint8_t)tc;
}


static void programDspAutoInit(void) {
    uint16_t blockMinusOne;

    dspWrite(DSP_SPEAKER_ON);
    dspWrite(DSP_TIME_CONST);
    dspWrite(dspTimeConst(MIX_RATE));

    blockMinusOne = (uint16_t)(MIX_HALF - 1);
    dspWrite(DSP_BLOCK_SIZE);
    dspWrite((uint8_t)(blockMinusOne & 0xFF));
    dspWrite((uint8_t)((blockMinusOne >> 8) & 0xFF));

    dspWrite(DSP_OUT_8_AUTO);
}


static void haltDspDma(void) {
    dspWrite(DSP_HALT_DMA8);
    dspWrite(DSP_EXIT_AUTO);
    dspWrite(DSP_SPEAKER_OFF);
}


// Decode an IRQ number to its PIC Interrupt Mask Register port and the
// mask bit within that register. IRQ 0-7 live in PIC1, 8-15 in PIC2.
static void picIrqPortBit(uint8_t irq, uint16_t *port, uint8_t *bit) {
    if (irq < 8) {
        *port = PIC1_IMR;
        *bit  = (uint8_t)(1u << irq);
    } else {
        *port = PIC2_IMR;
        *bit  = (uint8_t)(1u << (irq - 8));
    }
}


// Unmask the IRQ line in the PIC's Interrupt Mask Register so the IRQ
// can reach the CPU. Some environments (DOSBox in particular) leave the
// SB IRQ masked at boot; without this the hooked vector never fires
// and audio drops out after the first half-buffer.
static void picUnmaskIrq(uint8_t irq) {
    uint16_t port;
    uint8_t  bit;
    uint8_t  mask;

    picIrqPortBit(irq, &port, &bit);
    mask = inportb(port);
    mask = (uint8_t)(mask & ~bit);
    outportb(port, mask);
    if (irq >= 8) {
        // Cascade line on PIC1 must also be unmasked for PIC2 to reach
        // the CPU.
        mask = inportb(PIC1_IMR);
        mask = (uint8_t)(mask & ~(1u << PIC_CASCADE_IRQ));
        outportb(PIC1_IMR, mask);
    }
}


static void picMaskIrq(uint8_t irq) {
    uint16_t port;
    uint8_t  bit;
    uint8_t  mask;

    picIrqPortBit(irq, &port, &bit);
    mask = inportb(port);
    mask = (uint8_t)(mask | bit);
    outportb(port, mask);
}


// Refill one half of the DMA buffer from libxmp-lite's mixer.
// Called from jlpAudioFrameTick on the main thread (NOT in IRQ
// context); the IRQ only flags which halves need refilling.
// xmp_play_buffer fills our bytes regardless of whether a module is
// loaded -- it returns silence (centered 0x80) when idle.
static void refillHalf(uint8_t halfIdx) {
    uint8_t *dst;

    dst = gDmaBuf + ((uint32_t)halfIdx * MIX_HALF);
    if (gXmpCtx != NULL && gXmpStarted) {
        // libxmp emits unsigned 8-bit when XMP_FORMAT_UNSIGNED is set
        // in xmp_start_player, matching SB's DMA format -- no extra
        // signed/unsigned conversion needed here.
        xmp_play_buffer(gXmpCtx, dst, MIX_HALF, gXmpLoopCount);
    } else {
        memset(dst, 0x80, MIX_HALF);
    }
    audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
}


// SB IRQ. Acks the SB (read $0E), marks the half that just played
// for refill, EOIs the PIC. The actual mixer work happens in
// jlpAudioFrameTick on the main thread.
static void sbIsr(void) {
    uint8_t fillHalf;

    fillHalf              = gNextHalf;
    gNextHalf             = (uint8_t)(gNextHalf ^ 1);
    gNeedRefill[fillHalf] = true;

    (void)inportb(gSbBase + SB_DATA_AVAIL_OFFSET);

    if (gSbIrq >= 8) {
        outportb(PIC2_CMD, PIC_EOI);
    }
    outportb(PIC1_CMD, PIC_EOI);
}


// ----- HAL API (alphabetical) -----

bool jlpAudioInit(void) {
    int major;
    int minor;
    int vector;

    if (gDspPresent) {
        return true;
    }
    parseBlasterEnv();
    if (!dspReset()) {
        return false;
    }
    dspWrite(DSP_GET_VERSION);
    major = dspRead();
    minor = dspRead();
    if (major < 0 || minor < 0) {
        return false;
    }
    gDspMajor = (uint8_t)major;
    gDspMinor = (uint8_t)minor;

    // Lock in the effective playback rate from the same time constant
    // the SB DAC will receive, so libxmp and the SFX resampler agree
    // with the hardware instead of using the nominal MIX_RATE.
    gEffRate = (uint16_t)(SB_DSP_CLOCK_HZ / (256u - dspTimeConst(MIX_RATE)));

    if (!allocDmaBuffer()) {
        return false;
    }

    gXmpCtx = xmp_create_context();
    if (gXmpCtx == NULL) {
        freeDmaBuffer();
        return false;
    }

    // Allocate the iret wrapper first so its failure path needs no
    // unlock; the page locks below are only required once the vector
    // is actually hooked.
    vector = picVectorForIrq(gSbIrq);
    _go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
    gNewIrqVec.pm_offset   = (unsigned long)sbIsr;
    gNewIrqVec.pm_selector = _go32_my_cs();
    if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
        xmp_free_context(gXmpCtx);
        gXmpCtx = NULL;
        freeDmaBuffer();
        return false;
    }

    // Lock ISR code + the data sbIsr touches at IRQ time. The mixer
    // refill no longer runs in interrupt context, so refillHalf and
    // libxmp internals do not need locking.
    _go32_dpmi_lock_code(sbIsr,                ISR_LOCK_SIZE);
    _go32_dpmi_lock_data((void *)&gNextHalf,    sizeof(gNextHalf));
    _go32_dpmi_lock_data((void *)gNeedRefill,   sizeof(gNeedRefill));
    _go32_dpmi_lock_data((void *)&gSbBase,      sizeof(gSbBase));
    _go32_dpmi_lock_data((void *)&gSbIrq,       sizeof(gSbIrq));

    _go32_dpmi_set_protected_mode_interrupt_vector(vector, &gNewIrqVec);
    gIrqHooked = true;

    gNextHalf = 0;
    programDmaController();
    programDspAutoInit();
    picUnmaskIrq(gSbIrq);

    gDspPresent = true;
    return true;
}


void jlpAudioShutdown(void) {
    int vector;

    if (!gDspPresent) {
        return;
    }
    haltDspDma();
    if (gIrqHooked) {
        picMaskIrq(gSbIrq);
        vector = picVectorForIrq(gSbIrq);
        _go32_dpmi_set_protected_mode_interrupt_vector(vector, &gOldIrqVec);
        _go32_dpmi_free_iret_wrapper(&gNewIrqVec);
        gIrqHooked = false;
    }
    if (gXmpCtx != NULL) {
        if (gXmpStarted) {
            xmp_end_player(gXmpCtx);
            gXmpStarted = false;
        }
        if (gXmpLoaded) {
            xmp_release_module(gXmpCtx);
            gXmpLoaded = false;
        }
        xmp_free_context(gXmpCtx);
        gXmpCtx = NULL;
    }
    freeDmaBuffer();
    (void)dspReset();
    gDspPresent = false;
}


bool jlpAudioIsPlayingMod(void) {
    return gXmpStarted;
}


void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
    if (!gDspPresent || gXmpCtx == NULL) {
        return;
    }
    if (gXmpStarted) {
        xmp_end_player(gXmpCtx);
        gXmpStarted = false;
    }
    if (gXmpLoaded) {
        xmp_release_module(gXmpCtx);
        gXmpLoaded = false;
    }
    if (xmp_load_module_from_memory(gXmpCtx, (void *)data, (long)length) != 0) {
        return;
    }
    gXmpLoaded = true;
    if (xmp_start_player(gXmpCtx, gEffRate,
            XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
        xmp_release_module(gXmpCtx);
        gXmpLoaded = false;
        return;
    }
    // libxmp-lite controls looping per-buffer-call via the last arg of
    // xmp_play_buffer (0 = stop on song end, nonzero = max loop count
    // before stop, -1 = forever). We honor `loop` by feeding that
    // value through gXmpLoopCount; the IRQ hands it to play_buffer.
    gXmpLoopCount = loop ? -1 : 0;
    gXmpStarted   = true;
}


void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
    if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
        return;
    }
    audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, gEffRate);
}


void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
    if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
        return;
    }
    audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gEffRate);
}


// PIT counter 2 + speaker gate. AGI-era games (KQ1-3 etc.) drove
// notes by writing a divisor here and toggling port 0x61 bits 0/1
// to enable speaker output. CPU cost during playback is zero --
// the chip plays until we change it.
//
// freqHz == 0 -> silence: clear port 0x61 bits 0 and 1.
// otherwise   -> divisor = PIT_HZ / freqHz, program counter 2.
//
// Port 0x43 control byte 0xB6 = "select counter 2, R/W LSB then
// MSB, mode 3 (square wave), binary".
//
// Port 0x42 takes the 16-bit divisor as two byte writes (lo, hi).
//
// Port 0x61: bit 0 = timer 2 gate, bit 1 = speaker data line.
// Setting both routes the timer's output to the speaker; clearing
// both silences it.
#define PC_SPEAKER_PIT_HZ       1193182u
#define PC_SPEAKER_PORT_CTRL    0x43u
#define PC_SPEAKER_PORT_TIMER2  0x42u
#define PC_SPEAKER_PORT_GATE    0x61u
#define PC_SPEAKER_CTRL_BYTE    0xB6u

void jlpAudioTone(uint16_t freqHz) {
    uint32_t  divisor;
    uint8_t   gate;

    if (freqHz == 0u) {
        gate = (uint8_t)(inportb(PC_SPEAKER_PORT_GATE) & 0xFCu);
        outportb(PC_SPEAKER_PORT_GATE, gate);
        return;
    }
    divisor = PC_SPEAKER_PIT_HZ / (uint32_t)freqHz;
    if (divisor < 2u) {
        divisor = 2u;
    }
    if (divisor > 0xFFFFu) {
        divisor = 0xFFFFu;
    }
    outportb(PC_SPEAKER_PORT_CTRL,   PC_SPEAKER_CTRL_BYTE);
    outportb(PC_SPEAKER_PORT_TIMER2, (uint8_t)(divisor & 0xFFu));
    outportb(PC_SPEAKER_PORT_TIMER2, (uint8_t)((divisor >> 8) & 0xFFu));
    gate = (uint8_t)(inportb(PC_SPEAKER_PORT_GATE) | 0x03u);
    outportb(PC_SPEAKER_PORT_GATE, gate);
}


// AdLib OPL2 (Yamaha YM3812): two register-mapped ports.
//   $388 - index register / status read
//   $389 - data write
// 9 melodic channels, each two-operator FM. We use only channels 0/1/2.
// Each channel's two operators live in non-contiguous "slot" indices:
// ch 0 -> slot 0 (modulator) + slot 3 (carrier).
// ch 1 -> slot 1 + slot 4.
// ch 2 -> slot 2 + slot 5.
//
// Register layout per slot N:
//   $20+N : tremolo / vibrato / EG-type / KSR / multiplier
//   $40+N : key-scale-level / total-level (volume; 6-bit, 0=loud)
//   $60+N : attack / decay
//   $80+N : sustain / release
//   $E0+N : waveform select (0 sine, 1 half-sine, 2 full-rectified-sine,
//                            3 quarter-sine pulse)
//
// Per channel C:
//   $A0+C : F-number low (8 bits)
//   $B0+C : key-on (bit 5), block (bits 2..4), F-number high (bits 0..1)
//   $C0+C : feedback (bits 1..3), algorithm (bit 0)
//
// F-number / block relate to frequency by:
//   f_note = F * 49716 / 2^(20 - block)
//   F      = f_note * 2^(20 - block) / 49716
// 10-bit F range 0..1023; block 0..7. Sweet spot: F in 256..1023 for
// best resolution, pick block to land there.
//
// Original chip required 3.3 us between $388 register-select and $389
// data write (about 6 inportb dummy reads on an 8088). DOSBox ignores
// the delay; we keep a tiny safety pause for real-hardware sanity.
#define ADLIB_PORT_INDEX    0x388u
#define ADLIB_PORT_DATA     0x389u
#define ADLIB_VOICE_COUNT   3u

static const uint8_t kAdlibSlotMap[ADLIB_VOICE_COUNT * 2] = {
    // modulator, carrier per voice
    0x00, 0x03,   // voice 0
    0x01, 0x04,   // voice 1
    0x02, 0x05    // voice 2
};

// Rhythm-mode snare drum (the OPL2's LFSR noise source) carries the
// jlpAudioNoise voice. Rhythm mode repurposes only channels 6-8 (the
// tone voices use 0-2). Ch 7's carrier (slot 0x14) IS the snare, but
// its pitch is governed by ch 7's frequency through the HI-HAT
// operator (ch 7 modulator, slot 0x11) phase generator -- so the
// envelope/MULT setup that the pitch map depends on targets slot
// 0x11, not the snare slot. OPL2 slot offsets are non-contiguous
// (0x00-0x05, 0x08-0x0D, 0x10-0x15); keep these as named constants.
#define ADLIB_SLOT_HH       0x11u
#define ADLIB_SLOT_SD       0x14u
#define ADLIB_CH_NOISE      7u
#define ADLIB_BD_RHYTHM     0x20u    // reg 0xBD bit 5: rhythm mode enable
#define ADLIB_BD_KEY_SD     0x08u    // reg 0xBD bit 3: snare key
#define ADLIB_NOISE_MAX_HZ  6200u    // pitch 0 tops out the bright end

static bool gAdlibInited      = false;
static bool gAdlibNoiseInited = false;


static void adlibWrite(uint8_t reg, uint8_t val) {
    // The original YM3812 needs 3.3 us after $388 select and 23 us
    // after $389 data before the next register access. On real ISA
    // hardware the 8.33 MHz bus's natural inportb cycle (8 cycles ~=
    // 1 us) covers most of that with a small loop. DOSBox emulates
    // the chip in software and has no such timing requirement, so a
    // single status-port read after each write satisfies both.
    outportb(ADLIB_PORT_INDEX, reg);
    (void)inportb(ADLIB_PORT_INDEX);
    outportb(ADLIB_PORT_DATA, val);
    (void)inportb(ADLIB_PORT_INDEX);
}


// Convert a tone frequency to OPL2 F-number + block. Starts at block
// 4 and shifts until F is in 256..1023 (the high-resolution range).
// At AGI's note range (~50 Hz - 6 kHz) this lands in block 1..7.
// Shared by jlpAudioVoice and jlpAudioNoise.
static uint8_t adlibFreqToFnum(uint16_t freqHz, uint16_t *fnumOut) {
    uint32_t fnum;
    uint8_t  block;

    fnum  = ((uint32_t)freqHz * 65536u) / 49716u;
    block = 4u;
    while (fnum > 1023u && block < 7u) {
        fnum >>= 1;
        block++;
    }
    while (fnum < 256u && block > 0u) {
        fnum <<= 1;
        block--;
    }
    if (fnum > 1023u) {
        fnum = 1023u;
    }
    *fnumOut = (uint16_t)fnum;
    return block;
}


static void adlibInit(void) {
    uint8_t v;
    uint8_t i;
    uint8_t mod;
    uint8_t car;

    // Reset the OPL2: silence + zero state on every channel and operator.
    adlibWrite(0x01u, 0x00u);   // test register; clear (also disables WSE)
    adlibWrite(0x08u, 0x00u);   // CSM mode off, key-split disabled
    adlibWrite(0xBDu, 0x00u);   // tremolo/vibrato depth normal, rhythm off
    gAdlibNoiseInited = false;  // 0xBD clear kills rhythm mode; re-arm lazily
    for (i = 0xB0u; i <= 0xB8u; i++) {
        adlibWrite(i, 0x00u);   // key-off on every melodic channel
    }
    // Enable waveform select (WSE bit) so $E0 reg picks something other
    // than sine. On OPL2 this lives in test reg $01 bit 5.
    adlibWrite(0x01u, 0x20u);

    // Configure our three voices with a clean PSG-square-like patch.
    // Carrier waveform 1 = half-sine: positive lobe of sine, zero
    // during the negative half. Has strong odd harmonics like a
    // square wave but without the harshness of full clipping. Sounds
    // closer to a real PSG square at AGI's note pitches than sine.
    for (v = 0u; v < ADLIB_VOICE_COUNT; v++) {
        mod = kAdlibSlotMap[v * 2u + 0u];
        car = kAdlibSlotMap[v * 2u + 1u];

        // Both ops: KSR=0, EG-type=sustaining, no trem/vib, multiplier=1.
        adlibWrite((uint8_t)(0x20u + mod), 0x21u);
        adlibWrite((uint8_t)(0x20u + car), 0x21u);

        // Modulator silent (TL = 63 = -47.25 dB). With algorithm=1 we
        // sum modulator + carrier, so silencing the modulator gives a
        // pure carrier waveform.
        adlibWrite((uint8_t)(0x40u + mod), 0x3Fu);
        // Carrier loud by default; per-note jlAudioVoice overrides this.
        adlibWrite((uint8_t)(0x40u + car), 0x00u);

        // Attack as fast as possible, no decay -> square-wave envelope.
        adlibWrite((uint8_t)(0x60u + mod), 0xF0u);
        adlibWrite((uint8_t)(0x60u + car), 0xF0u);
        // Sustain at full, no release -> tone holds at full volume
        // until we key-off in jlAudioVoice.
        adlibWrite((uint8_t)(0x80u + mod), 0x00u);
        adlibWrite((uint8_t)(0x80u + car), 0x00u);

        // Waveform select. Modulator irrelevant (silenced); carrier
        // sine. Half-sine (waveform 1) is brighter and reads as
        // higher-pitched against PC-speaker memory; pure sine is
        // closer to what listeners recall as "Tandy 3-voice".
        adlibWrite((uint8_t)(0xE0u + mod), 0x00u);
        adlibWrite((uint8_t)(0xE0u + car), 0x00u);

        // Algorithm 1 (additive), no self-feedback.
        adlibWrite((uint8_t)(0xC0u + v), 0x01u);
    }
    gAdlibInited = true;
}


// One-time rhythm-mode setup for the noise voice. Must run after
// adlibInit (which clears reg 0xBD); adlibInit re-arms the lazy flag
// so a re-init path would rebuild this too.
static void adlibNoiseInit(void) {
    // Hi-hat operator (ch 7 modulator): its phase generator drives
    // the snare's pitch, so it gets the sustaining envelope + MULT=1
    // the 6200/(pitch+1) map assumes. Output level parked silent --
    // its own 0xBD key bit is never set.
    adlibWrite((uint8_t)(0x20u + ADLIB_SLOT_HH), 0x21u);
    adlibWrite((uint8_t)(0x40u + ADLIB_SLOT_HH), 0x3Fu);

    // Snare carrier: sustaining envelope, instant attack, no decay,
    // full sustain, release 0. Key-off alone never silences (release
    // rate 0), so the off path forces TL -- same contract as the
    // tone patch.
    adlibWrite((uint8_t)(0x20u + ADLIB_SLOT_SD), 0x21u);
    adlibWrite((uint8_t)(0x40u + ADLIB_SLOT_SD), 0x3Fu);
    adlibWrite((uint8_t)(0x60u + ADLIB_SLOT_SD), 0xF0u);
    adlibWrite((uint8_t)(0x80u + ADLIB_SLOT_SD), 0x00u);
    adlibWrite((uint8_t)(0xE0u + ADLIB_SLOT_SD), 0x00u);

    // Park the remaining rhythm operators' output levels. Rhythm mode
    // clocks the ch 6/8 phase generators regardless of key bits, and
    // a stale sustaining patch (EG-type 1, release 0 -- the shape our
    // own tone patch uses) can ring even keyed off.
    adlibWrite(0x50u, 0x3Fu);   // 0x40 + 0x10: bass drum modulator
    adlibWrite(0x52u, 0x3Fu);   // 0x40 + 0x12: tom-tom
    adlibWrite(0x53u, 0x3Fu);   // 0x40 + 0x13: bass drum carrier
    adlibWrite(0x55u, 0x3Fu);   // 0x40 + 0x15: top cymbal

    // Ch 7 feedback/connection: real OPL2 ignores it in rhythm mode
    // but some emulators honor modulator feedback -- force it clean.
    adlibWrite((uint8_t)(0xC0u + ADLIB_CH_NOISE), 0x00u);

    // Rhythm mode on, all five drum key bits off, trem/vib depth 0.
    adlibWrite(0xBDu, ADLIB_BD_RHYTHM);
    gAdlibNoiseInited = true;
}


// ----- PIT IRQ-driven tick dispatcher -----
//
// PIT counter 0 fires INT $08 (IRQ 0) at 18.2 Hz by default. AGI
// sound wants 60 Hz with note-accurate timing, so we reprogram the
// counter to the requested rate and hook the vector. Two
// constraints have to hold afterward:
//
//   1. BIOS tick counter at $0040:$006C must keep advancing at the
//      canonical 18.2 Hz so anything that reads it (DOS time() /
//      uclock() / RTC-less time-of-day tracking) stays right. We
//      use an accumulator: each IRQ adds the PIT divisor to it,
//      and every time it crosses 65536 we bump the BIOS counter
//      and subtract -- the math comes out to exactly 18.2065 Hz
//      regardless of our own IRQ rate.
//
//   2. jlpMillisElapsed must report wall-clock ms even though
//      uclock()'s sub-tick PIT read is now wrong (PIT cycles
//      0..gPitDivisor, not 0..65535 as uclock assumes). We expose
//      an own-ISR-counter-driven ms via the gAudioIsr* globals
//      below; src/dos/hal.c::jlpMillisElapsed prefers it
//      whenever gAudioIsrActive is true.
//
// Literal "chain to old INT $08" doesn't work cleanly under DPMI
// because the old PM handler ends in IRET, which corrupts the
// stack frame of any CALL into it. INT $1C (user timer hook) and
// floppy motor timeout are dropped; nothing AGI cares about uses
// either.
#define PIT_CTRL_PORT       0x43u
#define PIT_TIMER0_PORT     0x40u
#define PIT_CTRL_T0_MODE3   0x36u    // counter 0, R/W LSB then MSB, mode 3 (square wave), binary
#define PIT_CLOCK_HZ        1193182uL
#define BIOS_TICK_PERIOD    65536uL
#define BIOS_TICK_ADDR      0x46Cu
#define IRQ0_INT_VECTOR     0x08

// Exposed (extern in hal.c) so jlpMillisElapsed can read wall-clock
// ms straight from the ISR counter and bypass the broken uclock.
volatile uint32_t              gAudioIsrFires  = 0;
uint16_t                       gAudioIsrHz     = 0;
volatile bool                  gAudioIsrActive = false;

static volatile jlAudioTickFnT gAudioTickFn    = NULL;
static volatile uint32_t       gBiosTickAccum  = 0;
static volatile uint16_t       gPitDivisor     = 0;
static _go32_dpmi_seginfo      gOldTimerVec;
static _go32_dpmi_seginfo      gNewTimerVec;
static bool                    gTimerHooked    = false;


static void audioTickIsr(void) {
    gAudioIsrFires++;
    if (gAudioTickFn != NULL) {
        gAudioTickFn();
    }
    // BIOS counter at 18.2 Hz exact via accumulator. Equivalent to
    // calling the original BIOS handler every PIT_default/N IRQs,
    // without the IRET-frame headache of an actual chain call.
    gBiosTickAccum += gPitDivisor;
    while (gBiosTickAccum >= BIOS_TICK_PERIOD) {
        gBiosTickAccum -= BIOS_TICK_PERIOD;
        _farpokel(_dos_ds, BIOS_TICK_ADDR,
                  _farpeekl(_dos_ds, BIOS_TICK_ADDR) + 1u);
    }
    outportb(PIC1_CMD, PIC_EOI);
}


static void programPit0(uint16_t divisor) {
    // divisor == 0 in the I/O port writes means "65536" to the
    // hardware -- the BIOS default that gives 18.2 Hz.
    outportb(PIT_CTRL_PORT,   PIT_CTRL_T0_MODE3);
    outportb(PIT_TIMER0_PORT, (uint8_t)(divisor & 0xFFu));
    outportb(PIT_TIMER0_PORT, (uint8_t)((divisor >> 8) & 0xFFu));
}


bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
    uint32_t  divisor;

    if (fn == NULL) {
        if (gTimerHooked) {
            __asm__ __volatile__("cli" ::: "memory");
            gAudioTickFn    = NULL;
            gAudioIsrActive = false;
            programPit0(0u);   // restore BIOS-default 18.2 Hz
            _go32_dpmi_set_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gOldTimerVec);
            __asm__ __volatile__("sti" ::: "memory");
            _go32_dpmi_free_iret_wrapper(&gNewTimerVec);
            gTimerHooked = false;
        }
        return true;
    }

    divisor = PIT_CLOCK_HZ / (uint32_t)hz;
    if (divisor < 16uL)    { divisor = 16uL; }
    if (divisor > 65535uL) { divisor = 65535uL; }

    if (!gTimerHooked) {
        _go32_dpmi_lock_code(audioTickIsr, 4096);
        _go32_dpmi_lock_data((void *)&gAudioTickFn,    sizeof(gAudioTickFn));
        _go32_dpmi_lock_data((void *)&gAudioIsrFires,  sizeof(gAudioIsrFires));
        _go32_dpmi_lock_data((void *)&gAudioIsrActive, sizeof(gAudioIsrActive));
        _go32_dpmi_lock_data((void *)&gBiosTickAccum,  sizeof(gBiosTickAccum));
        _go32_dpmi_lock_data((void *)&gPitDivisor,     sizeof(gPitDivisor));

        _go32_dpmi_get_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gOldTimerVec);
        gNewTimerVec.pm_offset   = (unsigned long)audioTickIsr;
        gNewTimerVec.pm_selector = _go32_my_cs();
        if (_go32_dpmi_allocate_iret_wrapper(&gNewTimerVec) != 0) {
            return false;
        }
        gTimerHooked = true;
    }

    __asm__ __volatile__("cli" ::: "memory");
    gAudioTickFn    = fn;
    gPitDivisor     = (uint16_t)divisor;
    gBiosTickAccum  = 0;
    gAudioIsrFires  = 0;
    gAudioIsrHz     = hz;
    gAudioIsrActive = true;
    _go32_dpmi_set_protected_mode_interrupt_vector(IRQ0_INT_VECTOR, &gNewTimerVec);
    programPit0((uint16_t)divisor);
    __asm__ __volatile__("sti" ::: "memory");
    return true;
}


void jlpAudioCriticalEnter(void) {
    // "memory" clobber forces GCC to commit pending register-cached
    // writes to memory before the cli (and reload after sti), so
    // the timer ISR can't observe a half-written critical section.
    __asm__ __volatile__("cli" ::: "memory");
}


void jlpAudioCriticalExit(void) {
    __asm__ __volatile__("sti" ::: "memory");
}


void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
    uint16_t freqHz;
    uint16_t fnum;
    uint8_t  block;
    uint8_t  tl;

    if (!gAdlibInited) {
        adlibInit();
    }
    if (!gAdlibNoiseInited) {
        adlibNoiseInit();
    }

    // Silence: drop the snare key bit and force its carrier silent
    // (release rate is 0, so key-off alone never fades). Rhythm mode
    // itself stays enabled.
    if (atten >= 15u) {
        adlibWrite(0xBDu, ADLIB_BD_RHYTHM);
        adlibWrite((uint8_t)(0x40u + ADLIB_SLOT_SD), 0x3Fu);
        return;
    }

    // Same attenuation map as the tone voices.
    tl = (uint8_t)((uint16_t)atten * 4u);
    if (tl > 60u) {
        tl = 60u;
    }
    adlibWrite((uint8_t)(0x40u + ADLIB_SLOT_SD), tl);

    // pitch 0..31 -> hyperbolic Hz, the YM noise-period semantic:
    // pitch 0 = 6200 Hz bright hiss, pitch 31 = ~194 Hz rumble.
    // (Core clamps pitch to <= 31 before the HAL sees it.)
    freqHz = (uint16_t)(ADLIB_NOISE_MAX_HZ / ((uint16_t)pitch + 1u));
    block  = adlibFreqToFnum(freqHz, &fnum);

    // Bit 5 (melodic key-on) must stay clear on 0xB7: in rhythm mode
    // ch 7 is keyed via 0xBD bit 3 only. F-number changes take effect
    // immediately while keyed, and 1->1 on the key bit is not a
    // retrigger edge, so live pitch/volume updates are click-free.
    adlibWrite((uint8_t)(0xA0u + ADLIB_CH_NOISE), (uint8_t)(fnum & 0xFFu));
    adlibWrite((uint8_t)(0xB0u + ADLIB_CH_NOISE), (uint8_t)(((block & 0x07u) << 2) | ((fnum >> 8) & 0x03u)));
    adlibWrite(0xBDu, (uint8_t)(ADLIB_BD_RHYTHM | ADLIB_BD_KEY_SD));
}


void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
    uint8_t   car;
    uint8_t   block;
    uint16_t  fnum;
    uint8_t   tl;

    if (voice >= ADLIB_VOICE_COUNT) {
        return;
    }
    if (!gAdlibInited) {
        adlibInit();
    }

    car = kAdlibSlotMap[voice * 2u + 1u];

    // Silence: key-off the channel and force the carrier to silent.
    // Holding the key-on bit clear lets the envelope release; we also
    // set TL=63 in case the channel is keyed back on with the same
    // freq later (no audible click from a stale "loud" carrier).
    if (freqHz == 0u || atten >= 15u) {
        adlibWrite((uint8_t)(0xB0u + voice), 0x00u);
        adlibWrite((uint8_t)(0x40u + car),   0x3Fu);
        return;
    }

    // AGI/SN76489 attenuation: 0=loud, 15=silent, 2 dB per step.
    // OPL2 total-level: 0=loud, 63=silent, 0.75 dB per step. So
    // tl = atten * (2 / 0.75) ~= atten * 2.67. atten*4 is close enough
    // and keeps the math integer. Clamp at 60 (a bit louder than the
    // hard 63 floor) so even "atten=14" stays barely audible.
    tl = (uint8_t)((uint16_t)atten * 4u);
    if (tl > 60u) {
        tl = 60u;
    }
    adlibWrite((uint8_t)(0x40u + car), tl);

    block = adlibFreqToFnum(freqHz, &fnum);

    // Write F-number low first, then key-on + block + F-number high.
    // The key-on edge on B0 latches the new F across both regs.
    adlibWrite((uint8_t)(0xA0u + voice), (uint8_t)(fnum & 0xFFu));
    adlibWrite((uint8_t)(0xB0u + voice),
               (uint8_t)(0x20u | ((block & 0x07u) << 2) | ((fnum >> 8) & 0x03u)));
}


void jlpAudioStopMod(void) {
    if (!gDspPresent || gXmpCtx == NULL) {
        return;
    }
    if (gXmpStarted) {
        xmp_end_player(gXmpCtx);
        gXmpStarted = false;
    }
    if (gXmpLoaded) {
        xmp_release_module(gXmpCtx);
        gXmpLoaded = false;
    }
}


void jlpAudioStopSfx(uint8_t slot) {
    if (slot >= JOEY_AUDIO_SFX_SLOTS) {
        return;
    }
    gSfxSlots[slot].active = false;
}


// Called once per game-loop iteration from the application. Drains
// the IRQ-raised refill flags and refills out-of-IRQ (so libxmp-lite
// is never re-entered from interrupt context). At 70 Hz polling a
// 22050 Hz half-buffer of 2048 samples drains ~143000 samples/sec
// of headroom, well above the playback rate, so a missed frame or
// two is OK without underflow.
void jlpAudioFrameTick(void) {
    int i;

    if (!gDspPresent) {
        return;
    }
    for (i = 0; i < 2; i++) {
        if (gNeedRefill[i]) {
            refillHalf((uint8_t)i);
            gNeedRefill[i] = false;
        }
    }
}
