// Sharp X68000 audio HAL -- YM2151 (OPM) tone/noise voices.
//
// SCOPE. This implements the CHIP side only: jlAudioVoice / jlAudioTone /
// jlAudioNoise, which is what the portable JYM1 chip-tracker (src/core/music.c)
// is built on. That makes jlMusicPlay work with no CPU mixing at all, the same
// way the Amiga and IIgs get it.
//
// The SAMPLE side (jlAudioPlayMod, the 5 jlAudioPlaySfx slots) is NOT here yet.
// It needs an OKI MSM6258 ADPCM encoder fed by DMAC channel 3 plus a 68000 asm
// mixer -- libxmp-lite's C mixer is not viable on any 68000 (measured at 874
// cycles per voice per output sample). Those ops fall through to the no-op
// generics, so a program calling them is silent rather than broken.
//
// The OPM and the ADPCM channel are SEPARATE devices, so when the sample side
// does land, chip music and digital audio will run concurrently with zero
// contention -- unlike the ST, where the PWM engine hammers YM register 8 and
// destroys tone voice 0.

#include <x68k/iocs.h>

#include "port.h"

// The sample side lives in audioPcm.c (libxmp-lite + MSM6258 ADPCM). It is
// driven from this file's Init/Shutdown/FrameTick so there is one owner of the
// audio lifecycle, matching how the other ports are laid out.
bool jlpAudioInitPcm(void);
void jlpAudioShutdownPcm(void);
void jlpAudioPcmFrameTick(void);


// ----- YM2151 (OPM) -----
//
// Two ports: write the register number to $E90001, then the value to $E90003.
// The chip needs a short settle between the address and data writes; the IOCS
// _OPMSET call handles the handshake, so use it rather than banging the ports
// directly and guessing at wait states.
#define OPM_REG_KEYON       0x08u   // bits 2-0 channel, bits 6-3 slot mask
#define OPM_REG_NOISE       0x0Fu   // bit 7 enable, bits 4-0 NFRQ
#define OPM_REG_RL_FB_CON   0x20u   // +channel
#define OPM_REG_KC          0x28u   // +channel: key code (octave<<4 | note)
#define OPM_REG_KF          0x30u   // +channel: key fraction (bits 7-2)
#define OPM_REG_TL          0x60u   // +slot: total level, 0 loudest .. 127 mute
#define OPM_REG_AR          0x80u   // +slot: attack rate
#define OPM_REG_D1R         0xA0u   // +slot: first decay
#define OPM_REG_D2R         0xC0u   // +slot: second decay
#define OPM_REG_RR_D1L      0xE0u   // +slot: release rate + first decay level

#define OPM_SLOTS_ALL       0x78u   // all four slots in the key-on mask
#define OPM_TL_MUTE         127u
#define OPM_NOISE_CHANNEL   7u      // noise rides channel 7 operator 4
#define OPM_NOISE_ENABLE    0x80u

// jlAudioVoice exposes 3 tone voices; they map to OPM channels 0-2, leaving
// channel 7 for noise and channels 3-6 spare.
#define X68K_TONE_CHANNELS  3u

// Connection 7 makes all four operators carriers, so a single operator's TL
// controls loudness directly and no modulator programming is needed. That is
// the simplest patch that produces a clean, predictable tone.
#define OPM_CONN_ALL_CARRIER 0x07u
#define OPM_PAN_BOTH         0xC0u

// Slot numbering is channel + 8*operator. Only operator 3 (slot ch+24) is
// driven; the other three are muted, which with connection 7 gives one voice.
#define OPM_SLOT_OP4(_ch)   ((uint8_t)((_ch) + 24u))

static bool gAudioReady = false;


static void opmWrite(uint8_t reg, uint8_t value) {
    _iocs_opmset((int)reg, (int)value);
}


// Program one channel as a plain always-on tone: instant attack, no decay,
// no release. The tracker gates notes with key-on/key-off, so an envelope
// would fight it.
static void opmInitChannel(uint8_t ch) {
    uint8_t slot;
    uint8_t op;

    opmWrite((uint8_t)(OPM_REG_RL_FB_CON + ch),
             (uint8_t)(OPM_PAN_BOTH | OPM_CONN_ALL_CARRIER));
    for (op = 0u; op < 4u; op++) {
        slot = (uint8_t)(ch + (op * 8u));
        opmWrite((uint8_t)(OPM_REG_AR + slot), 31u);        // fastest attack
        opmWrite((uint8_t)(OPM_REG_D1R + slot), 0u);        // no decay
        opmWrite((uint8_t)(OPM_REG_D2R + slot), 0u);
        opmWrite((uint8_t)(OPM_REG_RR_D1L + slot), 15u);    // fast release
        opmWrite((uint8_t)(OPM_REG_TL + slot), OPM_TL_MUTE);
    }
}


// Frequency -> OPM key code. The chip's pitch is (octave, note, fraction);
// notes are NOT contiguous -- only 12 of the 16 codes are used, skipping
// 3, 7, 11 and 15.
//
// NEEDS TUNING AGAINST HARDWARE: the octave-4 anchor below is derived from the
// standard YM2151 relationship at a 3.579545 MHz clock, and the X68000 runs its
// OPM at 4 MHz, so absolute pitch will be sharp by a factor of about 1.117
// until this is calibrated. The mapping is monotonic and octave-correct, so
// melodies play at the right intervals; they are simply transposed.
static const uint8_t kNoteCode[12] = {
    0u, 1u, 2u, 4u, 5u, 6u, 8u, 9u, 10u, 12u, 13u, 14u
};

// Semitone boundaries for octave 4 (C4..B4) in Hz, scaled by 16 to keep the
// comparison in integers without a divide.
static const uint16_t kOctave4Hz16[12] = {
    4186u, 4435u, 4699u, 4978u, 5274u, 5588u,
    5920u, 6272u, 6645u, 7040u, 7459u, 7902u
};


static uint8_t opmKeyCode(uint16_t freqHz) {
    uint32_t f;
    uint8_t  octave;
    uint8_t  note;

    if (freqHz == 0u) {
        return 0u;
    }
    // Fold into the octave whose C..B range covers freqHz, tracking how many
    // doublings that took. kOctave4Hz16 is 10x the real octave-4 pitches, so
    // work in the same scaled domain.
    f = (uint32_t)freqHz * 10u;
    octave = 4u;
    while (f < kOctave4Hz16[0] && octave > 0u) {
        f *= 2u;
        octave--;
    }
    while (f >= (uint32_t)kOctave4Hz16[11] * 2u && octave < 7u) {
        f /= 2u;
        octave++;
    }
    for (note = 0u; note < 11u; note++) {
        if (f < kOctave4Hz16[note + 1u]) {
            break;
        }
    }
    return (uint8_t)((octave << 4) | kNoteCode[note]);
}


// ----- HAL entry points (alphabetical) -----

void jlpAudioCriticalEnter(void) {
    // No IRQ-driven audio engine yet, so nothing to guard. When the ADPCM
    // path lands its DMAC interrupt handler, this masks it.
}


void jlpAudioCriticalExit(void) {
}


void jlpAudioFrameTick(void) {
    // Chip voices sustain in hardware, so the OPM needs nothing per frame.
    // The sample path refills one ADPCM block here.
    jlpAudioPcmFrameTick();
}


bool jlpAudioInit(void) {
    uint8_t ch;

    for (ch = 0u; ch < X68K_TONE_CHANNELS; ch++) {
        opmWrite(OPM_REG_KEYON, ch);            // all slots off
        opmInitChannel(ch);
    }
    opmInitChannel(OPM_NOISE_CHANNEL);
    opmWrite(OPM_REG_KEYON, OPM_NOISE_CHANNEL);
    opmWrite(OPM_REG_NOISE, 0u);                // noise generator off
    gAudioReady = true;
    // A PCM failure is not fatal: chip music still works, samples go silent.
    (void)jlpAudioInitPcm();
    return true;
}


void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
    uint8_t slot;

    if (!gAudioReady) {
        return;
    }
    slot = OPM_SLOT_OP4(OPM_NOISE_CHANNEL);
    if (atten >= 15u) {
        opmWrite(OPM_REG_NOISE, 0u);
        opmWrite((uint8_t)(OPM_REG_TL + slot), OPM_TL_MUTE);
        opmWrite(OPM_REG_KEYON, OPM_NOISE_CHANNEL);
        return;
    }
    // NFRQ is inverted: 0 is the highest pitch, 31 the lowest. jlAudioNoise
    // takes 0..31 with 0 lowest, matching the other ports, so flip it.
    opmWrite(OPM_REG_NOISE, (uint8_t)(OPM_NOISE_ENABLE | ((31u - (pitch & 31u)))));
    opmWrite((uint8_t)(OPM_REG_TL + slot), (uint8_t)((atten & 15u) * 4u));
    opmWrite(OPM_REG_KEYON, (uint8_t)(OPM_NOISE_CHANNEL | OPM_SLOTS_ALL));
}


void jlpAudioShutdown(void) {
    uint8_t ch;

    if (!gAudioReady) {
        return;
    }
    for (ch = 0u; ch < X68K_TONE_CHANNELS; ch++) {
        opmWrite(OPM_REG_KEYON, ch);
    }
    opmWrite(OPM_REG_NOISE, 0u);
    opmWrite(OPM_REG_KEYON, OPM_NOISE_CHANNEL);
    jlpAudioShutdownPcm();
    gAudioReady = false;
}


void jlpAudioTone(uint16_t freqHz) {
    jlpAudioVoice(0u, freqHz, (uint8_t)(freqHz == 0u ? 15u : 0u));
}


void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
    uint8_t slot;

    if (!gAudioReady || voice >= X68K_TONE_CHANNELS) {
        return;
    }
    slot = OPM_SLOT_OP4(voice);
    if (freqHz == 0u || atten >= 15u) {
        opmWrite((uint8_t)(OPM_REG_TL + slot), OPM_TL_MUTE);
        opmWrite(OPM_REG_KEYON, voice);         // key off, all slots
        return;
    }
    opmWrite((uint8_t)(OPM_REG_KC + voice), opmKeyCode(freqHz));
    opmWrite((uint8_t)(OPM_REG_KF + voice), 0u);
    // atten 0..14 -> TL. TL is 0 = loudest, and roughly 0.75 dB per step, so
    // 4 per attenuation step gives a usable range without reaching mute.
    opmWrite((uint8_t)(OPM_REG_TL + slot), (uint8_t)((atten & 15u) * 4u));
    opmWrite(OPM_REG_KEYON, (uint8_t)(voice | OPM_SLOTS_ALL));
}
