#include "audio_backend.h" #include "runtime_log.h" #include #include #include #include #include AudioBackend& AudioBackend::Instance() { static AudioBackend instance; return instance; } float AudioBackend::EffectiveGainLocked() const { return m_muted ? 0.0f : m_masterVolume; } void AudioBackend::ApplyGainLocked() { if (m_stream && !SDL_SetAudioStreamGain(m_stream, EffectiveGainLocked())) { RT_LOG(RT_TAG_AUDIO) << "SDL_SetAudioStreamGain failed: " << SDL_GetError() << std::endl; } } void AudioBackend::SetMasterVolume(float volume) { std::lock_guard lock(m_mutex); m_masterVolume = std::clamp(volume, 0.0f, 1.0f); ApplyGainLocked(); } void AudioBackend::SetMuted(bool muted) { std::lock_guard lock(m_mutex); m_muted = muted; ApplyGainLocked(); } bool AudioBackend::EnsureInitializedLocked(uint32_t sampleRate, uint32_t channels) { if (m_initialized && m_sampleRate == sampleRate && m_channels == channels) { return true; } if (m_stream) { SDL_DestroyAudioStream(m_stream); m_stream = nullptr; } if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) { RT_LOG(RT_TAG_AUDIO) << "SDL_InitSubSystem(SDL_INIT_AUDIO) failed: " << SDL_GetError() << std::endl; return false; } SDL_AudioSpec spec{}; spec.format = SDL_AUDIO_S16LE; spec.channels = static_cast(channels); spec.freq = static_cast(sampleRate); SDL_AudioStream* stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr); if (!stream) { RT_LOG(RT_TAG_AUDIO) << "SDL_OpenAudioDeviceStream failed: " << SDL_GetError() << std::endl; return false; } if (!SDL_ResumeAudioStreamDevice(stream)) { RT_LOG(RT_TAG_AUDIO) << "SDL_ResumeAudioStreamDevice failed: " << SDL_GetError() << std::endl; SDL_DestroyAudioStream(stream); return false; } if (!SDL_SetAudioStreamGain(stream, EffectiveGainLocked())) { RT_LOG(RT_TAG_AUDIO) << "SDL_SetAudioStreamGain failed: " << SDL_GetError() << std::endl; SDL_DestroyAudioStream(stream); return false; } m_stream = stream; m_spec = spec; m_sampleRate = sampleRate; m_channels = channels; m_initialized = true; return true; } bool AudioBackend::Init(uint32_t sampleRate, uint32_t channels) { std::lock_guard lock(m_mutex); return EnsureInitializedLocked(sampleRate, channels); } void AudioBackend::Shutdown() { std::lock_guard lock(m_mutex); if (m_stream) { SDL_DestroyAudioStream(m_stream); m_stream = nullptr; } if (m_initialized) { SDL_QuitSubSystem(SDL_INIT_AUDIO); } m_initialized = false; m_sampleRate = 0; m_channels = 0; m_reportedDroppedBlock = false; m_convertBuffer.clear(); } uint32_t AudioBackend::QueueLimitBytesLocked() const { constexpr uint32_t queueMs = 120; const uint64_t bytesPerSecond = static_cast(m_spec.freq) * static_cast(m_spec.channels) * sizeof(int16_t); return static_cast((bytesPerSecond * queueMs) / 1000u); } bool AudioBackend::QueueHasCapacityLocked(int incomingBytes) { if (!m_stream) { return false; } const int queued = SDL_GetAudioStreamQueued(m_stream); if (queued < 0) { return true; } const uint32_t maxQueued = QueueLimitBytesLocked(); if (static_cast(queued) + static_cast(std::max(incomingBytes, 0)) > maxQueued) { // Match Dolphin's FIFO overflow behavior: preserve the continuous audio // already queued and discard the new block. Clearing SDL's entire // stream creates an audible discontinuity (the severe crackle seen when // VI-batched DMA briefly outran playback). // A drop is audible; report the first one so it is not invisible. if (!m_reportedDroppedBlock) { m_reportedDroppedBlock = true; RT_LOG(RT_TAG_AUDIO) << "output queue full (" << queued << "/" << maxQueued << " bytes); dropping blocks to preserve continuity" << std::endl; } return false; } return true; } bool AudioBackend::PushWiiAiSamplesBE16(const uint8_t* data, size_t bytes) { if (!data || bytes == 0) { return false; } std::lock_guard lock(m_mutex); if (!m_initialized || !m_stream) { return false; } const size_t sampleCount = bytes / sizeof(int16_t); if (sampleCount == 0) { return false; } if (m_convertBuffer.size() < sampleCount) { m_convertBuffer.resize(sampleCount); } const size_t frameCount = sampleCount / 2; for (size_t frame = 0; frame < frameCount; ++frame) { const size_t rightOffset = frame * 4; const size_t leftOffset = rightOffset + 2; const uint16_t right = static_cast(data[rightOffset]) << 8 | static_cast(data[rightOffset + 1]); const uint16_t left = static_cast(data[leftOffset]) << 8 | static_cast(data[leftOffset + 1]); m_convertBuffer[frame * 2] = static_cast(left); m_convertBuffer[frame * 2 + 1] = static_cast(right); } const int lenBytes = static_cast(frameCount * 2 * sizeof(int16_t)); if (!QueueHasCapacityLocked(lenBytes)) { return true; } if (!SDL_PutAudioStreamData(m_stream, m_convertBuffer.data(), lenBytes)) { RT_LOG(RT_TAG_AUDIO) << "SDL_PutAudioStreamData failed: " << SDL_GetError() << std::endl; return false; } return true; } bool AudioBackend::PushSamplesLE16(const int16_t* samples, size_t sampleCount) { if (!samples || sampleCount == 0) { return false; } std::lock_guard lock(m_mutex); if (!m_initialized || !m_stream) { return false; } const int lenBytes = static_cast(sampleCount * sizeof(int16_t)); if (!QueueHasCapacityLocked(lenBytes)) { return true; } if (!SDL_PutAudioStreamData(m_stream, samples, lenBytes)) { RT_LOG(RT_TAG_AUDIO) << "SDL_PutAudioStreamData failed: " << SDL_GetError() << std::endl; return false; } return true; }