├── .github └── FUNDING.yml ├── Makefile ├── LICENSE ├── README └── FMOD_SDL.c /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [flibitijibibo] 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for FMOD_SDL 2 | # Written by Ethan "flibitijibibo" Lee 3 | 4 | FMOD_VERSION = 13 5 | 6 | all: 7 | $(CC) -O3 -Wall -pedantic -fpic -fPIC -shared -o libfmod_SDL.so FMOD_SDL.c -lSDL3 libfmod.so.$(FMOD_VERSION) 8 | 9 | preload: 10 | $(CC) -O3 -Wall -pedantic -fpic -fPIC -shared -o libfmod_SDL.so FMOD_SDL.c -DPRELOAD_MODE -lSDL3 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* FMOD_SDL: SDL Audio Output Plugin for FMOD Studio 2 | * 3 | * Copyright (c) 2018-2021 Ethan Lee 4 | * 5 | * This software is provided 'as-is', without any express or implied warranty. 6 | * In no event will the authors be held liable for any damages arising from 7 | * the use of this software. 8 | * 9 | * Permission is granted to anyone to use this software for any purpose, 10 | * including commercial applications, and to alter it and redistribute it 11 | * freely, subject to the following restrictions: 12 | * 13 | * 1. The origin of this software must not be misrepresented; you must not 14 | * claim that you wrote the original software. If you use this software in a 15 | * product, an acknowledgment in the product documentation would be 16 | * appreciated but is not required. 17 | * 18 | * 2. Altered source versions must be plainly marked as such, and must not be 19 | * misrepresented as being the original software. 20 | * 21 | * 3. This notice may not be removed or altered from any source distribution. 22 | * 23 | * Ethan "flibitijibibo" Lee 24 | * 25 | */ 26 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This is FMOD_SDL, an SDL Audio Output Plugin for FMOD Studio. 2 | 3 | Project Website: https://github.com/flibitijibibo/FMOD_SDL/ 4 | 5 | License 6 | ------- 7 | FMOD_SDL is released under the zlib license. See LICENSE for details. 8 | 9 | About FMOD_SDL 10 | -------------- 11 | FMOD_SDL was written to replace FMOD's poor Linux audio support. It's typically 12 | built as a shared library for C# games, then shoved into the FMOD init process. 13 | 14 | Building FMOD_SDL (Developers) 15 | ------------------------------ 16 | You will need the FMOD headers for your particular version, as well as the 17 | libfmod.so.x library. (Throw out all the symlinks in the SDK and name the actual 18 | sofile using that convention.) 19 | 20 | After placing those files into this folder, simply type `make`! 21 | 22 | Using FMOD_SDL (Developers) 23 | --------------------------- 24 | There is exactly one function for this entire library: 25 | 26 | void FMOD_SDL_Register(FMOD_SYSTEM *system); 27 | 28 | `system` refers to the object acquired by `Studio::System::getLowLevelSystem()`. 29 | 30 | If you wish to use this in C#, this is the DllImport declaration: 31 | 32 | [DllImport("fmod_SDL", CallingConvention = CallingConvention.Cdecl)] 33 | public static extern void FMOD_SDL_Register(IntPtr system); 34 | 35 | Building FMOD_SDL (Players) 36 | --------------------------- 37 | Try to figure out what version of FMOD the game uses, grab the FMOD Studio SDK, 38 | and throw the header files from 'lowlevel' and 'studio' into this folder. 39 | 40 | After placing those files in this folder, simply type `make preload`! 41 | 42 | Using FMOD_SDL (Players) 43 | ------------------------ 44 | C/C++ projects can be overloaded with LD_PRELOAD, while C# projects can be 45 | overloaded by changing the existing dllmap: 46 | 47 | 48 | 49 | 50 | 51 | 52 | Check the game folder for ".dll.config" and ".exe.config" files first! This will 53 | already exist somewhere, you just need to replace the existing Linux dllmap. 54 | 55 | Found an issue? 56 | --------------- 57 | Issues and patches can be reported via GitHub: 58 | 59 | https://github.com/flibitijibibo/FMOD_SDL/issues 60 | -------------------------------------------------------------------------------- /FMOD_SDL.c: -------------------------------------------------------------------------------- 1 | /* FMOD_SDL: SDL Audio Output Plugin for FMOD Studio 2 | * 3 | * Copyright (c) 2018-2025 Ethan Lee 4 | * 5 | * This software is provided 'as-is', without any express or implied warranty. 6 | * In no event will the authors be held liable for any damages arising from 7 | * the use of this software. 8 | * 9 | * Permission is granted to anyone to use this software for any purpose, 10 | * including commercial applications, and to alter it and redistribute it 11 | * freely, subject to the following restrictions: 12 | * 13 | * 1. The origin of this software must not be misrepresented; you must not 14 | * claim that you wrote the original software. If you use this software in a 15 | * product, an acknowledgment in the product documentation would be 16 | * appreciated but is not required. 17 | * 18 | * 2. Altered source versions must be plainly marked as such, and must not be 19 | * misrepresented as being the original software. 20 | * 21 | * 3. This notice may not be removed or altered from any source distribution. 22 | * 23 | * Ethan "flibitijibibo" Lee 24 | * 25 | */ 26 | 27 | #include 28 | #include "fmod.h" 29 | #include "fmod_output.h" 30 | #ifdef PRELOAD_MODE 31 | #include "fmod_studio.h" 32 | #endif 33 | 34 | /* Public API */ 35 | 36 | #define FMOD_SDL_VERSION 250220 37 | 38 | #ifdef __cplusplus 39 | extern "C" { 40 | #endif 41 | 42 | F_EXPORT void FMOD_SDL_Register(FMOD_SYSTEM *system); 43 | 44 | #ifdef __cplusplus 45 | } 46 | #endif 47 | 48 | /* Driver Implementation */ 49 | 50 | typedef struct FMOD_SDL_Device 51 | { 52 | SDL_AudioStream *device; 53 | void *stagingBuffer; 54 | size_t stagingLen; 55 | Uint8 frameSize; 56 | } FMOD_SDL_Device; 57 | 58 | static void FMOD_SDL_MixCallback( 59 | void *userdata, 60 | SDL_AudioStream *stream, 61 | int additional_amount, 62 | int total_amount 63 | ) { 64 | FMOD_OUTPUT_STATE *output_state = (FMOD_OUTPUT_STATE*) userdata; 65 | FMOD_SDL_Device *dev = (FMOD_SDL_Device*) output_state->plugindata; 66 | while (additional_amount > 0) 67 | { 68 | if (output_state->readfrommixer( 69 | output_state, 70 | dev->stagingBuffer, 71 | dev->stagingLen / dev->frameSize 72 | ) == FMOD_OK) { 73 | SDL_PutAudioStreamData( 74 | stream, 75 | dev->stagingBuffer, 76 | dev->stagingLen 77 | ); 78 | } 79 | else 80 | { 81 | SDL_Log("readfrommixer failed early"); 82 | } 83 | additional_amount -= dev->stagingLen; 84 | } 85 | } 86 | 87 | static FMOD_RESULT F_CALLBACK FMOD_SDL_GetNumDrivers( 88 | FMOD_OUTPUT_STATE *output_state, 89 | int *numdrivers 90 | ) { 91 | SDL_free(SDL_GetAudioPlaybackDevices(numdrivers)); 92 | if (*numdrivers > 0) 93 | { 94 | *numdrivers += 1; 95 | } 96 | return FMOD_OK; 97 | } 98 | 99 | static FMOD_RESULT F_CALLBACK FMOD_SDL_GetDriverInfo( 100 | FMOD_OUTPUT_STATE *output_state, 101 | int id, 102 | char *name, 103 | int namelen, 104 | FMOD_GUID *guid, 105 | int *systemrate, 106 | FMOD_SPEAKERMODE *speakermode, 107 | int *speakermodechannels 108 | ) { 109 | const char *envvar; 110 | SDL_AudioSpec spec; 111 | int devcount; 112 | SDL_AudioDeviceID *devs; 113 | 114 | devs = SDL_GetAudioPlaybackDevices(&devcount); 115 | 116 | SDL_strlcpy( 117 | name, 118 | (id == 0) ? "SDL Default" : SDL_GetAudioDeviceName(devs[id - 1]), 119 | namelen 120 | ); 121 | 122 | SDL_memset(guid, '\0', sizeof(FMOD_GUID)); 123 | 124 | 125 | /* Environment variables take precedence over all possible values */ 126 | envvar = SDL_getenv("SDL_AUDIO_FREQUENCY"); 127 | if (envvar != NULL) 128 | { 129 | *systemrate = SDL_atoi(envvar); 130 | } 131 | else 132 | { 133 | *systemrate = 0; 134 | } 135 | envvar = SDL_getenv("SDL_AUDIO_CHANNELS"); 136 | if (envvar != NULL) 137 | { 138 | *speakermodechannels = SDL_atoi(envvar); 139 | } 140 | else 141 | { 142 | *speakermodechannels = 0; 143 | } 144 | 145 | if (id == 0) 146 | { 147 | if (!SDL_GetAudioDeviceFormat(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL)) 148 | { 149 | SDL_zero(spec); 150 | } 151 | } 152 | else 153 | { 154 | SDL_GetAudioDeviceFormat(devs[id - 1], &spec, NULL); 155 | } 156 | SDL_free(devs); 157 | if ((spec.freq > 0) && (*systemrate <= 0)) 158 | { 159 | *systemrate = spec.freq; 160 | } 161 | if ((spec.channels > 0) && (*speakermodechannels <= 0)) 162 | { 163 | *speakermodechannels = spec.channels; 164 | } 165 | 166 | /* If we make it all the way here with no format, hardcode a sane one */ 167 | if (*systemrate <= 0) 168 | { 169 | *systemrate = 48000; 170 | } 171 | if (*speakermodechannels <= 0) 172 | { 173 | *speakermodechannels = 2; 174 | } 175 | 176 | switch (*speakermodechannels) 177 | { 178 | #define SPEAKERS(count, type) \ 179 | case count: *speakermode = FMOD_SPEAKERMODE_##type; break; 180 | SPEAKERS(1, MONO) 181 | SPEAKERS(2, STEREO) 182 | SPEAKERS(4, QUAD) 183 | SPEAKERS(5, SURROUND) 184 | SPEAKERS(6, 5POINT1) 185 | SPEAKERS(8, 7POINT1) 186 | SPEAKERS(12, 7POINT1POINT4) 187 | #undef SPEAKERS 188 | default: 189 | SDL_Log("Unrecognized speaker layout!"); 190 | return FMOD_ERR_OUTPUT_FORMAT; 191 | } 192 | 193 | return FMOD_OK; 194 | } 195 | 196 | static FMOD_RESULT F_CALLBACK FMOD_SDL_Init( 197 | FMOD_OUTPUT_STATE *output_state, 198 | int selecteddriver, 199 | FMOD_INITFLAGS flags, 200 | int *outputrate, 201 | FMOD_SPEAKERMODE *speakermode, 202 | int *speakermodechannels, 203 | FMOD_SOUND_FORMAT *outputformat, 204 | int dspbufferlength, 205 | #if FMOD_VERSION >= 0x00020203 206 | int *dspnumbuffers, 207 | int *dspnumadditionalbuffers, 208 | #else 209 | int dspnumbuffers, 210 | #endif 211 | void *extradriverdata 212 | ) { 213 | FMOD_SDL_Device *device; 214 | SDL_AudioDeviceID devID; 215 | SDL_AudioSpec spec; 216 | const char *envvar; 217 | 218 | /* Before we start: Replicate FMOD's PulseAudio stream name support: 219 | * https://www.fmod.org/questions/question/how-to-set-pulseaudio-program-name/ 220 | */ 221 | if (extradriverdata != NULL) 222 | { 223 | const char *streamname = (char*) extradriverdata; 224 | SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_ICON_NAME, streamname); 225 | } 226 | 227 | if (selecteddriver == 0) 228 | { 229 | devID = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; 230 | } 231 | else 232 | { 233 | int devcount; 234 | SDL_AudioDeviceID *devs = SDL_GetAudioPlaybackDevices(&devcount); 235 | 236 | /* Bounds checking is done before this function is called */ 237 | devID = devs[selecteddriver - 1]; 238 | 239 | SDL_free(devs); 240 | } 241 | if (!SDL_GetAudioDeviceFormat(devID, &spec, NULL)) 242 | { 243 | SDL_zero(spec); 244 | 245 | envvar = SDL_getenv("SDL_AUDIO_FREQUENCY"); 246 | if (envvar != NULL) 247 | { 248 | spec.freq = SDL_atoi(envvar); 249 | } 250 | envvar = SDL_getenv("SDL_AUDIO_CHANNELS"); 251 | if (envvar != NULL) 252 | { 253 | spec.channels = SDL_atoi(envvar); 254 | } 255 | } 256 | 257 | /* What do we want? */ 258 | if (*outputrate > 0) 259 | { 260 | spec.freq = *outputrate; 261 | } 262 | if (*speakermodechannels > 0) 263 | { 264 | spec.channels = *speakermodechannels; 265 | } 266 | switch (*outputformat) 267 | { 268 | #define FORMAT(fmod, sdl) \ 269 | case FMOD_SOUND_FORMAT_##fmod: spec.format = SDL_AUDIO_##sdl; break; 270 | FORMAT(PCM8, S8) 271 | FORMAT(PCM16, S16) 272 | FORMAT(PCM32, S32) 273 | FORMAT(PCMFLOAT, F32) 274 | #undef FORMAT 275 | default: 276 | SDL_Log("Unsupported FMOD PCM format!"); 277 | return FMOD_ERR_OUTPUT_FORMAT; 278 | } 279 | 280 | /* Create the device, finally. */ 281 | device = (FMOD_SDL_Device*) SDL_malloc( 282 | sizeof(FMOD_SDL_Device) 283 | ); 284 | device->device = SDL_OpenAudioDeviceStream( 285 | devID, 286 | &spec, 287 | FMOD_SDL_MixCallback, 288 | output_state 289 | ); 290 | if (device->device == NULL) 291 | { 292 | SDL_free(device); 293 | SDL_Log("SDL_OpenAudioDeviceStream failed: %s", SDL_GetError()); 294 | return FMOD_ERR_OUTPUT_INIT; 295 | } 296 | 297 | /* What did we get? */ 298 | *outputrate = spec.freq; 299 | *speakermodechannels = spec.channels; 300 | switch (spec.channels) 301 | { 302 | #define SPEAKERS(count, type) \ 303 | case count: *speakermode = FMOD_SPEAKERMODE_##type; break; 304 | SPEAKERS(1, MONO) 305 | SPEAKERS(2, STEREO) 306 | SPEAKERS(4, QUAD) 307 | SPEAKERS(5, SURROUND) 308 | SPEAKERS(6, 5POINT1) 309 | SPEAKERS(8, 7POINT1) 310 | SPEAKERS(12, 7POINT1POINT4) 311 | #undef SPEAKERS 312 | default: 313 | SDL_DestroyAudioStream(device->device); 314 | SDL_free(device); 315 | SDL_Log("Unrecognized speaker layout!"); 316 | return FMOD_ERR_OUTPUT_INIT; 317 | } 318 | switch (spec.format) 319 | { 320 | #define FORMAT(sdl, fmod, size) \ 321 | case SDL_AUDIO_##sdl: \ 322 | *outputformat = FMOD_SOUND_FORMAT_##fmod; \ 323 | device->frameSize = size; \ 324 | break; 325 | FORMAT(S8, PCM8, 1) 326 | FORMAT(S16, PCM16, 2) 327 | FORMAT(S32, PCM32, 4) 328 | FORMAT(F32, PCMFLOAT, 4) 329 | #undef FORMAT 330 | default: 331 | SDL_DestroyAudioStream(device->device); 332 | SDL_free(device); 333 | SDL_Log("Unexpected SDL audio format!"); 334 | return FMOD_ERR_OUTPUT_INIT; 335 | } 336 | device->frameSize *= spec.channels; 337 | 338 | device->stagingLen = dspbufferlength * device->frameSize; 339 | device->stagingBuffer = SDL_malloc(device->stagingLen); 340 | if (device->stagingBuffer == NULL) 341 | { 342 | SDL_DestroyAudioStream(device->device); 343 | SDL_free(device); 344 | return FMOD_ERR_OUTPUT_INIT; 345 | } 346 | 347 | /* We're ready to go! */ 348 | output_state->plugindata = device; 349 | return FMOD_OK; 350 | } 351 | 352 | static FMOD_RESULT F_CALLBACK FMOD_SDL_Start(FMOD_OUTPUT_STATE *output_state) 353 | { 354 | FMOD_SDL_Device *dev = (FMOD_SDL_Device*) 355 | output_state->plugindata; 356 | SDL_ResumeAudioStreamDevice(dev->device); 357 | return FMOD_OK; 358 | } 359 | 360 | static FMOD_RESULT F_CALLBACK FMOD_SDL_Stop(FMOD_OUTPUT_STATE *output_state) 361 | { 362 | FMOD_SDL_Device *dev = (FMOD_SDL_Device*) 363 | output_state->plugindata; 364 | SDL_PauseAudioStreamDevice(dev->device); 365 | return FMOD_OK; 366 | } 367 | 368 | static FMOD_RESULT F_CALLBACK FMOD_SDL_Close(FMOD_OUTPUT_STATE *output_state) 369 | { 370 | FMOD_SDL_Device *dev = (FMOD_SDL_Device*) 371 | output_state->plugindata; 372 | SDL_DestroyAudioStream(dev->device); 373 | SDL_free(dev->stagingBuffer); 374 | SDL_free(dev); 375 | return FMOD_OK; 376 | } 377 | 378 | static FMOD_OUTPUT_DESCRIPTION FMOD_SDL_Driver = 379 | { 380 | FMOD_OUTPUT_PLUGIN_VERSION, 381 | "FMOD_SDL", 382 | FMOD_SDL_VERSION, 383 | #if FMOD_VERSION >= 0x00020000 384 | FMOD_OUTPUT_METHOD_MIX_DIRECT, /* We have our own thread! */ 385 | #else 386 | 0, 387 | #endif 388 | FMOD_SDL_GetNumDrivers, 389 | FMOD_SDL_GetDriverInfo, 390 | FMOD_SDL_Init, 391 | FMOD_SDL_Start, 392 | FMOD_SDL_Stop, 393 | FMOD_SDL_Close, 394 | NULL, /* Does anyone really want the native handle? */ 395 | NULL, /* We have our own thread! */ 396 | NULL, /* We have our own thread! */ 397 | NULL, /* We have our own thread! */ 398 | NULL, /* We have our own thread! */ 399 | NULL, /* 3D object hardware...? */ 400 | NULL, /* 3D object hardware...? */ 401 | NULL, /* Auxiliary ports...? */ 402 | NULL /* Auxiliary ports...? */ 403 | #if FMOD_VERSION >= 0x00020000 404 | , NULL /* FIXME: AUDIODEVICE events? */ 405 | #endif 406 | }; 407 | 408 | /* Public API Implementation */ 409 | 410 | #ifndef PRELOAD_MODE 411 | F_EXPORT void FMOD_SDL_Register(FMOD_SYSTEM *system) 412 | { 413 | unsigned int handle; 414 | if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) 415 | { 416 | SDL_Log("SDL_INIT_AUDIO failed: %s", SDL_GetError()); 417 | return; 418 | } 419 | FMOD_System_RegisterOutput(system, &FMOD_SDL_Driver, &handle); 420 | FMOD_System_SetOutputByPlugin(system, handle); 421 | } 422 | #else 423 | typedef FMOD_RESULT (*studioSystemCreateFunc)( 424 | FMOD_STUDIO_SYSTEM **system, 425 | unsigned int headerVersion 426 | ); 427 | typedef FMOD_RESULT (*studioSystemGetCoreFunc)( 428 | FMOD_STUDIO_SYSTEM *system, 429 | FMOD_SYSTEM **coreSystem 430 | ); 431 | typedef FMOD_RESULT (*systemRegisterOutputFunc)( 432 | FMOD_SYSTEM *system, 433 | FMOD_OUTPUT_DESCRIPTION *description, 434 | unsigned int *handle 435 | ); 436 | typedef FMOD_RESULT (*systemSetOutputByPluginFunc)( 437 | FMOD_SYSTEM *system, 438 | unsigned int handle 439 | ); 440 | FMOD_RESULT F_API FMOD_Studio_System_Create( 441 | FMOD_STUDIO_SYSTEM **system, 442 | unsigned int headerVersion 443 | ) { 444 | void* fmodlib; 445 | char fmodname[32]; 446 | FMOD_RESULT result; 447 | unsigned int handle; 448 | FMOD_SYSTEM *core = NULL; 449 | studioSystemCreateFunc studioSystemCreate; 450 | studioSystemGetCoreFunc studioSystemGetCore; 451 | systemRegisterOutputFunc systemRegisterOutput; 452 | systemSetOutputByPluginFunc systemSetOutputByPlugin; 453 | 454 | /* Can't mix up versions, ABI breakages urrywhur */ 455 | SDL_Log( 456 | "headerVersion: %X FMOD_VERSION: %X", 457 | headerVersion, 458 | FMOD_VERSION 459 | ); 460 | SDL_assert(headerVersion == FMOD_VERSION); 461 | 462 | #define LOAD_FUNC(var, func) \ 463 | var = (var##Func) SDL_LoadFunction(fmodlib, func); 464 | 465 | /* FMOD Studio entry points */ 466 | SDL_snprintf( 467 | fmodname, 468 | sizeof(fmodname), 469 | #if FMOD_VERSION >= 0x00020000 470 | "libfmodstudio.so.11" /* FIXME: FMOD screwed up their sonames! */ 471 | #else 472 | "libfmodstudio.so.%X", (headerVersion >> 8) & 0xFF 473 | #endif 474 | ); 475 | fmodlib = SDL_LoadObject(fmodname); 476 | LOAD_FUNC(studioSystemCreate, "FMOD_Studio_System_Create") 477 | #if FMOD_VERSION >= 0x00020000 478 | LOAD_FUNC(studioSystemGetCore, "FMOD_Studio_System_GetCoreSystem") 479 | #else 480 | /* Technically not the right name anymore, but whatever... */ 481 | LOAD_FUNC(studioSystemGetCore, "FMOD_Studio_System_GetLowLevelSystem") 482 | #endif 483 | 484 | /* Overloaded function */ 485 | result = studioSystemCreate(system, headerVersion); 486 | if (result != FMOD_OK) 487 | { 488 | return result; 489 | } 490 | result = studioSystemGetCore(*system, &core); 491 | if (result != FMOD_OK) 492 | { 493 | return result; 494 | } 495 | /* mono needs this to leak :| SDL_UnloadObject(fmodlib); */ 496 | 497 | /* FMOD entry points */ 498 | SDL_snprintf( 499 | fmodname, 500 | sizeof(fmodname), 501 | "libfmod.so.%X", 502 | (headerVersion >> 8) & 0xFF 503 | ); 504 | fmodlib = SDL_LoadObject(fmodname); 505 | LOAD_FUNC(systemRegisterOutput, "FMOD_System_RegisterOutput") 506 | LOAD_FUNC(systemSetOutputByPlugin, "FMOD_System_SetOutputByPlugin") 507 | 508 | /* FMOD_SDL_Register */ 509 | if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) 510 | { 511 | SDL_Log("SDL_INIT_AUDIO failed: %s", SDL_GetError()); 512 | return FMOD_OK; 513 | } 514 | systemRegisterOutput(core, &FMOD_SDL_Driver, &handle); 515 | systemSetOutputByPlugin(core, handle); 516 | /* mono needs this to leak :| SDL_UnloadObject(fmodlib); */ 517 | 518 | #undef LOAD_FUNC 519 | 520 | /* We out. */ 521 | SDL_Log("FMOD_SDL is registered!"); 522 | return FMOD_OK; 523 | } 524 | #endif 525 | --------------------------------------------------------------------------------