mirror of
https://github.com/emu-russia/pureikyubu.git
synced 2025-04-02 10:42:15 -04:00
567 lines
15 KiB
C++
567 lines
15 KiB
C++
// AI - audio interface
|
|
#include "pch.h"
|
|
|
|
// all AI timers update is based on TBR.
|
|
|
|
/*/
|
|
AI Interrupts
|
|
-------------
|
|
|
|
The Audio Interface API is responsible for managing two interrupts to the
|
|
host CPU:
|
|
The streamed sample counter interrupt (AIINT).
|
|
The AI DMA interrupt (AIDINT).
|
|
Note that the streamed sample counter interrupt is generated directly by
|
|
the AI hardware. The AI DMA interrupt, however, comes from the memory
|
|
controller of the audio subsystem.
|
|
|
|
Audio streaming sample counter interrupt (AIINT)
|
|
The Audio Interface provides facilities for counting the number of streamed
|
|
(left/right) samples played and asserting an interrupt at some programmable
|
|
trigger value. Note that only audio samples streamed from the optical disc
|
|
are counted. Note also that samples are counted after the sample rate
|
|
conversion stage-thus, the stream will always be at a 48KHz sample rate.
|
|
|
|
AI DMA interrupt (AIDINT)
|
|
The Audio Interface API provides control over the AI DMA. The actual DMA
|
|
controller resides within the audio subsystem of the Graphics Processor ASIC.
|
|
The AI DMA feeds data from main memory to the AI FIFO, which is 32 bytes in
|
|
length (the size of a single DMA block). The AI FIFO consumes data at a rate
|
|
of either 48,000 or 32,000 stereo samples per second. The sample rate of the
|
|
AI FIFO DMA may be controlled through AISetDSPSampleRate().
|
|
/*/
|
|
|
|
using namespace Debug;
|
|
|
|
// AI state (registers and other data)
|
|
AIControl ai;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AIDCR
|
|
|
|
static void write_aidcr(uint32_t addr, uint32_t data)
|
|
{
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AI, "AIDCR: 0x%04X (RESETMOD:%i, DSPINTMSK:%i, DSPINT:%i, ARINTMSK:%i, ARINT:%i, AIINTMSK:%i, AIINT:%i, HALT:%i, DINT:%i, RES:%i\n",
|
|
data,
|
|
data & AIDCR_RESETMOD ? 1 : 0,
|
|
data & AIDCR_DSPINTMSK ? 1 : 0,
|
|
data & AIDCR_DSPINT ? 1 : 0,
|
|
data & AIDCR_ARINTMSK ? 1 : 0,
|
|
data & AIDCR_ARINT ? 1 : 0,
|
|
data & AIDCR_AIINTMSK ? 1 : 0,
|
|
data & AIDCR_AIINT ? 1 : 0,
|
|
data & AIDCR_HALT ? 1 : 0,
|
|
data & AIDCR_DINT ? 1 : 0,
|
|
data & AIDCR_RES ? 1 : 0 );
|
|
}
|
|
|
|
// set mask
|
|
if (data & AIDCR_DSPINTMSK)
|
|
{
|
|
AIDCR |= AIDCR_DSPINTMSK;
|
|
}
|
|
else
|
|
{
|
|
AIDCR &= ~AIDCR_DSPINTMSK;
|
|
}
|
|
if (data & AIDCR_ARINTMSK)
|
|
{
|
|
AIDCR |= AIDCR_ARINTMSK;
|
|
}
|
|
else
|
|
{
|
|
AIDCR &= ~AIDCR_ARINTMSK;
|
|
}
|
|
if (data & AIDCR_AIINTMSK)
|
|
{
|
|
AIDCR |= AIDCR_AIINTMSK;
|
|
}
|
|
else
|
|
{
|
|
AIDCR &= ~AIDCR_AIINTMSK;
|
|
}
|
|
|
|
// clear pending interrupts
|
|
if(data & AIDCR_DSPINT)
|
|
{
|
|
AIDCR &= ~AIDCR_DSPINT;
|
|
}
|
|
if(data & AIDCR_ARINT)
|
|
{
|
|
AIDCR &= ~AIDCR_ARINT;
|
|
}
|
|
if(data & AIDCR_AIINT)
|
|
{
|
|
AIDCR &= ~AIDCR_AIINT;
|
|
}
|
|
|
|
if ((AIDCR & AIDCR_DSPINT) == 0 && (AIDCR & AIDCR_ARINT) == 0 && (AIDCR & AIDCR_AIINT) == 0)
|
|
{
|
|
PIClearInt(PI_INTERRUPT_DSP);
|
|
}
|
|
|
|
// DSP DMA always ready
|
|
AIDCR &= ~AIDCR_DSPDMA;
|
|
|
|
// Reset modifier bit
|
|
if (data & AIDCR_RESETMOD)
|
|
{
|
|
AIDCR |= AIDCR_RESETMOD;
|
|
}
|
|
else
|
|
{
|
|
AIDCR &= ~AIDCR_RESETMOD;
|
|
}
|
|
|
|
// DSP controls
|
|
Flipper::HW->DSP->DSPSetResetBit((data >> 0) & 1);
|
|
Flipper::HW->DSP->DSPSetIntBit ((data >> 1) & 1);
|
|
Flipper::HW->DSP->DSPSetHaltBit ((data >> 2) & 1);
|
|
}
|
|
|
|
static void read_aidcr(uint32_t addr, uint32_t *reg)
|
|
{
|
|
// DSP controls
|
|
AIDCR &= ~7;
|
|
AIDCR |= Flipper::HW->DSP->DSPGetResetBit() << 0;
|
|
AIDCR |= Flipper::HW->DSP->DSPGetIntBit() << 1;
|
|
AIDCR |= Flipper::HW->DSP->DSPGetHaltBit() << 2;
|
|
|
|
*reg = AIDCR;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DMA
|
|
|
|
// dma transfer complete (when AIDCNT == 0)
|
|
void AIDINT()
|
|
{
|
|
AIDCR |= AIDCR_AIINT;
|
|
if(AIDCR & AIDCR_AIINTMSK)
|
|
{
|
|
PIAssertInt(PI_INTERRUPT_DSP);
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AI, "AIDINT");
|
|
}
|
|
}
|
|
}
|
|
|
|
// how much time AI DMA need to playback "n" bytes in Gekko ticks
|
|
static int64_t AIGetTime(size_t dmaBytes, long rate)
|
|
{
|
|
size_t samples = dmaBytes / 4; // left+right, 16-bit
|
|
return samples * (ai.one_second / rate);
|
|
}
|
|
|
|
static void AIStartDMA()
|
|
{
|
|
ai.dcnt = ai.len & ~AID_EN;
|
|
ai.dmaTime = Gekko::Gekko->GetTicks() + AIGetTime(32, ai.dmaRate);
|
|
ai.currentDmaAddr = (ai.madr_hi << 16) | ai.madr_lo;
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AI, "DMA started: %08X, %i bytes\n", ai.currentDmaAddr, ai.dcnt * 32);
|
|
}
|
|
ai.audioThread->Resume();
|
|
}
|
|
|
|
// Simulate AI FIFO
|
|
static void AIFeedMixer()
|
|
{
|
|
int bytes = 32;
|
|
|
|
if (ai.dcnt == 0 || (ai.len & AID_EN) == 0)
|
|
{
|
|
Flipper::HW->Mixer->PushBytes(Flipper::AxChannel::AudioDma, ai.zeroes, bytes);
|
|
}
|
|
else
|
|
{
|
|
Flipper::HW->Mixer->PushBytes(Flipper::AxChannel::AudioDma, &mi.ram[ai.currentDmaAddr & RAMMASK], bytes);
|
|
ai.currentDmaAddr += bytes;
|
|
ai.dcnt--;
|
|
}
|
|
|
|
ai.dmaTime = Gekko::Gekko->GetTicks() + AIGetTime(bytes, ai.dmaRate);
|
|
}
|
|
|
|
static void AIStopDMA()
|
|
{
|
|
ai.dmaTime = -1;
|
|
ai.dcnt = 0;
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AI, "DMA stopped\n");
|
|
}
|
|
}
|
|
|
|
static void AISetDMASampleRate(Flipper::AudioSampleRate rate)
|
|
{
|
|
Flipper::HW->Mixer->SetSampleRate(Flipper::AxChannel::AudioDma, rate);
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AI, "DMA sample rate: %i\n", rate == Flipper::AudioSampleRate::Rate_32000 ? 32000 : 48000);
|
|
}
|
|
}
|
|
|
|
static void AISetDvdAudioSampleRate(Flipper::AudioSampleRate rate)
|
|
{
|
|
Flipper::HW->Mixer->SetSampleRate(Flipper::AxChannel::DvdAudio, rate);
|
|
|
|
if (rate == Flipper::AudioSampleRate::Rate_48000)
|
|
{
|
|
DVD::DDU->SetDvdAudioSampleRate(DVD::DvdAudioSampleRate::Rate_48000);
|
|
}
|
|
else
|
|
{
|
|
DVD::DDU->SetDvdAudioSampleRate(DVD::DvdAudioSampleRate::Rate_32000);
|
|
}
|
|
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AIS, "DVD Audio sample rate: %i\n", rate == Flipper::AudioSampleRate::Rate_32000 ? 32000 : 48000);
|
|
}
|
|
}
|
|
|
|
//
|
|
// dma buffer address
|
|
//
|
|
|
|
static void write_dmah(uint32_t addr, uint32_t data)
|
|
{
|
|
ai.madr_hi = (uint16_t)data & 0x3FF;
|
|
}
|
|
|
|
static void write_dmal(uint32_t addr, uint32_t data)
|
|
{
|
|
ai.madr_lo = (uint16_t)data & ~0x1F;
|
|
}
|
|
|
|
static void read_dmah(uint32_t addr, uint32_t *reg) { *reg = ai.madr_hi & 0x3FF; }
|
|
static void read_dmal(uint32_t addr, uint32_t *reg) { *reg = ai.madr_lo & ~0x1F; }
|
|
|
|
//
|
|
// dma length / control
|
|
//
|
|
|
|
static void write_len(uint32_t addr, uint32_t data)
|
|
{
|
|
ai.len = (uint16_t)data;
|
|
|
|
// start/stop audio dma transfer
|
|
if(ai.len & AID_EN)
|
|
{
|
|
AIStartDMA();
|
|
if (!Flipper::HW->Mixer->IsEnabled(Flipper::AxChannel::AudioDma))
|
|
{
|
|
Flipper::HW->Mixer->Enable(Flipper::AxChannel::AudioDma, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AIStopDMA();
|
|
if (Flipper::HW->Mixer->IsEnabled(Flipper::AxChannel::AudioDma))
|
|
{
|
|
Flipper::HW->Mixer->Enable(Flipper::AxChannel::AudioDma, false);
|
|
}
|
|
}
|
|
}
|
|
static void read_len(uint32_t addr, uint32_t *reg)
|
|
{
|
|
*reg = ai.len;
|
|
}
|
|
|
|
//
|
|
// read sample block (32b) counter
|
|
//
|
|
|
|
static void read_dcnt(uint32_t addr, uint32_t *reg)
|
|
{
|
|
*reg = ai.dcnt & 0x7FFF;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// streaming
|
|
|
|
// streaming trigger and counter coincidence
|
|
void AISINT()
|
|
{
|
|
// only if AIINT is validated
|
|
if((ai.cr & AICR_AIINTVLD) == 0)
|
|
{
|
|
ai.cr |= AICR_AIINT;
|
|
if(ai.cr & AICR_AIINTMSK)
|
|
{
|
|
PIAssertInt(PI_INTERRUPT_AI);
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AIS, "AISINT\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// AI control register
|
|
static void write_cr(uint32_t addr, uint32_t data)
|
|
{
|
|
ai.cr = data & 0x7F;
|
|
|
|
// clear stream interrupt
|
|
if(ai.cr & AICR_AIINT)
|
|
{
|
|
ai.cr &= ~AICR_AIINT;
|
|
PIClearInt(PI_INTERRUPT_AI);
|
|
}
|
|
|
|
// enable sample counter
|
|
if (ai.cr & AICR_PSTAT)
|
|
{
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AIS, "start streaming clock\n");
|
|
}
|
|
DVD::DDU->EnableAudioStreamClock(true);
|
|
Flipper::HW->Mixer->Enable(Flipper::AxChannel::DvdAudio, true);
|
|
ai.streamFifoPtr = 0;
|
|
}
|
|
else
|
|
{
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AIS, "stop streaming clock\n");
|
|
}
|
|
DVD::DDU->EnableAudioStreamClock(false);
|
|
Flipper::HW->Mixer->Enable(Flipper::AxChannel::DvdAudio, false);
|
|
}
|
|
|
|
// reset sample counter
|
|
if(ai.cr & AICR_SCRESET)
|
|
{
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AIS, "reset sample counter\n");
|
|
}
|
|
ai.scnt = 0;
|
|
ai.cr &= ~AICR_SCRESET;
|
|
}
|
|
|
|
// set DMA sample rate
|
|
if (ai.cr & AICR_DFR)
|
|
{
|
|
ai.dmaRate = 32000;
|
|
AISetDMASampleRate(Flipper::AudioSampleRate::Rate_32000);
|
|
}
|
|
else
|
|
{
|
|
ai.dmaRate = 48000;
|
|
AISetDMASampleRate(Flipper::AudioSampleRate::Rate_48000);
|
|
}
|
|
|
|
// set DVD Audio sample rate
|
|
if (ai.cr & AICR_AFR) AISetDvdAudioSampleRate(Flipper::AudioSampleRate::Rate_48000);
|
|
else AISetDvdAudioSampleRate(Flipper::AudioSampleRate::Rate_32000);
|
|
}
|
|
static void read_cr(uint32_t addr, uint32_t *reg)
|
|
{
|
|
*reg = ai.cr;
|
|
}
|
|
|
|
// stream samples counter
|
|
static void read_scnt(uint32_t addr, uint32_t *reg)
|
|
{
|
|
*reg = ai.scnt;
|
|
}
|
|
|
|
// interrupt trigger
|
|
static void write_it(uint32_t addr, uint32_t data)
|
|
{
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AIS, "set trigger to : 0x%08X\n", data);
|
|
}
|
|
ai.it = data;
|
|
}
|
|
static void read_it(uint32_t addr, uint32_t *reg) { *reg = ai.it; }
|
|
|
|
// stream volume register
|
|
static void write_vr(uint32_t addr, uint32_t data)
|
|
{
|
|
ai.vr = (uint16_t)data;
|
|
}
|
|
static void read_vr(uint32_t addr, uint32_t *reg)
|
|
{
|
|
*reg = ai.vr;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DSPCore interface (mailbox and interrupt)
|
|
|
|
static void write_out_mbox_h(uint32_t addr, uint32_t data) { Flipper::HW->DSP->CpuToDspWriteHi((uint16_t)data); }
|
|
static void write_out_mbox_l(uint32_t addr, uint32_t data) { Flipper::HW->DSP->CpuToDspWriteLo((uint16_t)data); }
|
|
static void read_out_mbox_h(uint32_t addr, uint32_t* reg) { *reg = Flipper::HW->DSP->CpuToDspReadHi(false); }
|
|
static void read_out_mbox_l(uint32_t addr, uint32_t* reg) { *reg = Flipper::HW->DSP->CpuToDspReadLo(false); }
|
|
|
|
static void read_in_mbox_h(uint32_t addr, uint32_t* reg) { *reg = Flipper::HW->DSP->DspToCpuReadHi(false); }
|
|
static void read_in_mbox_l(uint32_t addr, uint32_t* reg) { *reg = Flipper::HW->DSP->DspToCpuReadLo(false); }
|
|
|
|
static void write_in_mbox_h(uint32_t addr, uint32_t data) { Halt("Processor is not allowed to write DSP Mailbox!"); }
|
|
static void write_in_mbox_l(uint32_t addr, uint32_t data) { Halt("Processor is not allowed to write DSP Mailbox!"); }
|
|
|
|
void DSPAssertInt()
|
|
{
|
|
if (ai.log)
|
|
{
|
|
Report(Channel::AI, "DSPAssertInt\n");
|
|
}
|
|
|
|
AIDCR |= AIDCR_DSPINT;
|
|
if (AIDCR & AIDCR_DSPINTMSK)
|
|
{
|
|
PIAssertInt(PI_INTERRUPT_DSP);
|
|
}
|
|
}
|
|
|
|
bool DSPGetInterruptStatus()
|
|
{
|
|
return (AIDCR & AIDCR_DSPINT) != 0;
|
|
}
|
|
|
|
bool DSPGetResetModifier()
|
|
{
|
|
return (AIDCR & AIDCR_RESETMOD) != 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// AI DMA and DVD Audio are played uncompetitively from different streams.
|
|
// All work on Sample Rate Conversion and sound mixing for convenience is done in Mixer (AX.cpp).
|
|
|
|
static uint16_t AdjustVolume(uint16_t sampleValue, int volume)
|
|
{
|
|
// Let's try how this conversion will behave on a float, if it slows down, then translate it to ints.
|
|
// In theory, on modern processors, float is fast.
|
|
float value = (float)sampleValue / (float)0xFFFF;
|
|
float volumeF = (float)volume / (float)0xFF;
|
|
float adjusted = value * volumeF;
|
|
return (uint16_t)(adjusted * (float)0xFFFF);
|
|
}
|
|
|
|
// Called from DDU Core when DVD Audio decodes the next sample
|
|
static void AIStreamCallback(uint16_t l, uint16_t r)
|
|
{
|
|
// Check FIFO overflow
|
|
if (ai.streamFifoPtr >= sizeof(ai.streamFifo))
|
|
{
|
|
ai.streamFifoPtr = 0;
|
|
// Feed mixer
|
|
Flipper::HW->Mixer->PushBytes(Flipper::AxChannel::DvdAudio, ai.streamFifo, sizeof(ai.streamFifo));
|
|
}
|
|
|
|
// Adjust volume and swap endianess
|
|
int leftVolume = (uint8_t)ai.vr;
|
|
int rightVolume = (uint8_t)(ai.vr >> 8);
|
|
l = _byteswap_ushort(l);
|
|
r = _byteswap_ushort(r);
|
|
//l = AdjustVolume(l, leftVolume);
|
|
//r = AdjustVolume(r, rightVolume);
|
|
|
|
// Put sample in FIFO
|
|
uint16_t* ptr = (uint16_t *)&ai.streamFifo[ai.streamFifoPtr];
|
|
|
|
ptr[0] = l;
|
|
ptr[1] = r;
|
|
|
|
ai.streamFifoPtr += 4;
|
|
|
|
// update stream sample counter
|
|
if (ai.cr & AICR_PSTAT)
|
|
{
|
|
ai.scnt++;
|
|
if (ai.scnt >= ai.it)
|
|
{
|
|
AISINT();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update audio DMA thread
|
|
static void AIUpdate(void *Parameter)
|
|
{
|
|
while (true)
|
|
{
|
|
if ((uint64_t)Gekko::Gekko->GetTicks() >= ai.dmaTime)
|
|
{
|
|
if (ai.dcnt == 0)
|
|
{
|
|
if (ai.len & AID_EN)
|
|
{
|
|
// Restart Dma and signal AID_INT
|
|
ai.currentDmaAddr = (ai.madr_hi << 16) | ai.madr_lo;
|
|
ai.dcnt = ai.len & ~AID_EN;
|
|
AIDINT();
|
|
}
|
|
else
|
|
{
|
|
ai.audioThread->Suspend();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ai.len & AID_EN)
|
|
{
|
|
AIFeedMixer();
|
|
}
|
|
else
|
|
{
|
|
ai.audioThread->Suspend();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AIOpen(HWConfig* config)
|
|
{
|
|
Report(Channel::AI, "Audio interface (DMA, DVD Streaming and DSP)\n");
|
|
|
|
// clear regs
|
|
memset(&ai, 0, sizeof(AIControl));
|
|
|
|
DVD::DDU->SetStreamCallback(AIStreamCallback);
|
|
|
|
ai.audioThread = new Thread(AIUpdate, true, nullptr, "AI");
|
|
|
|
ai.one_second = Gekko::Gekko->OneSecond();
|
|
ai.dmaRate = ai.cr & AICR_DFR ? 32000 : 48000;
|
|
ai.dmaTime = Gekko::Gekko->GetTicks() + AIGetTime(32, ai.dmaRate);
|
|
ai.log = false;
|
|
AIStopDMA();
|
|
|
|
// set register traps
|
|
MISetTrap(16, AI_DCR, read_aidcr, write_aidcr);
|
|
|
|
MISetTrap(16, DSP_OUTMBOXH, read_out_mbox_h, write_out_mbox_h);
|
|
MISetTrap(16, DSP_OUTMBOXL, read_out_mbox_l, write_out_mbox_l);
|
|
MISetTrap(16, DSP_INMBOXH , read_in_mbox_h , write_in_mbox_h);
|
|
MISetTrap(16, DSP_INMBOXL , read_in_mbox_l , write_in_mbox_l);
|
|
|
|
MISetTrap(16, AID_MADRH , read_dmah, write_dmah);
|
|
MISetTrap(16, AID_MADRL , read_dmal, write_dmal);
|
|
MISetTrap(16, AID_LEN , read_len , write_len);
|
|
MISetTrap(16, AID_CNT , read_dcnt, nullptr);
|
|
|
|
MISetTrap(32, AIS_CR , read_cr , write_cr);
|
|
MISetTrap(32, AIS_VR , read_vr , write_vr);
|
|
MISetTrap(32, AIS_SCNT, read_scnt, nullptr);
|
|
MISetTrap(32, AIS_IT , read_it , write_it);
|
|
}
|
|
|
|
void AIClose()
|
|
{
|
|
AIStopDMA();
|
|
delete ai.audioThread;
|
|
ai.audioThread = nullptr;
|
|
DVD::DDU->SetStreamCallback(nullptr);
|
|
}
|