mirror of
https://github.com/libretro/parallel-gx2.git
synced 2026-11-07 12:37:54 -10:00
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:
1 parent
cea8203e38
commit
6c29741e06
5 files changed
+152
-16
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user