/* ======================================================================
 * pmaudio.c  -  PokeyMAX audio diagnostic / device walk-through
 *
 * A standalone Atari 8-bit program (build -> XEX) that detects which
 * audio devices a PokeyMAX core exposes (via the CAPABILITY register)
 * and then plays a short, identifiable chord on each one in turn:
 *
 *     POKEY (mono / stereo / quad)   SID1 / SID2   PSG1 / PSG2
 *     COVOX (8-bit manual DAC)       SAMPLE (DMA block-RAM player)
 *
 * It is written as plain C (cc65) so it doubles as a worked example of
 * driving the PokeyMAX register map from 6502 C.
 *
 * Two things this program is deliberately careful about:
 *
 *   1. SHADOW REGISTERS.  The Atari OS keeps RAM shadows of the two
 *      POKEY control registers it relies on for IRQs:
 *          POKMSK ($0010)  -> mirror of IRQEN  ($D20E, write-only)
 *          SSKCTL ($0232)  -> mirror of SKCTL  ($D20F)
 *      The OS keyboard / SIO / timer IRQ handler re-writes IRQEN from
 *      POKMSK, and the stage-2 VBI re-writes SKCTL from SSKCTL.  If we
 *      poke those hardware registers directly without updating the
 *      shadows, the next interrupt stamps our values back.  AUDF/AUDC/
 *      AUDCTL have *no* OS shadow, so writing them directly is safe.
 *      We therefore touch IRQEN/SKCTL only through their shadows, and
 *      we save/restore the full audio state around the test.
 *
 *   2. STEREO vs QUAD CANNOT BE HEARD BY PLAYING VOICES IN TURN.
 *      On a stereo core, POKEY3/4 are address shadows of POKEY1/2, and
 *      POKEY1/3 -> left, POKEY2/4 -> right.  Playing 1,2,3,4 one at a
 *      time gives L,R,L,R on *both* stereo and quad - identical to the
 *      ear.  The only audible difference is when voices 1 and 3 (and 2
 *      and 4) sound *simultaneously at different pitches*:
 *          stereo -> writing "voice 3" overwrites voice 1, so the left
 *                    channel carries a single pitch.
 *          quad   -> voices 1 and 3 are independent and SUM on the left,
 *                    so the left channel carries a two-note chord.
 *      So the POKEY test plays a 4-note chord across all four register
 *      banks at once.  We also read $D211 to report the installed
 *      configuration directly, rather than leaving it to the ear.
 * ====================================================================== */

#include <stdint.h>
#include <string.h>

/* Test duration.  Atari self-test style: each audible stage holds for
 * TEST_SECONDS.  Adjust TEST_FRAMES_PER_SECOND to 60 for NTSC if you want
 * wall-clock seconds to be closer on NTSC machines. */
#define TEST_SECONDS           2
#define TEST_FRAMES_PER_SECOND 50
#define TEST_HOLD_FRAMES       (TEST_SECONDS * TEST_FRAMES_PER_SECOND)


/* ----------------------------------------------------------------------
 * Hardware register map (see PokeyMAX developer guide v1.30)
 * -------------------------------------------------------------------- */
#define POKEY1      ((volatile uint8_t *)0xD200)
#define POKEY2      ((volatile uint8_t *)0xD210)
#define POKEY3      ((volatile uint8_t *)0xD220)
#define POKEY4      ((volatile uint8_t *)0xD230)

/* POKEY register offsets within a 16-byte bank */
#define AUDF1       0x00
#define AUDC1       0x01
#define AUDF2       0x02
#define AUDC2       0x03
#define AUDF3       0x04
#define AUDC3       0x05
#define AUDF4       0x06
#define AUDC4       0x07
#define AUDCTL      0x08
#define STIMER      0x09   /* write: start/reset POKEY timers            */
#define IRQEN       0x0E   /* write: IRQ enable   (OS shadow POKMSK $0010) */
#define IRQST       0x0E   /* read : IRQ status                            */
#define SKCTL       0x0F   /* serial / keyboard control (shadow SSKCTL)    */

/* Configuration bank: write 0x3F to CONFIG ($D20C) to map config regs
 * into $D210-$D21F.  While mapped, POKEY2 is temporarily shadowed. */
#define CONFIG      ((volatile uint8_t *)0xD20C)
#define CFG_ENABLE  0x3F
#define CFG_DISABLE 0x00

#define CFG_MODE    ((volatile uint8_t *)0xD210)  /* RW */
#define CFG_CAPS    ((volatile uint8_t *)0xD211)  /* RO - what is installed */
#define CFG_VERSION ((volatile uint8_t *)0xD214)  /* version string window  */
#define CFG_PSGMODE ((volatile uint8_t *)0xD215)
#define CFG_SIDMODE ((volatile uint8_t *)0xD216)
#define CFG_RESTRICT ((volatile uint8_t *)0xD217)

/* CAPABILITY ($D211) bit fields */
#define CAP_POKEY_MASK 0x03   /* hardware: 0=single 1=two(stereo) 3=four(quad) */
#define CAP_SID        0x04   /* two SID chips present                      */
#define CAP_PSG        0x08   /* two PSG chips present                      */
#define CAP_COVOX      0x10   /* four 8-bit manual COVOX volume registers   */
#define CAP_SAMPLE     0x20   /* 42 KiB block-RAM DMA sample player         */
#define CAP_FLASH      0x40   /* flash (UFM/CFM) access                     */
#define CAP_SAMPLE_64KB 0x80   /* 64 KiB block-RAM DMA sample player         */

/* SID:  SID1 = $D240 (left), SID2 = $D260 (right) */
#define SID1        ((volatile uint8_t *)0xD240)
#define SID2        ((volatile uint8_t *)0xD260)
/* 6581/8580 register offsets (voice 1) */
#define SID_FREQLO  0x00
#define SID_FREQHI  0x01
#define SID_PWLO    0x02
#define SID_PWHI    0x03
#define SID_CTRL    0x04
#define SID_AD      0x05
#define SID_SR      0x06
#define SID_FCLO    0x15
#define SID_FCHI    0x16
#define SID_RESFILT 0x17
#define SID_MODEVOL 0x18

/* PSG:  PSG1 = $D2A0 (left bias), PSG2 = $D2B0.  Registers are directly
 * memory-mapped: $D2A0 = R0 ... $D2AF = RF (no address/data latch). */
#define PSG1        ((volatile uint8_t *)0xD2A0)
#define PSG2        ((volatile uint8_t *)0xD2B0)

/* COVOX manual 8-bit volume registers.
 * In stereo+covox / quad+covox layouts COVOX sits at $D280-$D283.
 *   CH1,CH4 -> left ;  CH2,CH3 -> right  (per guide). */
#define COVOX_CH1   ((volatile uint8_t *)0xD280)
#define COVOX_CH2   ((volatile uint8_t *)0xD281)
#define COVOX_CH3   ((volatile uint8_t *)0xD282)
#define COVOX_CH4   ((volatile uint8_t *)0xD283)

/* SAMPLE (DMA block-RAM player) register map */
#define SAM_RAMADDRL ((volatile uint8_t *)0xD284)
#define SAM_RAMADDRH ((volatile uint8_t *)0xD285)
#define SAM_RAMDATA  ((volatile uint8_t *)0xD286)
#define SAM_RAMDATAI ((volatile uint8_t *)0xD287)  /* write + auto-increment */
#define SAM_CHANSEL  ((volatile uint8_t *)0xD288)
#define SAM_ADDRL    ((volatile uint8_t *)0xD289)
#define SAM_ADDRH    ((volatile uint8_t *)0xD28A)
#define SAM_LENL     ((volatile uint8_t *)0xD28B)
#define SAM_LENH     ((volatile uint8_t *)0xD28C)
#define SAM_PERL     ((volatile uint8_t *)0xD28D)
#define SAM_PERH     ((volatile uint8_t *)0xD28E)
#define SAM_VOL      ((volatile uint8_t *)0xD28F)
#define SAM_DMA      ((volatile uint8_t *)0xD290)
#define SAM_IRQEN    ((volatile uint8_t *)0xD291)
#define SAM_IRQACT   ((volatile uint8_t *)0xD292)
#define SAM_CFG      ((volatile uint8_t *)0xD293)

/* ----------------------------------------------------------------------
 * Atari OS shadows / hardware we must be polite about
 * -------------------------------------------------------------------- */
#define POKMSK      (*(volatile uint8_t *)0x0010) /* IRQEN shadow          */
#define SSKCTL      (*(volatile uint8_t *)0x0232) /* SKCTL shadow          */
#define RTCLOK2     (*(volatile uint8_t *)0x0014) /* frame counter (low)   */
#define CH          (*(volatile uint8_t *)0x02FC) /* last key code (0xFF=none) */
#define CONSOL      (*(volatile uint8_t *)0xD01F) /* console keys (GTIA)   */

#define IRQEN_KEYBOARD 0x40  /* POKEY keyboard IRQ enable bit */
#define IRQEN_TIMER1   0x01  /* POKEY timer 1 IRQ enable/status bit       */

/* ----------------------------------------------------------------------
 * Tiny text/screen helpers (no conio dependency, so this stays a clear
 * register-level example).  Uses the OS screen via E: through the
 * resident editor would pull in a lot; instead we write ATASCII to the
 * default GR.0 screen RAM pointed to by SAVMSC ($0058).
 * -------------------------------------------------------------------- */
#define SAVMSC      (*(volatile uint16_t *)0x0058)

static volatile uint8_t *scr;   /* screen RAM base */
static uint8_t  cur_row;

/* ATASCII -> screen (internal) code conversion for the common range */
static uint8_t scrcode(uint8_t c)
{
    if (c < 0x20)      return c + 0x40;
    if (c < 0x60)      return c - 0x20;
    return c;
}

static void put_at(uint8_t row, uint8_t col, const char *s)
{
    volatile uint8_t *p = scr + (uint16_t)row * 40 + col;
    while (*s) *p++ = scrcode((uint8_t)*s++);
}

static void clear_screen(void)
{
    uint16_t i;
    for (i = 0; i < 40 * 24; i++) scr[i] = 0;
    cur_row = 0;
}

static void line(const char *s)
{
    if (cur_row < 24) put_at(cur_row, 1, s);
    cur_row++;
}

/* ----------------------------------------------------------------------
 * Crude timing: spin for ~n video frames using the OS frame counter.
 * The OS VBI keeps RTCLOK2 ticking (50 Hz PAL / 60 Hz NTSC) even while
 * we are busy, so this is a portable "hold this sound" delay.
 * -------------------------------------------------------------------- */
static void wait_frames(uint16_t n)
{
    while (n--) {
        uint8_t start = RTCLOK2;
        while (RTCLOK2 == start) { /* spin for next frame */ }
    }
}

/* ----------------------------------------------------------------------
 * POKEY ownership.  We do NOT use SEI/CLI from C; instead we keep the OS
 * IRQs running but stop them from fighting us:
 *   - Tell the OS (via POKMSK shadow + IRQEN) to disable the timer/serial
 *     IRQ sources we don't want, but KEEP the keyboard IRQ enabled.
 *     If POKMSK/IRQEN is set to zero, CH ($02FC) will never update and
 *     the final "press any key" loop appears to hang.
 *   - SKCTL: we set bit1|bit0 (fast pot scan + keyboard debounce) the way
 *     the OS expects, written through SSKCTL so the next VBI agrees.
 * AUDF/AUDC/AUDCTL have no shadow, so direct writes stick.
 * -------------------------------------------------------------------- */
static uint8_t saved_pokmsk;
static uint8_t saved_sskctl;
static uint8_t saved_audctl;
static uint8_t saved_psgmode;

static void pokey_silence_all(void)
{
    uint8_t i;
    /* AUDC volume nibble = 0 on every voice of every bank.  Writing all
     * four banks is harmless on mono/stereo (they are shadows). */
    for (i = AUDC1; i <= AUDC4; i += 2) {
        POKEY1[i] = 0; POKEY2[i] = 0; POKEY3[i] = 0; POKEY4[i] = 0;
    }
    COVOX_CH1[0] = 0x80; COVOX_CH2[0] = 0x80;   /* DAC mid-scale (silence) */
    COVOX_CH3[0] = 0x80; COVOX_CH4[0] = 0x80;
}

static void audio_acquire(void)
{
    saved_pokmsk = POKMSK;
    saved_sskctl = SSKCTL;
    saved_audctl = 0;            /* AUDCTL has no shadow; assume OS default */

    /* Save PSG mode while the config bank is mapped. */
    *CONFIG = CFG_ENABLE;
    saved_psgmode = *CFG_PSGMODE;
    *CONFIG = CFG_DISABLE;

    /* AUDCTL = 0: 64 kHz clock base, no high-pass, no 16-bit joins,
     * 15 kHz off.  Predictable starting point for all voices. */
    POKEY1[AUDCTL] = 0;
    POKEY2[AUDCTL] = 0;
    POKEY3[AUDCTL] = 0;
    POKEY4[AUDCTL] = 0;

    /* Each physical POKEY has its own SKCTL/IRQEN.  The OS only keeps
     * shadows for POKEY1, so initialise POKEY2-4 explicitly.  Without
     * SKCTL=$03, extra POKEYs can remain held/reset and produce no sound. */
    SSKCTL = 0x03;
    POKEY1[SKCTL] = 0x03;
    POKEY2[SKCTL] = 0x03;
    POKEY3[SKCTL] = 0x03;
    POKEY4[SKCTL] = 0x03;
    /* Keep keyboard IRQs alive on the real OS POKEY.  Extra POKEYs do not
     * service the keyboard, so their IRQEN registers are kept quiet. */
    POKMSK = IRQEN_KEYBOARD;
    POKEY1[IRQEN] = IRQEN_KEYBOARD;
    POKEY2[IRQEN] = 0x00;
    POKEY3[IRQEN] = 0x00;
    POKEY4[IRQEN] = 0x00;

    pokey_silence_all();
}

static void audio_release(void)
{
    pokey_silence_all();
    SAM_DMA[0] = 0x00;
    SAM_IRQEN[0] = 0x00;
    *CONFIG = CFG_ENABLE;
    *CFG_PSGMODE = saved_psgmode;
    *CONFIG = CFG_DISABLE;

    POKEY1[AUDCTL] = saved_audctl;
    /* Restore the OS control shadows and the live registers together. */
    SSKCTL = saved_sskctl;
    POKEY1[SKCTL] = saved_sskctl;
    POKMSK = saved_pokmsk;
    POKEY1[IRQEN] = saved_pokmsk;
}

/* ----------------------------------------------------------------------
 * CAPABILITY read.  Map config bank in ($D20C <- 0x3F), read $D211,
 * map it back out.  POKEY2 is shadowed only during this window.
 * -------------------------------------------------------------------- */
static uint8_t read_caps(uint8_t *version_out /* 8 bytes, may be NULL */)
{
    uint8_t caps;
    uint8_t i;

    *CONFIG = CFG_ENABLE;
    caps = *CFG_CAPS;
    if (version_out) {
        for (i = 0; i < 8; i++) {
            *CFG_VERSION = i;          /* select char index */
            version_out[i] = *CFG_VERSION;
        }
    }
    *CONFIG = CFG_DISABLE;
    return caps;
}

/* Probe for PokeyMAX at all: CONFIG read returns 1 if installed. */
static uint8_t pokeymax_present(void)
{
    uint8_t id;
    id = *CONFIG;         /* reads back ID (=1 if PokeyMAX) */
    return (id == 1);
}

/* ----------------------------------------------------------------------
 * Device tests.  Each plays for ~`hold` frames then is silenced by the
 * caller's pokey_silence_all() / explicit gate-off.
 * -------------------------------------------------------------------- */

/* A pleasant-ish A-major-ish set of POKEY dividers (64 kHz clock, 8-bit).
 * Lower divider = higher pitch.  These are only meant to be distinct. */
#define DIV_LOW   0xC8   /* ~ low  note */
#define DIV_MID1  0x96
#define DIV_MID2  0x64
#define DIV_HIGH  0x4B   /* ~ high note */
#define AUDC_TONE 0xA8   /* distortion A (pure tone) + volume 8           */

/* POKEY chord: write four DIFFERENT pitches to the four register banks
 * simultaneously.  See header note - this is what separates stereo from
 * quad audibly:
 *   bank1 -> left   bank2 -> right   bank3 -> left   bank4 -> right
 * On quad, bank1+bank3 sum (two-note chord) on left, bank2+bank4 on right.
 * On stereo, bank3/4 alias bank1/2 so each side carries a single pitch. */
static void test_pokey_chord(uint16_t hold)
{
    POKEY1[AUDF1] = DIV_LOW;  POKEY1[AUDC1] = AUDC_TONE;  /* left  */
    POKEY2[AUDF1] = DIV_MID1; POKEY2[AUDC1] = AUDC_TONE;  /* right */
    POKEY3[AUDF1] = DIV_MID2; POKEY3[AUDC1] = AUDC_TONE;  /* left (quad) */
    POKEY4[AUDF1] = DIV_HIGH; POKEY4[AUDC1] = AUDC_TONE;  /* right(quad) */
    wait_frames(hold);
    pokey_silence_all();
}

/* Single POKEY bank, one voice - used to walk L vs R individually. */
static void test_pokey_one(volatile uint8_t *bank, uint8_t div, uint16_t hold)
{
    bank[AUDF1] = div;
    bank[AUDC1] = AUDC_TONE;
    wait_frames(hold);
    bank[AUDC1] = 0;
}

/* SID voice 1 triangle note. freq word = Fout * 16777216 / clock.
 * Clock ~1 MHz, so freq ~= Fn * 16.78.  We just pick a register value. */
static void test_sid(volatile uint8_t *sid, uint16_t freq, uint16_t hold)
{
    sid[SID_MODEVOL] = 0x0F;             /* master volume max, filters off */
    sid[SID_AD]      = 0x00;             /* attack 2ms / decay 6ms         */
    sid[SID_SR]      = 0xF0;             /* sustain max / release fast     */
    sid[SID_PWLO]    = 0x00;
    sid[SID_PWHI]    = 0x08;             /* 50% pulse width (if pulse)     */
    sid[SID_FREQLO]  = (uint8_t)(freq & 0xFF);
    sid[SID_FREQHI]  = (uint8_t)(freq >> 8);
    sid[SID_CTRL]    = 0x41;             /* pulse + GATE on             */
    wait_frames(hold);
    sid[SID_CTRL]    = 0x10;             /* GATE off -> release            */
    wait_frames(4);
    sid[SID_MODEVOL] = 0x00;
}

static void psg_silence(volatile uint8_t *psg)
{
    psg[0x08] = 0x00;
    psg[0x09] = 0x00;
    psg[0x0A] = 0x00;
    psg[0x07] = 0x3F;        /* all tone/noise disabled (active-low mixer) */
}

static void psg_set_max_stereo(void)
{
    uint8_t mode;
    *CONFIG = CFG_ENABLE;
    mode = *CFG_PSGMODE;
    mode &= (uint8_t)~0x0C;   /* clear STEREO bits */
    mode |= 0x0C;             /* MAX: PSG1 left, PSG2 right */
    *CFG_PSGMODE = mode;
    *CONFIG = CFG_DISABLE;
}

static void psg_restore_mode(void)
{
    *CONFIG = CFG_ENABLE;
    *CFG_PSGMODE = saved_psgmode;
    *CONFIG = CFG_DISABLE;
}

/* PSG (YM2149) tone on channel A.  Registers are directly mapped:
 *   R0/R1 = ch A period (12-bit), R7 = mixer (active low),
 *   R8 = ch A level.  Period = clock / (16 * f). */
static void test_psg(volatile uint8_t *psg, uint16_t period, uint16_t hold)
{
    psg_silence(PSG1);
    psg_silence(PSG2);
    psg[0x00] = (uint8_t)(period & 0xFF);
    psg[0x01] = (uint8_t)((period >> 8) & 0x0F);
    psg[0x07] = 0x3E;        /* mixer: tone A on (bit0=0), rest off        */
    psg[0x08] = 0x0F;        /* ch A amplitude = 15 (fixed, not envelope)  */
    wait_frames(hold);
    psg[0x08] = 0x00;        /* level 0 = silent                           */
}

/* ----------------------------------------------------------------------
 * COVOX: four CPU-written 8-bit DACs ($D280-$D283).  Unlike the SAMPLE
 * player (which is DMA from block RAM), COVOX must be fed one byte at a
 * time by the 6502.  Goal: play the SAME waveform over the SAME duration
 * (hence same pitch) as the sample-player test, driven purely by the CPU.
 *
 * WHY WE DOWNSAMPLE 4x.  The sample player advances one sample every
 * SAMPER ticks of its 2*PHI2 clock (guide: "2*PHI2/(L+H*256)"), so the
 * inter-sample interval in PHI2 cycles is SAMPER/2:
 *     left  SAMPER $0068 = 104 -> 52 PHI2 cycles/sample (~34 kHz)
 *     right SAMPER $0058 =  88 -> 44 PHI2 cycles/sample (~40 kHz)
 * Servicing a sample through the Atari OS IRQ path costs ~100-112 cycles
 * (6502 IRQ entry + the OS IRQST-polling dispatcher + our handler), so the
 * CPU cannot match the DMA rate per-sample - that is exactly why the
 * sample player is DMA.  Instead we update COVOX once per FOUR DMA samples
 * and run the timer 4x slower.  Over a full waveform this is the same
 * total time and therefore the same pitch; it is just a 4x-decimated copy
 * of the identical waveform.  4x (not 2x or 3x) because: (a) at 2x/3x the
 * period (88-156 cyc) the OS-dispatched ISR still does not reliably fit,
 * and (b) 256/4 = 64 divides evenly so the decimated cycle closes without
 * a phase glitch at the wrap.
 *
 * RATE.  COVOX period in PHI2 cycles = 4 * (SAMPER/2):
 *     left  4 * 52 = 208 ;  right 4 * 44 = 176
 * POKEY1 timer 1 clocked from PHI2 (AUDCTL bit6) underflows every AUDF+4
 * PHI2 cycles (the 1.79 MHz clock offsets the period by 4, not 1):
 *     left  AUDF1 = 208 - 4 = 204
 *     right AUDF1 = 176 - 4 = 172
 *
 * ACCURACY.  Polling IRQST in C is not cycle-exact (loop/branch latency
 * varies), so we drive the DAC from the timer-1 IRQ (VTIMR1, $0210): fixed
 * interrupt latency means each write lands a constant offset after the
 * underflow.  With the screen off there is no playfield DMA to add jitter.
 *
 * WAVEFORM.  The ISR walks the 256-byte table in steps of 4 (0,4,8,...),
 * i.e. the 4x decimation of the exact bytes the DMA player reads (unsigned
 * DAC = signed sample + 128).  Same table, every fourth entry. */

#define COVOX_AUDCTL_CH1_PHI2   0x40
#define COVOX_TIMER_AUDF_LEFT   204  /* 4 * 52 PHI2 - 4 reload offset */
#define COVOX_TIMER_AUDF_RIGHT  172  /* 4 * 44 PHI2 - 4 reload offset */
#define COVOX_DECIMATE          4    /* table index step per IRQ      */

/* OS IRQ vector for POKEY timer 1 (jumped through by the OS dispatcher). */
#define VTIMR1 (*(volatile uint16_t *)0x0210)

/* Shared state for the ISR.  NOT static: covox_isr.s imports these.
 * cc65 exports C globals with a leading underscore (_covox_tab, etc). */
volatile uint8_t  covox_tab[256];   /* pre-baked play sequence      */
volatile uint8_t  covox_idx;        /* current table index          */
volatile uint8_t  covox_dac_a;      /* first DAC reg offset (0..3)   */
volatile uint8_t  covox_dac_b;      /* second DAC reg offset         */

/* The COVOX base, so the ISR can index by offset. */
#define COVOX_BASE ((volatile uint8_t *)0xD280)

/* Saved OS vector so we can restore it. */
static uint16_t covox_saved_vtimr1;

/* ----------------------------------------------------------------------
 * Timer-1 IRQ handler lives in covox_isr.s (assembled separately) for
 * fixed, minimal interrupt latency.  See that file for the stack/ack
 * convention.  We only declare it here so we can install its address.
 * -------------------------------------------------------------------- */
extern void covox_isr(void);

static void covox_midscale(void)
{
    COVOX_CH1[0] = 0x80; COVOX_CH2[0] = 0x80;
    COVOX_CH3[0] = 0x80; COVOX_CH4[0] = 0x80;
}

/* Same byte the DMA table holds, as an unsigned DAC value:
 *   signed triangle  t = (p<128)? p*2-128 : 383-p*2
 *   unsigned DAC     = t + 128  ==  (p<128)? p*2 : 255-((p&0x7F)<<1)
 * with p = (i*step) mod 256. */
static uint8_t covox_sample_byte(uint8_t i, uint8_t step)
{
    uint8_t p = (uint8_t)(i * step);
    return (p < 128) ? (uint8_t)(p * 2)
                     : (uint8_t)(255 - ((p & 0x7F) << 1));
}

static void covox_build_table(uint8_t step)
{
    uint16_t i;
    for (i = 0; i < 256; i++)
        covox_tab[i] = covox_sample_byte((uint8_t)i, step);
}

/* COVOX: 8-bit manual DAC, CPU-fed via timer-1 IRQ.
 * CH1/CH4 -> left, CH2/CH3 -> right.  Plays the exact same waveform and
 * rate as test_sample_lr() for the matching side. */
static void test_covox_lr(uint8_t right_side, uint16_t hold)
{
    uint8_t step = right_side ? 3 : 1;
    uint8_t audf = right_side ? COVOX_TIMER_AUDF_RIGHT : COVOX_TIMER_AUDF_LEFT;

    covox_build_table(step);
    covox_idx = 0;
    if (right_side) {            /* CH2 ($D281) + CH3 ($D282) */
        covox_dac_a = 1;
        covox_dac_b = 2;
    } else {                     /* CH1 ($D280) + CH4 ($D283) */
        covox_dac_a = 0;
        covox_dac_b = 3;
    }

    covox_midscale();
    wait_frames(25);

    /* Screen off: remove playfield DMA so IRQ latency is constant. */
    *(unsigned char *)(559) = 0;

    __asm__("sei");

    /* Take over the OS timer-1 vector. */
    covox_saved_vtimr1 = VTIMR1;
    VTIMR1 = (uint16_t)&covox_isr;

    /* POKEY1 timer 1 in 1.79 MHz mode; channel silent (volume nibble 0). */
    POKEY1[AUDCTL] = COVOX_AUDCTL_CH1_PHI2;
    POKEY1[AUDC1]  = 0x00;
    POKEY1[AUDF1]  = audf;

    /* Enable timer-1 IRQ alongside the keyboard IRQ, via the shadow so the
     * OS path stays consistent, then write the live register. */
    POKMSK = (uint8_t)(IRQEN_KEYBOARD | IRQEN_TIMER1);
    POKEY1[IRQEN] = (uint8_t)(IRQEN_KEYBOARD | IRQEN_TIMER1);
    POKEY1[STIMER] = 0x00;       /* reset/reload the timers */

    __asm__("cli");

    wait_frames(hold);

    __asm__("sei");
    /* Disable timer-1 IRQ, keep keyboard. */
    POKMSK = IRQEN_KEYBOARD;
    POKEY1[IRQEN] = IRQEN_KEYBOARD;
    POKEY1[AUDCTL] = 0x00;
    VTIMR1 = covox_saved_vtimr1;
    __asm__("cli");

    *(unsigned char *)(559) = 0x22;
    covox_midscale();
}

/* SAMPLE player: synthesise two one-cycle triangle waves into block RAM,
 * then loop-play them via DMA.  Channels follow the COVOX panning: CH1/CH4
 * left, CH2/CH3 right. */
#define SAMP_LEN  256
#define SAMP_LEFT_ADDR   0x0000
#define SAMP_RIGHT_ADDR  0x0100

static void write_triangle_sample(uint16_t addr, uint8_t step)
{
    uint16_t i;
    SAM_RAMADDRL[0] = (uint8_t)(addr & 0xFF);
    SAM_RAMADDRH[0] = (uint8_t)(addr >> 8);
    for (i = 0; i < SAMP_LEN; i++) {
        uint8_t p = (uint8_t)(i * step);
        int16_t t = (p < 128) ? (p * 2 - 128) : (383 - p * 2);
        SAM_RAMDATAI[0] = (uint8_t)(int8_t)t;  /* signed 8-bit, auto-inc */
    }
}

static void sample_setup_chan(uint8_t chan, uint16_t addr, uint16_t period)
{
    SAM_CHANSEL[0] = chan;
    SAM_ADDRL[0]   = (uint8_t)(addr & 0xFF);
    SAM_ADDRH[0]   = (uint8_t)(addr >> 8);
    SAM_LENL[0]    = (uint8_t)((SAMP_LEN - 1) & 0xFF);
    SAM_LENH[0]    = (uint8_t)((SAMP_LEN - 1) >> 8);
    SAM_PERL[0]    = (uint8_t)(period & 0xFF);
    SAM_PERH[0]    = (uint8_t)(period >> 8);
    SAM_VOL[0]     = 0x30;
}

static void test_sample_lr(uint8_t right_side, uint16_t hold)
{
    SAM_DMA[0] = 0x00;
    SAM_IRQEN[0] = 0x00;
    SAM_IRQACT[0] = 0x00;

    write_triangle_sample(SAMP_LEFT_ADDR, 1);
    write_triangle_sample(SAMP_RIGHT_ADDR, 3);

    /* 8-bit mode for channels 1 and 2; ADPCM off. */
    SAM_CFG[0] = 0x30;

    if (right_side) {
        sample_setup_chan(2, SAMP_RIGHT_ADDR, 0x0058);
        SAM_DMA[0] = 0x02;
    } else {
        sample_setup_chan(1, SAMP_LEFT_ADDR, 0x0068);
        SAM_DMA[0] = 0x01;
    }

    wait_frames(hold);

    SAM_DMA[0] = 0x00;
    SAM_CHANSEL[0] = right_side ? 2 : 1;
    SAM_VOL[0] = 0x00;
    covox_midscale();
}

/* ----------------------------------------------------------------------
 * Presentation: run the whole walk-through once.
 * -------------------------------------------------------------------- */
static void show_caps(uint8_t caps, const uint8_t *ver)
{
    char buf[40];
    uint8_t pk = caps & CAP_POKEY_MASK;
    const char *pkstr = (pk == 0) ? "MONO (1 POKEY)" :
                        (pk == 1) ? "STEREO (2 POKEY)" :
                        (pk == 3) ? "QUAD (4 POKEY)" : "?";
    uint8_t i;

    clear_screen();
    line("POKEYMAX AUDIO TEST");
    line("-------------------");
    line("");

    strcpy(buf, "CORE VER: ");
    for (i = 0; i < 8 && ver[i] >= 0x20 && ver[i] < 0x7F; i++)
        buf[10 + i] = ver[i];
    buf[10 + i] = 0;
    line(buf);
    line("");

    strcpy(buf, "POKEY:  ");
    strcpy(buf + 8, pkstr);
    line(buf);


    line((caps & CAP_SID)    ? "SID:    PRESENT (2 CHIPS)" : "SID:    absent");
    line((caps & CAP_PSG)    ? "PSG:    PRESENT (2 CHIPS)" : "PSG:    absent");
    line((caps & CAP_COVOX)  ? "COVOX:  PRESENT"           : "COVOX:  absent");
    if (caps & CAP_SAMPLE)
    {
	    if (caps & CAP_SAMPLE_64KB) // TODO: add to docs
		    line("SAMPLE: PRESENT (64K RAM)");
	    else
		    line("SAMPLE: PRESENT (42K RAM)");
    }
    else
		    line("SAMPLE: absent");
    line((caps & CAP_FLASH)  ? "FLASH:  PRESENT"           : "FLASH:  absent");
    line("");
}

static void status(const char *s)
{
    /* overwrite the status line (row 20) */
    volatile uint8_t *p = scr + 20 * 40;
    uint8_t i;
    for (i = 0; i < 39; i++) p[i] = 0;
    put_at(20, 1, s);
}

static void run_once(uint8_t caps)
{
    uint8_t pk = caps & CAP_POKEY_MASK;

    /* POKEY - always present.
     * Do the right channel first.  PokeyMAX stereo auto-detection/fallback can
     * mirror POKEY1 to both outputs until it has seen right-side activity,
     * especially after several seconds of silence.  Starting with POKEY2 avoids
     * a misleading first "left" test that sounds centred/mono. */
/*    status("POKEY: LEFT channel (voice 1)");
    test_pokey_one(POKEY1, DIV_MID1, TEST_HOLD_FRAMES);
    status("POKEY: RIGHT channel (voice 2)");
    test_pokey_one(POKEY2, DIV_MID1, TEST_HOLD_FRAMES);
    for (;;)
    {
    status("POKEY: LEFT channel (voice 1)");
    test_pokey_one(POKEY1, DIV_MID1, TEST_HOLD_FRAMES);
    }*/

    if (pk == 3) {
        status("POKEY QUAD: 4-note chord (2+2 L/R)");
        test_pokey_chord(TEST_HOLD_FRAMES);
    } else if (pk == 1) {
        status("POKEY STEREO: chord (1 pitch each side)");
        test_pokey_chord(TEST_HOLD_FRAMES);
    } else {
        status("POKEY MONO: single chord");
        test_pokey_chord(TEST_HOLD_FRAMES);
    }

    status("POKEY: LEFT channel (voice 1)");
    test_pokey_one(POKEY1, DIV_MID1, TEST_HOLD_FRAMES);
    status("POKEY: RIGHT channel (voice 2)");
    test_pokey_one(POKEY2, DIV_MID1, TEST_HOLD_FRAMES);


    if (caps & CAP_SID) {
        status("SID 1 (left)  square");
        test_sid(SID1, 0x2000, TEST_HOLD_FRAMES);
        status("SID 2 (right) square");
        test_sid(SID2, 0x2800, TEST_HOLD_FRAMES);
    }

    if (caps & CAP_PSG) {
        psg_set_max_stereo();
        status("PSG 1 (left)  tone A");
        test_psg(PSG1, 0x011D, TEST_HOLD_FRAMES);
        status("PSG 2 (right) tone A");
        test_psg(PSG2, 0x00EA, TEST_HOLD_FRAMES);
        psg_restore_mode();
    }

    if (caps & CAP_COVOX) {
        status("COVOX left DAC voice");
        test_covox_lr(0, TEST_HOLD_FRAMES);
        status("COVOX right DAC voice");
        test_covox_lr(1, TEST_HOLD_FRAMES);
    }

    if (caps & CAP_SAMPLE) {
        status("SAMPLE left DMA voice");
        test_sample_lr(0, TEST_HOLD_FRAMES);
        status("SAMPLE right DMA voice");
        test_sample_lr(1, TEST_HOLD_FRAMES);
    }

    status("DONE.  Any key = repeat, ESC = quit.");
}

/* ----------------------------------------------------------------------
 * Entry
 * -------------------------------------------------------------------- */
int main(void)
{
    uint8_t  caps;
    uint8_t  ver[8];

    scr = (volatile uint8_t *)SAVMSC;

    if (!pokeymax_present()) {
        clear_screen();
        line("NO POKEYMAX DETECTED.");
        line("");
        line("This program needs a PokeyMAX core.");
        line("Press ESC to quit.");
        for (;;) {
            if (CH == 0x1C) break;   /* ESC */
        }
        return 0;
    }

    caps = read_caps(ver);

    audio_acquire();

    for (;;) {
        show_caps(caps, ver);
        status("Press START to run, ESC to quit.");

        /* wait for START (CONSOL bit0 = 0 when pressed) or ESC */
        for (;;) {
            if ((CONSOL & 0x01) == 0) break;     /* START pressed */
            if (CH == 0x1C) { audio_release(); return 0; } /* ESC */
        }
        CH = 0xFF;                  /* consume key */

        run_once(caps);

        /* wait for any key (repeat) or ESC (quit) */
        for (;;) {
            uint8_t k = CH;
            if (k == 0x1C) { audio_release(); return 0; } /* ESC */
            if (k != 0xFF) { CH = 0xFF; break; }          /* any key: loop */
        }
    }
}
