mirror of
https://github.com/libretro/tyrquake.git
synced 2026-11-07 12:47:53 -10:00
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:
1 parent
1def2c4e9a
commit
96fb8506c8
4 files changed
+206
-1
No files matched your search
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user