pureikyubu/SRC/Hardware/AI.cpp
2020-07-27 00:51:09 +03:00

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);
}