From 60602b735e764bc352e5ceaadaaddaadb9c18110 Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Tue, 16 Dec 2025 22:45:06 -0500 Subject: [PATCH] got audio working --- android/app/src/main/cpp/model3_stubs.cpp | 199 +++++++++++++++++++--- android/app/src/main/cpp/native-lib.cpp | 18 +- 2 files changed, 195 insertions(+), 22 deletions(-) diff --git a/android/app/src/main/cpp/model3_stubs.cpp b/android/app/src/main/cpp/model3_stubs.cpp index 133cc25..84ff93a 100644 --- a/android/app/src/main/cpp/model3_stubs.cpp +++ b/android/app/src/main/cpp/model3_stubs.cpp @@ -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 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(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(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(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(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(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(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(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(len)); + return; + } + if (g_audioMutex) + SDL_LockMutex(g_audioMutex); + const uint32_t got = RingRead(stream, static_cast(len)); + const uint32_t usedAfterRead = RingUsed(); + if (g_audioMutex) + SDL_UnlockMutex(g_audioMutex); + if (got < static_cast(len)) + std::memset(stream + got, 0, static_cast(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(g_audioSpec.freq) * + static_cast(g_audioSpec.channels) * + static_cast(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 stereo; - stereo.reserve(numSamples * 2); + // Downmix quad to stereo (S16 @ 44100 Hz). + static thread_local std::vector stereo; + stereo.clear(); + stereo.resize(static_cast(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(l)); - stereo.push_back(static_cast(r)); + stereo[(i * 2) + 0] = static_cast(l); + stereo[(i * 2) + 1] = static_cast(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(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(sizeof(tmp))); + if (got <= 0) + break; + if (g_audioMutex) + SDL_LockMutex(g_audioMutex); + RingWrite(tmp, static_cast(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; diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index da0dc26..70b6cb9 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -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();