sound: optional float audio output via GET_AUDIO_SAMPLE_BATCH_FLOAT

Negotiate float audio output once at retro_load_game time through the
experimental RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT callback.
When the frontend answers true, the engine transfers the paintbuffer as
normalized float [-1,1] (Snd_WriteLinearBlastStereoFloat /
S_TransferStereoFloat) and audio_step pushes it through the float batch
callback; otherwise everything falls back to the existing int16 path
unchanged. Negotiation happens once per loaded game and the format is
never mixed; older frontends return false and keep the int16 path
transparently.

The paintbuffer accumulator carries 8 fractional bits, so normalizing by
256*32768 preserves sub-int16 precision the >>8 int16 blast would
otherwise discard. tyrquake's int16 blast hard-saturates rather than
soft-clipping, so the float blast hard-clamps to [-1,1] to keep the two
paths tonally identical -- verified float == int16/32768 to within one
LSB across a full accumulator sweep, with matching full-scale and
over-range clip behaviour.
This commit is contained in:
libretroadmin authored and libretroadmin committed 2026-06-29 23:29:27 +02:00
1 parent 1def2c4e9a
commit 96fb8506c8
4 files changed
+206 -1

No files matched your search

+74
View File
@@ -153,6 +153,11 @@ static uint16_t audio_samplerate = AUDIO_SAMPLERATE_DEFAULT;
* straight to audio_batch_cb. No ring, no wrap. */
static int16_t audio_out_buffer[AUDIO_BUFFER_SIZE];
/* Float output buffer, used only when float output was negotiated. The engine
* writes normalized float [-1,1] here (snd_float_buffer points at it) instead
* of audio_out_buffer; audio_step then pushes it via audio_batch_cb_float. */
static float audio_out_buffer_f[AUDIO_BUFFER_SIZE];
/* Initial cap on stereo frames per audio_batch_cb
* invocation. RetroArch typically accepts up to
* ~4096 per call; if the frontend partial-writes
@@ -362,6 +367,13 @@ retro_video_refresh_t video_cb;
* retro_set_audio_sample below -- we accept the
* callback registration but throw it away. */
static retro_audio_sample_batch_t audio_batch_cb;
/* Float audio output, negotiated once in retro_load_game via
* RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT. use_float_output stays 0
* (and audio_batch_cb_float NULL) on any frontend that doesn't support it,
* which keeps the deterministic int16 path. s_float_output / snd_float_buffer
* live in snd_mix.c and are declared in sound.h (included above). */
static retro_audio_sample_batch_float_t audio_batch_cb_float = NULL;
static int use_float_output = 0;
retro_environment_t environ_cb;
static retro_input_poll_t poll_cb;
static retro_input_state_t input_cb;
@@ -1422,6 +1434,26 @@ bool retro_load_game(const struct retro_game_info *info)
* backends, not just the Vulkan one. */
vid.numpages = 0x40000000;
/* Negotiate float audio output once, now that the game is loaded and the
* audio path is up (Host_Init -> SNDDMA_Init ran above). If the frontend
* supports it we commit to float for this game's lifetime; otherwise the
* int16 path is used unchanged. Contract: negotiate once per loaded game,
* never mix formats. Older frontends return false here, so they keep the
* int16 path transparently. */
use_float_output = 0;
audio_batch_cb_float = NULL;
{
struct retro_audio_sample_float_callback fcb;
fcb.batch = NULL;
if (environ_cb(RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT, &fcb)
&& fcb.batch)
{
audio_batch_cb_float = fcb.batch;
use_float_output = 1;
}
}
s_float_output = use_float_output;
return true;
}
@@ -1429,6 +1461,13 @@ bool retro_load_game(const struct retro_game_info *info)
void retro_unload_game(void)
{
/* Tear down float-output negotiation; a fresh retro_load_game re-negotiates
* against whatever frontend loads the next game. */
audio_batch_cb_float = NULL;
use_float_output = 0;
s_float_output = 0;
snd_float_buffer = NULL;
rhi_shutdown();
}
@@ -1816,6 +1855,35 @@ static void audio_step(void)
* the unwritten tail will be retried in the next
* loop iteration. */
audio_frames_remaining = audio_samplerate / framerate;
/* Float output path: push the engine's normalized-float buffer through the
* negotiated float batch callback. Identical chunking / partial-write
* backpressure to the int16 path below -- only the buffer type and the
* callback differ. */
if (use_float_output)
{
float *audio_out_ptr_f = audio_out_buffer_f;
do
{
unsigned audio_frames_to_write =
(audio_frames_remaining > audio_batch_frames_max) ?
audio_batch_frames_max : audio_frames_remaining;
unsigned audio_frames_written =
audio_batch_cb_float(audio_out_ptr_f, audio_frames_to_write);
if (audio_frames_written == 0)
break; /* frontend can't accept any more this frame */
if (audio_frames_written < audio_frames_to_write)
audio_batch_frames_max = audio_frames_written;
audio_frames_remaining -= audio_frames_written;
audio_out_ptr_f += audio_frames_written << 1;
}
while (audio_frames_remaining > 0);
return;
}
audio_out_ptr = audio_out_buffer;
do
{
@@ -1853,6 +1921,12 @@ qboolean SNDDMA_Init(dma_t *dma)
* that buffer straight to audio_batch_cb. */
shm->buffer = (unsigned char *volatile)audio_out_buffer;
/* Float output buffer shares the same one-frame linear layout. Hand the
* engine its pointer; s_float_output is (re)asserted from the negotiation
* result so a reload picks up the current frontend's capability. */
snd_float_buffer = audio_out_buffer_f;
s_float_output = use_float_output;
return true;
}
+51 -1
View File
@@ -64,6 +64,16 @@ static portable_samplepair_t paintbuffer[PAINTBUFFER_SIZE];
static int *snd_p, snd_linear_count;
short *snd_out;
/* Float output path (see RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT in
* the libretro layer). When s_float_output is set the paintbuffer is
* transferred as normalized float [-1,1] into snd_float_buffer instead of the
* int16 shm->buffer; both point at libretro-layer buffers. snd_out_f mirrors
* snd_out for the float blast. These stay 0/NULL on any frontend that does
* not negotiate float output, so the deterministic int16 path is unchanged. */
int s_float_output = 0;
float *snd_float_buffer = NULL;
static float *snd_out_f;
static int snd_vol;
static void Snd_WriteLinearBlastStereo16 (void)
@@ -144,6 +154,43 @@ static void S_TransferStereo16 (int dst_offset, int count)
Snd_WriteLinearBlastStereo16();
}
/* Float counterpart of Snd_WriteLinearBlastStereo16. The paintbuffer
* accumulator carries 8 fractional bits (the int16 blast does snd_p[i] >> 8),
* so normalizing by 256*32768 keeps that sub-int16 precision instead of
* discarding it. tyrquake's int16 blast hard-saturates (clamp to
* 0x8000..0x7fff) rather than soft-clipping, so the float blast hard-clamps to
* [-1,1] to keep float and int16 output tonally identical: the float path only
* removes the int16 quantization (and the frontend's int16->float widen), it
* does not reshape the signal. */
#define SND_FLOAT_NORM (1.0f / 8388608.0f) /* 1 / (256 * 32768) */
static void Snd_WriteLinearBlastStereoFloat (void)
{
int i;
float f;
for (i = 0; i < snd_linear_count; i++)
{
f = (float)snd_p[i] * SND_FLOAT_NORM;
if (f > 1.0f)
f = 1.0f;
else if (f < -1.0f)
f = -1.0f;
snd_out_f[i] = f;
}
}
/* Float counterpart of S_TransferStereo16. Writes into snd_float_buffer (a
* libretro-layer float buffer) at the same linear dst_offset the int16 path
* uses for shm->buffer. Only reached when float output was negotiated. */
static void S_TransferStereoFloat (int dst_offset, int count)
{
snd_p = (int*)paintbuffer;
snd_out_f = snd_float_buffer + (dst_offset << 1);
snd_linear_count = count << 1;
Snd_WriteLinearBlastStereoFloat();
}
/*
===============================================================================
@@ -283,7 +330,10 @@ void S_PaintChannels (int samples_to_paint)
}
/* transfer out according to DMA format */
S_TransferStereo16(painted, chunk);
if (s_float_output && snd_float_buffer)
S_TransferStereoFloat(painted, chunk);
else
S_TransferStereo16(painted, chunk);
painted += chunk;
}
}
+7
View File
@@ -204,6 +204,13 @@ extern int snd_blocked;
#define MAX_RAW_SAMPLES 8192
extern portable_samplepair_t s_rawsamples[MAX_RAW_SAMPLES];
/* Float audio output state, owned by snd_mix.c and driven by the libretro
* layer's RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT negotiation. When
* s_float_output is set the engine transfers the paintbuffer as normalized
* float [-1,1] into snd_float_buffer; otherwise the int16 path is used. */
extern int s_float_output;
extern float *snd_float_buffer;
void S_LocalSound(const char *s);
sfxcache_t *S_LoadSound(sfx_t *s);
+74
View File
@@ -2603,6 +2603,46 @@ enum retro_mod
*/
#define RETRO_ENVIRONMENT_GET_NETPLAY_CLIENT_INDEX (82 | RETRO_ENVIRONMENT_EXPERIMENTAL)
/**
* Queries whether the frontend can accept audio samples in 32-bit
* native-endian IEEE-754 float format, and, if so, obtains a float
* sample-batch callback the core may use in place of the standard
* int16 \c retro_audio_sample_batch_t callback.
*
* Rationale: frontend resamplers and DSP chains operate on float, and
* most modern audio drivers expose a native float output path. A core
* whose audio is float-native (e.g. one with a float software mixer or
* a float decoder) currently has to squash its output down to int16 at
* the libretro boundary, only for the frontend to immediately widen it
* back to float. Negotiating float output here removes that redundant
* int16<->float round-trip on both sides of the boundary.
*
* On success the frontend sets \c batch in the supplied
* \c retro_audio_sample_float_callback. The core may call that function
* from within \c retro_run() (or from the audio callback registered via
* \c RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK), passing interleaved stereo
* frames of float samples normalized to the range [-1.0, 1.0]. The
* return value has the same meaning as \c retro_audio_sample_batch_t.
*
* Contract:
* - The core must commit to a single output format for the lifetime of
* a loaded game; it must not mix int16 and float batch calls. Perform
* negotiation once, during \c retro_load_game() (after any
* \c RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK call).
* - If this returns \c false the core must keep using the int16
* \c retro_audio_sample_batch_t / \c retro_audio_sample_t callbacks.
* - The \c batch function pointer is owned by the frontend and remains
* valid until \c retro_unload_game().
* - Frontends that do not recognize this call return \c false, so older
* frontends transparently keep the int16 path.
*
* @param[out] data <tt>struct retro_audio_sample_float_callback *</tt>.
* @return \c true if float audio output is supported, \c false otherwise.
* @see retro_audio_sample_batch_float_t
* @see retro_audio_sample_float_callback
*/
#define RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT (85 | RETRO_ENVIRONMENT_EXPERIMENTAL)
/**@}*/
/**
@@ -7488,6 +7528,40 @@ typedef void (RETRO_CALLCONV *retro_audio_sample_t)(int16_t left, int16_t right)
typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_t)(const int16_t *data,
size_t frames);
/**
* Renders multiple audio frames in one go, in float format.
*
* This is the float counterpart of \c retro_audio_sample_batch_t. It is
* only valid after the frontend has answered \c true to
* \c RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT, and must not be
* mixed with the int16 callbacks within the same loaded game.
*
* @param data A pointer to interleaved stereo float sample frames,
* normalized to the range [-1.0, 1.0]. One frame is a left/right
* pair, e.g. <tt>float buf[4] = { l, r, l, r };</tt> is 2 frames.
* @param frames The number of frames represented in \c data.
*
* @return The number of frames that were processed.
*
* @see RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT
* @see retro_audio_sample_batch_t
*/
typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_float_t)(
const float *data, size_t frames);
/**
* Float audio sample-batch callback handed to the core in response to
* \c RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT.
*
* @see RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT
*/
struct retro_audio_sample_float_callback
{
/* Set by the frontend. The core calls this instead of the int16
* batch callback once float output has been negotiated. */
retro_audio_sample_batch_float_t batch;
};
/**
* Polls input.
*