SDL_Libretro
SDL3-power libretro frontend.
Loading...
Searching...
No Matches
SDL_libretro_audio.h
Go to the documentation of this file.
1
9#if defined(SDL_LIBRETRO_IMPLEMENTATION) && !defined(SDL_LIBRETRO_AUDIO_IMPL_ONCE)
10#define SDL_LIBRETRO_AUDIO_IMPL_ONCE
11
12#ifndef SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS
19#define SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS 100
20#endif
21
22#ifndef SDL_LIBRETRO_AUDIO_DEFAULT_SAMPLE_RATE
26#define SDL_LIBRETRO_AUDIO_DEFAULT_SAMPLE_RATE 48000
27#endif
28
29#ifndef SDL_LIBRETRO_AUDIO_DRC_MAX_DELTA
37#define SDL_LIBRETRO_AUDIO_DRC_MAX_DELTA 0.005
38#endif
39
40static void SDL_Libretro_QueueAudio(SDL_Libretro* lr, const float* samples, int bytes) {
41 if (!lr->core.audioStream || bytes <= 0) return;
42
43 int queued = SDL_GetAudioStreamQueued(lr->core.audioStream);
44 if (queued >= lr->core.audioQueueThresholdBytes) {
45 // Dropping is expected back-pressure when ramping up, so this can be debug noise.
46 if (lr->core.audioDropWarnCount < 10) {
47 SDL_LogDebug(SDL_LOG_CATEGORY_AUDIO,
48 "[SDL_Libretro] Audio queue full with %d bytes queued, dropping %d bytes",
49 queued, bytes);
50 lr->core.audioDropWarnCount++;
51 }
52 return;
53 }
54
55 SDL_PutAudioStreamData(lr->core.audioStream, samples, bytes);
56}
57
68static void SDL_Libretro_QueueAudioReversed(SDL_Libretro* lr, const int16_t* data, size_t frames) {
69 float convBuf[512 * 2];
70 size_t remaining = frames;
71 while (remaining > 0) {
72 size_t chunk = remaining > 512 ? 512 : remaining;
73 for (size_t i = 0; i < chunk; i++) {
74 size_t src = remaining - 1 - i; /* walk backward from the batch tail */
75 convBuf[i * 2] = (float)data[src * 2] * (1.0f / 32768.0f);
76 convBuf[i * 2 + 1] = (float)data[src * 2 + 1] * (1.0f / 32768.0f);
77 }
78 SDL_Libretro_QueueAudio(lr, convBuf, (int)(chunk * sizeof(float) * 2));
79 remaining -= chunk;
80 }
81}
82
94static void SDL_Libretro_UpdateDRC(SDL_Libretro* lr, float speed) {
95 if (!lr || !lr->core.audioStream || !lr->core.drcEnabled) return;
96
97 // Without a valid fill target, there's nothing to tweak.
98 if (lr->core.audioQueueThresholdBytes <= 0) {
99 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, speed);
100 return;
101 }
102
103 int queued = SDL_GetAudioStreamQueued(lr->core.audioStream);
104 if (queued < 0) return; // Ignore SDL errors.
105
106 // 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).
107 double drift = (double)queued / (double)lr->core.audioQueueThresholdBytes - 0.5; // > + full, - empty
108 lr->core.drcDriftAvg += 0.0625 * (drift - lr->core.drcDriftAvg);
109 double adj = 1.0 + SDL_LIBRETRO_AUDIO_DRC_MAX_DELTA * (2.0 * lr->core.drcDriftAvg);
111
112 lr->core.drcAdjustment = (float)adj;
113 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, speed * (float)adj);
114}
115
123static unsigned SDL_Libretro_UpdateAudioThreshold(SDL_Libretro* lr) {
124 unsigned latencyMs = lr->core.minimumAudioLatencyMs;
125 if (latencyMs == 0) latencyMs = SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS;
126
127 int threshold = (int)(SDL_Libretro_GetSampleRate(lr) * latencyMs / 1000.0 * (double)(sizeof(float) * 2));
128
129 // 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.
130 int minThreshold = 4 * (SDL_LIBRETRO_AUDIO_SINGLE_SAMPLE_BUFFER_SIZE * (int)(sizeof(float) * 2));
131 lr->core.audioQueueThresholdBytes = threshold > minThreshold ? threshold : minThreshold;
132 return latencyMs;
133}
134
144static void SDL_Libretro_ReportAudioBufferStatus(SDL_Libretro* lr) {
145 if (!lr || !lr->core.audio_buffer_status.callback) return;
146
150 bool active = (lr->core.audioStream != NULL);
151
159 unsigned occupancy = 0;
160
164 bool underrunLikely = false;
165
166 if (active && lr->core.audioQueueThresholdBytes > 0) {
167 int queued = SDL_GetAudioStreamQueued(lr->core.audioStream);
168 if (queued < 0) queued = 0;
169
170 double pct = (double)queued * 100.0 / (double)lr->core.audioQueueThresholdBytes;
171 occupancy = (unsigned)(pct > 100.0 ? 100.0 : pct);
172
173 // One frame drains sampleRate/fps stereo float samples; if fewer than that are queued, the next frame risks starving the device.
174 double fps = lr->core.fps > 0.0 ? lr->core.fps : 60.0;
175 int frameBytes = (int)(SDL_Libretro_GetSampleRate(lr) / fps * (double)(sizeof(float) * 2));
176 underrunLikely = (queued < frameBytes);
177 }
178
179 lr->core.audio_buffer_status.callback(active, occupancy, underrunLikely);
180}
181
185static bool SDL_Libretro_InitAudio(SDL_Libretro* lr) {
186 if (!lr || !lr->core.loaded) {
187 SDL_SetError("[SDL_Libretro] No core loaded");
188 return false;
189 }
190
191 SDL_Libretro_CloseAudio(lr);
192
193 unsigned latencyMs = SDL_Libretro_UpdateAudioThreshold(lr);
194 double sampleRate = SDL_Libretro_GetSampleRate(lr);
195
196 SDL_AudioSpec spec;
197 spec.freq = (int)sampleRate;
198 spec.format = SDL_AUDIO_F32;
199 spec.channels = 2;
200
201 lr->core.audioStream = SDL_OpenAudioDeviceStream(
202 SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
203 &spec,
204 NULL,
205 (void*)lr);
206
207 if (!lr->core.audioStream) {
208 SDL_SetError("[SDL_Libretro] Failed to open audio device: %s", SDL_GetError());
209 return false;
210 }
211
212 // Fresh drop-warning budget for this stream (InitAudio may run again on reopen).
213 lr->core.audioDropWarnCount = 0;
214 lr->core.audioReinitPending = false;
215
216 // Dynamic Rate Control
217 lr->core.drcAdjustment = 1.0f;
218 lr->core.drcEnabled = true;
219 lr->core.drcDriftAvg = 0.0;
220 if (!SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, lr->speed * lr->core.drcAdjustment)) {
221 SDL_SetError("[SDL_Libretro] Failed to set audio frequency: %s", SDL_GetError());
222 SDL_DestroyAudioStream(lr->core.audioStream);
223 lr->core.audioStream = NULL;
224 return false;
225 }
226
227 // Volume
228 SDL_SetAudioStreamGain(lr->core.audioStream, lr->volume);
229 SDL_ResumeAudioStreamDevice(lr->core.audioStream);
230
231 // Audio Callback
232 if (lr->core.audio_callback.set_state) {
233 lr->core.audio_callback.set_state(true);
234 }
235
236 SDL_Log("[SDL_Libretro] Audio initialized [%d Channels @ %d Hz]", spec.channels, spec.freq);
237
238 return true;
239}
240
241static void SDL_Libretro_CloseAudio(SDL_Libretro* lr) {
242 if (!lr) return;
243
244 // Audio Callback
245 if (lr->core.audio_callback.set_state) {
246 lr->core.audio_callback.set_state(false);
247 }
248
249 if (lr->core.audioStream) {
250 SDL_DestroyAudioStream(lr->core.audioStream);
251 lr->core.audioStream = NULL;
252 }
253}
254
260void SDL_Libretro_SetAudioLatency(SDL_Libretro* lr, unsigned latencyMs) {
261 if (!lr) return;
262 lr->core.minimumAudioLatencyMs = latencyMs;
263
264 // When the Audio Latency changes, make sure to update the threshold too.
265 if (lr->core.audioStream) {
266 SDL_Libretro_UpdateAudioThreshold(lr);
267 }
268}
269
270unsigned SDL_Libretro_GetAudioLatency(const SDL_Libretro* lr) {
271 if (!lr) return 0;
272 return lr->core.minimumAudioLatencyMs ? lr->core.minimumAudioLatencyMs : SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS;
273}
274
275double SDL_Libretro_GetSampleRate(const SDL_Libretro* lr) {
276 return lr ? lr->core.sampleRate : SDL_LIBRETRO_AUDIO_DEFAULT_SAMPLE_RATE;
277}
278
279static void SDL_Libretro_AudioSample(int16_t left, int16_t right) {
280 SDL_Libretro* lr = SDL_Libretro_active;
281 if (!lr) return;
282
283 if (lr->core.singleSampleCount >= SDL_LIBRETRO_AUDIO_SINGLE_SAMPLE_BUFFER_SIZE) {
284 SDL_Libretro_FlushSingleSamples(lr);
285 }
286
287 size_t idx = lr->core.singleSampleCount * 2;
288 lr->core.singleSampleBuffer[idx] = left;
289 lr->core.singleSampleBuffer[idx + 1] = right;
290 lr->core.singleSampleCount++;
291}
292
293static size_t SDL_Libretro_AudioSampleBatch(const int16_t* data, size_t frames) {
294 SDL_Libretro* lr = SDL_Libretro_active;
295 if (!lr || !lr->core.audioStream || frames == 0) return frames;
296
297 if (lr->core.singleSampleCount > 0) {
298 SDL_Libretro_FlushSingleSamples(lr);
299 }
300
301 // While rewinding, emit this frame's audio reversed for a clean rewind sound.
302 if (lr->rewindActive) {
303 SDL_Libretro_QueueAudioReversed(lr, data, frames);
304 return frames;
305 }
306
307 float convBuf[512 * 2];
308 size_t written = 0;
309 while (written < frames) {
310 size_t chunk = frames - written;
311 if (chunk > 512) chunk = 512;
312
313 for (size_t i = 0; i < chunk * 2; i++) {
314 convBuf[i] = (float)data[written * 2 + i] * (1.0f / 32768.0f);
315 }
316
317 SDL_Libretro_QueueAudio(lr, convBuf, (int)(chunk * sizeof(float) * 2));
318 written += chunk;
319 }
320
321 return frames;
322}
323
324static void SDL_Libretro_FlushSingleSamples(SDL_Libretro* lr) {
325 if (!lr || lr->core.singleSampleCount == 0) return;
326
327 // While rewinding, emit the buffered frames reversed for a clean rewind sound.
328 if (lr->rewindActive) {
329 SDL_Libretro_QueueAudioReversed(lr, lr->core.singleSampleBuffer, lr->core.singleSampleCount);
330 lr->core.singleSampleCount = 0;
331 return;
332 }
333
334 float convBuf[SDL_LIBRETRO_AUDIO_SINGLE_SAMPLE_BUFFER_SIZE * 2];
335 for (size_t i = 0; i < lr->core.singleSampleCount * 2; i++) {
336 convBuf[i] = (float)lr->core.singleSampleBuffer[i] * (1.0f / 32768.0f);
337 }
338
339 SDL_Libretro_QueueAudio(lr, convBuf,
340 (int)(lr->core.singleSampleCount * sizeof(float) * 2));
341 lr->core.singleSampleCount = 0;
342}
343
344// Microphone
345
346#ifndef SDL_LIBRETRO_MIC_DEFAULT_RATE
352#define SDL_LIBRETRO_MIC_DEFAULT_RATE 44100
353#endif
354
355static retro_microphone_t* SDL_Libretro_MicOpen(const retro_microphone_params_t* params) {
356 SDL_Libretro* lr = SDL_Libretro_active;
357 if (!lr) return NULL;
358
359 if (lr->core.microphone) {
360 SDL_LogWarn(SDL_LOG_CATEGORY_AUDIO, "[SDL_libretro] Microphone already open");
361 return (retro_microphone_t*)lr->core.microphone;
362 }
363
364 unsigned rate = (params && params->rate > 0) ? params->rate : SDL_LIBRETRO_MIC_DEFAULT_RATE;
365
366 SDL_AudioSpec spec;
367 spec.freq = (int)rate;
368 spec.format = SDL_AUDIO_S16;
369 spec.channels = 1;
370
371 SDL_AudioStream* stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, (void*)lr);
372 if (!stream) {
373 SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "SDL_libretro: Failed to open microphone: %s", SDL_GetError());
374 return NULL;
375 }
376
377 SDL_LibretroMicrophone* mic = (SDL_LibretroMicrophone*)SDL_calloc(1, sizeof(*mic));
378 if (!mic) {
379 SDL_DestroyAudioStream(stream);
380 SDL_OutOfMemory();
381 return NULL;
382 }
383
384 mic->stream = stream;
385 mic->rate = rate;
386 mic->active = false;
387 mic->lr = lr;
388 lr->core.microphone = mic;
389
390 SDL_Log("SDL_libretro: Microphone opened (%u Hz)", rate);
391 return (retro_microphone_t*)mic;
392}
393
394static void SDL_Libretro_MicClose(retro_microphone_t* microphone) {
396 if (!mic) return;
397
398 // Dereference from the core if it's actively referencing itself.
399 if (mic->lr && mic->lr->core.microphone == mic) {
400 mic->lr->core.microphone = NULL;
401 }
402
403 // Destroy the stream.
404 if (mic->stream) {
405 SDL_DestroyAudioStream(mic->stream);
406 mic->stream = NULL;
407 }
408 SDL_free(mic);
409}
410
411static bool SDL_Libretro_MicGetParams(const retro_microphone_t* microphone, retro_microphone_params_t* params) {
412 const SDL_LibretroMicrophone* mic = (const SDL_LibretroMicrophone*)microphone;
413 if (!mic || !params) return false;
414 params->rate = mic->rate;
415 return true;
416}
417
418static bool SDL_Libretro_MicSetState(retro_microphone_t* microphone, bool state) {
420 if (!mic || !mic->stream) return false;
421
422 bool ok;
423 if (state) {
424 ok = SDL_ResumeAudioStreamDevice(mic->stream);
425 }
426 else {
427 ok = SDL_PauseAudioStreamDevice(mic->stream);
428 }
429
430 // If the state changed successfully, update it accordingly.
431 if (ok) {
432 mic->active = state;
433 }
434 return ok;
435}
436
437static bool SDL_Libretro_MicGetState(const retro_microphone_t* microphone) {
438 const SDL_LibretroMicrophone* mic = (const SDL_LibretroMicrophone*)microphone;
439 if (!mic) return false;
440 return mic->active;
441}
442
443static int SDL_Libretro_MicRead(retro_microphone_t* microphone, int16_t* samples, size_t num_samples) {
445 if (!mic || !mic->stream || !samples || num_samples == 0) return 0;
446
447 int bytes = SDL_GetAudioStreamData(mic->stream, samples, (int)(num_samples * sizeof(int16_t)));
448 if (bytes < 0) return 0;
449 return bytes / (int)sizeof(int16_t);
450}
451
452static void SDL_Libretro_CloseMicrophone(SDL_Libretro* lr) {
453 if (!lr || !lr->core.microphone) return;
454 SDL_Libretro_MicClose((retro_microphone_t*)lr->core.microphone);
455 lr->core.microphone = NULL;
456}
457
458#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.
void SDL_Libretro_SetAudioLatency(SDL_Libretro *lr, unsigned latencyMs)
Set the desired minimum audio latency for the audio stream.
SDL_Libretro * lr
Whether or not the micropohne device is active.
bool active
The sample rate.