Torque3D/Engine/lib/openal-soft/alc/backends/otherio.cpp

701 lines
20 KiB
C++

/**
* OpenAL cross platform audio library
* Copyright (C) 2024 by authors.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
* Or go to http://www.gnu.org/copyleft/lgpl.html
*/
#include "config.h"
#include "otherio.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winreg.h>
#include <cstdio>
#include <cstdlib>
#include <memory.h>
#include <wtypes.h>
#include <cguid.h>
#include <devpropdef.h>
#include <mmreg.h>
#include <propsys.h>
#include <propkey.h>
#include <devpkey.h>
#ifndef _WAVEFORMATEXTENSIBLE_
#include <ks.h>
#include <ksmedia.h>
#endif
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <future>
#include <mutex>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include "albit.h"
#include "alnumeric.h"
#include "althrd_setname.h"
#include "comptr.h"
#include "core/converter.h"
#include "core/device.h"
#include "core/helpers.h"
#include "core/logging.h"
#include "strutils.h"
/* A custom C++ interface that should be capable of interoperating with ASIO
* drivers.
*/
enum class ORIOError : LONG {
Okay = 0,
Success = 0x3f4847a0,
NotPresent = -1000,
HWMalfunction,
InvalidParameter,
InvalidMode,
SPNotAdvancing,
NoClock,
NoMemory,
};
/* A 64-bit integer or double, which has the most significant 32-bit word first. */
struct ORIO64Bit {
uint32_t hi;
uint32_t lo;
template<typename T>
auto as() const -> T = delete;
};
template<> [[nodiscard]]
auto ORIO64Bit::as() const -> uint64_t { return (uint64_t{hi}<<32) | lo; }
template<> [[nodiscard]]
auto ORIO64Bit::as() const -> int64_t { return static_cast<int64_t>(as<uint64_t>()); }
template<> [[nodiscard]]
auto ORIO64Bit::as() const -> double { return al::bit_cast<double>(as<uint64_t>()); }
enum class ORIOSampleType : LONG {
Int16BE = 0,
Int24BE = 1,
Int32BE = 2,
Float32BE = 3,
Float64BE = 4,
Int32BE16 = 8,
Int32BE18 = 9,
Int32BE20 = 10,
Int32BE24 = 11,
Int16LE = 16,
Int24LE = 17,
Int32LE = 18,
Float32LE = 19,
Float64LE = 20,
Int32LE16 = 24,
Int32LE18 = 25,
Int32LE20 = 26,
Int32LE24 = 27,
DSDInt8LSB1 = 32,
DSDInt8MSB1 = 33,
DSDInt8 = 40,
};
struct ORIOClockSource {
LONG mIndex;
LONG mAssocChannel;
LONG mAssocGroup;
LONG mIsCurrent;
std::array<char,32> mName;
};
struct ORIOChannelInfo {
LONG mChannel;
LONG mIsInput;
LONG mIsActive;
LONG mGroup;
ORIOSampleType mSampleType;
std::array<char,32> mName;
};
struct ORIOBufferInfo {
LONG mIsInput;
LONG mChannelNum;
std::array<void*,2> mBuffers;
};
struct ORIOTime {
struct TimeInfo {
double mSpeed;
ORIO64Bit mSystemTime;
ORIO64Bit mSamplePosition;
double mSampleRate;
ULONG mFlags;
std::array<char,12> mReserved;
};
struct TimeCode {
double mSpeed;
ORIO64Bit mTimeCodeSamples;
ULONG mFlags;
std::array<char,64> mFuture;
};
std::array<LONG,4> mReserved;
TimeInfo mTimeInfo;
TimeCode mTimeCode;
};
#ifdef _WIN64
#define ORIO_CALLBACK CALLBACK
#else
#define ORIO_CALLBACK
#endif
struct ORIOCallbacks {
void (ORIO_CALLBACK*BufferSwitch)(LONG bufferIndex, LONG directProcess) noexcept;
void (ORIO_CALLBACK*SampleRateDidChange)(double srate) noexcept;
auto (ORIO_CALLBACK*Message)(LONG selector, LONG value, void *message, double *opt) noexcept -> LONG;
auto (ORIO_CALLBACK*BufferSwitchTimeInfo)(ORIOTime *timeInfo, LONG bufferIndex, LONG directProcess) noexcept -> ORIOTime*;
};
/* COM interfaces don't include a virtual destructor in their pure-virtual
* classes, and we can't add one without breaking ABI.
*/
#ifdef __GNUC__
_Pragma("GCC diagnostic push")
_Pragma("GCC diagnostic ignored \"-Wnon-virtual-dtor\"")
#endif
/* NOLINTNEXTLINE(cppcoreguidelines-virtual-class-destructor) */
struct ORIOiface : public IUnknown {
STDMETHOD_(LONG, Init)(void *sysHandle) = 0;
/* A fixed-length span should be passed exactly the same as one pointer.
* This ensures an appropriately-sized buffer for the driver.
*/
STDMETHOD_(void, GetDriverName)(al::span<char,32> name) = 0;
STDMETHOD_(LONG, GetDriverVersion)() = 0;
STDMETHOD_(void, GetErrorMessage)(al::span<char,124> message) = 0;
STDMETHOD_(ORIOError, Start)() = 0;
STDMETHOD_(ORIOError, Stop)() = 0;
STDMETHOD_(ORIOError, GetChannels)(LONG *numInput, LONG *numOutput) = 0;
STDMETHOD_(ORIOError, GetLatencies)(LONG *inputLatency, LONG *outputLatency) = 0;
STDMETHOD_(ORIOError, GetBufferSize)(LONG *minSize, LONG *maxSize, LONG *preferredSize, LONG *granularity) = 0;
STDMETHOD_(ORIOError, CanSampleRate)(double srate) = 0;
STDMETHOD_(ORIOError, GetSampleRate)(double *srate) = 0;
STDMETHOD_(ORIOError, SetSampleRate)(double srate) = 0;
STDMETHOD_(ORIOError, GetClockSources)(ORIOClockSource *clocks, LONG *numSources) = 0;
STDMETHOD_(ORIOError, SetClockSource)(LONG index) = 0;
STDMETHOD_(ORIOError, GetSamplePosition)(ORIO64Bit *splPos, ORIO64Bit *tstampNS) = 0;
STDMETHOD_(ORIOError, GetChannelInfo)(ORIOChannelInfo *info) = 0;
STDMETHOD_(ORIOError, CreateBuffers)(ORIOBufferInfo *infos, LONG numInfos, LONG bufferSize, ORIOCallbacks *callbacks) = 0;
STDMETHOD_(ORIOError, DisposeBuffers)() = 0;
STDMETHOD_(ORIOError, ControlPanel)() = 0;
STDMETHOD_(ORIOError, Future)(LONG selector, void *opt) = 0;
STDMETHOD_(ORIOError, OutputReady)() = 0;
ORIOiface() = default;
ORIOiface(const ORIOiface&) = delete;
auto operator=(const ORIOiface&) -> ORIOiface& = delete;
~ORIOiface() = delete;
};
#ifdef __GNUC__
_Pragma("GCC diagnostic pop")
#endif
namespace {
using namespace std::string_view_literals;
using std::chrono::nanoseconds;
using std::chrono::milliseconds;
using std::chrono::seconds;
struct DeviceEntry {
std::string mDrvName;
CLSID mDrvGuid{};
};
std::vector<DeviceEntry> gDeviceList;
struct KeyCloser {
void operator()(HKEY key) { RegCloseKey(key); }
};
using KeyPtr = std::unique_ptr<std::remove_pointer_t<HKEY>,KeyCloser>;
[[nodiscard]]
auto PopulateDeviceList() -> HRESULT
{
auto regbase = KeyPtr{};
auto res = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\ASIO", 0, KEY_READ,
al::out_ptr(regbase));
if(res != ERROR_SUCCESS)
{
ERR("Error opening HKLM\\Software\\ASIO: {}", res);
return E_NOINTERFACE;
}
auto numkeys = DWORD{};
auto maxkeylen = DWORD{};
res = RegQueryInfoKeyW(regbase.get(), nullptr, nullptr, nullptr, &numkeys, &maxkeylen, nullptr,
nullptr, nullptr, nullptr, nullptr, nullptr);
if(res != ERROR_SUCCESS)
{
ERR("Error querying HKLM\\Software\\ASIO info: {}", res);
return E_FAIL;
}
/* maxkeylen is the max number of unicode characters a subkey is. A unicode
* character can occupy two WCHARs, so ensure there's enough space for them
* and the null char.
*/
auto keyname = std::vector<WCHAR>(maxkeylen*2 + 1);
for(DWORD i{0};i < numkeys;++i)
{
auto namelen = static_cast<DWORD>(keyname.size());
res = RegEnumKeyExW(regbase.get(), i, keyname.data(), &namelen, nullptr, nullptr, nullptr,
nullptr);
if(res != ERROR_SUCCESS)
{
ERR("Error querying HKLM\\Software\\ASIO subkey {}: {}", i, res);
continue;
}
if(namelen == 0)
{
ERR("HKLM\\Software\\ASIO subkey {} is blank?", i);
continue;
}
auto subkeyname = wstr_to_utf8({keyname.data(), namelen});
auto subkey = KeyPtr{};
res = RegOpenKeyExW(regbase.get(), keyname.data(), 0, KEY_READ, al::out_ptr(subkey));
if(res != ERROR_SUCCESS)
{
ERR("Error opening HKLM\\Software\\ASIO\\{}: {}", subkeyname, res);
continue;
}
auto idstr = std::array<WCHAR,48>{};
auto readsize = DWORD{idstr.size()*sizeof(WCHAR)};
res = RegGetValueW(subkey.get(), L"", L"CLSID", RRF_RT_REG_SZ, nullptr, idstr.data(),
&readsize);
if(res != ERROR_SUCCESS)
{
ERR("Failed to read HKLM\\Software\\ASIO\\{}\\CLSID: {}", subkeyname, res);
continue;
}
idstr.back() = 0;
auto guid = CLSID{};
if(auto hr = CLSIDFromString(idstr.data(), &guid); FAILED(hr))
{
ERR("Failed to parse CLSID \"{}\": {:#x}", wstr_to_utf8(idstr.data()),
as_unsigned(hr));
continue;
}
/* The CLSID is also used for the IID. */
auto iface = ComPtr<ORIOiface>{};
auto hr = CoCreateInstance(guid, nullptr, CLSCTX_INPROC_SERVER, guid, al::out_ptr(iface));
if(SUCCEEDED(hr))
{
#if !ALSOFT_UWP
if(!iface->Init(GetForegroundWindow()))
#else
if(!iface->Init(nullptr))
#endif
{
ERR("Failed to initialize {}", subkeyname);
continue;
}
auto drvname = std::array<char,32>{};
iface->GetDriverName(drvname);
auto drvver = iface->GetDriverVersion();
auto &entry = gDeviceList.emplace_back();
entry.mDrvName = drvname.data();
entry.mDrvGuid = guid;
TRACE("Got {} v{}, CLSID {{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}}",
entry.mDrvName, drvver, guid.Data1, guid.Data2, guid.Data3, guid.Data4[0],
guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5],
guid.Data4[6], guid.Data4[7]);
}
else
ERR("Failed to create {} instance for CLSID {{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}}: {:#x}",
subkeyname.c_str(), guid.Data1, guid.Data2, guid.Data3, guid.Data4[0],
guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5],
guid.Data4[6], guid.Data4[7], as_unsigned(hr));
}
return S_OK;
}
enum class MsgType {
OpenDevice,
ResetDevice,
StartDevice,
StopDevice,
CloseDevice,
QuitThread
};
constexpr const char *GetMessageTypeName(MsgType type) noexcept
{
switch(type)
{
case MsgType::OpenDevice: return "Open Device";
case MsgType::ResetDevice: return "Reset Device";
case MsgType::StartDevice: return "Start Device";
case MsgType::StopDevice: return "Stop Device";
case MsgType::CloseDevice: return "Close Device";
case MsgType::QuitThread: break;
}
return "";
}
/* Proxy interface used by the message handler, to ensure COM objects are used
* on a thread where COM is initialized.
*/
struct OtherIOProxy {
OtherIOProxy() = default;
OtherIOProxy(const OtherIOProxy&) = delete;
OtherIOProxy(OtherIOProxy&&) = delete;
virtual ~OtherIOProxy() = default;
void operator=(const OtherIOProxy&) = delete;
void operator=(OtherIOProxy&&) = delete;
virtual HRESULT openProxy(std::string_view name) = 0;
virtual void closeProxy() = 0;
virtual HRESULT resetProxy() = 0;
virtual HRESULT startProxy() = 0;
virtual void stopProxy() = 0;
struct Msg {
MsgType mType;
OtherIOProxy *mProxy;
std::string_view mParam;
std::promise<HRESULT> mPromise;
explicit operator bool() const noexcept { return mType != MsgType::QuitThread; }
};
static inline std::deque<Msg> mMsgQueue;
static inline std::mutex mMsgQueueLock;
static inline std::condition_variable mMsgQueueCond;
auto pushMessage(MsgType type, std::string_view param={}) -> std::future<HRESULT>
{
auto promise = std::promise<HRESULT>{};
auto future = std::future<HRESULT>{promise.get_future()};
{
auto msglock = std::lock_guard{mMsgQueueLock};
mMsgQueue.emplace_back(Msg{type, this, param, std::move(promise)});
}
mMsgQueueCond.notify_one();
return future;
}
static auto popMessage() -> Msg
{
auto lock = std::unique_lock{mMsgQueueLock};
mMsgQueueCond.wait(lock, []{return !mMsgQueue.empty();});
auto msg = Msg{std::move(mMsgQueue.front())};
mMsgQueue.pop_front();
return msg;
}
static void messageHandler(std::promise<HRESULT> *promise);
};
void OtherIOProxy::messageHandler(std::promise<HRESULT> *promise)
{
TRACE("Starting COM message thread");
auto com = ComWrapper{COINIT_APARTMENTTHREADED};
if(!com)
{
WARN("Failed to initialize COM: {:#x}", as_unsigned(com.status()));
promise->set_value(com.status());
return;
}
auto hr = PopulateDeviceList();
if(FAILED(hr))
{
promise->set_value(hr);
return;
}
promise->set_value(S_OK);
promise = nullptr;
TRACE("Starting message loop");
while(Msg msg{popMessage()})
{
TRACE("Got message \"{}\" ({:#04x}, this={}, param=\"{}\")",
GetMessageTypeName(msg.mType), static_cast<uint>(msg.mType),
static_cast<void*>(msg.mProxy), msg.mParam);
switch(msg.mType)
{
case MsgType::OpenDevice:
hr = msg.mProxy->openProxy(msg.mParam);
msg.mPromise.set_value(hr);
continue;
case MsgType::ResetDevice:
hr = msg.mProxy->resetProxy();
msg.mPromise.set_value(hr);
continue;
case MsgType::StartDevice:
hr = msg.mProxy->startProxy();
msg.mPromise.set_value(hr);
continue;
case MsgType::StopDevice:
msg.mProxy->stopProxy();
msg.mPromise.set_value(S_OK);
continue;
case MsgType::CloseDevice:
msg.mProxy->closeProxy();
msg.mPromise.set_value(S_OK);
continue;
case MsgType::QuitThread:
break;
}
ERR("Unexpected message: {}", int{al::to_underlying(msg.mType)});
msg.mPromise.set_value(E_FAIL);
}
TRACE("Message loop finished");
}
struct OtherIOPlayback final : public BackendBase, OtherIOProxy {
explicit OtherIOPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
~OtherIOPlayback() final;
void mixerProc();
void open(std::string_view name) final;
auto openProxy(std::string_view name) -> HRESULT final;
void closeProxy() final;
auto reset() -> bool final;
auto resetProxy() -> HRESULT final;
void start() final;
auto startProxy() -> HRESULT final;
void stop() final;
void stopProxy() final;
HRESULT mOpenStatus{E_FAIL};
std::atomic<bool> mKillNow{true};
std::thread mThread;
};
OtherIOPlayback::~OtherIOPlayback()
{
if(SUCCEEDED(mOpenStatus))
pushMessage(MsgType::CloseDevice).wait();
}
void OtherIOPlayback::mixerProc()
{
const auto restTime = milliseconds{mDevice->mUpdateSize*1000/mDevice->mSampleRate / 2};
SetRTPriority();
althrd_setname(GetMixerThreadName());
auto done = int64_t{0};
auto start = std::chrono::steady_clock::now();
while(!mKillNow.load(std::memory_order_acquire)
&& mDevice->Connected.load(std::memory_order_acquire))
{
auto now = std::chrono::steady_clock::now();
/* This converts from nanoseconds to nanosamples, then to samples. */
const auto avail = int64_t{std::chrono::duration_cast<seconds>((now-start)
* mDevice->mSampleRate).count()};
if(avail-done < mDevice->mUpdateSize)
{
std::this_thread::sleep_for(restTime);
continue;
}
while(avail-done >= mDevice->mUpdateSize)
{
mDevice->renderSamples(nullptr, mDevice->mUpdateSize, 0u);
done += mDevice->mUpdateSize;
}
if(done >= mDevice->mSampleRate)
{
auto s = seconds{done/mDevice->mSampleRate};
start += s;
done -= mDevice->mSampleRate*s.count();
}
}
}
void OtherIOPlayback::open(std::string_view name)
{
if(name.empty() && !gDeviceList.empty())
name = gDeviceList[0].mDrvName;
else
{
auto iter = std::find_if(gDeviceList.cbegin(), gDeviceList.cend(),
[name](const DeviceEntry &entry) { return entry.mDrvName == name; });
if(iter == gDeviceList.cend())
throw al::backend_exception{al::backend_error::NoDevice,
"Device name \"{}\" not found", name};
}
mOpenStatus = pushMessage(MsgType::OpenDevice, name).get();
if(FAILED(mOpenStatus))
throw al::backend_exception{al::backend_error::DeviceError, "Failed to open \"{}\"", name};
mDeviceName = name;
}
auto OtherIOPlayback::openProxy(std::string_view name [[maybe_unused]]) -> HRESULT
{
return S_OK;
}
void OtherIOPlayback::closeProxy()
{
}
auto OtherIOPlayback::reset() -> bool
{
return SUCCEEDED(pushMessage(MsgType::ResetDevice).get());
}
auto OtherIOPlayback::resetProxy() -> HRESULT
{
setDefaultWFXChannelOrder();
return S_OK;
}
void OtherIOPlayback::start()
{
auto hr = pushMessage(MsgType::StartDevice).get();
if(FAILED(hr))
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to start playback: {:#x}", as_unsigned(hr)};
}
auto OtherIOPlayback::startProxy() -> HRESULT
{
try {
mKillNow.store(false, std::memory_order_release);
mThread = std::thread{&OtherIOPlayback::mixerProc, this};
return S_OK;
}
catch(std::exception& e) {
ERR("Failed to start mixing thread: {}", e.what());
}
return E_FAIL;
}
void OtherIOPlayback::stop()
{
pushMessage(MsgType::StopDevice).wait();
}
void OtherIOPlayback::stopProxy()
{
if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
return;
mThread.join();
}
} // namespace
auto OtherIOBackendFactory::init() -> bool
{
static HRESULT InitResult{E_FAIL};
if(FAILED(InitResult)) try
{
auto promise = std::promise<HRESULT>{};
auto future = promise.get_future();
std::thread{&OtherIOProxy::messageHandler, &promise}.detach();
InitResult = future.get();
}
catch(...) {
}
return SUCCEEDED(InitResult);
}
auto OtherIOBackendFactory::querySupport(BackendType type) -> bool
{ return type == BackendType::Playback; }
auto OtherIOBackendFactory::enumerate(BackendType type) -> std::vector<std::string>
{
std::vector<std::string> outnames;
switch(type)
{
case BackendType::Playback:
std::for_each(gDeviceList.cbegin(), gDeviceList.cend(),
[&outnames](const DeviceEntry &entry) { outnames.emplace_back(entry.mDrvName); });
break;
case BackendType::Capture:
break;
}
return outnames;
}
auto OtherIOBackendFactory::createBackend(DeviceBase *device, BackendType type) -> BackendPtr
{
if(type == BackendType::Playback)
return BackendPtr{new OtherIOPlayback{device}};
return nullptr;
}
auto OtherIOBackendFactory::getFactory() -> BackendFactory&
{
static auto factory = OtherIOBackendFactory{};
return factory;
}
auto OtherIOBackendFactory::queryEventSupport(alc::EventType, BackendType) -> alc::EventSupport
{
return alc::EventSupport::NoSupport;
}