got audio working

This commit is contained in:
izzy2lost
2025-12-16 22:45:06 -05:00
parent 23d795e60a
commit 60602b735e
2 changed files with 195 additions and 22 deletions
+180 -19
View File
@@ -77,10 +77,90 @@ void SetAudioType(Game::AudioTypes type) { g_audioType = type; }
bool BeginFrameVideo() { return true; }
void EndFrameVideo() {}
// Simple SDL audio backend: mix 4-channel Model3 audio down to stereo and queue
// to the SDL device.
// SDL audio backend:
// - Mix 4-channel Model3 output down to stereo (S16).
// - Resample/convert to the opened device format via SDL_AudioStream.
// - Feed a ring buffer consumed by the SDL audio callback (prevents choppy audio
// due to small queue bursts or timing jitter).
static SDL_AudioDeviceID g_audioDevice = 0;
static SDL_AudioSpec g_audioSpec = {};
static SDL_AudioStream* g_audioStream = nullptr;
static std::vector<uint8_t> g_ring;
static uint32_t g_ringRead = 0;
static uint32_t g_ringWrite = 0;
static SDL_mutex* g_audioMutex = nullptr;
static uint32_t g_bytesPerSecond = 0;
static uint32_t g_targetFillBytes = 0;
static uint32_t RingUsed()
{
if (g_ring.empty())
return 0;
if (g_ringWrite >= g_ringRead)
return g_ringWrite - g_ringRead;
return static_cast<uint32_t>(g_ring.size()) - (g_ringRead - g_ringWrite);
}
static uint32_t RingFree()
{
if (g_ring.empty())
return 0;
// Keep one byte empty to distinguish full vs empty.
return static_cast<uint32_t>(g_ring.size() - 1) - RingUsed();
}
static void RingDropOldest(uint32_t bytes)
{
if (g_ring.empty())
return;
const uint32_t used = RingUsed();
if (bytes >= used)
{
g_ringRead = g_ringWrite;
return;
}
g_ringRead = (g_ringRead + bytes) % static_cast<uint32_t>(g_ring.size());
}
static void RingWrite(const uint8_t* data, uint32_t bytes)
{
if (g_ring.empty() || bytes == 0)
return;
// If we don't have room, drop oldest audio to keep latency bounded.
const uint32_t free = RingFree();
if (bytes > free)
RingDropOldest(bytes - free);
uint32_t remaining = bytes;
while (remaining > 0)
{
const uint32_t toEnd = static_cast<uint32_t>(g_ring.size()) - g_ringWrite;
const uint32_t chunk = (remaining < toEnd) ? remaining : toEnd;
std::memcpy(&g_ring[g_ringWrite], data + (bytes - remaining), chunk);
g_ringWrite = (g_ringWrite + chunk) % static_cast<uint32_t>(g_ring.size());
remaining -= chunk;
}
}
static uint32_t RingRead(uint8_t* out, uint32_t bytes)
{
if (g_ring.empty() || bytes == 0)
return 0;
const uint32_t used = RingUsed();
const uint32_t toRead = (bytes < used) ? bytes : used;
uint32_t remaining = toRead;
while (remaining > 0)
{
const uint32_t toEnd = static_cast<uint32_t>(g_ring.size()) - g_ringRead;
const uint32_t chunk = (remaining < toEnd) ? remaining : toEnd;
std::memcpy(out + (toRead - remaining), &g_ring[g_ringRead], chunk);
g_ringRead = (g_ringRead + chunk) % static_cast<uint32_t>(g_ring.size());
remaining -= chunk;
}
return toRead;
}
static INT16 Clamp16(int sample)
{
@@ -95,20 +175,70 @@ bool OpenAudio(const Util::Config::Node&)
return true;
SDL_AudioSpec desired{};
desired.freq = 44100;
desired.freq = 48000; // common Android output rate; SDL may still choose a different one
desired.format = AUDIO_S16SYS;
desired.channels = 2; // stereo out
desired.samples = 1024;
desired.callback = [](void*, Uint8*, int) {
// Using SDL_QueueAudio so callback is a no-op.
desired.samples = 4096;
desired.callback = [](void*, Uint8* stream, int len) {
if (!g_audioEnabled || g_ring.empty())
{
std::memset(stream, 0, static_cast<size_t>(len));
return;
}
if (g_audioMutex)
SDL_LockMutex(g_audioMutex);
const uint32_t got = RingRead(stream, static_cast<uint32_t>(len));
const uint32_t usedAfterRead = RingUsed();
if (g_audioMutex)
SDL_UnlockMutex(g_audioMutex);
if (got < static_cast<uint32_t>(len))
std::memset(stream + got, 0, static_cast<size_t>(len - got));
// When audio is running low, wake the emulator sound thread (if enabled).
// This matches the desktop "unsync'd sound board thread" design.
if (g_audioCallback && g_audioData && g_targetFillBytes != 0 && usedAfterRead < (g_targetFillBytes / 2))
g_audioCallback(g_audioData);
};
g_audioDevice = SDL_OpenAudioDevice(nullptr, 0, &desired, &g_audioSpec, 0);
// Allow SDL to pick a workable device format/rate; we'll convert via SDL_AudioStream.
g_audioDevice = SDL_OpenAudioDevice(nullptr, 0, &desired, &g_audioSpec,
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_FORMAT_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
if (g_audioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "SDL_OpenAudioDevice failed: %s", SDL_GetError());
return false;
}
g_bytesPerSecond = static_cast<uint32_t>(g_audioSpec.freq) *
static_cast<uint32_t>(g_audioSpec.channels) *
static_cast<uint32_t>(SDL_AUDIO_BITSIZE(g_audioSpec.format) / 8);
// Keep ~250ms buffered; ring holds 2s to bound worst-case jitter.
g_targetFillBytes = g_bytesPerSecond / 4;
g_ring.assign((g_bytesPerSecond * 2) + 1, 0);
g_ringRead = 0;
g_ringWrite = 0;
g_audioMutex = SDL_CreateMutex();
if (!g_audioMutex) {
SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "SDL_CreateMutex failed: %s", SDL_GetError());
SDL_CloseAudioDevice(g_audioDevice);
g_audioDevice = 0;
g_ring.clear();
return false;
}
// Convert from core mix format (S16 stereo @ 44100) to device format.
g_audioStream = SDL_NewAudioStream(AUDIO_S16SYS, 2, 44100, g_audioSpec.format, g_audioSpec.channels, g_audioSpec.freq);
if (!g_audioStream) {
SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "SDL_NewAudioStream failed: %s", SDL_GetError());
SDL_CloseAudioDevice(g_audioDevice);
g_audioDevice = 0;
g_ring.clear();
SDL_DestroyMutex(g_audioMutex);
g_audioMutex = nullptr;
return false;
}
SDL_PauseAudioDevice(g_audioDevice, 0);
return true;
}
@@ -118,9 +248,10 @@ bool OutputAudio(unsigned numSamples, INT16* leftFront, INT16* rightFront, INT16
if (!g_audioEnabled || g_audioDevice == 0)
return true;
// Downmix quad to stereo.
std::vector<INT16> stereo;
stereo.reserve(numSamples * 2);
// Downmix quad to stereo (S16 @ 44100 Hz).
static thread_local std::vector<INT16> stereo;
stereo.clear();
stereo.resize(static_cast<size_t>(numSamples) * 2);
for (unsigned i = 0; i < numSamples; ++i) {
int lf = leftFront ? leftFront[i] : 0;
int rf = rightFront ? rightFront[i] : 0;
@@ -129,22 +260,52 @@ bool OutputAudio(unsigned numSamples, INT16* leftFront, INT16* rightFront, INT16
int l = Clamp16((lf + lr) / 2);
int r = Clamp16((rf + rr) / 2);
if (flipStereo) std::swap(l, r);
stereo.push_back(static_cast<INT16>(l));
stereo.push_back(static_cast<INT16>(r));
stereo[(i * 2) + 0] = static_cast<INT16>(l);
stereo[(i * 2) + 1] = static_cast<INT16>(r);
}
if (SDL_QueueAudio(g_audioDevice, stereo.data(), stereo.size() * sizeof(INT16)) != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_AUDIO, "SDL_QueueAudio failed: %s", SDL_GetError());
if (g_audioStream)
{
SDL_AudioStreamPut(g_audioStream, stereo.data(), static_cast<int>(stereo.size() * sizeof(INT16)));
// Pull converted data in chunks into the ring buffer.
uint8_t tmp[8192];
while (true)
{
const int got = SDL_AudioStreamGet(g_audioStream, tmp, static_cast<int>(sizeof(tmp)));
if (got <= 0)
break;
if (g_audioMutex)
SDL_LockMutex(g_audioMutex);
RingWrite(tmp, static_cast<uint32_t>(got));
if (g_audioMutex)
SDL_UnlockMutex(g_audioMutex);
}
}
if (g_audioCallback && g_audioData) {
g_audioCallback(g_audioData);
}
return true;
// Tell the core whether the audio buffer is "full enough" (used by the
// unsync'd sound-board thread to decide whether to run extra frames).
bool fullEnough = true;
if (g_audioMutex)
SDL_LockMutex(g_audioMutex);
if (g_targetFillBytes != 0)
fullEnough = RingUsed() >= g_targetFillBytes;
if (g_audioMutex)
SDL_UnlockMutex(g_audioMutex);
return fullEnough;
}
void CloseAudio()
{
if (g_audioStream) {
SDL_FreeAudioStream(g_audioStream);
g_audioStream = nullptr;
}
if (g_audioMutex) {
SDL_DestroyMutex(g_audioMutex);
g_audioMutex = nullptr;
}
g_ring.clear();
g_bytesPerSecond = 0;
g_targetFillBytes = 0;
if (g_audioDevice != 0) {
SDL_CloseAudioDevice(g_audioDevice);
g_audioDevice = 0;
+15 -3
View File
@@ -126,7 +126,9 @@ struct Super3Host {
}
void ApplyDefaults() {
config.Set("MultiThreaded", false);
// Enable the core's sound-board thread so audio can stay smooth even if
// rendering/input causes occasional stalls on Android.
config.Set("MultiThreaded", true);
config.Set("GPUMultiThreaded", false);
config.Set("EmulateSound", true);
config.Set("EmulateDSB", true);
@@ -135,8 +137,9 @@ struct Super3Host {
config.Set("BalanceFrontRear", "0");
config.Set("NbSoundChannels", "4");
config.Set("SoundFreq", "57.6");
config.Set("SoundVolume", "100");
config.Set("MusicVolume", "100");
// Supermodel.ini commonly uses 200 as "100%".
config.Set("SoundVolume", "200");
config.Set("MusicVolume", "200");
config.Set("LegacySoundDSP", false);
config.Set("New3DEngine", false);
config.Set("QuadRendering", false);
@@ -401,6 +404,7 @@ extern "C" int SDL_main(int argc, char* argv[]) {
uint32_t lastStatusLog = SDL_GetTicks();
bool backgrounded = false;
bool loggedControls = false;
bool audioOpened = false;
while (running) {
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
@@ -448,6 +452,13 @@ extern "C" int SDL_main(int argc, char* argv[]) {
const int state = loadState.load(std::memory_order_acquire);
if (state == 1) {
if (!audioOpened) {
audioOpened = true;
SetAudioType(host.game.audio);
if (!OpenAudio(host.config)) {
SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "OpenAudio failed (continuing without audio)");
}
}
if (!loggedControls) {
loggedControls = true;
SDL_Log("Controls (touch): bottom-left=COIN, bottom-right=START, top-left=SERVICE, top-right=TEST, left-middle=DPAD/STEER, right-middle=THROTTLE/BRAKE");
@@ -506,6 +517,7 @@ extern "C" int SDL_main(int argc, char* argv[]) {
if (loadState.load(std::memory_order_acquire) == 1) {
host.SaveNVRAM();
}
CloseAudio();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();