Audio Channels (System): Mono, Stereo or Surround 5.1 - what the console reports for the TV and AX mixes for; 5.1 goes to RetroArch's multi-channel output, or mixed down to stereo without one (NNshi)

This commit is contained in:
WizzardSK committed 2026-10-10 12:18:15 +02:00
1 parent cea8203e38
commit 6c29741e06
5 files changed
+152 -16

No files matched your search

+23 -7
View File
@@ -193,14 +193,28 @@ namespace snd_core
{
if (s_drcMixBlockReady)
{
constexpr size_t kChannels = 2;
const size_t samplesToMix = (size_t)AX_SAMPLES_PER_3MS_48KHZ * (size_t)AX_FRAMES_PER_GROUP * kChannels;
for (size_t i = 0; i < samplesToMix; ++i)
{
int v = (int)tempTVChannelData[i] + (int)s_drcMixBlock[i];
// The GamePad's stereo onto the TV's front pair (both on
// a mono TV), whatever the TV's channel count
const size_t frames = (size_t)AX_SAMPLES_PER_3MS_48KHZ * (size_t)AX_FRAMES_PER_GROUP;
auto mix = [](sint16& out, int add) {
int v = (int)out + add;
if (v > 32767) v = 32767;
else if (v < -32768) v = -32768;
tempTVChannelData[i] = (sint16)v;
out = (sint16)v;
};
for (size_t f = 0; f < frames; ++f)
{
sint16* tv = tempTVChannelData + f * channels;
const int l = s_drcMixBlock[f * 2], r = s_drcMixBlock[f * 2 + 1];
if (channels == 1)
{
mix(tv[0], (l + r) / 2);
}
else
{
mix(tv[0], l);
mix(tv[1], r);
}
}
s_drcMixBlockReady = false;
}
@@ -437,7 +451,9 @@ namespace snd_core
try
{
// For libretro, use LibretroAudioAPI which routes audio to RetroArch
g_tvAudio = std::make_unique<LibretroAudioAPI>(48000, 2, snd_core::AX_SAMPLES_PER_3MS_48KHZ * AX_FRAMES_PER_GROUP, 16);
// As many channels as the TV the console reports (Audio
// Channels option); the audio API makes the frontend's of them
g_tvAudio = std::make_unique<LibretroAudioAPI>(48000, CemuConfig::AudioChannelsToNChannels(GetConfig().tv_channels), snd_core::AX_SAMPLES_PER_3MS_48KHZ * AX_FRAMES_PER_GROUP, 16);
cemuLog_log(LogType::Force, "Initialized LibretroAudioAPI for TV audio");
}
catch (std::runtime_error& ex)
+42
View File
@@ -260,6 +260,19 @@ static LibretroSystemImplementation s_systemImpl;
// Globals
// ============================================================================
#ifndef RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_MULTI
// RetroArch's multi-channel audio (master since 10.9.2026)
#define RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_MULTI (94 | RETRO_ENVIRONMENT_EXPERIMENTAL)
typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_multi_int16_t)(
const int16_t* data, size_t frames, unsigned channels, unsigned layout);
typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_multi_float_t)(
const float* data, size_t frames, unsigned channels, unsigned layout);
struct retro_audio_sample_multi_callback
{
retro_audio_sample_batch_multi_int16_t batch_int16;
retro_audio_sample_batch_multi_float_t batch_float;
};
#endif
static retro_environment_t environ_cb = nullptr;
// Defined further down, beside the cheat entry points they exist for.
@@ -2878,6 +2891,14 @@ static void libretro_apply_core_options()
cfg.gx2drawdone_sync = b;
}
// Audio Channels: what AVM reports for the TV, and so what AX mixes for
if (const char* v = libretro_get_option_value("cemu_audio_channels"))
{
if (libretro_iequals(v, "mono")) cfg.tv_channels = kMono;
else if (libretro_iequals(v, "surround")) cfg.tv_channels = kSurround;
else cfg.tv_channels = kStereo;
}
// Console language
if (const char* v = libretro_get_option_value("cemu_console_language"))
{
@@ -5617,6 +5638,27 @@ RETRO_API bool retro_load_game(const struct retro_game_info* game)
// hide a frontend that cannot give this core a context the second time.
s_use_hw_render = false;
// 5.1 when asked for and RetroArch has multi-channel output (asked once,
// at load); otherwise the audio goes out as stereo
{
static retro_audio_sample_multi_callback s_multi{};
s_multi = {};
const char* channels = libretro_get_option_value("cemu_audio_channels");
const bool surround = channels && libretro_iequals(channels, "surround");
if (surround &&
!(environ_cb(RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_MULTI, &s_multi) && s_multi.batch_int16))
{
libretro_log(RETRO_LOG_WARN, "audio: the frontend has no multi-channel output, 5.1 is mixed down to stereo\n");
s_multi = {};
}
LibretroAudioAPI::SetOutput(s_multi.batch_int16 ? 6 : 2, s_multi.batch_int16
? [](const int16_t* data, size_t frames, unsigned ch, unsigned layout) -> size_t {
return s_audio_submission_allowed && data && frames > 0 ? s_multi.batch_int16(data, frames, ch, layout) : 0;
}
: nullptr);
libretro_log(RETRO_LOG_INFO, "audio: %u channels to the frontend\n", s_multi.batch_int16 ? 6u : 2u);
}
// Set up pixel format
enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888;
if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
+57 -7
View File
@@ -1,5 +1,6 @@
#include "LibretroAudioAPI.h"
#include <algorithm>
#include <atomic>
#include <chrono>
@@ -24,6 +25,8 @@ uint64_t LibretroAudioAPI::s_stat_sent = 0;
uint64_t LibretroAudioAPI::s_stat_flushes = 0;
uint64_t LibretroAudioAPI::s_stat_empty_flushes = 0;
LibretroAudioRingBuffer LibretroAudioAPI::s_ring_buffer;
LibretroAudioAPI::MultiAudioCallback LibretroAudioAPI::s_multi_callback = nullptr;
unsigned LibretroAudioAPI::s_out_channels = 2;
std::vector<int16_t> LibretroAudioAPI::s_flush_buffer;
LibretroAudioAPI::LibretroAudioAPI(uint32 samplerate, uint32 channels, uint32 samples_per_block, uint32 bits_per_sample)
@@ -45,8 +48,9 @@ bool LibretroAudioAPI::NeedAdditionalBlocks() const
{
// Match upstream behavior: ask for more blocks while the ring has less
// than (audio_delay * samples_per_block) buffered.
// In the ring's samples, which are the output's channels
const size_t bufferedSamples = s_ring_buffer.GetReadAvailableSamples();
const size_t targetBufferSamples = GetAudioDelay() * (m_bytesPerBlock / sizeof(int16_t));
const size_t targetBufferSamples = GetAudioDelay() * m_samplesPerBlock * s_out_channels;
return bufferedSamples < targetBufferSamples;
}
@@ -55,8 +59,42 @@ bool LibretroAudioAPI::FeedBlock(sint16* data)
if (!data || !m_playing)
return false;
const size_t sampleCount = m_bytesPerBlock / sizeof(int16_t);
const size_t written = s_ring_buffer.Write(data, sampleCount);
// AX's channels (the TV mode: 1, 2 or 6) to the output's (2 or 6). 5.1
// is FL FR C LFE SL SR, as standalone hands it to cubeb.
const size_t frames = m_samplesPerBlock;
const unsigned in = m_channels, out = s_out_channels;
const int16_t* block = data;
if (in != out)
{
m_convert.resize(frames * out);
for (size_t f = 0; f < frames; f++)
{
const int16_t* s = data + f * in;
int16_t* d = m_convert.data() + f * out;
if (in == 1)
{
for (unsigned c = 0; c < out; c++)
d[c] = c < 2 ? s[0] : 0;
}
else if (in == 6 && out == 2)
{
// Centre and surrounds at -3 dB, LFE left out
const float c = s[2] * 0.7071f;
const float l = s[0] + c + s[4] * 0.7071f;
const float r = s[1] + c + s[5] * 0.7071f;
d[0] = (int16_t)std::clamp(l, -32768.0f, 32767.0f);
d[1] = (int16_t)std::clamp(r, -32768.0f, 32767.0f);
}
else
{
for (unsigned ch = 0; ch < out; ch++)
d[ch] = ch < in ? s[ch] : 0;
}
}
block = m_convert.data();
}
const size_t sampleCount = frames * out;
const size_t written = s_ring_buffer.Write(block, sampleCount);
AccountWrite(sampleCount, written);
return written > 0;
}
@@ -78,6 +116,13 @@ void LibretroAudioAPI::SetAudioCallback(AudioCallback cb)
s_audio_callback = cb;
}
void LibretroAudioAPI::SetOutput(unsigned channels, MultiAudioCallback multi)
{
s_out_channels = channels == 6 && multi ? 6 : 2;
s_multi_callback = s_out_channels == 6 ? multi : nullptr;
s_ring_buffer.Reset();
}
void LibretroAudioAPI::FlushAudio()
{
if (!s_audio_callback)
@@ -108,7 +153,8 @@ void LibretroAudioAPI::FlushAudio()
if (samplesRead == 0)
return;
const size_t framesRead = samplesRead / LibretroAudioRingBuffer::kChannels;
const size_t channels = s_out_channels;
const size_t framesRead = samplesRead / channels;
// retro_audio_sample_batch_t may consume fewer frames than offered; loop so
// a partial accept doesn't lose audio.
@@ -116,14 +162,18 @@ void LibretroAudioAPI::FlushAudio()
const int16_t* cursor = s_flush_buffer.data();
while (framesSent < framesRead)
{
const size_t accepted = s_audio_callback(cursor, framesRead - framesSent);
// 5.1: FL FR C LFE SL SR, as RetroArch's speaker bits run
constexpr unsigned k51 = 0x001 | 0x002 | 0x004 | 0x008 | 0x200 | 0x400;
const size_t accepted = channels == 6
? s_multi_callback(cursor, framesRead - framesSent, 6, k51)
: s_audio_callback(cursor, framesRead - framesSent);
if (accepted == 0)
break;
framesSent += accepted;
cursor += accepted * LibretroAudioRingBuffer::kChannels;
cursor += accepted * channels;
}
AccountFlush(samplesRead, framesSent * LibretroAudioRingBuffer::kChannels);
AccountFlush(samplesRead, framesSent * channels);
}
void LibretroAudioAPI::SetStatsLogging(bool enabled)
+15 -2
View File
@@ -12,14 +12,16 @@
class LibretroAudioRingBuffer
{
public:
// ~341 ms at 48 kHz stereo. It has to hold everything AX may make between
// ~341 ms at 48 kHz 5.1 (a second in stereo). It has to hold everything AX may make between
// two retro_runs, and a retro_run grants up to 250 ms (see
// AXOut_LibretroGrantSamples). At 128 ms a slow device's frames - 8 or 9
// a second in sco8487's heavier Deus Ex scenes - got grants the ring could
// not take, and the rest was dropped. Drained every retro_run, so the
// size adds no latency.
static constexpr size_t kBufferFrames = 16384;
static constexpr size_t kChannels = 2;
// Up to 5.1; the ring holds whole frames of the output's channels, 2 or
// 6, both of which divide its size
static constexpr size_t kChannels = 6;
static constexpr size_t kBufferSamples = kBufferFrames * kChannels;
LibretroAudioRingBuffer() = default;
@@ -89,6 +91,10 @@ class LibretroAudioAPI : public IAudioAPI
public:
// Callback type matches retro_audio_sample_batch_t: returns frames actually accepted.
using AudioCallback = size_t(*)(const int16_t* data, size_t frames);
// RetroArch's multi-channel batch: frames of `channels` samples, the
// speakers in `layout` (RETRO_AUDIO_SPEAKER_*)
using MultiAudioCallback = size_t(*)(const int16_t* data, size_t frames,
unsigned channels, unsigned layout);
LibretroAudioAPI(uint32 samplerate, uint32 channels, uint32 samples_per_block, uint32 bits_per_sample);
// The ring and the flush buffer are statics that outlive any one instance,
@@ -109,6 +115,9 @@ public:
// Register the frontend's audio_batch_cb (called from retro_init).
static void SetAudioCallback(AudioCallback cb);
// What the frontend is given: 2 (stereo through the batch callback) or 6
// (5.1 through the multi-channel one). Set at load, before AX starts.
static void SetOutput(unsigned channels, MultiAudioCallback multi);
// Drain whatever was produced since the last call, push to the frontend.
// Call once per retro_run.
@@ -130,6 +139,10 @@ private:
static void ReportStats();
static AudioCallback s_audio_callback;
static MultiAudioCallback s_multi_callback;
static unsigned s_out_channels;
// One block converted to the output's channels
std::vector<int16_t> m_convert;
static LibretroAudioRingBuffer s_ring_buffer;
static std::vector<int16_t> s_flush_buffer;
+15
View File
@@ -86,6 +86,21 @@ struct retro_core_option_v2_definition option_defs_us[] = {
},
"auto"
},
{
"cemu_audio_channels",
"Audio Channels (Restart)",
NULL,
"The TV the console reports to games, as standalone's TV channels setting: games that support it mix for that many speakers. Surround 5.1 goes to RetroArch as six channels when it has multi-channel audio output, else mixed down to stereo.",
NULL,
"system",
{
{ "mono", "Mono" },
{ "stereo", "Stereo" },
{ "surround", "Surround 5.1" },
{ NULL, NULL },
},
"stereo"
},
{
"cemu_console_language",
"Console Language",