#include "globalincs/pstypes.h" #include "sound/openal.h" #include "osapi/osregistry.h" #include "options/Option.h" #include "parse/parselo.h" #include "libs/jansson.h" #include #include #ifdef _WIN32 #define VC_EXTRALEAN #include #endif // Stupid windows workaround... #ifdef MessageBox #undef MessageBox #endif static SCP_string Playback_device; static SCP_string Capture_device; enum { OAL_DEVICE_GENERIC, OAL_DEVICE_DEFAULT, OAL_DEVICE_USER }; typedef struct OALdevice { SCP_string device_name; int type; bool usable; OALdevice() : type(OAL_DEVICE_GENERIC), usable(false) { } OALdevice(const char *name) : device_name(name), type(OAL_DEVICE_GENERIC), usable(false) { } } OALdevice; static SCP_vector PlaybackDevices; static SCP_vector CaptureDevices; // enumeration extension #ifndef ALC_ALL_DEVICES_SPECIFIER #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif // List of audio device names for the in-game option SCP_vector PlaybackDeviceList; static int playbackdevice_deserializer(const json_t* value) { const char* device; json_error_t err; if (json_unpack_ex((json_t*)value, &err, 0, "s", &device) != 0) { throw json_exception(err); } int id = openal_find_playback_device_by_name(device); if (SCP_vector_inbounds(PlaybackDeviceList, id)) { return id; } return -1; } static json_t* playbackdevice_serializer(int value) { if (!SCP_vector_inbounds(PlaybackDeviceList, value)) { return json_pack("s", ""); } return json_pack("s", PlaybackDeviceList[value].c_str()); } static SCP_vector playbackdevice_enumerator() { SCP_vector vals; for (int i = 0; i < static_cast(PlaybackDeviceList.size()); ++i) { vals.push_back(i); } return vals; } static SCP_string playbackdevice_display(int id) { if (!SCP_vector_inbounds(PlaybackDeviceList, id)) { return ""; } SCP_string out; sprintf(out, "(%d) %s", id + 1, PlaybackDeviceList[id].c_str()); return out; } static bool playbackdevice_change(int /*device*/, bool initial) { if (initial) { return false; // On game boot always return false } // Looking at ds.cpp's ds_init(), it's probably possible to not require a restart for this // but for now let's keep this simple and just restart. If/when this is implemented the device // parameter will be required. Currently playing audio will need to be copied from the old device // to the new as part of the initialization. This will require deeper changes to the audio code // which can be handled later in a follow-up PR. return false; } // coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton static auto PlaybackDeviceOption = options::OptionBuilder("Audio.PlaybackDevice", std::pair{"Playback Device", 1834}, std::pair{"The device used for audio playback", 1835}) .category(std::make_pair("Audio", 1826)) .level(options::ExpertLevel::Beginner) .deserializer(playbackdevice_deserializer) .serializer(playbackdevice_serializer) .enumerator(playbackdevice_enumerator) .display(playbackdevice_display) .flags({options::OptionFlags::ForceMultiValueSelection}) .default_val(0) .change_listener(playbackdevice_change) .importance(99) .finish(); // List of audio device names for the in-game option SCP_vector CaptureDeviceList; static int capturedevice_deserializer(const json_t* value) { const char* device; json_error_t err; if (json_unpack_ex((json_t*)value, &err, 0, "s", &device) != 0) { throw json_exception(err); } int id = openal_find_capture_device_by_name(device); if (SCP_vector_inbounds(CaptureDeviceList, id)) { return id; } return -1; } static json_t* capturedevice_serializer(int value) { if (!SCP_vector_inbounds(CaptureDeviceList, value)) { return json_pack("s", ""); } return json_pack("s", CaptureDeviceList[value].c_str()); } static SCP_vector capturedevice_enumerator() { SCP_vector vals; for (int i = 0; i < static_cast(CaptureDeviceList.size()); ++i) { vals.push_back(i); } return vals; } static SCP_string capturedevice_display(int id) { if (!SCP_vector_inbounds(CaptureDeviceList, id)) { return ""; } SCP_string out; sprintf(out, "(%d) %s", id + 1, CaptureDeviceList[id].c_str()); return out; } static bool capturedevice_change(int /*device*/, bool initial) { if (initial) { return false; // On game boot always return false } // Looking at ds.cpp's ds_init(), it's probably possible to not require a restart for this // but for now let's keep this simple and just restart. If/when this is implemented the device // parameter will be required. Currently playing audio will need to be copied from the old device // to the new as part of the initialization. This will require deeper changes to the audio code // which can be handled later in a follow-up PR. return false; } // coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton static auto CaptureDeviceOption = options::OptionBuilder("Audio.CaptureDevice", std::pair{"Capture Device", 1836}, std::pair{"The device used for audio capture", 1837}) .category(std::make_pair("Audio", 1826)) .level(options::ExpertLevel::Beginner) .deserializer(capturedevice_deserializer) .serializer(capturedevice_serializer) .enumerator(capturedevice_enumerator) .display(capturedevice_display) .flags({options::OptionFlags::ForceMultiValueSelection}) .default_val(0) .change_listener(capturedevice_change) .importance(99) .finish(); //-------------------------------------------------------------------------- // openal_error_string() // // Returns the human readable error string if there is an error or NULL if not // const char *openal_error_string(int get_alc) { int i; if (get_alc) { ALCdevice *device = alcGetContextsDevice( alcGetCurrentContext() ); i = alcGetError(device); if ( i != ALC_NO_ERROR ) return (const char*) alcGetString(device, i); } else { i = alGetError(); if ( i != AL_NO_ERROR ) return (const char*)alGetString(i); } return NULL; } ALenum openal_get_format(ALint bits, ALint n_channels) { ALenum format = AL_INVALID_VALUE; if ( (n_channels < 1) || (n_channels > 2) ) { return format; } switch (bits) { case 32: format = (n_channels == 1) ? AL_FORMAT_MONO_FLOAT32 : AL_FORMAT_STEREO_FLOAT32; break; case 16: format = (n_channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; break; case 8: format = (n_channels == 1) ? AL_FORMAT_MONO8 : AL_FORMAT_STEREO8; break; } return format; } static bool openal_device_sort_func(const OALdevice &d1, const OALdevice &d2) { if (d1.type > d2.type) { return true; } return false; } static void find_playback_device(OpenALInformation* info) { // First, build a list of device names for the in-game option to use and pull from for (auto& device : info->playback_devices) { OALdevice new_device(device.c_str()); PlaybackDeviceList.push_back(new_device.device_name); } const char* user_device = os_config_read_string("Sound", "PlaybackDevice", nullptr); if (Using_in_game_options) { if (SCP_vector_inbounds(PlaybackDeviceList, PlaybackDeviceOption->getValue())) { user_device = PlaybackDeviceList[PlaybackDeviceOption->getValue()].c_str(); } } const char *default_device = info->default_playback_device.c_str(); // in case they are the same, we only want to test it once if ( (user_device && default_device) && !strcmp(user_device, default_device) ) { user_device = NULL; } for (auto& device : info->playback_devices) { OALdevice new_device(device.c_str()); if (user_device && !strcmp(device.c_str(), user_device)) { new_device.type = OAL_DEVICE_USER; } else if (default_device && !strcmp(device.c_str(), default_device)) { new_device.type = OAL_DEVICE_DEFAULT; } PlaybackDevices.push_back( std::move(new_device) ); } if ( PlaybackDevices.empty() ) { return; } std::sort( PlaybackDevices.begin(), PlaybackDevices.end(), openal_device_sort_func ); ALCdevice *device = NULL; ALCcontext *context = NULL; // for each device that we have available, try and figure out which to use for (size_t idx = 0; idx < PlaybackDevices.size(); idx++) { OALdevice *pdev = &PlaybackDevices[idx]; // open our specfic device device = alcOpenDevice( (const ALCchar*) pdev->device_name.c_str() ); if (device == NULL) { continue; } context = alcCreateContext(device, NULL); if (context == NULL) { alcCloseDevice(device); continue; } alcMakeContextCurrent(context); alcGetError(device); // check how many sources we can create static const int MIN_SOURCES = 48; // MAX_CHANNELS + 16 spare int si = 0; for (si = 0; si < MIN_SOURCES; si++) { ALuint source_id = 0; alGenSources(1, &source_id); if (alGetError() != AL_NO_ERROR) { break; } alDeleteSources(1, &source_id); } if (si == MIN_SOURCES) { // ok, it supports our minimum requirements pdev->usable = true; // need this for the future Playback_device = pdev->device_name; // done break; } else { // clean up for next pass alcMakeContextCurrent(NULL); alcDestroyContext(context); alcCloseDevice(device); context = NULL; device = NULL; } } alcMakeContextCurrent(NULL); if (context) { alcDestroyContext(context); } if (device) { alcCloseDevice(device); } } static void find_capture_device(OpenALInformation* info) { // First, build a list of device names for the in-game option to use and pull from for (auto& device : info->capture_devices) { OALdevice new_device(device.c_str()); CaptureDeviceList.push_back(new_device.device_name); } const char* user_device = os_config_read_string("Sound", "CaptureDevice", nullptr); if (Using_in_game_options) { if (SCP_vector_inbounds(CaptureDevices, CaptureDeviceOption->getValue())) { user_device = CaptureDevices[CaptureDeviceOption->getValue()].device_name.c_str(); } } const char *default_device = info->default_capture_device.c_str(); // in case they are the same, we only want to test it once if ( (user_device && default_device) && !strcmp(user_device, default_device) ) { user_device = NULL; } for (auto& device : info->capture_devices) { OALdevice new_device(device.c_str()); if (user_device && !strcmp(device.c_str(), user_device)) { new_device.type = OAL_DEVICE_USER; } else if (default_device && !strcmp(device.c_str(), default_device)) { new_device.type = OAL_DEVICE_DEFAULT; } CaptureDevices.push_back( std::move(new_device) ); } if ( CaptureDevices.empty() ) { return; } std::sort( CaptureDevices.begin(), CaptureDevices.end(), openal_device_sort_func ); // for each device that we have available, try and figure out which to use for (size_t idx = 0; idx < CaptureDevices.size(); idx++) { const ALCchar *device_name = CaptureDevices[idx].device_name.c_str(); ALCdevice *device = alcCaptureOpenDevice(device_name, 22050, AL_FORMAT_MONO8, 22050 * 2); if (device == NULL) { continue; } if (alcGetError(device) != ALC_NO_ERROR) { alcCaptureCloseDevice(device); continue; } // ok, we should be good with this one Capture_device = CaptureDevices[idx].device_name; alcCaptureCloseDevice(device); break; } } // find a playback device's vector index int openal_find_playback_device_by_name(const SCP_string& device) { for (int i = 0; i < static_cast(PlaybackDeviceList.size()); i++) { if (!stricmp(PlaybackDeviceList[i].c_str(), device.c_str())) { return i; } } return -1; } // find a capture device's vector index int openal_find_capture_device_by_name(const SCP_string& device) { for (int i = 0; i < static_cast(CaptureDeviceList.size()); i++) { if (!stricmp(CaptureDeviceList[i].c_str(), device.c_str())) { return i; } } return -1; } // initializes hardware device from perferred/default/enumerated list bool openal_init_device(SCP_string *playback, SCP_string *capture) { if ( !Playback_device.empty() ) { if (playback) { *playback = Playback_device; } if (capture) { *capture = Capture_device; } return true; } if (playback) { playback->erase(); } if (capture) { capture->erase(); } // This reuses the code for the launcher to make sure everything is consistent auto platform_info = openal_get_platform_information(); if (platform_info.version_major <= 1 && platform_info.version_minor < 1) { os::dialogs::Message(os::dialogs::MESSAGEBOX_ERROR, "OpenAL 1.1 or newer is required for proper operation. On Linux and Windows OpenAL Soft is recommended. If you are on Mac OS X you need to upgrade your OS."); return false; } // go through and find out what devices we actually want to use ... find_playback_device(&platform_info); find_capture_device(&platform_info); if ( Playback_device.empty() ) { return false; } #ifndef NDEBUG if ( !PlaybackDevices.empty() ) { nprintf(("OpenAL", " Available Playback Devices:\n")); for (size_t idx = 0; idx < PlaybackDevices.size(); idx++) { nprintf(("OpenAL", " %s", PlaybackDevices[idx].device_name.c_str())); if (PlaybackDevices[idx].type == OAL_DEVICE_USER) { nprintf(("OpenAL", " *preferred*\n")); } else if (PlaybackDevices[idx].type == OAL_DEVICE_DEFAULT) { nprintf(("OpenAL", " *default*\n")); } else { nprintf(("OpenAL", "\n")); } } } if ( !CaptureDevices.empty() ) { if ( !PlaybackDevices.empty() ) { nprintf(("OpenAL", "\n")); } nprintf(("OpenAL", " Available Capture Devices:\n")); for (size_t idx = 0; idx < CaptureDevices.size(); idx++) { nprintf(("OpenAL", " %s", CaptureDevices[idx].device_name.c_str())); if (CaptureDevices[idx].type == OAL_DEVICE_USER) { nprintf(("OpenAL", " *preferred*\n")); } else if (CaptureDevices[idx].type == OAL_DEVICE_DEFAULT) { nprintf(("OpenAL", " *default*\n")); } else { nprintf(("OpenAL", "\n")); } } nprintf(("OpenAL", "\n")); } #endif // cleanup PlaybackDevices.clear(); CaptureDevices.clear(); if (playback) { *playback = Playback_device; } if (capture) { *capture = Capture_device; } return true; } static void get_version_info(OpenALInformation* info) { // initialize default setup first, for version check... ALCdevice *device = alcOpenDevice(NULL); if (device == NULL) { return; } ALCcontext *context = alcCreateContext(device, NULL); if (context == NULL) { alcCloseDevice(device); return; } alcMakeContextCurrent(context); // version check (for 1.0 or 1.1) ALCint AL_minor_version = 0; ALCint AL_major_version = 0; alcGetIntegerv(NULL, ALC_MAJOR_VERSION, sizeof(ALCint), &AL_major_version); alcGetIntegerv(NULL, ALC_MINOR_VERSION, sizeof(ALCint), &AL_minor_version); info->version_major = static_cast(AL_major_version); info->version_minor = static_cast(AL_minor_version); alcGetError(device); // close default device alcMakeContextCurrent(NULL); alcDestroyContext(context); alcCloseDevice(device); } static bool device_supports_efx(const char* device_name) { ALCdevice *device = alcOpenDevice(device_name); if (device == NULL) { return false; } bool result = false; if ( alcIsExtensionPresent(device, (const ALchar*)"ALC_EXT_EFX") == AL_TRUE ) { result = true; } alcCloseDevice(device); return result; } static void enumerate_playback_devices(OpenALInformation* info) { info->playback_devices.clear(); info->default_playback_device.clear(); auto default_device = alcGetString( NULL, ALC_DEFAULT_DEVICE_SPECIFIER ); if (default_device != nullptr) { info->default_playback_device = default_device; info->efx_support.push_back(std::make_pair(SCP_string(default_device), device_supports_efx(default_device))); } if ( alcIsExtensionPresent(NULL, (const ALCchar*)"ALC_ENUMERATION_EXT") == AL_TRUE ) { const char *all_devices = NULL; if ( alcIsExtensionPresent(NULL, (const ALCchar*)"ALC_ENUMERATE_ALL_EXT") == AL_TRUE ) { all_devices = (const char*) alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER); } else { all_devices = (const char*) alcGetString(NULL, ALC_DEVICE_SPECIFIER); } const char *str_list = all_devices; size_t ext_length = 0; if ( (str_list != NULL) && ((ext_length = strlen(str_list)) > 0) ) { while (ext_length) { info->playback_devices.push_back( SCP_string(str_list) ); info->efx_support.push_back(std::make_pair(SCP_string(str_list), device_supports_efx(str_list))); str_list += (ext_length + 1); ext_length = strlen(str_list); } } } else { if (default_device) { info->playback_devices.push_back(SCP_string(default_device)); } } } static void enumerate_capture_devices(OpenALInformation* info) { info->capture_devices.clear(); info->default_capture_device.clear(); auto default_device = alcGetString( NULL, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER ); if (default_device != nullptr) { info->default_capture_device = default_device; } if ( alcIsExtensionPresent(NULL, (const ALCchar*)"ALC_ENUMERATION_EXT") == AL_TRUE ) { const char *all_devices = (const char*) alcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER); const char *str_list = all_devices; size_t ext_length = 0; if ( (str_list != NULL) && ((ext_length = strlen(str_list)) > 0) ) { while (ext_length) { info->capture_devices.push_back( SCP_string(str_list)); str_list += (ext_length + 1); ext_length = strlen(str_list); } } } else { if (default_device) { info->capture_devices.push_back(SCP_string(default_device)); } } } OpenALInformation openal_get_platform_information() { OpenALInformation info; get_version_info(&info); enumerate_playback_devices(&info); enumerate_capture_devices(&info); return info; }