SDL_Libretro 0.1.0
SDL3-power libretro frontend.
Loading...
Searching...
No Matches
SDL_libretro_audio.h
Go to the documentation of this file.
1
10#if defined(SDL_LIBRETRO_IMPLEMENTATION) && !defined(SDL_LIBRETRO_AUDIO_IMPL_ONCE)
11#define SDL_LIBRETRO_AUDIO_IMPL_ONCE
12
13#ifndef SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS
20#define SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS 100
21#endif
22
23#ifndef SDL_LIBRETRO_AUDIO_DEFAULT_SAMPLE_RATE
27#define SDL_LIBRETRO_AUDIO_DEFAULT_SAMPLE_RATE 48000
28#endif
29
30#ifndef SDL_LIBRETRO_AUDIO_DRC_MAX_DELTA
38#define SDL_LIBRETRO_AUDIO_DRC_MAX_DELTA 0.005
39#endif
40
41static void SDL_Libretro_QueueAudio(SDL_Libretro* lr, const void* samples, int bytes) {
42 if (!lr->core.audioStream || bytes <= 0) return;
43
44 // DRC steers the queue toward half of audioQueueThresholdBytes, so the
45 // hard cap sits at double the threshold and only catches transient spikes
46 // DRC can't absorb. When it trips, keep whatever still fits and drop just
47 // the overflow: cutting a whole batch mid-waveform is an audible pop.
48 int cap = lr->core.audioQueueThresholdBytes * 2;
49 if (cap > 0) {
50 int queued = SDL_GetAudioStreamQueued(lr->core.audioStream);
51 if (queued >= 0 && bytes > cap - queued) {
52 int room = cap - queued;
53 if (room < 0) room = 0;
54 room -= room % (int)(sizeof(int16_t) * 2); // Keep whole frames.
55
56 // Dropping is expected back-pressure when ramping up, so this can be debug noise.
57 if (lr->core.audioDropWarnCount < 10) {
58 SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO, "[SDL_Libretro] Audio queue full with %d bytes queued, dropping %d bytes", queued, bytes - room);
59 lr->core.audioDropWarnCount++;
60 }
61
62 bytes = room;
63 if (bytes <= 0) return;
64 }
65 }
66
67 SDL_PutAudioStreamData(lr->core.audioStream, samples, bytes);
68}
69
79static void SDL_Libretro_QueueAudioReversed(SDL_Libretro* lr, const int16_t* data, size_t frames) {
80 int16_t revBuf[512 * 2];
81 size_t remaining = frames;
82 while (remaining > 0) {
83 size_t chunk = remaining > 512 ? 512 : remaining;
84 for (size_t i = 0; i < chunk; i++) {
85 size_t src = remaining - 1 - i; // Walk backwards from the end.
86 revBuf[i * 2] = data[src * 2];
87 revBuf[i * 2 + 1] = data[src * 2 + 1];
88 }
89 SDL_Libretro_QueueAudio(lr, revBuf, (int)(chunk * sizeof(int16_t) * 2));
90 remaining -= chunk;
91 }
92}
93
106static void SDL_Libretro_UpdateDRC(SDL_Libretro* lr, float speed) {
107 if (!lr || !lr->core.audioStream) return;
108
109 // There is no need to update DRC when the speed is <= 0, so leave
110 // the current ratio untouched while stopped.
111 if (speed <= 0.0f) return;
112
113 // When DRC is off, unwind any leftover nudge so the ratio tracks the
114 // plain speed instead of freezing at the last adjustment.
115 if (!lr->core.drcEnabled) {
116 if (lr->core.drcAdjustment != 1.0f) {
117 lr->core.drcAdjustment = 1.0f;
118 lr->core.drcDriftAvg = 0.0;
119 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, speed);
120 }
121 return;
122 }
123
124 // Without a valid fill target, there's nothing to tweak.
125 if (lr->core.audioQueueThresholdBytes <= 0) {
126 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, speed);
127 return;
128 }
129
130 int queued = SDL_GetAudioStreamQueued(lr->core.audioStream);
131 if (queued < 0) return; // Ignore SDL errors.
132
133 // Hold the queue near 50% fill at every speed. Consumption already tracks `speed` (ratio = speed * adj), so slow-mo and fast-forward stay balanced and the small nudge corrects residual clock drift either way. Gating this to normal speed let the queue drift and underrun (audible in slow motion, where audio bursts are spaced farther apart).
134 double drift = (double)queued / (double)lr->core.audioQueueThresholdBytes - 0.5; // > + full, - empty
135 lr->core.drcDriftAvg += 0.0625 * (drift - lr->core.drcDriftAvg);
136 double adj = 1.0 + SDL_LIBRETRO_AUDIO_DRC_MAX_DELTA * (2.0 * lr->core.drcDriftAvg);
138
139 lr->core.drcAdjustment = (float)adj;
140 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, speed * (float)adj);
141}
142
157static unsigned SDL_Libretro_UpdateAudioThreshold(SDL_Libretro* lr) {
158 unsigned latencyMs = SDL_Libretro_GetAudioLatency(lr);
159 if (lr->core.minimumAudioLatencyMs > latencyMs) latencyMs = lr->core.minimumAudioLatencyMs;
160
161 int threshold = (int)(SDL_Libretro_GetSampleRate(lr) * latencyMs / 1000.0 * (double)(sizeof(int16_t) * 2));
162
163 // Keep at least a few audio batches of headroom so back-pressure + DRC can function; a tiny latency (or a low sample rate, where each batch spans more time) would otherwise size the queue below a single batch and drop almost everything.
164 int minThreshold = 4 * (SDL_LIBRETRO_AUDIO_SINGLE_SAMPLE_BUFFER_SIZE * (int)(sizeof(int16_t) * 2));
165 lr->core.audioQueueThresholdBytes = threshold > minThreshold ? threshold : minThreshold;
166 return latencyMs;
167}
168
178static void SDL_Libretro_ReportAudioBufferStatus(SDL_Libretro* lr) {
179 if (!lr || !lr->core.audio_buffer_status.callback) return;
180
184 bool active = (lr->core.audioStream != NULL);
185
194 unsigned occupancy = 0;
195
199 bool underrunLikely = false;
200
201 if (active && lr->core.audioQueueThresholdBytes > 0) {
202 int queued = SDL_GetAudioStreamQueued(lr->core.audioStream);
203 if (queued < 0) queued = 0;
204
205 double pct = (double)queued * 100.0 / (double)lr->core.audioQueueThresholdBytes;
206 occupancy = (unsigned)(pct > 100.0 ? 100.0 : pct);
207
208 // One frame drains sampleRate/fps stereo int16 samples; if fewer than that are queued, the next frame risks starving the device.
209 double fps = lr->core.fps > 0.0 ? lr->core.fps : 60.0;
210 int frameBytes = (int)(SDL_Libretro_GetSampleRate(lr) / fps * (double)(sizeof(int16_t) * 2));
211 underrunLikely = (queued < frameBytes);
212 }
213
214 lr->core.audio_buffer_status.callback(active, occupancy, underrunLikely);
215}
216
220static bool SDL_Libretro_InitAudio(SDL_Libretro* lr) {
221 if (!SDL_Libretro_IsCoreReady(lr)) {
222 SDL_SetError("[SDL_Libretro] No core loaded");
223 return false;
224 }
225
226 SDL_Libretro_CloseAudio(lr);
227
228 SDL_Libretro_UpdateAudioThreshold(lr);
229 double sampleRate = SDL_Libretro_GetSampleRate(lr);
230
231 SDL_AudioSpec spec;
232 // Truncating a fractional core rate (e.g. the SNES's 32040.5 Hz) is fine:
233 // DRC absorbs sub-Hz drift, so don't bother rounding here.
234 spec.freq = (int)sampleRate;
235 // Match the S16 interleaved-stereo format cores deliver; SDL converts to
236 // the device format internally, so samples are pushed straight through.
237 spec.format = SDL_AUDIO_S16;
238 spec.channels = 2;
239
240 lr->core.audioStream = SDL_OpenAudioDeviceStream(
241 SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
242 &spec,
243 NULL,
244 (void*)lr);
245
246 if (!lr->core.audioStream) {
247 SDL_SetError("[SDL_Libretro] Failed to open audio device: %s", SDL_GetError());
248 return false;
249 }
250
251 // Fresh drop-warning budget for this stream (InitAudio may run again on reopen).
252 lr->core.audioDropWarnCount = 0;
253 lr->core.audioReinitPending = false;
254
255 // Dynamic Rate Control
256 lr->core.drcAdjustment = 1.0f;
257 lr->core.drcEnabled = true;
258 lr->core.drcDriftAvg = 0.0;
259
260 // A reinit can land while paused (speed 0) or rewinding (speed < 0), and
261 // SDL rejects frequency ratios outside [0.01, 100] — failing here would
262 // tear the stream back down and leave audio dead. Seed a valid ratio;
263 // SetSpeed/UpdateDRC apply the real one as soon as playback moves.
264 float ratio = SDL_fabsf(lr->core.speed) * lr->core.drcAdjustment;
265 if (ratio < 0.01f) ratio = 1.0f;
266 if (!SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, ratio)) {
267 SDL_SetError("[SDL_Libretro] Failed to set audio frequency: %s", SDL_GetError());
268 SDL_DestroyAudioStream(lr->core.audioStream);
269 lr->core.audioStream = NULL;
270 return false;
271 }
272
273 // Volume
274 SDL_SetAudioStreamGain(lr->core.audioStream, lr->volume);
275 SDL_ResumeAudioStreamDevice(lr->core.audioStream);
276
277 // Audio Callback
278 if (lr->core.audio_callback.set_state) {
279 lr->core.audio_callback.set_state(true);
280 }
281
282 SDL_Log("[SDL_Libretro] Audio initialized [%d Channels @ %d Hz]", spec.channels, spec.freq);
283
284 return true;
285}
286
287static void SDL_Libretro_CloseAudio(SDL_Libretro* lr) {
288 if (!lr) return;
289
290 // Audio Callback
291 if (lr->core.audio_callback.set_state) {
292 lr->core.audio_callback.set_state(false);
293 }
294
295 if (lr->core.audioStream) {
296 SDL_DestroyAudioStream(lr->core.audioStream);
297 lr->core.audioStream = NULL;
298 }
299}
300
314void SDL_Libretro_SetAudioLatency(SDL_Libretro* lr, unsigned latencyMs) {
315 if (!lr) return;
316 lr->audioLatencyMs = latencyMs;
317
318 // When the Audio Latency changes, make sure to update the threshold too.
319 if (lr->core.audioStream) {
320 SDL_Libretro_UpdateAudioThreshold(lr);
321 }
322}
323
332unsigned SDL_Libretro_GetAudioLatency(const SDL_Libretro* lr) {
333 if (!lr) return 0;
334 return lr->audioLatencyMs ? lr->audioLatencyMs : SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS;
335}
336
346double SDL_Libretro_GetSampleRate(const SDL_Libretro* lr) {
347 return (lr && lr->core.sampleRate > 0.0) ? lr->core.sampleRate : SDL_LIBRETRO_AUDIO_DEFAULT_SAMPLE_RATE;
348}
349
350static void SDL_Libretro_AudioSample(int16_t left, int16_t right) {
351 SDL_Libretro* lr = SDL_Libretro_active;
352 if (!lr) return;
353
354 if (lr->core.singleSampleCount >= SDL_LIBRETRO_AUDIO_SINGLE_SAMPLE_BUFFER_SIZE) {
355 SDL_Libretro_FlushSingleSamples(lr);
356 }
357
358 size_t idx = lr->core.singleSampleCount * 2;
359 lr->core.singleSampleBuffer[idx] = left;
360 lr->core.singleSampleBuffer[idx + 1] = right;
361 lr->core.singleSampleCount++;
362}
363
364static size_t SDL_Libretro_AudioSampleBatch(const int16_t* data, size_t frames) {
365 SDL_Libretro* lr = SDL_Libretro_active;
366 if (!lr || !lr->core.audioStream || frames == 0) return frames;
367
368 if (lr->core.singleSampleCount > 0) {
369 SDL_Libretro_FlushSingleSamples(lr);
370 }
371
372 // While rewinding, emit this frame's audio reversed for a clean rewind sound.
373 if (lr->rewindActive) {
374 SDL_Libretro_QueueAudioReversed(lr, data, frames);
375 return frames;
376 }
377
378 SDL_Libretro_QueueAudio(lr, data, (int)(frames * sizeof(int16_t) * 2));
379 return frames;
380}
381
382static void SDL_Libretro_FlushSingleSamples(SDL_Libretro* lr) {
383 if (!lr || lr->core.singleSampleCount == 0) return;
384
385 // While rewinding, emit the buffered frames reversed for a clean rewind sound.
386 if (lr->rewindActive) {
387 SDL_Libretro_QueueAudioReversed(lr, lr->core.singleSampleBuffer, lr->core.singleSampleCount);
388 lr->core.singleSampleCount = 0;
389 return;
390 }
391
392 SDL_Libretro_QueueAudio(lr, lr->core.singleSampleBuffer, (int)(lr->core.singleSampleCount * sizeof(int16_t) * 2));
393 lr->core.singleSampleCount = 0;
394}
395
396// Microphone
397
398#ifndef SDL_LIBRETRO_MIC_DEFAULT_RATE
404#define SDL_LIBRETRO_MIC_DEFAULT_RATE 44100
405#endif
406
407static retro_microphone_t* SDL_Libretro_MicOpen(const retro_microphone_params_t* params) {
408 SDL_Libretro* lr = SDL_Libretro_active;
409 if (!lr) return NULL;
410
411 // Only one microphone is supported. Handing back the open one would give
412 // the core two handles to the same mic, and closing either frees both.
413 if (lr->core.microphone) {
414 SDL_LogWarn(SDL_LOG_CATEGORY_AUDIO, "[SDL_Libretro] Microphone already open");
415 return NULL;
416 }
417
418 unsigned rate = (params && params->rate > 0) ? params->rate : SDL_LIBRETRO_MIC_DEFAULT_RATE;
419
420 SDL_AudioSpec spec;
421 spec.freq = (int)rate;
422 spec.format = SDL_AUDIO_S16;
423 spec.channels = 1;
424
425 SDL_AudioStream* stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, (void*)lr);
426 if (!stream) {
427 SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "[SDL_Libretro] Failed to open microphone: %s", SDL_GetError());
428 return NULL;
429 }
430
431 SDL_LibretroMicrophone* mic = (SDL_LibretroMicrophone*)SDL_calloc(1, sizeof(*mic));
432 if (!mic) {
433 SDL_DestroyAudioStream(stream);
434 SDL_OutOfMemory();
435 return NULL;
436 }
437
438 mic->stream = stream;
439 mic->rate = rate;
440 mic->active = false;
441 mic->lr = lr;
442 lr->core.microphone = mic;
443
444 SDL_Log("[SDL_Libretro] Microphone opened (%u Hz)", rate);
445 return (retro_microphone_t*)mic;
446}
447
448static void SDL_Libretro_MicClose(retro_microphone_t* microphone) {
449 SDL_LibretroMicrophone* mic = (SDL_LibretroMicrophone*)microphone;
450 if (!mic) return;
451
452 // Dereference from the core if it's actively referencing itself.
453 if (mic->lr && mic->lr->core.microphone == mic) {
454 mic->lr->core.microphone = NULL;
455 }
456
457 // Destroy the stream.
458 if (mic->stream) {
459 SDL_DestroyAudioStream(mic->stream);
460 mic->stream = NULL;
461 }
462 SDL_free(mic);
463}
464
465static bool SDL_Libretro_MicGetParams(const retro_microphone_t* microphone, retro_microphone_params_t* params) {
466 const SDL_LibretroMicrophone* mic = (const SDL_LibretroMicrophone*)microphone;
467 if (!mic || !params) return false;
468 params->rate = mic->rate;
469 return true;
470}
471
472static bool SDL_Libretro_MicSetState(retro_microphone_t* microphone, bool state) {
473 SDL_LibretroMicrophone* mic = (SDL_LibretroMicrophone*)microphone;
474 if (!mic || !mic->stream) return false;
475
476 bool ok;
477 if (state) {
478 ok = SDL_ResumeAudioStreamDevice(mic->stream);
479 }
480 else {
481 ok = SDL_PauseAudioStreamDevice(mic->stream);
482 }
483
484 // If the state changed successfully, update it accordingly.
485 if (ok) {
486 mic->active = state;
487 }
488 return ok;
489}
490
491static bool SDL_Libretro_MicGetState(const retro_microphone_t* microphone) {
492 const SDL_LibretroMicrophone* mic = (const SDL_LibretroMicrophone*)microphone;
493 if (!mic) return false;
494 return mic->active;
495}
496
497static int SDL_Libretro_MicRead(retro_microphone_t* microphone, int16_t* samples, size_t num_samples) {
498 SDL_LibretroMicrophone* mic = (SDL_LibretroMicrophone*)microphone;
499 if (!mic || !mic->stream || !samples || num_samples == 0) return 0;
500
501 int bytes = SDL_GetAudioStreamData(mic->stream, samples, (int)(num_samples * sizeof(int16_t)));
502 if (bytes < 0) return 0;
503 return bytes / (int)sizeof(int16_t);
504}
505
506static void SDL_Libretro_CloseMicrophone(SDL_Libretro* lr) {
507 if (!lr || !lr->core.microphone) return;
508 SDL_Libretro_MicClose((retro_microphone_t*)lr->core.microphone);
509 lr->core.microphone = NULL;
510}
511
512#endif /* SDL_LIBRETRO_AUDIO_IMPL_ONCE */
#define SDL_LIBRETRO_MIC_DEFAULT_RATE
The default sample rate for recording microphone input.
#define SDL_LIBRETRO_AUDIO_DRC_MAX_DELTA
The amount to push the DRC to correct audio pitch.
#define SDL_LIBRETRO_AUDIO_DEFAULT_SAMPLE_RATE
Fallback rate used when a core reports no audio sample rate (<= 0).
#define SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS
The threshold of audio latency in milliseconds.
bool SDL_Libretro_IsCoreReady(const SDL_Libretro *lr)
Indicates whether a core is loaded.
double SDL_Libretro_GetSampleRate(const SDL_Libretro *lr)
Get the audio sample rate the loaded core outputs at.
struct SDL_Libretro SDL_Libretro
A libretro frontend context: one loaded core, its game, and the settings that carry across cores.
void SDL_Libretro_SetAudioLatency(SDL_Libretro *lr, unsigned latencyMs)
Set the desired minimum audio latency for the audio stream.
unsigned SDL_Libretro_GetAudioLatency(const SDL_Libretro *lr)
Get the minimum audio latency for the audio stream.