From a92334b76856edf6334e6aa01f9879521bc4d88d Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 23 Sep 2019 01:26:28 -0400 Subject: [PATCH] Fix microphone volume level control. Previously, the "My Voice" control in the options GUI did nothing. The old plWinMicLevel code was originally for the WinMM library from 1991 (!!!). This rewrites all that junk to use the Windows Core Audio library upon which WinMM sits as of Windows Vista. When using OpenAL Soft, the primary backend is WASAPI, a Core Audio component. Further, when adjusting the level in Uru, you can observe the level changing in the Windows mixer as well. --- .../pfConsole/pfConsoleCommands.cpp | 8 +- .../FeatureLib/pfPython/pyAudioControl.cpp | 14 +- .../Plasma/PubUtilLib/plAudio/CMakeLists.txt | 4 +- .../plAudio/plAudioEndpointVolume.cpp | 355 ++++++++++++++++ ...lWinMicLevel.h => plAudioEndpointVolume.h} | 74 ++-- .../PubUtilLib/plAudio/plAudioSystem.cpp | 92 +++- .../Plasma/PubUtilLib/plAudio/plAudioSystem.h | 23 + .../plAudio/plAudioSystem_Private.h | 3 + .../PubUtilLib/plAudio/plWinMicLevel.cpp | 397 ------------------ 9 files changed, 506 insertions(+), 464 deletions(-) create mode 100644 Sources/Plasma/PubUtilLib/plAudio/plAudioEndpointVolume.cpp rename Sources/Plasma/PubUtilLib/plAudio/{plWinMicLevel.h => plAudioEndpointVolume.h} (51%) delete mode 100644 Sources/Plasma/PubUtilLib/plAudio/plWinMicLevel.cpp diff --git a/Sources/Plasma/FeatureLib/pfConsole/pfConsoleCommands.cpp b/Sources/Plasma/FeatureLib/pfConsole/pfConsoleCommands.cpp index 373c1e1efa..861c101dd0 100644 --- a/Sources/Plasma/FeatureLib/pfConsole/pfConsoleCommands.cpp +++ b/Sources/Plasma/FeatureLib/pfConsole/pfConsoleCommands.cpp @@ -77,7 +77,6 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "plSurface/plLayerOr.h" #include "plAudio/plAudioSystem.h" #include "plAudio/plVoiceChat.h" -#include "plAudio/plWinMicLevel.h" #include "plPipeline/plFogEnvironment.h" #include "plPipeline/plPlates.h" #include "plPipeline/plDynamicEnvMap.h" @@ -3429,11 +3428,8 @@ PF_CONSOLE_CMD( Audio, IsolateSound, PF_CONSOLE_CMD( Audio, SetMicVolume, "float volume", "Sets the microphone volume, in the range of 0 to 1" ) { - if( !plWinMicLevel::CanSetLevel() ) - PrintString( "Unable to set microphone level" ); - else - { - plWinMicLevel::SetLevel( (float)params[ 0 ] ); + if (!plgAudioSys::SetCaptureVolume((float)params[0])) { + PrintString("Unable to set microphone level"); } } diff --git a/Sources/Plasma/FeatureLib/pfPython/pyAudioControl.cpp b/Sources/Plasma/FeatureLib/pfPython/pyAudioControl.cpp index 6c94b66bb2..0fdaf4937f 100644 --- a/Sources/Plasma/FeatureLib/pfPython/pyAudioControl.cpp +++ b/Sources/Plasma/FeatureLib/pfPython/pyAudioControl.cpp @@ -51,7 +51,6 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "plAudio/plAudioSystem.h" #include "plAudio/plVoiceChat.h" -#include "plAudio/plWinMicLevel.h" // Sets the master volume of a given audio channel void pyAudioControl::SetSoundFXVolume(float volume) @@ -252,26 +251,19 @@ bool pyAudioControl::IsMuted() const //------------------------ // Voice Settings -// Sets the microphone volume, in the range of 0 to 1 bool pyAudioControl::CanSetMicLevel() const { - return plWinMicLevel::CanSetLevel(); + return plgAudioSys::CanChangeCaptureVolume(); } void pyAudioControl::SetMicLevel(float level) { - // make sure the volume is within range - if( level > 1.f ) - level = 1.f; - else if( level < 0.f ) - level = 0.f; - if( CanSetMicLevel() ) - plWinMicLevel::SetLevel( level ); + plgAudioSys::SetCaptureVolume(level); } float pyAudioControl::GetMicLevel() const { - return plWinMicLevel::GetLevel(); + return plgAudioSys::GetCaptureVolume(); } diff --git a/Sources/Plasma/PubUtilLib/plAudio/CMakeLists.txt b/Sources/Plasma/PubUtilLib/plAudio/CMakeLists.txt index b17270a913..4a5a83a21c 100644 --- a/Sources/Plasma/PubUtilLib/plAudio/CMakeLists.txt +++ b/Sources/Plasma/PubUtilLib/plAudio/CMakeLists.txt @@ -16,6 +16,7 @@ if(PLASMA_USE_SPEEX) endif() set(plAudio_SOURCES + plAudioEndpointVolume.cpp plAudioSystem.cpp plDSoundBuffer.cpp plEAXEffects.cpp @@ -28,12 +29,12 @@ set(plAudio_SOURCES plWin32Sound.cpp plWin32StaticSound.cpp plWin32StreamingSound.cpp - plWinMicLevel.cpp plWin32VideoSound.cpp ) set(plAudio_HEADERS plAudioCreatable.h + plAudioEndpointVolume.h plAudioSystem.h plAudioSystem_Private.h plDSoundBuffer.h @@ -48,7 +49,6 @@ set(plAudio_HEADERS plWin32Sound.h plWin32StaticSound.h plWin32StreamingSound.h - plWinMicLevel.h plWin32VideoSound.h ) diff --git a/Sources/Plasma/PubUtilLib/plAudio/plAudioEndpointVolume.cpp b/Sources/Plasma/PubUtilLib/plAudio/plAudioEndpointVolume.cpp new file mode 100644 index 0000000000..8bbecfe3bb --- /dev/null +++ b/Sources/Plasma/PubUtilLib/plAudio/plAudioEndpointVolume.cpp @@ -0,0 +1,355 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#include "plAudioEndpointVolume.h" +#include "plStatusLog/plStatusLog.h" +#include + +extern ST::string kDefaultDeviceMagic; + +#if defined(HS_BUILD_FOR_WIN32) + +#include +#include +#include +#include +#include + +class hsCOMInit +{ +public: + hsCOMInit() + { + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + hsAssert(SUCCEEDED(hr), "COM failed to init???"); + } + + ~hsCOMInit() + { + CoUninitialize(); + } +} s_COMInit; + +class plWinCoreAudioEndpointVolume : public plAudioEndpointVolume +{ + IMMDevice* fDevice; + + IAudioEndpointVolume* GetVolumeCtrl() const; + +public: + plWinCoreAudioEndpointVolume(); + ~plWinCoreAudioEndpointVolume(); + + float GetVolume() const HS_OVERRIDE; + bool SetDefaultDevice(plAudioEndpointType endpoint) HS_OVERRIDE; + bool SetDevice(plAudioEndpointType endpoint, const ST::string& deviceName) HS_OVERRIDE; + bool SetVolume(float fct) HS_OVERRIDE; + bool Supported() const HS_OVERRIDE; +}; + +// ============================================================================= + +plWinCoreAudioEndpointVolume::plWinCoreAudioEndpointVolume() + : fDevice() +{ +} + +plWinCoreAudioEndpointVolume::~plWinCoreAudioEndpointVolume() +{ + if (fDevice) + fDevice->Release(); +} + +template +static inline void ICOMRelease(T*& ptr) +{ + if (ptr) { + ptr->Release(); + ptr = nullptr; + } +} + +static inline EDataFlow IGetDataFlow(plAudioEndpointType endpoint) +{ + switch (endpoint) { + case plAudioEndpointType::kCapture: + return eCapture; + case plAudioEndpointType::kPlayback: + return eRender; + } + + return eAll; +} + +IAudioEndpointVolume* plWinCoreAudioEndpointVolume::GetVolumeCtrl() const +{ + if (!fDevice) + return nullptr; + + IAudioEndpointVolume* volume = nullptr; + HRESULT hr = fDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr, (void**)& volume); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::GetVolumeCtrl(): Failed to open volume control, %s", + std::system_category().message(hr).c_str()); + return nullptr; + } + + return volume; +} + +float plWinCoreAudioEndpointVolume::GetVolume() const +{ + float pct = 0.f; + auto volume = GetVolumeCtrl(); + if (volume) { + HRESULT hr = volume->GetMasterVolumeLevelScalar(&pct); + volume->Release(); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::GetVolume(): Failed to get master level, %s", + std::system_category().message(hr).c_str()); + } + } + return pct; +} + +bool plWinCoreAudioEndpointVolume::SetDefaultDevice(plAudioEndpointType endpoint) +{ + ICOMRelease(fDevice); + + IMMDeviceEnumerator* enumerator = nullptr; + HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), (void**)&enumerator); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::SetDefaultDevice(): Failed to get enumerator, %s", + std::system_category().message(hr).c_str()); + return false; + } + + ERole role = (endpoint == plAudioEndpointType::kCapture) ? eCommunications : eMultimedia; + hr = enumerator->GetDefaultAudioEndpoint(IGetDataFlow(endpoint), role, &fDevice); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::SetDefaultDevice(): Failed to get default device, %s", + std::system_category().message(hr).c_str()); + return false; + } + + enumerator->Release(); + return (fDevice != nullptr); +} + +bool plWinCoreAudioEndpointVolume::SetDevice(plAudioEndpointType endpoint, const ST::string& deviceName) +{ + if (deviceName == kDefaultDeviceMagic) + return SetDefaultDevice(endpoint); + + // Unset any previously bound device and prepare to bind a new one. + ICOMRelease(fDevice); + + IMMDeviceEnumerator* enumerator = nullptr; + IMMDeviceCollection* devices = nullptr; + IMMDevice* device = nullptr; + IPropertyStore* props = nullptr; + PROPVARIANT deviceFriendlyName{ 0 }; + + do { + // Windows Core Audio was added in Windows Vista. My hope is that on Windows XP, this will + // just return a failure. + HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, + __uuidof(IMMDeviceEnumerator), (void**)& enumerator); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::SetDevice(): Failed to get enumerator, %s", + std::system_category().message(hr).c_str()); + break; + } + + // Unfortunately for us, we cannot get the device name by string (I think). What we are using in + // in OpenAL-land is called the "Friendly Name" in the Win32 Core Audio API. So, we'll need to + // actually enumerate the devices and match. The API has support for friendly name collisions, + // but that shouldn't happen (I hope?) + hr = enumerator->EnumAudioEndpoints(IGetDataFlow(endpoint), DEVICE_STATE_ACTIVE, &devices); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::SetDevice(): Failed to get device collection, %s", + std::system_category().message(hr).c_str()); + break; + } + + UINT count = 0; + devices->GetCount(&count); + for (UINT i = 0; i < count; ++i) { + // In case of error on previous run... + PropVariantClear(&deviceFriendlyName); + ICOMRelease(props); + ICOMRelease(device); + + hr = devices->Item(i, &device); + // Unlikely error + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::SetDevice(): Failed to open device #%u, %s", + i, std::system_category().message(hr).c_str()); + continue; + } + + hr = device->OpenPropertyStore(STGM_READ, &props); + // Unlikely error + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::SetDevice(): Failed get device #%u properties, %s", + i, std::system_category().message(hr).c_str()); + continue; + } + + hr = props->GetValue(PKEY_Device_FriendlyName, &deviceFriendlyName); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::SetDevice(): Failed get device #%u friendly name, %s", + i, std::system_category().message(hr).c_str()); + continue; + } + + // While it would be nice to compare the strings exactly, OpenAL will prepend junk to the + // name of the device. That SHOULD have been filtered out by the caller of this method, but + // someone could always start using NewCoolAL, which uses a new prefix that ISN'T removed. + ST::string currDeviceName = ST::string::from_wchar(deviceFriendlyName.pwszVal); + if (deviceName.contains(currDeviceName)) { + fDevice = device; + device = nullptr; + break; + } + } + } while (0); + + PropVariantClear(&deviceFriendlyName); + ICOMRelease(props); + ICOMRelease(device); + ICOMRelease(devices); + ICOMRelease(enumerator); + + if (!fDevice) { + plStatusLog::AddLineS("audio.log", plStatusLog::kYellow, + "WinCoreAudioEndpointVolume::SetDevice(): Unable to find endpoint %s", + deviceName.c_str()); + return false; + } + + return true; +} + +bool plWinCoreAudioEndpointVolume::SetVolume(float pct) +{ + auto volume = GetVolumeCtrl(); + if (volume) { + // Maybe one day, we'll use C++17 and this can become std::clamp... + pct = std::max(0.f, std::min(1.f, pct)); + + HRESULT hr = volume->SetMasterVolumeLevelScalar(pct, nullptr); + volume->Release(); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::SetVolume(): Failed to set master volume, %s", + std::system_category().message(hr).c_str()); + return false; + } + return true; + } + + return false; +} + +bool plWinCoreAudioEndpointVolume::Supported() const +{ + if (!fDevice) + return false; + + DWORD state; + HRESULT hr = fDevice->GetState(&state); + if (FAILED(hr)) { + plStatusLog::AddLineS("audio.log", plStatusLog::kRed, + "WinCoreAudioEndpointVolume::Supported(): Failed get device state, %s", + std::system_category().message(hr).c_str()); + return false; + } + + if (state != DEVICE_STATE_ACTIVE) + return false; + + // Just for kicks, try to activate the device just like we would to change the volume... + auto volume = GetVolumeCtrl(); + if (volume) { + volume->Release(); + return true; + } else { + return false; + } +} + +// ============================================================================= + +plAudioEndpointVolume* plAudioEndpointVolume::Create() +{ + return new plWinCoreAudioEndpointVolume; +} + +#else + +class plNullAudioEndpointVolume : public plAudioEndpointVolume +{ +public: + float GetVolume() const HS_OVERRIDE { return 0.f; } + bool SetDefaultDevice(plAudioEndpointType) HS_OVERRIDE { return false; } + bool SetDevice(plAudioEndpointType, const ST::string&) HS_OVERRIDE { return false; } + bool SetVolume(float) HS_OVERRIDE { return false; } + bool Supported() const HS_OVERRIDE { return false; } +}; + +plAudioEndpointVolume* plAudioEndpointVolume::Create() +{ + return new plNullAudioEndpointVolume; +} + +#endif diff --git a/Sources/Plasma/PubUtilLib/plAudio/plWinMicLevel.h b/Sources/Plasma/PubUtilLib/plAudio/plAudioEndpointVolume.h similarity index 51% rename from Sources/Plasma/PubUtilLib/plAudio/plWinMicLevel.h rename to Sources/Plasma/PubUtilLib/plAudio/plAudioEndpointVolume.h index b8c27c7966..09ce092c80 100644 --- a/Sources/Plasma/PubUtilLib/plAudio/plWinMicLevel.h +++ b/Sources/Plasma/PubUtilLib/plAudio/plAudioEndpointVolume.h @@ -39,42 +39,56 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com Mead, WA 99021 *==LICENSE==*/ -#ifndef _plWinMicLevel_h -#define _plWinMicLevel_h -////////////////////////////////////////////////////////////////////////////// -// // -// plWinMicLevel - Annoying class to deal with the annoying problem of // -// setting the microphone recording volume in Windows. // -// Yeah, you'd THINK there'd be some easier way... // -// // -//// Notes /////////////////////////////////////////////////////////////////// -// // -// 5.8.2001 - Created by mcn. // -// // -////////////////////////////////////////////////////////////////////////////// +#ifndef _PLAUDIO_PLAUDIOENDPOINTVOLUME_H +#define _PLAUDIO_PLAUDIOENDPOINTVOLUME_H +#include "HeadSpin.h" +#include -//// Class Definition //////////////////////////////////////////////////////// +#include "plAudioSystem.h" -class plWinMicLevel +enum class plAudioEndpointType { -public: - - ~plWinMicLevel(); - // Gets the microphone volume, range 0-1, -1 if error - static float GetLevel( void ); - - // Sets the microphone volume, range 0-1 - static void SetLevel( float level ); + /** Represents an audio capture endpoint such as a microphone or loopback device. */ + kCapture, - // Returns whether we can set the level - static bool CanSetLevel( void ); + /** Represents an audio playback endpoint such as a speaker or headphones. */ + kPlayback, +}; -protected: - plWinMicLevel(); // Protected constructor for IGetInstance. Just to init some stuff - static plWinMicLevel &IGetInstance( void ); - void IShutdown( void ); +/** Volume controller for any arbitrary audio endpoint. */ +class plAudioEndpointVolume +{ +public: + /** + * Gets the volume of this audio endpoint. + * This gets the volume of the given audio endpoint as a percentage from 0.0 to 1.0, inclusive. + */ + virtual float GetVolume() const = 0; + + /** Binds to the default audio endpoint. */ + virtual bool SetDefaultDevice(plAudioEndpointType endpoint) = 0; + + /** Binds to an audio input by name. */ + virtual bool SetDevice(plAudioEndpointType endpoint, const ST::string& deviceName) = 0; + + /** + * Sets the volume of this audio endpoint. + * This sets the volume of the given audio endpoint as a percentage from 0.0 to 1.0, inclusive. + */ + virtual bool SetVolume(float pct) = 0; + + /** + * Returns if the endpoint's volume can be manipulated. + * \remarks A value of "false" can be for many reasons. For example, this operation may not be + * supported on the current platform, no endpoint was selected, an invalid endpoint was + * selected, or the endpoint just doesn't support volume operations. + */ + virtual bool Supported() const = 0; + + /** Creates an instance of the volume controller for the current platform. */ + static plAudioEndpointVolume* Create(); }; -#endif // _plWinMicLevel_h +#endif diff --git a/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem.cpp b/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem.cpp index 9eeb34ceef..98f2399678 100644 --- a/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem.cpp +++ b/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem.cpp @@ -46,11 +46,13 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include #endif #include +#include #include "plgDispatch.h" #include "plProfile.h" #include "hsTimer.h" +#include "plAudioEndpointVolume.h" #include "plAudioSystem.h" #include "plAudioSystem_Private.h" #include "plDSoundBuffer.h" @@ -69,7 +71,7 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "plMessage/plRenderMsg.h" #include "plStatusLog/plStatusLog.h" -static const ST::string s_defaultDeviceMagic = ST_LITERAL("(Default Device)"); +ST::string kDefaultDeviceMagic = ST_LITERAL("(Default Device)"); #define FADE_TIME 3 #define MAX_NUM_SOURCES 128 @@ -166,6 +168,9 @@ int32_t plAudioSystem::fNumSoundsSlop = 8; plAudioSystem::plAudioSystem() : fPlaybackDevice(), + fContext(), + fCaptureDevice(), + fCaptureLevel(plAudioEndpointVolume::Create()), fStartTime(), fListenerInit(), fSoftRegionSounds(), @@ -179,7 +184,6 @@ plAudioSystem::plAudioSystem() fDisplayNumBuffers(), fStartFade(), fFadeLength(FADE_TIME), - fCaptureDevice(), fEAXSupported(), fLastUpdateTimeMs() { @@ -190,7 +194,7 @@ plAudioSystem::plAudioSystem() std::vector plAudioSystem::GetPlaybackDevices() const { std::vector retval; - retval.push_back(s_defaultDeviceMagic); + retval.push_back(kDefaultDeviceMagic); const ALchar* devices = nullptr; if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { @@ -232,7 +236,7 @@ ST::string plAudioSystem::GetDefaultPlaybackDevice() const std::vector plAudioSystem::GetCaptureDevices() const { std::vector retval; - retval.push_back(s_defaultDeviceMagic); + retval.push_back(kDefaultDeviceMagic); const ALchar* devices = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER); const ALchar* ptr = devices; @@ -258,17 +262,18 @@ bool plAudioSystem::Init() fMaxNumSources = 0; plSoundBuffer::Init(); + fCaptureLevel->SetDevice(plAudioEndpointType::kCapture, plgAudioSys::GetCaptureDeviceFriendly()); // Try to init using the provided device. Otherwise, fall back to the default. std::vector devices = GetPlaybackDevices(); for (const ST::string& device : devices) { - if (device != s_defaultDeviceMagic) + if (device != kDefaultDeviceMagic) plStatusLog::AddLineS("audio.log", plStatusLog::kGreen, "ASYS: Found device %s", device.c_str()); } ST::string deviceName = plgAudioSys::fPlaybackDeviceName; - bool defaultDeviceRequested = (deviceName.empty() || deviceName == s_defaultDeviceMagic); + bool defaultDeviceRequested = (deviceName.empty() || deviceName == kDefaultDeviceMagic); if (defaultDeviceRequested) - deviceName = s_defaultDeviceMagic; + deviceName = kDefaultDeviceMagic; plStatusLog::AddLineS("audio.log", plStatusLog::kBlue, "ASYS: Device '%s' selected", deviceName.c_str()); if (!defaultDeviceRequested) { @@ -285,7 +290,7 @@ bool plAudioSystem::Init() } if (!fPlaybackDevice) { - plgAudioSys::fPlaybackDeviceName = s_defaultDeviceMagic; + plgAudioSys::fPlaybackDeviceName = kDefaultDeviceMagic; fPlaybackDevice = alcOpenDevice(nullptr); if (!fPlaybackDevice) { plStatusLog::AddLineS("audio.log", plStatusLog::kRed, "ASYS: ERROR! alcOpenDevice failed on default device."); @@ -896,9 +901,9 @@ bool plAudioSystem::MsgReceive(plMessage* msg) bool plAudioSystem::OpenCaptureDevice() { const ST::string& deviceName = plgAudioSys::fCaptureDeviceName; - bool defaultDeviceRequested = (deviceName.empty() || deviceName == s_defaultDeviceMagic); + bool defaultDeviceRequested = (deviceName.empty() || deviceName == kDefaultDeviceMagic); if (defaultDeviceRequested) - plgAudioSys::fCaptureDeviceName = s_defaultDeviceMagic; + plgAudioSys::fCaptureDeviceName = kDefaultDeviceMagic; uint32_t frequency = plgAudioSys::fCaptureSampleRate; ALCsizei bufferSize = frequency * sizeof(int16_t) * BUFFER_LEN_SECONDS; @@ -910,7 +915,7 @@ bool plAudioSystem::OpenCaptureDevice() } if (!fCaptureDevice) { - plgAudioSys::fCaptureDeviceName = s_defaultDeviceMagic; + plgAudioSys::fCaptureDeviceName = kDefaultDeviceMagic; fCaptureDevice = alcCaptureOpenDevice(nullptr, frequency, AL_FORMAT_MONO16, bufferSize); if (!fCaptureDevice) { plStatusLog::AddLineS("audio.log", plStatusLog::kRed, "ASYS: ERROR! Failed to open default capture device."); @@ -922,6 +927,7 @@ bool plAudioSystem::OpenCaptureDevice() bool plAudioSystem::RestartCapture() { + fCaptureLevel->SetDevice(plAudioEndpointType::kCapture, plgAudioSys::GetCaptureDeviceFriendly()); if (IsCapturing()) { if (!EndCapture()) return false; @@ -999,8 +1005,8 @@ uint8_t plgAudioSys::fPriorityCutoff = 9; // We cut off sounds bool plgAudioSys::fEnableExtendedLogs = false; float plgAudioSys::fGlobalFadeVolume = 1.f; bool plgAudioSys::fLogStreamingUpdates = false; -ST::string plgAudioSys::fPlaybackDeviceName = s_defaultDeviceMagic; -ST::string plgAudioSys::fCaptureDeviceName = s_defaultDeviceMagic; +ST::string plgAudioSys::fPlaybackDeviceName = kDefaultDeviceMagic; +ST::string plgAudioSys::fCaptureDeviceName = kDefaultDeviceMagic; bool plgAudioSys::fRestarting = false; bool plgAudioSys::fMutedStateChange = false; uint32_t plgAudioSys::fCaptureSampleRate = FREQUENCY; @@ -1206,36 +1212,65 @@ void plgAudioSys::SetDistanceModel(int type) void plgAudioSys::SetCaptureDevice(const ST::string& name) { fCaptureDeviceName = name; - if (fSys) + if (fSys) { fSys->RestartCapture(); + } +} + +ST::string plgAudioSys::GetFriendlyDeviceName(const ST::string& deviceName) +{ + // These hardcoded strings represent the device prefixes prepended by the Creative OpenAL SDK + // and the OpenAL Soft implementation. It would be nice if they avoided doing this crap, but + // beggars can't be choosers, so there you go. If we support any other implementations in + // the future, they will likely need to be added here. + static std::array defaultNames = { ST_LITERAL("Generic Software"), + ST_LITERAL("Generic Hardware"), + ST_LITERAL("OpenAL Soft") }; + for (const auto& it : defaultNames) { + if (deviceName == it) { + return kDefaultDeviceMagic; + } + } + + static std::array devicePrefixes = { ST_LITERAL("Generic Software on "), + ST_LITERAL("Generic Hardware on "), + ST_LITERAL("OpenAL Soft on ") }; + for (const auto& it : devicePrefixes) { + if (deviceName.starts_with(it)) { + return deviceName.substr(it.size()); + } + } + + // Failure. + return deviceName; } std::vector plgAudioSys::GetPlaybackDevices() { if (fSys) return fSys->GetPlaybackDevices(); - return { s_defaultDeviceMagic }; + return { kDefaultDeviceMagic }; } ST::string plgAudioSys::GetDefaultPlaybackDevice() { if (fSys) return fSys->GetDefaultPlaybackDevice(); - return s_defaultDeviceMagic; + return kDefaultDeviceMagic; } std::vector plgAudioSys::GetCaptureDevices() { if (fSys) return fSys->GetCaptureDevices(); - return { s_defaultDeviceMagic }; + return { kDefaultDeviceMagic }; } ST::string plgAudioSys::GetDefaultCaptureDevice() { if (fSys) return fSys->GetDefaultCaptureDevice(); - return s_defaultDeviceMagic; + return kDefaultDeviceMagic; } bool plgAudioSys::SetCaptureSampleRate(uint32_t frequency) @@ -1283,3 +1318,24 @@ bool plgAudioSys::EndCapture() return fSys->EndCapture(); return false; } + +bool plgAudioSys::CanChangeCaptureVolume() +{ + if (fSys) + return fSys->fCaptureLevel->Supported(); + return false; +} + +float plgAudioSys::GetCaptureVolume() +{ + if (fSys) + return fSys->fCaptureLevel->GetVolume(); + return 0.f; +} + +bool plgAudioSys::SetCaptureVolume(float pct) +{ + if (fSys) + return fSys->fCaptureLevel->SetVolume(pct); + return false; +} diff --git a/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem.h b/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem.h index 4f38c35c33..d69b187c64 100644 --- a/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem.h +++ b/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem.h @@ -126,6 +126,9 @@ class plgAudioSys static void SetDistanceModel(int type); + /** Returns the device name without any OpenAL device name prefixes applied. */ + static ST::string GetFriendlyDeviceName(const ST::string& deviceName); + static ST::string GetPlaybackDevice() { return fPlaybackDeviceName; } /** @@ -159,7 +162,12 @@ class plgAudioSys /** Gets the name of the default audio capture device. */ static ST::string GetDefaultCaptureDevice(); + /** Gets the internal device name of the current audio capture device. */ static ST::string GetCaptureDevice() { return fCaptureDeviceName; } + + /** Gets the friendly user facing name of the current audio capture device. */ + static ST::string GetCaptureDeviceFriendly() { return GetFriendlyDeviceName(fCaptureDeviceName); } + static void SetCaptureDevice(const ST::string& name); static bool SetCaptureSampleRate(uint32_t frequency); @@ -185,6 +193,21 @@ class plgAudioSys static bool IsCapturing(); static bool EndCapture(); + /** Returns if the endpoint's volume can be manipulated. */ + static bool CanChangeCaptureVolume(); + + /** + * Gets the volume of this audio endpoint. + * This gets the volume of the given audio endpoint as a percentage from 0.0 to 1.0, inclusive. + */ + static float GetCaptureVolume(); + + /** + * Sets the volume of this audio endpoint. + * This sets the volume of the given audio endpoint as a percentage from 0.0 to 1.0, inclusive. + */ + static bool SetCaptureVolume(float pct); + private: friend class plAudioSystem; diff --git a/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem_Private.h b/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem_Private.h index 96ad26428e..2604d7b369 100644 --- a/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem_Private.h +++ b/Sources/Plasma/PubUtilLib/plAudio/plAudioSystem_Private.h @@ -50,11 +50,13 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #ifdef EAX_SDK_AVAILABLE #include #endif +#include #include #include "hsGeometry3.h" #include "pnKeyedObject/hsKeyedObject.h" +class plAudioEndpointVolume; class plEAXListenerMod; class plSoftSoundNode; class plStatusLog; @@ -104,6 +106,7 @@ class plAudioSystem : public hsKeyedObject ALCdevice* fPlaybackDevice; ALCcontext* fContext; ALCdevice* fCaptureDevice; + std::unique_ptr fCaptureLevel; plSoftSoundNode* fSoftRegionSounds; plSoftSoundNode* fActiveSofts; diff --git a/Sources/Plasma/PubUtilLib/plAudio/plWinMicLevel.cpp b/Sources/Plasma/PubUtilLib/plAudio/plWinMicLevel.cpp deleted file mode 100644 index 4291f9e620..0000000000 --- a/Sources/Plasma/PubUtilLib/plAudio/plWinMicLevel.cpp +++ /dev/null @@ -1,397 +0,0 @@ -/*==LICENSE==* - -CyanWorlds.com Engine - MMOG client, server and tools -Copyright (C) 2011 Cyan Worlds, Inc. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -Additional permissions under GNU GPL version 3 section 7 - -If you modify this Program, or any covered work, by linking or -combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, -NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent -JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK -(or a modified version of those libraries), -containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, -PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG -JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the -licensors of this Program grant you additional -permission to convey the resulting work. Corresponding Source for a -non-source form of such a combination shall include the source code for -the parts of OpenSSL and IJG JPEG Library used as well as that of the covered -work. - -You can contact Cyan Worlds, Inc. by email legal@cyan.com - or by snail mail at: - Cyan Worlds, Inc. - 14617 N Newport Hwy - Mead, WA 99021 - -*==LICENSE==*/ -////////////////////////////////////////////////////////////////////////////// -// // -// plWinMicLevel - Annoying class to deal with the annoying problem of // -// setting the microphone recording volume in Windows. // -// Yeah, you'd THINK there'd be some easier way... // -// // -//// Notes /////////////////////////////////////////////////////////////////// -// // -// 5.8.2001 - Created by mcn. // -// // -////////////////////////////////////////////////////////////////////////////// - - -#include "HeadSpin.h" -#include "plWinMicLevel.h" - - -#if HS_BUILD_FOR_WIN32 -#include "hsWindows.h" -#include - - -//// Our Local Static Data /////////////////////////////////////////////////// - -int sNumMixers = 0; -HMIXER sMixerHandle = nil; -MIXERCAPS sMixerCaps; - -DWORD sMinValue = 0, sMaxValue = 0; -DWORD sVolControlID = 0; - - -//// Local Static Helpers //////////////////////////////////////////////////// - -bool IGetMuxMicVolumeControl( void ); -bool IGetBaseMicVolumeControl( void ); - -bool IGetControlValue( DWORD &value ); -bool ISetControlValue( DWORD value ); - -MIXERLINE *IGetLineByType( DWORD type ); -MIXERLINE *IGetLineByID( DWORD id ); -MIXERCONTROL *IGetControlByType( MIXERLINE *line, DWORD type ); -MIXERLINE *IGetMixerSubLineByType( MIXERCONTROL *mux, DWORD type ); -#endif - -//// The Publics ///////////////////////////////////////////////////////////// - -float plWinMicLevel::GetLevel( void ) -{ - if( !CanSetLevel() ) - return -1; - -#if HS_BUILD_FOR_WIN32 - DWORD rawValue; - if( !IGetControlValue( rawValue ) ) - return -1; - - return (float)( rawValue - sMinValue ) / (float)( sMaxValue - sMinValue ); -#else - return -1; -#endif -} - -void plWinMicLevel::SetLevel( float level ) -{ - if( !CanSetLevel() ) - return; - -#if HS_BUILD_FOR_WIN32 - DWORD rawValue = (DWORD)(( level * ( sMaxValue - sMinValue ) ) + sMinValue); - - ISetControlValue( rawValue ); -#endif -} - -bool plWinMicLevel::CanSetLevel( void ) -{ - // Just to init - plWinMicLevel &instance = IGetInstance(); - -#if HS_BUILD_FOR_WIN32 - return ( sMixerHandle != nil ) ? true : false; -#else - return false; -#endif -} - - -//// Protected Init Stuff //////////////////////////////////////////////////// - -plWinMicLevel &plWinMicLevel::IGetInstance( void ) -{ - static plWinMicLevel sInstance; - return sInstance; -} - -plWinMicLevel::plWinMicLevel() -{ -#if HS_BUILD_FOR_WIN32 - sMixerHandle = nil; - memset( &sMixerCaps, 0, sizeof( sMixerCaps ) ); - - // Get the number of mixers in the system - sNumMixers = ::mixerGetNumDevs(); - - // So long as we have one, open the first one - if( sNumMixers == 0 ) - return; - - if( ::mixerOpen( &sMixerHandle, 0, - 0, // Window handle to receive callback messages - 0, MIXER_OBJECTF_MIXER ) != MMSYSERR_NOERROR ) - { - sMixerHandle = nil; // Just to be sure - return; - } - - if( ::mixerGetDevCaps( (UINT)sMixerHandle, &sMixerCaps, sizeof( sMixerCaps ) ) != MMSYSERR_NOERROR ) - { - // Oh well, who cares - } - - // Try to get the Mux/mixer-based mic volume control first, since that seems to work better/more often/at all - if( !IGetMuxMicVolumeControl() ) - { - // Failed, so try getting the volume control from the base mic-in line - if( !IGetBaseMicVolumeControl() ) - { - IShutdown(); - return; - } - } -#endif -} - -plWinMicLevel::~plWinMicLevel() -{ - IShutdown(); -} - -void plWinMicLevel::IShutdown( void ) -{ -#if HS_BUILD_FOR_WIN32 - if( sMixerHandle != nil ) - ::mixerClose( sMixerHandle ); - - sMixerHandle = nil; -#endif -} - -#if HS_BUILD_FOR_WIN32 -//// IGetMuxMicVolumeControl ///////////////////////////////////////////////// -// Tries to get the volume control of the microphone subline of the MUX or -// mixer control of the WaveIn destination line of the audio system (whew!) -// Note: testing indcates that this works but the direct SRC_MICROPHONE -// doesn't, hence we try this one first. - -bool IGetMuxMicVolumeControl( void ) -{ - if( sMixerHandle == nil ) - return false; - - // Get the WaveIn destination line - MIXERLINE *waveInLine = IGetLineByType( MIXERLINE_COMPONENTTYPE_DST_WAVEIN ); - if( waveInLine == nil ) - return false; - - // Get the mixer or MUX controller from the line - MIXERCONTROL *control = IGetControlByType( waveInLine, MIXERCONTROL_CONTROLTYPE_MIXER ); - if( control == nil ) - control = IGetControlByType( waveInLine, MIXERCONTROL_CONTROLTYPE_MUX ); - if( control == nil ) - return false; - - // Get the microphone sub-component - // Note: this eventually calls IGetLineByType(), which destroys the waveInLine pointer we had before - MIXERLINE *micLine = IGetMixerSubLineByType( control, MIXERLINE_COMPONENTTYPE_SRC_MICROPHONE ); - if( micLine == nil ) - return false; - - // Get the volume subcontroller - MIXERCONTROL *micVolCtrl = IGetControlByType( micLine, MIXERCONTROL_CONTROLTYPE_VOLUME ); - if( micVolCtrl == nil ) - return false; - - // Found it! store our values - char *dbgLineName = micLine->szName; - char *dbgControlName = micVolCtrl->szName; - sMinValue = micVolCtrl->Bounds.dwMinimum; - sMaxValue = micVolCtrl->Bounds.dwMaximum; - sVolControlID = micVolCtrl->dwControlID; - - return true; -} - -//// IGetBaseMicVolumeControl //////////////////////////////////////////////// -// Tries to get the volume control of the mic-in line. See -// IGetMuxMicVolumeControl for why we don't do this one first. - -bool IGetBaseMicVolumeControl( void ) -{ - if( sMixerHandle == nil ) - return false; - - // Get the mic source line - MIXERLINE *micLine = IGetLineByType( MIXERLINE_COMPONENTTYPE_SRC_MICROPHONE ); - if( micLine == nil ) - return false; - - // Get the volume subcontroller - MIXERCONTROL *micVolCtrl = IGetControlByType( micLine, MIXERCONTROL_CONTROLTYPE_VOLUME ); - if( micVolCtrl == nil ) - return false; - - // Found it! store our values - char *dbgLineName = micLine->szName; - char *dbgControlName = micVolCtrl->szName; - sMinValue = micVolCtrl->Bounds.dwMinimum; - sMaxValue = micVolCtrl->Bounds.dwMaximum; - sVolControlID = micVolCtrl->dwControlID; - - return true; -} - - -//// IGetControlValue //////////////////////////////////////////////////////// -// Gets the raw value of the current volume control. - -bool IGetControlValue( DWORD &value ) -{ - if( sMixerHandle == nil ) - return false; - - MIXERCONTROLDETAILS_UNSIGNED mxcdVolume; - MIXERCONTROLDETAILS mxcd; - mxcd.cbStruct = sizeof( MIXERCONTROLDETAILS ); - mxcd.dwControlID = sVolControlID; - mxcd.cChannels = 1; - mxcd.cMultipleItems = 0; - mxcd.cbDetails = sizeof( MIXERCONTROLDETAILS_UNSIGNED ); - mxcd.paDetails = &mxcdVolume; - - if( ::mixerGetControlDetails( (HMIXEROBJ)sMixerHandle, &mxcd, - MIXER_OBJECTF_HMIXER | MIXER_GETCONTROLDETAILSF_VALUE ) != MMSYSERR_NOERROR ) - return false; - - value = mxcdVolume.dwValue; - return true; -} - -//// ISetControlValue //////////////////////////////////////////////////////// -// Sets the raw value of the current volume control. - -bool ISetControlValue( DWORD value ) -{ - if( sMixerHandle == nil ) - return false; - - MIXERCONTROLDETAILS_UNSIGNED mxcdVolume = { value }; - MIXERCONTROLDETAILS mxcd; - mxcd.cbStruct = sizeof( MIXERCONTROLDETAILS ); - mxcd.dwControlID = sVolControlID; - mxcd.cChannels = 1; - mxcd.cMultipleItems = 0; - mxcd.cbDetails = sizeof( MIXERCONTROLDETAILS_UNSIGNED ); - mxcd.paDetails = &mxcdVolume; - - if( ::mixerSetControlDetails( (HMIXEROBJ)sMixerHandle, &mxcd, - MIXER_OBJECTF_HMIXER | MIXER_SETCONTROLDETAILSF_VALUE ) != MMSYSERR_NOERROR ) - return false; - - return true; -} - - -//// Helper Functions //////////////////////////////////////////////////////// - -MIXERLINE *IGetLineByType( DWORD type ) -{ - static MIXERLINE mxl; - - mxl.cbStruct = sizeof( MIXERLINE ); - mxl.dwComponentType = type; - if( ::mixerGetLineInfo( (HMIXEROBJ)sMixerHandle, &mxl, MIXER_OBJECTF_HMIXER | MIXER_GETLINEINFOF_COMPONENTTYPE ) != MMSYSERR_NOERROR ) - return nil; - - return &mxl; -} - -MIXERLINE *IGetLineByID( DWORD id ) -{ - static MIXERLINE mxl; - - mxl.cbStruct = sizeof( MIXERLINE ); - mxl.dwLineID = id; - if( ::mixerGetLineInfo( (HMIXEROBJ)sMixerHandle, &mxl, MIXER_OBJECTF_HMIXER | MIXER_GETLINEINFOF_LINEID ) != MMSYSERR_NOERROR ) - return nil; - - return &mxl; -} - -MIXERCONTROL *IGetControlByType( MIXERLINE *line, DWORD type ) -{ - static MIXERCONTROL mxc; - - MIXERLINECONTROLS mxlc; - mxlc.cbStruct = sizeof( MIXERLINECONTROLS ); - mxlc.dwLineID = line->dwLineID; - mxlc.dwControlType = type; - mxlc.cControls = 1; - mxlc.cbmxctrl = sizeof( MIXERCONTROL ); - mxlc.pamxctrl = &mxc; - if( ::mixerGetLineControls( (HMIXEROBJ)sMixerHandle, &mxlc, MIXER_OBJECTF_HMIXER | MIXER_GETLINECONTROLSF_ONEBYTYPE ) != MMSYSERR_NOERROR ) - return nil; - - return &mxc; -} - -MIXERLINE *IGetMixerSubLineByType( MIXERCONTROL *mux, DWORD type ) -{ - // A mixer or MUX is really a combination of MORE lines. And beautifully, you can't - // just ask for a single one off of it, you have to ask for them all and search through yourself - MIXERCONTROLDETAILS_LISTTEXT *lineInfo = new MIXERCONTROLDETAILS_LISTTEXT[ mux->cMultipleItems ]; - if( lineInfo == nil ) - return nil; - - MIXERCONTROLDETAILS details; - details.cbStruct = sizeof( MIXERCONTROLDETAILS ); - details.dwControlID = mux->dwControlID; - details.cChannels = 1; - details.cMultipleItems = mux->cMultipleItems; - details.cbDetails = sizeof( MIXERCONTROLDETAILS_LISTTEXT ); - details.paDetails = lineInfo; - if( ::mixerGetControlDetails( (HMIXEROBJ)sMixerHandle, &details, MIXER_OBJECTF_HMIXER | MIXER_GETCONTROLDETAILSF_LISTTEXT ) != MMSYSERR_NOERROR ) - { - delete [] lineInfo; - return nil; - } - - // Loop through and find the one with the right component type. But of course it doesn't give us that offhand... - for( unsigned int i = 0; i < mux->cMultipleItems; i++ ) - { - MIXERLINE *line = IGetLineByID( lineInfo[ i ].dwParam1 ); - if( line->dwComponentType == type ) - { - delete [] lineInfo; - return line; - } - } - - delete [] lineInfo; - return nil; -} - -#endif