mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
Persist memcards and add SPU audio output
This commit is contained in:
@@ -20,6 +20,7 @@ int memcard_write_sector(int card, int sector, const uint8_t* buf);
|
||||
|
||||
/* Flush pending writes to disk */
|
||||
void memcard_flush(int card);
|
||||
void memcard_flush_all(void);
|
||||
|
||||
/* Check if card is present */
|
||||
int memcard_is_present(int card);
|
||||
|
||||
@@ -8,6 +8,21 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
void spu_init(void);
|
||||
void spu_render(int16_t* out_stereo, int frames);
|
||||
|
||||
typedef struct SpuDebugInfo {
|
||||
uint32_t ctrl;
|
||||
uint32_t active_mask;
|
||||
int16_t main_l;
|
||||
int16_t main_r;
|
||||
uint32_t key_on_count;
|
||||
uint64_t render_frames;
|
||||
uint64_t nonzero_frames;
|
||||
int32_t last_peak;
|
||||
int32_t peak;
|
||||
} SpuDebugInfo;
|
||||
|
||||
void spu_debug_info(SpuDebugInfo* out);
|
||||
|
||||
/* MMIO read/write (0x1F801C00-0x1F801FFF) */
|
||||
uint32_t spu_read(uint32_t addr);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "gpu.h"
|
||||
#include "sio.h"
|
||||
#include "memcard.h"
|
||||
#include "spu.h"
|
||||
#include "interrupts.h"
|
||||
#include "psx_cycles.h"
|
||||
#include "card_read_summary.h"
|
||||
@@ -2032,30 +2033,84 @@ static void handle_sio_state(int id, const char *json)
|
||||
}
|
||||
|
||||
/* ---- Memory card disk-load status (per-slot) ---- */
|
||||
static void json_escape_string(char *dst, size_t dst_size, const char *src)
|
||||
{
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
size_t out = 0;
|
||||
if (!dst || dst_size == 0) return;
|
||||
if (!src) src = "";
|
||||
|
||||
for (size_t in = 0; src[in] && out + 1 < dst_size; in++) {
|
||||
unsigned char c = (unsigned char)src[in];
|
||||
if (c == '"' || c == '\\') {
|
||||
if (out + 2 >= dst_size) break;
|
||||
dst[out++] = '\\';
|
||||
dst[out++] = (char)c;
|
||||
} else if (c < 0x20) {
|
||||
if (out + 6 >= dst_size) break;
|
||||
dst[out++] = '\\';
|
||||
dst[out++] = 'u';
|
||||
dst[out++] = '0';
|
||||
dst[out++] = '0';
|
||||
dst[out++] = hex[c >> 4];
|
||||
dst[out++] = hex[c & 0x0F];
|
||||
} else {
|
||||
dst[out++] = (char)c;
|
||||
}
|
||||
}
|
||||
dst[out] = '\0';
|
||||
}
|
||||
|
||||
static void handle_mc_status(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
const char *p0 = "", *p1 = "";
|
||||
char p0_json[1024], p1_json[1024];
|
||||
uint8_t m0[2] = {0,0}, m1[2] = {0,0};
|
||||
int pres0 = 0, pres1 = 0, dirty0 = 0, dirty1 = 0;
|
||||
memcard_debug_info(0, &p0, m0, &pres0, &dirty0);
|
||||
memcard_debug_info(1, &p1, m1, &pres1, &dirty1);
|
||||
json_escape_string(p0_json, sizeof(p0_json), p0);
|
||||
json_escape_string(p1_json, sizeof(p1_json), p1);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||||
"\"slot0\":{\"present\":%s,\"dirty\":%s,\"path\":\"%s\","
|
||||
"\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\"},"
|
||||
"\"slot1\":{\"present\":%s,\"dirty\":%s,\"path\":\"%s\","
|
||||
"\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\"}}",
|
||||
id,
|
||||
pres0 ? "true" : "false", dirty0 ? "true" : "false", p0,
|
||||
pres0 ? "true" : "false", dirty0 ? "true" : "false", p0_json,
|
||||
(m0[0] >= 0x20 && m0[0] < 0x7F) ? m0[0] : '?',
|
||||
(m0[1] >= 0x20 && m0[1] < 0x7F) ? m0[1] : '?',
|
||||
m0[0], m0[1],
|
||||
pres1 ? "true" : "false", dirty1 ? "true" : "false", p1,
|
||||
pres1 ? "true" : "false", dirty1 ? "true" : "false", p1_json,
|
||||
(m1[0] >= 0x20 && m1[0] < 0x7F) ? m1[0] : '?',
|
||||
(m1[1] >= 0x20 && m1[1] < 0x7F) ? m1[1] : '?',
|
||||
m1[0], m1[1]);
|
||||
}
|
||||
|
||||
static void handle_spu_status(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
SpuDebugInfo info;
|
||||
spu_debug_info(&info);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||||
"\"ctrl\":\"0x%04X\",\"active_mask\":\"0x%06X\","
|
||||
"\"main_l\":%d,\"main_r\":%d,"
|
||||
"\"key_on_count\":%u,"
|
||||
"\"render_frames\":%llu,\"nonzero_frames\":%llu,"
|
||||
"\"last_peak\":%d,\"peak\":%d}",
|
||||
id,
|
||||
info.ctrl & 0xFFFFu,
|
||||
info.active_mask & 0xFFFFFFu,
|
||||
info.main_l,
|
||||
info.main_r,
|
||||
info.key_on_count,
|
||||
(unsigned long long)info.render_frames,
|
||||
(unsigned long long)info.nonzero_frames,
|
||||
info.last_peak,
|
||||
info.peak);
|
||||
}
|
||||
|
||||
/* ---- SIO IRQ-arm audit -----------------------------------------------
|
||||
* Reports counts of TX writes that reached the IRQ-arm decision in
|
||||
* sio_write SIO_TX_DATA, partitioned by active_device. Tells us at the
|
||||
@@ -3844,6 +3899,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "irq_state", handle_irq_state },
|
||||
{ "sio_state", handle_sio_state },
|
||||
{ "mc_status", handle_mc_status },
|
||||
{ "spu_status", handle_spu_status },
|
||||
{ "card_buffer_dump", handle_card_buffer_dump },
|
||||
{ "sio_arm_audit", handle_sio_arm_audit },
|
||||
{ "sio_burst_stats", handle_sio_burst_stats },
|
||||
|
||||
+128
-6
@@ -18,6 +18,9 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern "C" uint64_t gte_get_exec_count(void);
|
||||
|
||||
@@ -45,6 +48,101 @@ static SDL_Window* sdl_window;
|
||||
static SDL_Renderer* sdl_renderer;
|
||||
static SDL_Texture* sdl_texture;
|
||||
static uint32_t sdl_pixel_buf[640 * 512]; /* ARGB8888 staging buffer */
|
||||
static SDL_AudioDeviceID sdl_audio_device;
|
||||
static int16_t sdl_audio_buf[2048 * 2];
|
||||
|
||||
static std::filesystem::path find_upward(std::filesystem::path start,
|
||||
const std::filesystem::path& marker) {
|
||||
std::error_code ec;
|
||||
start = std::filesystem::absolute(start, ec);
|
||||
if (ec) start = std::filesystem::current_path();
|
||||
if (!std::filesystem::is_directory(start, ec)) start = start.parent_path();
|
||||
|
||||
for (;;) {
|
||||
if (std::filesystem::exists(start / marker, ec)) return start;
|
||||
if (!start.has_parent_path() || start.parent_path() == start) break;
|
||||
start = start.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static std::filesystem::path resolve_bios_path(const char* requested, const char* argv0) {
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
fs::path p(requested);
|
||||
if (fs::exists(p, ec)) {
|
||||
fs::path abs = fs::absolute(p, ec);
|
||||
return ec ? p : abs;
|
||||
}
|
||||
if (p.is_absolute()) return p;
|
||||
|
||||
std::vector<fs::path> roots;
|
||||
roots.push_back(fs::current_path());
|
||||
if (argv0 && argv0[0]) roots.push_back(fs::absolute(argv0, ec).parent_path());
|
||||
|
||||
for (const fs::path& root : roots) {
|
||||
fs::path found = find_upward(root, p);
|
||||
if (!found.empty()) return found / p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static std::filesystem::path resolve_memcard_dir(const char* requested,
|
||||
const std::filesystem::path& bios_path,
|
||||
const char* argv0) {
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
if (requested && requested[0]) return fs::path(requested);
|
||||
|
||||
fs::path absolute_bios = fs::absolute(bios_path, ec);
|
||||
if (ec) absolute_bios = bios_path;
|
||||
fs::path project_root = absolute_bios.parent_path().parent_path();
|
||||
if (fs::exists(project_root / "card1.mcd", ec) ||
|
||||
fs::exists(project_root / "card2.mcd", ec))
|
||||
return project_root;
|
||||
|
||||
fs::path cwd = fs::current_path();
|
||||
if (fs::exists(cwd / "card1.mcd", ec) || fs::exists(cwd / "card2.mcd", ec))
|
||||
return cwd;
|
||||
|
||||
if (argv0 && argv0[0]) {
|
||||
fs::path exe_dir = fs::absolute(argv0, ec).parent_path();
|
||||
if (fs::exists(exe_dir / "card1.mcd", ec) ||
|
||||
fs::exists(exe_dir / "card2.mcd", ec))
|
||||
return exe_dir;
|
||||
}
|
||||
|
||||
return cwd;
|
||||
}
|
||||
|
||||
static void shutdown_runtime(void) {
|
||||
memcard_flush_all();
|
||||
if (sdl_audio_device) {
|
||||
SDL_ClearQueuedAudio(sdl_audio_device);
|
||||
SDL_CloseAudioDevice(sdl_audio_device);
|
||||
sdl_audio_device = 0;
|
||||
}
|
||||
debug_server_shutdown();
|
||||
}
|
||||
|
||||
static void sdl_audio_pump(void) {
|
||||
if (!sdl_audio_device) return;
|
||||
|
||||
const uint32_t bytes_per_frame = sizeof(int16_t) * 2u;
|
||||
const uint32_t max_queue_bytes = 44100u * bytes_per_frame / 5u;
|
||||
if (SDL_GetQueuedAudioSize(sdl_audio_device) > max_queue_bytes) return;
|
||||
|
||||
static double sample_accum = 0.0;
|
||||
sample_accum += 44100.0 / 60.0;
|
||||
int frames = (int)sample_accum;
|
||||
sample_accum -= (double)frames;
|
||||
if (frames <= 0) return;
|
||||
if (frames > 2048) frames = 2048;
|
||||
|
||||
spu_render(sdl_audio_buf, frames);
|
||||
SDL_QueueAudio(sdl_audio_device, sdl_audio_buf,
|
||||
(uint32_t)frames * bytes_per_frame);
|
||||
}
|
||||
|
||||
/* Convert PS1 16-bit color (1-5-5-5) to ARGB8888 */
|
||||
static inline uint32_t psx16_to_argb(uint16_t c) {
|
||||
@@ -111,7 +209,7 @@ static void sdl_vblank_present(void) {
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (ev.type == SDL_QUIT) {
|
||||
debug_server_shutdown();
|
||||
shutdown_runtime();
|
||||
std::exit(0);
|
||||
}
|
||||
}
|
||||
@@ -126,6 +224,7 @@ static void sdl_vblank_present(void) {
|
||||
pad_buttons_this_frame = sio_get_pad_buttons();
|
||||
}
|
||||
sio_set_pad_state(pad_buttons_this_frame);
|
||||
sdl_audio_pump();
|
||||
|
||||
/* Turbo mode: while TAB is held, skip both VRAM->ARGB conversion and
|
||||
* SDL_RenderPresent. The recompiled BIOS still advances simulated
|
||||
@@ -182,15 +281,24 @@ int main(int argc, char** argv) {
|
||||
psx_crash_trace_install_handlers();
|
||||
|
||||
const char* bios_path = "bios/SCPH1001.BIN";
|
||||
const char* memcard_dir_arg = nullptr;
|
||||
/* Parse positional. First non-flag arg = bios_path. */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (argv[i][0] != '-') {
|
||||
if (std::strcmp(argv[i], "--memcard-dir") == 0 && i + 1 < argc) {
|
||||
memcard_dir_arg = argv[++i];
|
||||
} else if (argv[i][0] != '-') {
|
||||
bios_path = argv[i];
|
||||
}
|
||||
}
|
||||
|
||||
std::fprintf(stdout, "psxrecomp-v4 runtime: loading BIOS from %s\n", bios_path);
|
||||
memory_init(bios_path);
|
||||
std::filesystem::path resolved_bios = resolve_bios_path(bios_path, argv[0]);
|
||||
std::filesystem::path memcard_dir =
|
||||
resolve_memcard_dir(memcard_dir_arg, resolved_bios, argv[0]);
|
||||
std::string bios_path_str = resolved_bios.string();
|
||||
std::string memcard_dir_str = memcard_dir.string();
|
||||
|
||||
std::fprintf(stdout, "psxrecomp-v4 runtime: loading BIOS from %s\n", bios_path_str.c_str());
|
||||
memory_init(bios_path_str.c_str());
|
||||
gpu_init();
|
||||
dma_init();
|
||||
timers_init();
|
||||
@@ -199,7 +307,8 @@ int main(int argc, char** argv) {
|
||||
sio_connect_pad(0); /* Controller on port 1 */
|
||||
spu_init();
|
||||
cdrom_init(NULL); /* No disc for BIOS-only boot */
|
||||
memcard_init("."); /* Look for card1.mcd / card2.mcd in working directory */
|
||||
memcard_init(memcard_dir_str.c_str());
|
||||
std::atexit(memcard_flush_all);
|
||||
#ifdef DEFAULT_DEBUG_PORT
|
||||
debug_server_init(DEFAULT_DEBUG_PORT);
|
||||
#else
|
||||
@@ -211,6 +320,19 @@ int main(int argc, char** argv) {
|
||||
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) == 0) {
|
||||
SDL_AudioSpec want;
|
||||
SDL_AudioSpec have;
|
||||
SDL_zero(want);
|
||||
want.freq = 44100;
|
||||
want.format = AUDIO_S16SYS;
|
||||
want.channels = 2;
|
||||
want.samples = 1024;
|
||||
sdl_audio_device = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||
if (sdl_audio_device) {
|
||||
SDL_PauseAudioDevice(sdl_audio_device, 0);
|
||||
}
|
||||
}
|
||||
|
||||
sdl_window = SDL_CreateWindow(
|
||||
"psxrecomp-v4 — BIOS",
|
||||
@@ -355,7 +477,7 @@ int main(int argc, char** argv) {
|
||||
/* If we reach here, all execution completed without MMIO abort. */
|
||||
std::fprintf(stdout, "psxrecomp-v4 runtime: execution completed, PC=0x%08X\n", cpu.pc);
|
||||
|
||||
debug_server_shutdown();
|
||||
shutdown_runtime();
|
||||
SDL_DestroyTexture(sdl_texture);
|
||||
SDL_DestroyRenderer(sdl_renderer);
|
||||
SDL_DestroyWindow(sdl_window);
|
||||
|
||||
+12
-3
@@ -129,9 +129,18 @@ void memcard_flush(int card) {
|
||||
|
||||
FILE* f = fopen(cards[card].filepath, "wb");
|
||||
if (f) {
|
||||
fwrite(cards[card].data, 1, MEMCARD_SIZE, f);
|
||||
fclose(f);
|
||||
cards[card].dirty = 0;
|
||||
size_t n = fwrite(cards[card].data, 1, MEMCARD_SIZE, f);
|
||||
int flush_ok = (fflush(f) == 0);
|
||||
int close_ok = (fclose(f) == 0);
|
||||
if (n == MEMCARD_SIZE && flush_ok && close_ok) {
|
||||
cards[card].dirty = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void memcard_flush_all(void) {
|
||||
for (int i = 0; i < MAX_CARDS; i++) {
|
||||
memcard_flush(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+231
-25
@@ -1,63 +1,270 @@
|
||||
/*
|
||||
* spu.c — PS1 Sound Processing Unit register-level simulation
|
||||
* spu.c - PS1 Sound Processing Unit register and direct ADPCM voice model.
|
||||
*
|
||||
* Accepts all SPU register reads/writes at 0x1F801C00-0x1F801FFF and
|
||||
* DMA4 transfers into 512KB SPU RAM. No audio synthesis yet — this
|
||||
* exists so the BIOS and games can write SPU registers without crashing.
|
||||
*
|
||||
* Audio output will be added in a later phase using SDL_audio with the
|
||||
* ADPCM decoder from psxrecomp-v2/runner/src/spu.cpp as reference.
|
||||
*
|
||||
* Reference: nocash PSX specs — SPU section
|
||||
* Register map: v2 spu.cpp header comment (voice regs, global regs)
|
||||
* This is intentionally still a compact hardware model: it accepts SPU
|
||||
* register reads/writes, DMA4 transfers into 512KB SPU RAM, and mixes the
|
||||
* 24 direct ADPCM voices. Reverb, noise, sweep volumes, XA/CD input and IRQ
|
||||
* timing are not modeled yet.
|
||||
*/
|
||||
|
||||
#include "spu.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define SPU_RAM_SIZE (512 * 1024)
|
||||
#define SPU_REG_COUNT 256 /* 0x1F801C00-0x1F801DFF as uint16_t[256] */
|
||||
#define SPU_RAM_SIZE (512 * 1024)
|
||||
#define SPU_REG_COUNT 256
|
||||
#define SPU_VOICE_COUNT 24
|
||||
#define SPU_BLOCK_SAMPLES 28
|
||||
|
||||
static uint8_t spu_ram[SPU_RAM_SIZE];
|
||||
static uint16_t spu_regs[SPU_REG_COUNT];
|
||||
static uint32_t transfer_addr; /* byte address in SPU RAM */
|
||||
static uint32_t transfer_addr;
|
||||
static uint32_t key_on_count;
|
||||
static uint64_t render_frames;
|
||||
static uint64_t nonzero_frames;
|
||||
static int32_t last_peak;
|
||||
static int32_t peak;
|
||||
|
||||
typedef struct {
|
||||
int active;
|
||||
uint32_t cur_addr;
|
||||
uint32_t repeat_addr;
|
||||
int16_t samples[SPU_BLOCK_SAMPLES];
|
||||
int sample_idx;
|
||||
uint32_t phase;
|
||||
int16_t hist1;
|
||||
int16_t hist2;
|
||||
uint8_t flags;
|
||||
} SpuVoice;
|
||||
|
||||
static SpuVoice voices[SPU_VOICE_COUNT];
|
||||
|
||||
static inline int16_t clamp16(int32_t v) {
|
||||
if (v > 32767) return 32767;
|
||||
if (v < -32768) return -32768;
|
||||
return (int16_t)v;
|
||||
}
|
||||
|
||||
static inline int32_t abs32(int32_t v) {
|
||||
return v < 0 ? -v : v;
|
||||
}
|
||||
|
||||
static inline uint32_t reg_index(uint32_t addr) {
|
||||
return (addr - 0x1F801C00u) >> 1;
|
||||
}
|
||||
|
||||
static inline uint16_t voice_reg(int voice, int reg) {
|
||||
return spu_regs[(uint32_t)voice * 8u + (uint32_t)reg];
|
||||
}
|
||||
|
||||
static inline int16_t direct_volume(uint16_t raw) {
|
||||
int32_t v;
|
||||
if (raw & 0x8000u) {
|
||||
/* Sweep mode is not modeled; use the magnitude as a direct volume. */
|
||||
v = (int32_t)(raw & 0x7FFFu);
|
||||
} else {
|
||||
v = (int32_t)(raw & 0x7FFFu);
|
||||
if (v & 0x4000) v -= 0x8000;
|
||||
}
|
||||
if (v > 0x3FFF) v = 0x3FFF;
|
||||
if (v < -0x4000) v = -0x4000;
|
||||
return (int16_t)v;
|
||||
}
|
||||
|
||||
static void decode_block(SpuVoice *v) {
|
||||
static const int f0[5] = { 0, 60, 115, 98, 122 };
|
||||
static const int f1[5] = { 0, 0, -52, -55, -60 };
|
||||
|
||||
uint32_t addr = v->cur_addr & (SPU_RAM_SIZE - 1u);
|
||||
if (addr + 16u > SPU_RAM_SIZE) addr = 0;
|
||||
|
||||
uint8_t header = spu_ram[addr + 0u];
|
||||
uint8_t flags = spu_ram[addr + 1u];
|
||||
int shift = header & 0x0F;
|
||||
int filter = (header >> 4) & 0x0F;
|
||||
if (filter > 4) filter = 0;
|
||||
if (shift > 12) shift = 12;
|
||||
|
||||
int out = 0;
|
||||
for (int b = 0; b < 14; b++) {
|
||||
uint8_t packed = spu_ram[addr + 2u + (uint32_t)b];
|
||||
for (int n = 0; n < 2; n++) {
|
||||
int sample4 = (n == 0) ? (packed & 0x0F) : (packed >> 4);
|
||||
if (sample4 & 0x08) sample4 -= 0x10;
|
||||
|
||||
int32_t s = sample4 << 12;
|
||||
s >>= shift;
|
||||
s += ((int32_t)v->hist1 * f0[filter] +
|
||||
(int32_t)v->hist2 * f1[filter] + 32) >> 6;
|
||||
s = clamp16(s);
|
||||
v->hist2 = v->hist1;
|
||||
v->hist1 = (int16_t)s;
|
||||
v->samples[out++] = (int16_t)s;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & 0x04u) v->repeat_addr = addr;
|
||||
v->flags = flags;
|
||||
v->sample_idx = 0;
|
||||
v->cur_addr = (addr + 16u) & (SPU_RAM_SIZE - 1u);
|
||||
}
|
||||
|
||||
static int16_t voice_next_sample(int idx) {
|
||||
SpuVoice *v = &voices[idx];
|
||||
if (!v->active) return 0;
|
||||
|
||||
if (v->sample_idx >= SPU_BLOCK_SAMPLES) {
|
||||
if (v->flags & 0x01u) {
|
||||
if (v->flags & 0x02u) {
|
||||
v->cur_addr = v->repeat_addr & (SPU_RAM_SIZE - 1u);
|
||||
} else {
|
||||
v->active = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
decode_block(v);
|
||||
}
|
||||
|
||||
int16_t s = v->samples[v->sample_idx];
|
||||
uint32_t pitch = voice_reg(idx, 2) & 0x3FFFu;
|
||||
if (pitch == 0) pitch = 0x1000u;
|
||||
v->phase += pitch;
|
||||
while (v->phase >= 0x1000u) {
|
||||
v->phase -= 0x1000u;
|
||||
v->sample_idx++;
|
||||
if (v->sample_idx >= SPU_BLOCK_SAMPLES) break;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static void key_on(uint32_t mask) {
|
||||
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
|
||||
if (!(mask & (1u << i))) continue;
|
||||
SpuVoice *v = &voices[i];
|
||||
memset(v, 0, sizeof(*v));
|
||||
v->active = 1;
|
||||
v->cur_addr = ((uint32_t)voice_reg(i, 3) << 3) & (SPU_RAM_SIZE - 1u);
|
||||
v->repeat_addr = ((uint32_t)voice_reg(i, 7) << 3) & (SPU_RAM_SIZE - 1u);
|
||||
v->sample_idx = SPU_BLOCK_SAMPLES;
|
||||
key_on_count++;
|
||||
}
|
||||
}
|
||||
|
||||
static void key_off(uint32_t mask) {
|
||||
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
|
||||
if (mask & (1u << i)) voices[i].active = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void spu_init(void) {
|
||||
memset(spu_ram, 0, sizeof(spu_ram));
|
||||
memset(spu_regs, 0, sizeof(spu_regs));
|
||||
memset(voices, 0, sizeof(voices));
|
||||
transfer_addr = 0;
|
||||
key_on_count = 0;
|
||||
render_frames = 0;
|
||||
nonzero_frames = 0;
|
||||
last_peak = 0;
|
||||
peak = 0;
|
||||
}
|
||||
|
||||
void spu_render(int16_t* out_stereo, int frames) {
|
||||
if (!out_stereo || frames <= 0) return;
|
||||
|
||||
uint16_t ctrl = spu_regs[reg_index(0x1F801DAAu)];
|
||||
int enabled = (ctrl & 0x8000u) != 0;
|
||||
int16_t main_l = direct_volume(spu_regs[reg_index(0x1F801D80u)]);
|
||||
int16_t main_r = direct_volume(spu_regs[reg_index(0x1F801D82u)]);
|
||||
|
||||
int32_t block_peak = 0;
|
||||
for (int f = 0; f < frames; f++) {
|
||||
int32_t mix_l = 0;
|
||||
int32_t mix_r = 0;
|
||||
|
||||
if (enabled) {
|
||||
for (int v = 0; v < SPU_VOICE_COUNT; v++) {
|
||||
int16_t s = voice_next_sample(v);
|
||||
if (!s) continue;
|
||||
int16_t vl = direct_volume(voice_reg(v, 0));
|
||||
int16_t vr = direct_volume(voice_reg(v, 1));
|
||||
mix_l += ((int32_t)s * vl) >> 14;
|
||||
mix_r += ((int32_t)s * vr) >> 14;
|
||||
}
|
||||
mix_l = (mix_l * main_l) >> 14;
|
||||
mix_r = (mix_r * main_r) >> 14;
|
||||
}
|
||||
|
||||
out_stereo[f * 2 + 0] = clamp16(mix_l);
|
||||
out_stereo[f * 2 + 1] = clamp16(mix_r);
|
||||
int32_t frame_peak = abs32(out_stereo[f * 2 + 0]);
|
||||
int32_t right_peak = abs32(out_stereo[f * 2 + 1]);
|
||||
if (right_peak > frame_peak) frame_peak = right_peak;
|
||||
if (frame_peak) nonzero_frames++;
|
||||
if (frame_peak > block_peak) block_peak = frame_peak;
|
||||
}
|
||||
render_frames += (uint64_t)frames;
|
||||
last_peak = block_peak;
|
||||
if (block_peak > peak) peak = block_peak;
|
||||
}
|
||||
|
||||
void spu_debug_info(SpuDebugInfo* out) {
|
||||
if (!out) return;
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->ctrl = spu_regs[reg_index(0x1F801DAAu)];
|
||||
out->main_l = direct_volume(spu_regs[reg_index(0x1F801D80u)]);
|
||||
out->main_r = direct_volume(spu_regs[reg_index(0x1F801D82u)]);
|
||||
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
|
||||
if (voices[i].active) out->active_mask |= (1u << i);
|
||||
}
|
||||
out->key_on_count = key_on_count;
|
||||
out->render_frames = render_frames;
|
||||
out->nonzero_frames = nonzero_frames;
|
||||
out->last_peak = last_peak;
|
||||
out->peak = peak;
|
||||
}
|
||||
|
||||
uint32_t spu_read(uint32_t addr) {
|
||||
if (addr >= 0x1F801C00u && addr <= 0x1F801DFFu) {
|
||||
uint32_t idx = (addr - 0x1F801C00u) >> 1;
|
||||
uint32_t idx = reg_index(addr);
|
||||
if (idx < SPU_REG_COUNT) {
|
||||
/* SPUSTAT (0x1F801DAE): report ready, not busy */
|
||||
if (addr == 0x1F801DAEu) {
|
||||
return 0x0000;
|
||||
return 0x0400; /* SPUSTAT: ready */
|
||||
}
|
||||
if (addr == 0x1F801D9Cu) {
|
||||
uint32_t bits = 0;
|
||||
for (int i = 0; i < 16; i++)
|
||||
if (voices[i].active) bits |= (1u << i);
|
||||
return bits;
|
||||
}
|
||||
if (addr == 0x1F801D9Eu) {
|
||||
uint32_t bits = 0;
|
||||
for (int i = 16; i < SPU_VOICE_COUNT; i++)
|
||||
if (voices[i].active) bits |= (1u << (i - 16));
|
||||
return bits;
|
||||
}
|
||||
return spu_regs[idx];
|
||||
}
|
||||
}
|
||||
/* 0x1F801E00-0x1F801FFF: voice current volume (read-only, return 0) */
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void spu_write(uint32_t addr, uint32_t value) {
|
||||
if (addr >= 0x1F801C00u && addr <= 0x1F801DFFu) {
|
||||
uint32_t idx = (addr - 0x1F801C00u) >> 1;
|
||||
uint32_t idx = reg_index(addr);
|
||||
if (idx < SPU_REG_COUNT) {
|
||||
spu_regs[idx] = (uint16_t)value;
|
||||
|
||||
/* Transfer Address register (0x1F801DA6):
|
||||
* value = byte_addr >> 3 (units of 8 bytes) */
|
||||
if (addr == 0x1F801D88u) key_on((uint32_t)(uint16_t)value);
|
||||
if (addr == 0x1F801D8Au) key_on((uint32_t)(uint16_t)value << 16);
|
||||
if (addr == 0x1F801D8Cu) key_off((uint32_t)(uint16_t)value);
|
||||
if (addr == 0x1F801D8Eu) key_off((uint32_t)(uint16_t)value << 16);
|
||||
|
||||
if (addr == 0x1F801DA6u) {
|
||||
transfer_addr = ((uint32_t)(uint16_t)value) << 3;
|
||||
if (transfer_addr >= SPU_RAM_SIZE)
|
||||
transfer_addr = 0;
|
||||
if (transfer_addr >= SPU_RAM_SIZE) transfer_addr = 0;
|
||||
}
|
||||
|
||||
/* Transfer Data FIFO (0x1F801DA8): write 16-bit to SPU RAM */
|
||||
if (addr == 0x1F801DA8u) {
|
||||
if (transfer_addr + 1 < SPU_RAM_SIZE) {
|
||||
spu_ram[transfer_addr] = (uint8_t)(value & 0xFF);
|
||||
@@ -70,7 +277,6 @@ void spu_write(uint32_t addr, uint32_t value) {
|
||||
}
|
||||
|
||||
void spu_dma_write(uint32_t word) {
|
||||
/* DMA4 writes 32-bit words (two 16-bit samples) to SPU RAM */
|
||||
if (transfer_addr + 3 < SPU_RAM_SIZE) {
|
||||
spu_ram[transfer_addr] = (uint8_t)(word & 0xFF);
|
||||
spu_ram[transfer_addr + 1] = (uint8_t)((word >> 8) & 0xFF);
|
||||
@@ -81,7 +287,7 @@ void spu_dma_write(uint32_t word) {
|
||||
}
|
||||
|
||||
int spu_dma_ready(void) {
|
||||
return 1; /* always ready — synchronous model */
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint8_t* spu_get_ram(void) {
|
||||
|
||||
Reference in New Issue
Block a user