Files
parallel-gx1/Source/Core/DolphinLibretro/Audio.cpp
T
LibretroAdmin 49abc7d61f libretro: call the frontend only from retro_run
In dual core the CPU thread called into the frontend while the
frontend's own loop was running: every button and axis read went to
input_cb from SI polling, each field's audio went to the batch
callback, and rumble went to the rumble interface.

retro_run now reads every input of the libretro devices once, right
after poll_cb, and emulation reads those values. Rumble the emulation
asks for is kept until the end of retro_run, which hands it over
together with the audio the frame produced.
2026-11-05 23:34:29 +01:00

424 lines
11 KiB
C++

#include <libretro.h>
#include "Audio.h"
#include "Common/Logging/Log.h"
#include "AudioCommon/SoundStream.h"
#include "VideoCommon/Present.h"
#include "DolphinLibretro/Common/Globals.h"
namespace Libretro
{
namespace Audio
{
retro_audio_sample_batch_t batch_cb = nullptr;
static std::atomic<bool> g_buf_support{false};
static std::atomic<unsigned> g_buf_occupancy{0};
static std::atomic<bool> g_buf_underrun{false};
static int g_use_call_back_audio{0};
static bool g_audio_state_cb{false};
static bool g_is_fast_forwarding{false};
enum CallBackMode
{
// Dolphin will Push samples into the soundstream
PUSH_SAMPLES = 0,
// One emulated frame's samples are pushed in each retro_run, so the frontend's audio sync and
// rate control pace the core
SYNC_PER_FRAME = 1,
// RetroArch requests audio samples based on its callbacks (target refresh rate/buffer status)
ASYNC_CALLBACK = 2,
};
unsigned int GetCoreSampleRate()
{
// when called from retro_run
SoundStream* sound_stream = Core::System::GetInstance().GetSoundStream();
double sampleRate = LEGACY_DEFAULT_SAMPLE_RATE;
if (sound_stream && sound_stream->GetMixer()->GetSampleRate() != 0)
return sound_stream->GetMixer()->GetSampleRate();
// used in SoundStream constructor
if (Core::System::GetInstance().IsWii())
return sampleRate;
else if (sampleRate == 32000u)
return 32029;
return 48043;
}
unsigned int GetRetroSampleRate()
{
unsigned int sampleRate = DEFAULT_SAMPLE_RATE;
if(!Libretro::environ_cb(RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE, &sampleRate))
DEBUG_LOG_FMT(VIDEO, "Get target sample Rate not supported");
return sampleRate;
}
unsigned int GetActiveSampleRate()
{
// when called from retro_run
SoundStream* sound_stream = Core::System::GetInstance().GetSoundStream();
if (sound_stream && sound_stream->GetMixer()->GetSampleRate() != 0)
return sound_stream->GetMixer()->GetSampleRate();
// this is more or less how the core has used audio for several years
if(g_use_call_back_audio == CallBackMode::PUSH_SAMPLES)
return GetCoreSampleRate();
// alternatively, use the value returned from RetroArch frontend
return GetRetroSampleRate();
}
void Reset()
{
g_use_call_back_audio = Libretro::Options::GetCached<int>(Libretro::Options::audio::CALL_BACK_AUDIO,
CallBackMode::SYNC_PER_FRAME);
g_audio_state_cb = false;
}
void Init()
{
Reset();
// don't use any callback, let dolphin push audio samples
if (g_use_call_back_audio == CallBackMode::PUSH_SAMPLES)
return;
// a frontend audio callback would make the frontend treat the core's
// audio as callback driven, outside its audio sync and rate control
if (g_use_call_back_audio == CallBackMode::SYNC_PER_FRAME)
return;
retro_audio_callback racb = {};
racb.callback = &retroarch_audio_cb;
racb.set_state = &retroarch_audio_state_cb;
if (!Libretro::environ_cb(RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK, &racb))
{
g_use_call_back_audio = CallBackMode::PUSH_SAMPLES;
DEBUG_LOG_FMT(VIDEO, "Async audio callback not supported; fallback to sync");
return;
}
// check if frame timing call backs were successful
if(!FrameTiming::IsEnabled())
{
g_use_call_back_audio = CallBackMode::PUSH_SAMPLES;
DEBUG_LOG_FMT(VIDEO, "Async audio callback not enabled as FrameTiming not available");
return;
}
// buffer status callback
retro_audio_buffer_status_callback bs{};
bs.callback = &retroarch_audio_buffer_status_cb;
if (Libretro::environ_cb(RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK, &bs))
{
g_buf_support.store(true, std::memory_order_relaxed);
DEBUG_LOG_FMT(VIDEO, "Registered async audio buffer status callback");
}
else
{
g_buf_support.store(false, std::memory_order_relaxed);
DEBUG_LOG_FMT(VIDEO, "Audio buffer status callback not supported");
}
}
inline unsigned GetSamplesForFrame(unsigned sample_rate)
{
double frame_time_sec =
FrameTiming::target_frame_duration_usec.load(std::memory_order_relaxed) * 1e-6;
return std::clamp(static_cast<unsigned>(std::lround(frame_time_sec * sample_rate)),
SoundStream::MIN_SAMPLES, SoundStream::MAX_SAMPLES);
}
} // namespace Audio
} // namespace Libretro
using namespace Libretro::Audio;
SoundStream::SoundStream()
: m_mixer(std::make_unique<Mixer>(GetActiveSampleRate())), m_sample_rate(DEFAULT_SAMPLE_RATE)
{
}
void SoundStream::Init()
{
// this is called much later than retro_get_system_av_info when not using callbacks
m_sample_rate = GetActiveSampleRate();
m_mixer->SetSampleRate(m_sample_rate);
}
void SoundStream::Update(unsigned int num_samples)
{
if (g_use_call_back_audio != CallBackMode::PUSH_SAMPLES)
return;
retro_atomic_fetch_add_int(&m_pending_samples, static_cast<int>(num_samples));
}
void SoundStream::NewField()
{
retro_atomic_fetch_add_int(&m_pending_fields, 1);
}
void SoundStream::PushPending()
{
if (!batch_cb)
return;
if (const int samples = retro_atomic_exchange_int(&m_pending_samples, 0))
PushSamples(static_cast<unsigned int>(samples));
for (int fields = retro_atomic_exchange_int(&m_pending_fields, 0); fields > 0; --fields)
PushAudioForFrame();
}
void SoundStream::PushSamples(unsigned int num_samples)
{
static unsigned pending = 0;
pending += num_samples;
if (pending < MIN_SAMPLES)
return; // not enough yet
unsigned avail = pending;
pending = 0; // consume all
// First push the minimum threshold block
m_mixer->Mix(m_buffer, MIN_SAMPLES);
batch_cb(m_buffer, MIN_SAMPLES);
avail -= MIN_SAMPLES;
// Then push any remaining in MAX_SAMPLES chunks
while (avail > MAX_SAMPLES)
{
m_mixer->Mix(m_buffer, MAX_SAMPLES);
batch_cb(m_buffer, MAX_SAMPLES);
avail -= MAX_SAMPLES;
}
if (avail)
{
m_mixer->Mix(m_buffer, avail);
batch_cb(m_buffer, avail);
}
}
void SoundStream::MixAndPush(unsigned int num_samples)
{
static unsigned pending = 0;
pending += num_samples;
if (pending < MIN_SAMPLES)
return; // not enough yet
unsigned avail = pending;
pending = 0; // consume all
// Then push any remaining in MAX_SAMPLES chunk
while (avail >= MAX_SAMPLES)
{
m_mixer->Mix(m_buffer, MAX_SAMPLES);
batch_cb(m_buffer, MAX_SAMPLES);
avail -= MAX_SAMPLES;
}
if (avail >= MIN_SAMPLES)
{
m_mixer->Mix(m_buffer, avail);
batch_cb(m_buffer, avail);
}
else if (avail > 0)
{
pending = avail;
}
}
void SoundStream::PushAudioForFrame()
{
if (g_use_call_back_audio != CallBackMode::SYNC_PER_FRAME || !batch_cb)
return;
// The DSP fills the mixer at the game's refresh rate, not the
// display's, so that rate is used and the fraction carried over: the mixer then neither fills up
// nor runs dry, and the frontend's rate control covers the display's difference.
double frame_rate = Libretro::g_core_refresh_rate;
if (frame_rate <= 0.0)
frame_rate = (retro_get_region() == RETRO_REGION_NTSC) ? 60.0 / 1.001 : 50.0;
static double carry = 0.0;
const double exact = m_sample_rate / frame_rate + carry;
unsigned samples_for_frame = static_cast<unsigned>(exact);
carry = exact - samples_for_frame;
samples_for_frame = std::clamp(samples_for_frame, MIN_SAMPLES, MAX_SAMPLES);
MixAndPush(samples_for_frame);
}
// Input:
// GameCube DMA: 32029 Hz
// GameCube Streaming: 48043 Hz
// Wii DMA: 32000 Hz
// Wii Streaming: 48000 Hz
// Output is 48000 Hz (Wii) (or 48043 Hz for GameCube)
// Wii: Uses divisor 1125 * 2 = 2250 = exactly 48000 Hz
// GameCube: Uses divisor 1124 * 2 = 2248 = 48043 Hz
void SoundStream::ProcessCallBack()
{
if (!batch_cb || !Libretro::g_emuthread_launched ||
g_use_call_back_audio != CallBackMode::ASYNC_CALLBACK)
return;
// True: Audio driver in frontend is active
// False: Audio driver in frontend is paused or inactive
if (!Libretro::Audio::g_audio_state_cb)
return;
auto& system = Core::System::GetInstance();
if (!system.IsSoundStreamRunning())
return;
if (Libretro::Audio::g_buf_support.load(std::memory_order_relaxed))
{
constexpr unsigned FULL_PERCENT = 75;
unsigned occ = Libretro::Audio::g_buf_occupancy.load(std::memory_order_relaxed);
if (occ >= FULL_PERCENT)
return;
// Use frame time to decide how much to push
unsigned to_mix = GetSamplesForFrame(m_sample_rate);
m_mixer->Mix(m_buffer, to_mix);
batch_cb(m_buffer, to_mix);
return;
}
unsigned to_mix = GetSamplesForFrame(m_sample_rate);
// Clamp to sane range
to_mix = std::clamp(to_mix, MIN_SAMPLES, MAX_SAMPLES);
m_mixer->Mix(m_buffer, to_mix);
batch_cb(m_buffer, to_mix);
}
namespace Libretro
{
namespace FrameTiming
{
std::atomic<retro_usec_t> target_frame_duration_usec{16667};
std::atomic<retro_usec_t> measured_frame_duration_usec{16667};
bool g_have_frame_time_cb {false};
static retro_frame_time_callback ftcb = {};
void Reset()
{
g_have_frame_time_cb = false;
}
void Init()
{
Reset();
// Get target refresh rate from frontend
float refresh_rate = 60.0f;
if (!Libretro::environ_cb(RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE, &refresh_rate))
{
DEBUG_LOG_FMT(VIDEO, "frame timing: unable to get target refresh rate");
return;
}
if (refresh_rate < 1.0f)
refresh_rate = 60.0f;
// Register frame time callback to track actual frame times
ftcb.callback = [](retro_usec_t usec) {
measured_frame_duration_usec.store(usec, std::memory_order_relaxed);
};
ftcb.reference = static_cast<retro_usec_t>(1000000.0f / refresh_rate);
target_frame_duration_usec.store(ftcb.reference, std::memory_order_relaxed);
if (!Libretro::environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, &ftcb))
{
DEBUG_LOG_FMT(VIDEO, "frame timing: unable to set frame time callback");
return;
}
DEBUG_LOG_FMT(VIDEO, "frame timing enabled: target={} usec ({} Hz)",
ftcb.reference, refresh_rate);
// successful callbacks
g_have_frame_time_cb = true;
}
bool IsEnabled()
{
return g_have_frame_time_cb;
}
bool IsFastForwarding()
{
return Libretro::Audio::g_is_fast_forwarding;
}
void CheckForFastForwarding()
{
if (!Libretro::environ_cb)
return;
bool is_fast_forwarding = false;
// Query the fast-forward state from RetroArch
if (Libretro::environ_cb(RETRO_ENVIRONMENT_GET_FASTFORWARDING, &is_fast_forwarding))
{
Libretro::Audio::g_is_fast_forwarding = is_fast_forwarding;
return;
}
// Environment call not supported
Libretro::Audio::g_is_fast_forwarding = false;
}
} // namespace FrameTiming
} // namespace Libretro
// Call backs
void retroarch_audio_state_cb(bool enable)
{
Libretro::Audio::g_audio_state_cb = enable;
}
// Notifies libretro that audio data should be written
void retroarch_audio_cb()
{
if (auto* s = Core::System::GetInstance().GetSoundStream())
s->ProcessCallBack();
}
void retroarch_audio_buffer_status_cb(bool active,
unsigned occupancy,
bool underrun_likely)
{
if (!active)
DEBUG_LOG_FMT(VIDEO, "retroarch_audio_buffer_status_cb reports that it is not active");
Libretro::Audio::g_buf_occupancy.store(occupancy, std::memory_order_relaxed);
Libretro::Audio::g_buf_underrun.store(underrun_likely, std::memory_order_relaxed);
}
// retroarch hooks
extern "C" {
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb)
{
Libretro::Audio::batch_cb = cb;
}
void retro_set_audio_sample(retro_audio_sample_t cb)
{
}
} // extern "C"