diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f947d11..2d9a3426 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(core STATIC DSi_Camera.cpp DSi_DSP.cpp DSi_I2C.cpp + DSi_I2S.cpp DSi_NAND.cpp DSi_NDMA.cpp DSi_NWifi.cpp diff --git a/src/DSi.cpp b/src/DSi.cpp index 00ed8da0..7c5f80b1 100644 --- a/src/DSi.cpp +++ b/src/DSi.cpp @@ -35,6 +35,7 @@ #include "DSi_NDMA.h" #include "DSi_I2C.h" +#include "DSi_I2S.h" #include "DSi_SD.h" #include "DSi_AES.h" #include "DSi_NAND.h" @@ -108,6 +109,7 @@ DSi::DSi(DSiArgs&& args, void* userdata) noexcept : SDMMC(*this, std::move(args.NANDImage), std::move(args.DSiSDCard)), SDIO(*this), I2C(*this), + I2S(*this), CamModule(*this), AES(*this) { @@ -141,6 +143,7 @@ void DSi::Reset() for (int i = 0; i < 8; i++) NDMAs[i].Reset(); I2C.Reset(); + I2S.Reset(); CamModule.Reset(); DSP.Reset(); @@ -210,6 +213,13 @@ void DSi::CamInputFrame(int cam, const u32* data, int width, int height, bool rg } } +void DSi::MicInputFrame(s16* data, int samples) +{ + SPI.GetTSC()->MicInputFrame(data, samples); + I2S.MicInputFrame(data, samples); + // TODO: Need to send the mic samples to the DSP! +} + void DSi::DoSavestateExtra(Savestate* file) { file->Section("DSIG"); @@ -285,6 +295,7 @@ void DSi::DoSavestateExtra(Savestate* file) CamModule.DoSavestate(file); DSP.DoSavestate(file); I2C.DoSavestate(file); + I2S.DoSavestate(file); SDMMC.DoSavestate(file); SDIO.DoSavestate(file); } @@ -644,6 +655,8 @@ void DSi::SetupDirectBoot() SPI.GetFirmwareMem()->SetupDirectBoot(); + I2S.WriteSndExCnt(0x8008, 0xFFFF); + ARM9.CP15Write(0x100, 0x00056078); ARM9.CP15Write(0x200, 0x0000004A); ARM9.CP15Write(0x201, 0x0000004A); @@ -690,6 +703,9 @@ void DSi::SoftReset() NDS::MapSharedWRAM(3); + // TODO: is this actually reset? + I2S.Reset(); + // TODO: does the DSP get reset? NWRAM doesn't, so I'm assuming no // *HOWEVER*, the bootrom (which does get rerun) does remap NWRAM, and thus // the DSP most likely gets reset @@ -2707,8 +2723,16 @@ u8 DSi::ARM7IORead8(u32 addr) case 0x04004D07: if (SCFG_BIOS & (1<<10)) return 0; return SDMMC.GetNAND()->GetConsoleID() >> 56; case 0x04004D08: return 0; - case 0x4004700: return DSP.ReadSNDExCnt() & 0xFF; - case 0x4004701: return DSP.ReadSNDExCnt() >> 8; + case 0x4004600: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicCnt() & 0xFF; + case 0x4004601: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicCnt() >> 8; + case 0x4004602: return 0; + case 0x4004603: return 0; + case 0x4004604: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData() & 0xFF; + case 0x4004605: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return (I2S.ReadMicData() >> 8) & 0xFF; + case 0x4004606: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return (I2S.ReadMicData() >> 16) & 0xFF; + case 0x4004607: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData() >> 24; + case 0x4004700: if (!(SCFG_EXT[1] & (1 << 21))) return 0; return I2S.ReadSndExCnt() & 0xFF; + case 0x4004701: if (!(SCFG_EXT[1] & (1 << 21))) return 0; return I2S.ReadSndExCnt() >> 8; case 0x04004C00: return GPIO_Data; case 0x04004C01: return GPIO_Dir; @@ -2751,7 +2775,11 @@ u16 DSi::ARM7IORead16(u32 addr) case 0x04004D06: if (SCFG_BIOS & (1<<10)) return 0; return SDMMC.GetNAND()->GetConsoleID() >> 48; case 0x04004D08: return 0; - case 0x4004700: return DSP.ReadSNDExCnt(); + case 0x4004600: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicCnt(); + case 0x4004602: return 0; + case 0x4004604: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData() >> 16; + case 0x4004606: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData() & 0xFFFF; + case 0x4004700: if (!(SCFG_EXT[1] & (1 << 21))) return 0; return I2S.ReadSndExCnt(); case 0x04004C00: return GPIO_Data | ((u16)GPIO_Dir << 8); case 0x04004C02: return GPIO_IEdgeSel | ((u16)GPIO_IE << 8); @@ -2829,9 +2857,9 @@ u32 DSi::ARM7IORead32(u32 addr) case 0x04004D04: if (SCFG_BIOS & (1<<10)) return 0; return SDMMC.GetNAND()->GetConsoleID() >> 32; case 0x04004D08: return 0; - case 0x4004700: - Log(LogLevel::Debug, "32-Bit SNDExCnt read? %08X\n", ARM7.R[15]); - return DSP.ReadSNDExCnt(); + case 0x4004600: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicCnt(); + case 0x4004604: if (!(SCFG_EXT[1] & (1 << 20))) return 0; return I2S.ReadMicData(); + case 0x4004700: if (!(SCFG_EXT[1] & (1 << 21))) return 0; return I2S.ReadSndExCnt(); } if (addr >= 0x04004800 && addr < 0x04004A00) @@ -2884,11 +2912,25 @@ void DSi::ARM7IOWrite8(u32 addr, u8 val) case 0x04004500: I2C.WriteData(val); return; case 0x04004501: I2C.WriteCnt(val); return; + case 0x4004600: + if (!(SCFG_EXT[1] & (1 << 20))) + return; + I2S.WriteMicCnt((u16)val, 0xFF); + return; + case 0x4004601: + if (!(SCFG_EXT[1] & (1 << 20))) + return; + I2S.WriteMicCnt(((u16)val << 8), 0xFF00); + return; case 0x4004700: - DSP.WriteSNDExCnt((u16)val, 0xFF); + if (!(SCFG_EXT[1] & (1 << 21))) + return; + I2S.WriteSndExCnt((u16)val, 0xFF); return; case 0x4004701: - DSP.WriteSNDExCnt(((u16)val << 8), 0xFF00); + if (!(SCFG_EXT[1] & (1 << 21))) + return; + I2S.WriteSndExCnt(((u16)val << 8), 0xFF00); return; case 0x04004C00: @@ -2987,8 +3029,15 @@ void DSi::ARM7IOWrite16(u32 addr, u16 val) AES.WriteBlkCnt(val<<16); return; + case 0x4004600: + if (!(SCFG_EXT[1] & (1 << 20))) + return; + I2S.WriteMicCnt(val, 0xFFFF); + return; case 0x4004700: - DSP.WriteSNDExCnt(val, 0xFFFF); + if (!(SCFG_EXT[1] & (1 << 21))) + return; + I2S.WriteSndExCnt(val, 0xFFFF); return; case 0x04004C00: @@ -3136,9 +3185,15 @@ void DSi::ARM7IOWrite32(u32 addr, u32 val) case 0x04004404: AES.WriteBlkCnt(val); return; case 0x04004408: AES.WriteInputFIFO(val); return; + case 0x4004600: + if (!(SCFG_EXT[1] & (1 << 20))) + return; + I2S.WriteMicCnt(val, 0xFFFF); + return; case 0x4004700: - Log(LogLevel::Debug, "32-Bit SNDExCnt write? %08X %08X\n", val, ARM7.R[15]); - DSP.WriteSNDExCnt(val, 0xFFFF); + if (!(SCFG_EXT[1] & (1 << 21))) + return; + I2S.WriteSndExCnt(val, 0xFFFF); return; } diff --git a/src/DSi.h b/src/DSi.h index 23a2460c..dfee42cf 100644 --- a/src/DSi.h +++ b/src/DSi.h @@ -21,6 +21,7 @@ #include "NDS.h" #include "DSi_NDMA.h" +#include "DSi_I2S.h" #include "DSi_SD.h" #include "DSi_DSP.h" #include "DSi_AES.h" @@ -30,6 +31,7 @@ namespace melonDS { class DSi_I2CHost; +class DSi_I2S; class DSi_CamModule; class DSi_AES; class DSi_DSP; @@ -69,6 +71,7 @@ public: u32 NWRAMMask[2][3]; DSi_I2CHost I2C; + DSi_I2S I2S; DSi_CamModule CamModule; DSi_AES AES; DSi_DSP DSP; @@ -155,6 +158,7 @@ public: void SetSDCard(std::optional&& sdcard) noexcept { SDMMC.SetSDCard(std::move(sdcard)); } void CamInputFrame(int cam, const u32* data, int width, int height, bool rgb) override; + void MicInputFrame(s16* data, int samples) override; bool DMAsInMode(u32 cpu, u32 mode) const override; bool DMAsRunning(u32 cpu) const override; void StopDMAs(u32 cpu, u32 mode) override; diff --git a/src/DSi_DSP.cpp b/src/DSi_DSP.cpp index 25abd474..90834053 100644 --- a/src/DSi_DSP.cpp +++ b/src/DSi_DSP.cpp @@ -178,8 +178,6 @@ void DSi_DSP::Reset() TeakraCore->Reset(); DSi.CancelEvent(Event_DSi_DSP); - - SNDExCnt = 0; } bool DSi_DSP::IsRstReleased() const @@ -536,23 +534,6 @@ void DSi_DSP::Write32(u32 addr, u32 val) Write16(addr, val & 0xFFFF); } -void DSi_DSP::WriteSNDExCnt(u16 val, u16 mask) -{ - val = (val & mask) | (SNDExCnt & ~mask); - - // it can be written even in NDS mode - - // mic frequency can only be changed if it was disabled - // before the write - if (SNDExCnt & 0x8000) - { - val &= ~0x2000; - val |= SNDExCnt & 0x2000; - } - - SNDExCnt = val & 0xE00F; -} - void DSi_DSP::Run(u32 cycles) { if (!IsDSPCoreEnabled()) diff --git a/src/DSi_DSP.h b/src/DSi_DSP.h index f76b4202..f5ba3c47 100644 --- a/src/DSi_DSP.h +++ b/src/DSi_DSP.h @@ -54,9 +54,6 @@ public: u32 Read32(u32 addr); void Write32(u32 addr, u32 val); - u16 ReadSNDExCnt() const { return SNDExCnt; } - void WriteSNDExCnt(u16 val, u16 mask); - // NOTE: checks SCFG_CLK9 void Run(u32 cycles); @@ -70,8 +67,6 @@ public: private: melonDS::DSi& DSi; - // not sure whether to not rather put it somewhere else - u16 SNDExCnt; Teakra::Teakra* TeakraCore; diff --git a/src/DSi_I2S.cpp b/src/DSi_I2S.cpp new file mode 100644 index 00000000..cd523aa7 --- /dev/null +++ b/src/DSi_I2S.cpp @@ -0,0 +1,219 @@ +/* + Copyright 2016-2024 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#include +#include "DSi.h" +#include "DSi_I2S.h" +#include "Platform.h" + +namespace melonDS +{ +using Platform::Log; +using Platform::LogLevel; + + +DSi_I2S::DSi_I2S(melonDS::DSi& dsi) : DSi(dsi) +{ + DSi.RegisterEventFunc(Event_DSi_I2S, 0, MemberEventFunc(DSi_I2S, Clock)); + + MicCnt = 0; + SndExCnt = 0; + MicClockDivider = 0; + + MicBufferLen = 0; +} + +DSi_I2S::~DSi_I2S() +{ + DSi.UnregisterEventFunc(Event_DSi_I2S, 0); +} + +void DSi_I2S::Reset() +{ + MicCnt = 0; + SndExCnt = 0; + MicClockDivider = 0; + + MicFifo.Clear(); + + MicBufferLen = 0; + + DSi.ScheduleEvent(Event_DSi_I2S, false, 1024, 0, I2S_Freq_32728Hz); +} + +void DSi_I2S::DoSavestate(Savestate* file) +{ + file->Section("I2Si"); + + file->Var16(&MicCnt); + file->Var16(&SndExCnt); + file->Var8(&MicClockDivider); + + MicFifo.DoSavestate(file); +} + +void DSi_I2S::MicInputFrame(const s16* data, int samples) +{ + if (!data) + { + MicBufferLen = 0; + return; + } + + if (samples > 1024) samples = 1024; + memcpy(MicBuffer, data, samples * sizeof(s16)); + MicBufferLen = samples; +} + +u16 DSi_I2S::ReadMicCnt() +{ + u16 ret = MicCnt; + if (MicFifo.Level() == 0) ret |= 1 << 8; + if (MicFifo.Level() >= 16) ret |= 1 << 9; + if (MicFifo.Level() >= 32) ret |= 1 << 10; + return ret; +} + +void DSi_I2S::WriteMicCnt(u16 val, u16 mask) +{ + val = (val & mask) | (MicCnt & ~mask); + + // FIFO clear can only happen if the mic was disabled before the write + if (!(MicCnt & (1 << 15)) && (val & (1 << 12))) + { + MicCnt &= ~(1 << 11); + MicFifo.Clear(); + } + + MicCnt = (val & 0xE00F) | (MicCnt & (1 << 11)); +} + +u32 DSi_I2S::ReadMicData() +{ + // CHECKME: This is a complete guess on how mic data reads work + // gbatek states the FIFO is 16 words large, with 1 word having 2 samples + u32 ret = MicFifo.IsEmpty() ? 0 : (u16)MicFifo.Read(); + ret |= (MicFifo.IsEmpty() ? 0 : (u16)MicFifo.Read()) << 16; + return ret; +} + +u16 DSi_I2S::ReadSndExCnt() +{ + return SndExCnt; +} + +void DSi_I2S::WriteSndExCnt(u16 val, u16 mask) +{ + val = (val & mask) | (SndExCnt & ~mask); + + // Note: SNDEXCNT can be accessed in "NDS mode" + // This is due to the corresponding SCFG_EXT flag not being disabled + // If it is disabled (with homebrew), SNDEXCNT cannot be accessed + // This is more purely a software mistake on the DSi menu's part + + // I2S frequency can only be changed if it was disabled before the write + if (SndExCnt & (1 << 15)) + { + val &= ~(1 << 13); + val |= SndExCnt & (1 << 13); + } + + if ((SndExCnt ^ val) & (1 << 13)) + { + Log(LogLevel::Debug, "Changed I2S frequency to %dHz\n", (SndExCnt & (1 << 13)) ? 47605 : 32728); + } + + SndExCnt = val & 0xE00F; +} + +void DSi_I2S::Clock(u32 freq) +{ + if (SndExCnt & (1 << 15)) + { + // CHECKME (from gbatek) + // "The Sampling Rate becomes zero (no data arriving) when SNDEXCNT.Bit15=0, or when MIC_CNT.bit0-1=3, or when MIC_CNT.bit15=0, or when Overrun has occurred." + // This likely means on any of these conditions the mic completely ignores any I2S clocks + // Although perhaps it might still acknowledge the clocks in some capacity (maybe affecting below clock division) + if ((MicCnt & (1 << 15)) && !(MicCnt & (1 << 11)) && (MicCnt & 3) != 3) + { + // CHECKME (from gbatek) + // "2-3 Sampling Rate (0..3=F/1, F/2, F/3, F/4)" + // This likely works with an internal counter compared with the sampling rate + // This counter is then likely reset on mic sample + // But this is completely untested + + MicClockDivider++; + u8 micRate = (MicCnt >> 2) & 3; + if (MicClockDivider > micRate) + { + MicClockDivider = 0; + + s16 sample = 0; + if (MicBufferLen > 0) + { + // 560190 cycles per frame + u32 cyclepos = (u32)DSi.GetSysClockCycles(2); + u32 samplepos = (cyclepos * MicBufferLen) / 560190; + if (samplepos >= MicBufferLen) samplepos = MicBufferLen - 1; + sample = MicBuffer[samplepos]; + } + + u32 oldLevel = MicFifo.Level(); + if ((MicCnt & 3) == 0) + { + // stereo (this just duplicates the sample, as the mic itself is mono) + if (MicFifo.IsFull()) MicCnt |= 1 << 11; + MicFifo.Write(sample); + if (MicFifo.IsFull()) MicCnt |= 1 << 11; + MicFifo.Write(sample); + } + else + { + // mono + if (MicFifo.IsFull()) MicCnt |= 1 << 11; + MicFifo.Write(sample); + } + + // if bit 13 is set, an IRQ is generated when the mic FIFO is half full + if (MicCnt & (1 << 13)) + { + if (oldLevel < 16 && MicFifo.Level() >= 16) DSi.SetIRQ2(IRQ2_DSi_MicExt); + } + // if bit 13 is not set and bit 14 is set, an IRQ is generated when the mic FIFO is full + else if (MicCnt & (1 << 14)) + { + if (oldLevel < 32 && MicFifo.Level() >= 32) DSi.SetIRQ2(IRQ2_DSi_MicExt); + } + } + } + + // TODO: SPU and DSP sampling should happen here + // use passed freq to know how much to advance SPU by? + } + + if (SndExCnt & (1 << 13)) + { + DSi.ScheduleEvent(Event_DSi_I2S, false, 704, 0, I2S_Freq_47605Hz); + } + else + { + DSi.ScheduleEvent(Event_DSi_I2S, false, 1024, 0, I2S_Freq_32728Hz); + } +} + +} \ No newline at end of file diff --git a/src/DSi_I2S.h b/src/DSi_I2S.h new file mode 100644 index 00000000..76c33295 --- /dev/null +++ b/src/DSi_I2S.h @@ -0,0 +1,68 @@ +/* + Copyright 2016-2024 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#ifndef DSI_I2S_H +#define DSI_I2S_H + +#include "FIFO.h" +#include "types.h" +#include "Savestate.h" + +namespace melonDS +{ +class DSi; +class DSi_I2S +{ +public: + DSi_I2S(melonDS::DSi& dsi); + ~DSi_I2S(); + void Reset(); + void DoSavestate(Savestate* file); + + void MicInputFrame(const s16* data, int samples); + + u16 ReadMicCnt(); + void WriteMicCnt(u16 val, u16 mask); + + u32 ReadMicData(); + + u16 ReadSndExCnt(); + void WriteSndExCnt(u16 val, u16 mask); + +private: + melonDS::DSi& DSi; + + u16 MicCnt; + u16 SndExCnt; + u8 MicClockDivider; + FIFO MicFifo; + + s16 MicBuffer[1024]; + int MicBufferLen; + + enum + { + I2S_Freq_32728Hz, + I2S_Freq_47605Hz + }; + + void Clock(u32 freq); +}; + +} +#endif // DSI_I2S_H diff --git a/src/NDS.h b/src/NDS.h index b2bfb385..eeea8819 100644 --- a/src/NDS.h +++ b/src/NDS.h @@ -72,6 +72,7 @@ enum Event_DSi_CamIRQ, Event_DSi_CamTransfer, Event_DSi_DSP, + Event_DSi_I2S, Event_MAX }; @@ -400,7 +401,7 @@ public: // TODO: Encapsulate the rest of these members void SetLidClosed(bool closed); virtual void CamInputFrame(int cam, const u32* data, int width, int height, bool rgb) {} - void MicInputFrame(s16* data, int samples); + virtual void MicInputFrame(s16* data, int samples); void RegisterEventFunc(u32 id, u32 funcid, EventFunc func); void UnregisterEventFunc(u32 id, u32 funcid);