FreeRDP
Loading...
Searching...
No Matches
rdpsnd_sdl.c
1
20#include <freerdp/config.h>
21
22#include <stdlib.h>
23#include <string.h>
24
25#include <winpr/assert.h>
26#include <winpr/cast.h>
27#include <winpr/crt.h>
28#include <winpr/cmdline.h>
29#include <winpr/synch.h>
30
31#include <freerdp/types.h>
32#include <freerdp/channels/log.h>
33
34#include <SDL3/SDL.h>
35
36#include "rdpsnd_main.h"
37
38#define RDPSND_SDL_DEFAULT_LATENCY_MS 100
39#define RDPSND_SDL_MAX_LATENCY_MS 1000
40/* Queued audio beyond the latency, same slack as rdpsnd_detect_overrun(). */
41#define RDPSND_SDL_OVERRUN_SLACK_MS 200
42
43#define RDPSND_SDL_MIN_RATE 8000
44#define RDPSND_SDL_MAX_RATE 192000
45
46typedef struct
47{
48 rdpsndDevicePlugin device;
49
50 char* device_name;
51 SDL_AudioStream* stream;
52 SDL_AudioSpec spec;
53 size_t frame_size;
54 UINT32 latency;
55 UINT32 volume;
56
57 /* Play and Close run on different threads. */
59} rdpsndSdlPlugin;
60
61/* Shared by the SDL audio channels: SDL_InitSubSystem is not thread safe. */
62#define RDPSND_SDL_LOCK_PROPERTY "FreeRDP.SDL.audio.lock"
63
64static void SDLCALL rdpsnd_sdl_destroy_lock(WINPR_ATTR_UNUSED void* userdata, void* value)
65{
66 SDL_DestroyMutex(value);
67}
68
69WINPR_ATTR_NODISCARD
70static SDL_Mutex* rdpsnd_sdl_audio_lock(void)
71{
72 const SDL_PropertiesID props = SDL_GetGlobalProperties();
73 if (!props || !SDL_LockProperties(props))
74 return nullptr;
75
76 SDL_Mutex* lock = SDL_GetPointerProperty(props, RDPSND_SDL_LOCK_PROPERTY, nullptr);
77 if (!lock)
78 {
79 lock = SDL_CreateMutex();
80 /* On failure the cleanup callback already destroyed the mutex. */
81 if (lock && !SDL_SetPointerPropertyWithCleanup(props, RDPSND_SDL_LOCK_PROPERTY, lock,
82 rdpsnd_sdl_destroy_lock, nullptr))
83 lock = nullptr;
84 }
85 SDL_UnlockProperties(props);
86 return lock;
87}
88
89WINPR_ATTR_NODISCARD
90static BOOL rdpsnd_sdl_init_audio(void)
91{
92 SDL_Mutex* lock = rdpsnd_sdl_audio_lock();
93 if (!lock)
94 return FALSE;
95
96 SDL_LockMutex(lock);
97 const BOOL rc = SDL_InitSubSystem(SDL_INIT_AUDIO);
98 SDL_UnlockMutex(lock);
99 return rc;
100}
101
102static void rdpsnd_sdl_quit_audio(void)
103{
104 SDL_Mutex* lock = rdpsnd_sdl_audio_lock();
105 if (!lock)
106 WLog_WARN(TAG, "quitting SDL audio without the lock: %s", SDL_GetError());
107
108 SDL_LockMutex(lock);
109 SDL_QuitSubSystem(SDL_INIT_AUDIO);
110 SDL_UnlockMutex(lock);
111}
112
113WINPR_ATTR_NODISCARD
114static BOOL rdpsnd_sdl_format_to_spec(const AUDIO_FORMAT* format, SDL_AudioSpec* spec)
115{
116 WINPR_ASSERT(format);
117 WINPR_ASSERT(spec);
118
119 if ((format->wFormatTag != WAVE_FORMAT_PCM) || (format->nChannels < 1) ||
120 (format->nChannels > 8) || (format->nSamplesPerSec < RDPSND_SDL_MIN_RATE) ||
121 (format->nSamplesPerSec > RDPSND_SDL_MAX_RATE))
122 return FALSE;
123
124 switch (format->wBitsPerSample)
125 {
126 case 8:
127 spec->format = SDL_AUDIO_U8;
128 break;
129 case 16:
130 spec->format = SDL_AUDIO_S16LE;
131 break;
132 default:
133 return FALSE;
134 }
135
136 spec->channels = format->nChannels;
137 spec->freq = WINPR_ASSERTING_INT_CAST(int, format->nSamplesPerSec);
138 return TRUE;
139}
140
141WINPR_ATTR_NODISCARD
142static SDL_AudioDeviceID rdpsnd_sdl_find_device(const char* name)
143{
144 SDL_AudioDeviceID id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
145 if (!name)
146 return id;
147
148 int count = 0;
149 SDL_AudioDeviceID* devices = SDL_GetAudioPlaybackDevices(&count);
150 if (!devices)
151 {
152 WLog_WARN(TAG, "SDL_GetAudioPlaybackDevices: %s", SDL_GetError());
153 return id;
154 }
155
156 BOOL found = FALSE;
157 for (int x = 0; x < count; x++)
158 {
159 const char* cur = SDL_GetAudioDeviceName(devices[x]);
160 if (cur && (strcmp(cur, name) == 0))
161 {
162 id = devices[x];
163 found = TRUE;
164 break;
165 }
166 }
167 SDL_free(devices);
168
169 if (!found)
170 WLog_WARN(TAG, "playback device '%s' not found, using the default device", name);
171 return id;
172}
173
174static void rdpsnd_sdl_apply_volume(rdpsndSdlPlugin* sdl)
175{
176 WINPR_ASSERT(sdl);
177
178 if (!sdl->stream)
179 return;
180
181 /* SDL has one gain per stream, use the louder channel. */
182 const UINT32 left = sdl->volume & 0xFFFFU;
183 const UINT32 right = (sdl->volume >> 16) & 0xFFFFU;
184 const float gain = (float)MAX(left, right) / (float)0xFFFFU;
185 if (!SDL_SetAudioStreamGain(sdl->stream, gain))
186 WLog_WARN(TAG, "SDL_SetAudioStreamGain: %s", SDL_GetError());
187}
188
189WINPR_ATTR_NODISCARD
190static BOOL rdpsnd_sdl_format_supported(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format)
191{
192 WINPR_ASSERT(device);
193 WINPR_ASSERT(format);
194
195 SDL_AudioSpec spec = WINPR_C_ARRAY_INIT;
196 return rdpsnd_sdl_format_to_spec(format, &spec);
197}
198
199WINPR_ATTR_NODISCARD
200static BOOL rdpsnd_sdl_default_format(rdpsndDevicePlugin* device, const AUDIO_FORMAT* desired,
201 AUDIO_FORMAT* defaultFormat)
202{
203 WINPR_ASSERT(device);
204 WINPR_ASSERT(defaultFormat);
205
206 if (desired)
207 *defaultFormat = *desired;
208
209 defaultFormat->wFormatTag = WAVE_FORMAT_PCM;
210 defaultFormat->wBitsPerSample = 16;
211 defaultFormat->cbSize = 0;
212 defaultFormat->data = nullptr;
213
214 if ((defaultFormat->nChannels < 1) || (defaultFormat->nChannels > 8))
215 defaultFormat->nChannels = 2;
216 if ((defaultFormat->nSamplesPerSec < RDPSND_SDL_MIN_RATE) ||
217 (defaultFormat->nSamplesPerSec > RDPSND_SDL_MAX_RATE))
218 defaultFormat->nSamplesPerSec = 48000;
219
220 defaultFormat->nBlockAlign = defaultFormat->nChannels * defaultFormat->wBitsPerSample / 8;
221 defaultFormat->nAvgBytesPerSec = defaultFormat->nBlockAlign * defaultFormat->nSamplesPerSec;
222 return TRUE;
223}
224
225static void rdpsnd_sdl_close(rdpsndDevicePlugin* device)
226{
227 rdpsndSdlPlugin* sdl = (rdpsndSdlPlugin*)device;
228 WINPR_ASSERT(sdl);
229
230 EnterCriticalSection(&sdl->lock);
231 SDL_DestroyAudioStream(sdl->stream);
232 sdl->stream = nullptr;
233 LeaveCriticalSection(&sdl->lock);
234}
235
236WINPR_ATTR_NODISCARD
237static BOOL rdpsnd_sdl_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format, UINT32 latency)
238{
239 rdpsndSdlPlugin* sdl = (rdpsndSdlPlugin*)device;
240 WINPR_ASSERT(sdl);
241 WINPR_ASSERT(format);
242
243 SDL_AudioSpec spec = WINPR_C_ARRAY_INIT;
244 if (!rdpsnd_sdl_format_to_spec(format, &spec))
245 {
246 WLog_ERR(TAG, "unsupported format %s", audio_format_get_tag_string(format->wFormatTag));
247 return FALSE;
248 }
249
250 EnterCriticalSection(&sdl->lock);
251 SDL_DestroyAudioStream(sdl->stream);
252 sdl->latency =
253 (latency > 0) ? MIN(latency, RDPSND_SDL_MAX_LATENCY_MS) : RDPSND_SDL_DEFAULT_LATENCY_MS;
254 sdl->spec = spec;
255 sdl->frame_size = WINPR_ASSERTING_INT_CAST(size_t, SDL_AUDIO_FRAMESIZE(spec));
256 sdl->stream = SDL_OpenAudioDeviceStream(rdpsnd_sdl_find_device(sdl->device_name), &spec,
257 nullptr, nullptr);
258
259 BOOL rc = FALSE;
260 if (!sdl->stream)
261 WLog_ERR(TAG, "SDL_OpenAudioDeviceStream: %s", SDL_GetError());
262 else if (!SDL_ResumeAudioStreamDevice(sdl->stream))
263 WLog_ERR(TAG, "SDL_ResumeAudioStreamDevice: %s", SDL_GetError());
264 else
265 {
266 rdpsnd_sdl_apply_volume(sdl);
267 rc = TRUE;
268 }
269
270 if (!rc)
271 {
272 SDL_DestroyAudioStream(sdl->stream);
273 sdl->stream = nullptr;
274 }
275 LeaveCriticalSection(&sdl->lock);
276 return rc;
277}
278
279static void rdpsnd_sdl_free(rdpsndDevicePlugin* device)
280{
281 rdpsndSdlPlugin* sdl = (rdpsndSdlPlugin*)device;
282 if (!sdl)
283 return;
284
285 rdpsnd_sdl_close(device);
286 rdpsnd_sdl_quit_audio();
287 DeleteCriticalSection(&sdl->lock);
288 free(sdl->device_name);
289 free(sdl);
290}
291
292WINPR_ATTR_NODISCARD
293static UINT32 rdpsnd_sdl_get_volume(rdpsndDevicePlugin* device)
294{
295 rdpsndSdlPlugin* sdl = (rdpsndSdlPlugin*)device;
296 WINPR_ASSERT(sdl);
297
298 return sdl->volume;
299}
300
301WINPR_ATTR_NODISCARD
302static BOOL rdpsnd_sdl_set_volume(rdpsndDevicePlugin* device, UINT32 value)
303{
304 rdpsndSdlPlugin* sdl = (rdpsndSdlPlugin*)device;
305 WINPR_ASSERT(sdl);
306
307 EnterCriticalSection(&sdl->lock);
308 sdl->volume = value;
309 rdpsnd_sdl_apply_volume(sdl);
310 LeaveCriticalSection(&sdl->lock);
311 return TRUE;
312}
313
314/* Silence in front of a sound that starts from an empty stream. */
315WINPR_ATTR_NODISCARD
316static BOOL rdpsnd_sdl_put_silence(rdpsndSdlPlugin* sdl)
317{
318 WINPR_ASSERT(sdl);
319 WINPR_ASSERT(sdl->stream);
320
321 BYTE chunk[4096];
322 const size_t frameSize = sdl->frame_size;
323 if (frameSize == 0)
324 return FALSE;
325
326 const size_t chunkSize = sizeof(chunk) / frameSize * frameSize;
327 memset(chunk, SDL_GetSilenceValueForFormat(sdl->spec.format), sizeof(chunk));
328
329 size_t left =
330 1ULL * sdl->latency * WINPR_ASSERTING_INT_CAST(size_t, sdl->spec.freq) / 1000 * frameSize;
331 while (left > 0)
332 {
333 const size_t len = MIN(left, chunkSize);
334 if (!SDL_PutAudioStreamData(sdl->stream, chunk, WINPR_ASSERTING_INT_CAST(int, len)))
335 {
336 WLog_WARN(TAG, "SDL_PutAudioStreamData: %s", SDL_GetError());
337 return FALSE;
338 }
339 left -= len;
340 }
341 return TRUE;
342}
343
344WINPR_ATTR_NODISCARD
345static UINT rdpsnd_sdl_bytes_to_ms(const rdpsndSdlPlugin* sdl, size_t bytes)
346{
347 WINPR_ASSERT(sdl);
348
349 const UINT64 ms = 1000ULL * bytes / sdl->frame_size / (UINT64)sdl->spec.freq;
350 return WINPR_ASSERTING_INT_CAST(UINT, ms);
351}
352
353/* Called with the lock held. */
354WINPR_ATTR_NODISCARD
355static UINT rdpsnd_sdl_queue(rdpsndSdlPlugin* sdl, const BYTE* data, size_t size)
356{
357 WINPR_ASSERT(sdl);
358
359 const size_t frameSize = sdl->frame_size;
360 if (!sdl->stream || (frameSize == 0) || (sdl->spec.freq <= 0))
361 return 0;
362
363 /* SDL rejects a buffer that ends in a partial frame. */
364 size -= size % frameSize;
365 if ((size == 0) || (size > INT32_MAX))
366 return 0;
367
368 /* SDL holds a few frames back for resampling, a drained stream is not always empty. */
369 const int before = SDL_GetAudioStreamQueued(sdl->stream);
370 const size_t drained = frameSize * WINPR_ASSERTING_INT_CAST(size_t, sdl->spec.freq) / 100;
371 if ((before >= 0) && ((size_t)before < drained) && !rdpsnd_sdl_put_silence(sdl))
372 return 0;
373
374 /* A burst after a server stall would delay the sound for the rest of the session. */
375 const UINT queuedMs = (before > 0) ? rdpsnd_sdl_bytes_to_ms(sdl, (size_t)before) : 0;
376 const UINT sizeMs = rdpsnd_sdl_bytes_to_ms(sdl, size);
377 if (queuedMs > sizeMs + sdl->latency + RDPSND_SDL_OVERRUN_SLACK_MS)
378 {
379 WLog_DBG(TAG, "%" PRIu32 " ms queued, dropping %" PRIu32 " ms", queuedMs, sizeMs);
380 return queuedMs;
381 }
382
383 if (!SDL_PutAudioStreamData(sdl->stream, data, WINPR_ASSERTING_INT_CAST(int, size)))
384 {
385 WLog_ERR(TAG, "SDL_PutAudioStreamData: %s", SDL_GetError());
386 return 0;
387 }
388
389 const int queued = SDL_GetAudioStreamQueued(sdl->stream);
390 if (queued <= 0)
391 return 0;
392
393 return rdpsnd_sdl_bytes_to_ms(sdl, (size_t)queued);
394}
395
396WINPR_ATTR_NODISCARD
397static UINT rdpsnd_sdl_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size)
398{
399 rdpsndSdlPlugin* sdl = (rdpsndSdlPlugin*)device;
400 WINPR_ASSERT(sdl);
401 WINPR_ASSERT(data);
402
403 EnterCriticalSection(&sdl->lock);
404 const UINT latency = rdpsnd_sdl_queue(sdl, data, size);
405 LeaveCriticalSection(&sdl->lock);
406 return latency;
407}
408
409WINPR_ATTR_NODISCARD
410static UINT rdpsnd_sdl_parse_addin_args(rdpsndSdlPlugin* sdl, const ADDIN_ARGV* args)
411{
412 COMMAND_LINE_ARGUMENT_A rdpsnd_sdl_args[] = {
413 { "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr, "device" },
414 { nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
415 };
416 const DWORD flags =
417 COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
418
419 WINPR_ASSERT(sdl);
420 WINPR_ASSERT(args);
421
422 const int status = CommandLineParseArgumentsA(args->argc, args->argv, rdpsnd_sdl_args, flags,
423 sdl, nullptr, nullptr);
424 if (status < 0)
425 return ERROR_INVALID_DATA;
426
427 const COMMAND_LINE_ARGUMENT_A* arg = rdpsnd_sdl_args;
428 do
429 {
430 if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
431 continue;
432
433 CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
434 {
435 free(sdl->device_name);
436 sdl->device_name = _strdup(arg->Value);
437 if (!sdl->device_name)
438 return ERROR_OUTOFMEMORY;
439 }
440 CommandLineSwitchEnd(arg)
441 } while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
442
443 return CHANNEL_RC_OK;
444}
445
446FREERDP_ENTRY_POINT(UINT VCAPITYPE sdl_freerdp_rdpsnd_client_subsystem_entry(
448{
449 WINPR_ASSERT(pEntryPoints);
450 WINPR_ASSERT(pEntryPoints->pRegisterRdpsndDevice);
451
452 rdpsndSdlPlugin* sdl = calloc(1, sizeof(rdpsndSdlPlugin));
453 if (!sdl)
454 return CHANNEL_RC_NO_MEMORY;
455
456 if (!rdpsnd_sdl_init_audio())
457 {
458 WLog_ERR(TAG, "SDL_InitSubSystem(SDL_INIT_AUDIO): %s", SDL_GetError());
459 free(sdl);
460 return ERROR_INTERNAL_ERROR;
461 }
462
463 InitializeCriticalSection(&sdl->lock);
464 sdl->device.Open = rdpsnd_sdl_open;
465 sdl->device.FormatSupported = rdpsnd_sdl_format_supported;
466 sdl->device.GetVolume = rdpsnd_sdl_get_volume;
467 sdl->device.SetVolume = rdpsnd_sdl_set_volume;
468 sdl->device.Play = rdpsnd_sdl_play;
469 sdl->device.Close = rdpsnd_sdl_close;
470 sdl->device.Free = rdpsnd_sdl_free;
471 sdl->device.DefaultFormat = rdpsnd_sdl_default_format;
472 sdl->volume = 0xFFFFFFFF;
473
474 if (pEntryPoints->args)
475 {
476 const UINT rc = rdpsnd_sdl_parse_addin_args(sdl, pEntryPoints->args);
477 if (rc != CHANNEL_RC_OK)
478 {
479 WLog_ERR(TAG, "error parsing arguments");
480 rdpsnd_sdl_free(&sdl->device);
481 return rc;
482 }
483 }
484
485 pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, &sdl->device);
486 return CHANNEL_RC_OK;
487}