Enable proper full C++ exception handling on MSVC
[openal-soft.git] / alc / backends / otherio.cpp
blob41a64c16be230bae3301c87f67700956a4c5e18c
1 /**
2 * OpenAL cross platform audio library
3 * Copyright (C) 2024 by authors.
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
14 * You should have received a copy of the GNU Library General Public
15 * License along with this library; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 * Or go to http://www.gnu.org/copyleft/lgpl.html
21 #include "config.h"
23 #include "otherio.h"
25 #define WIN32_LEAN_AND_MEAN
26 #include <windows.h>
27 #include <winreg.h>
29 #include <cstdio>
30 #include <cstdlib>
31 #include <memory.h>
33 #include <wtypes.h>
34 #include <cguid.h>
35 #include <devpropdef.h>
36 #include <mmreg.h>
37 #include <propsys.h>
38 #include <propkey.h>
39 #include <devpkey.h>
40 #ifndef _WAVEFORMATEXTENSIBLE_
41 #include <ks.h>
42 #include <ksmedia.h>
43 #endif
45 #include <algorithm>
46 #include <atomic>
47 #include <chrono>
48 #include <condition_variable>
49 #include <cstring>
50 #include <deque>
51 #include <functional>
52 #include <future>
53 #include <mutex>
54 #include <string>
55 #include <string_view>
56 #include <thread>
57 #include <vector>
59 #include "albit.h"
60 #include "alstring.h"
61 #include "althrd_setname.h"
62 #include "comptr.h"
63 #include "core/converter.h"
64 #include "core/device.h"
65 #include "core/helpers.h"
66 #include "core/logging.h"
67 #include "strutils.h"
70 /* A custom C++ interface that should be capable of interoperating with ASIO
71 * drivers.
73 enum class ORIOError : LONG {
74 Okay = 0,
75 Success = 0x3f4847a0,
76 NotPresent = -1000,
77 HWMalfunction,
78 InvalidParameter,
79 InvalidMode,
80 SPNotAdvancing,
81 NoClock,
82 NoMemory,
85 /* A 64-bit integer or double, which has the most significant 32-bit word first. */
86 struct ORIO64Bit {
87 uint32_t hi;
88 uint32_t lo;
90 template<typename T>
91 auto as() const -> T = delete;
94 template<> [[nodiscard]]
95 auto ORIO64Bit::as() const -> uint64_t { return (uint64_t{hi}<<32) | lo; }
96 template<> [[nodiscard]]
97 auto ORIO64Bit::as() const -> int64_t { return static_cast<int64_t>(as<uint64_t>()); }
98 template<> [[nodiscard]]
99 auto ORIO64Bit::as() const -> double { return al::bit_cast<double>(as<uint64_t>()); }
102 enum class ORIOSampleType : LONG {
103 Int16BE = 0,
104 Int24BE = 1,
105 Int32BE = 2,
106 Float32BE = 3,
107 Float64BE = 4,
108 Int32BE16 = 8,
109 Int32BE18 = 9,
110 Int32BE20 = 10,
111 Int32BE24 = 11,
113 Int16LE = 16,
114 Int24LE = 17,
115 Int32LE = 18,
116 Float32LE = 19,
117 Float64LE = 20,
118 Int32LE16 = 24,
119 Int32LE18 = 25,
120 Int32LE20 = 26,
121 Int32LE24 = 27,
123 DSDInt8LSB1 = 32,
124 DSDInt8MSB1 = 33,
126 DSDInt8 = 40,
129 struct ORIOClockSource {
130 LONG mIndex;
131 LONG mAssocChannel;
132 LONG mAssocGroup;
133 LONG mIsCurrent;
134 std::array<char,32> mName;
137 struct ORIOChannelInfo {
138 LONG mChannel;
139 LONG mIsInput;
140 LONG mIsActive;
141 LONG mGroup;
142 ORIOSampleType mSampleType;
143 std::array<char,32> mName;
146 struct ORIOBufferInfo {
147 LONG mIsInput;
148 LONG mChannelNum;
149 std::array<void*,2> mBuffers;
152 struct ORIOTime {
153 struct TimeInfo {
154 double mSpeed;
155 ORIO64Bit mSystemTime;
156 ORIO64Bit mSamplePosition;
157 double mSampleRate;
158 ULONG mFlags;
159 std::array<char,12> mReserved;
161 struct TimeCode {
162 double mSpeed;
163 ORIO64Bit mTimeCodeSamples;
164 ULONG mFlags;
165 std::array<char,64> mFuture;
168 std::array<LONG,4> mReserved;
169 TimeInfo mTimeInfo;
170 TimeCode mTimeCode;
173 #ifdef _WIN64
174 #define ORIO_CALLBACK CALLBACK
175 #else
176 #define ORIO_CALLBACK
177 #endif
179 struct ORIOCallbacks {
180 void (ORIO_CALLBACK*BufferSwitch)(LONG bufferIndex, LONG directProcess) noexcept;
181 void (ORIO_CALLBACK*SampleRateDidChange)(double srate) noexcept;
182 auto (ORIO_CALLBACK*Message)(LONG selector, LONG value, void *message, double *opt) noexcept -> LONG;
183 auto (ORIO_CALLBACK*BufferSwitchTimeInfo)(ORIOTime *timeInfo, LONG bufferIndex, LONG directProcess) noexcept -> ORIOTime*;
186 /* COM interfaces don't include a virtual destructor in their pure-virtual
187 * classes, and we can't add one without breaking ABI.
189 #ifdef __GNUC__
190 _Pragma("GCC diagnostic push")
191 _Pragma("GCC diagnostic ignored \"-Wnon-virtual-dtor\"")
192 #endif
193 /* NOLINTNEXTLINE(cppcoreguidelines-virtual-class-destructor) */
194 struct ORIOiface : public IUnknown {
195 STDMETHOD_(LONG, Init)(void *sysHandle) = 0;
196 /* A fixed-length span should be passed exactly the same as one pointer.
197 * This ensures an appropriately-sized buffer for the driver.
199 STDMETHOD_(void, GetDriverName)(al::span<char,32> name) = 0;
200 STDMETHOD_(LONG, GetDriverVersion)() = 0;
201 STDMETHOD_(void, GetErrorMessage)(al::span<char,124> message) = 0;
202 STDMETHOD_(ORIOError, Start)() = 0;
203 STDMETHOD_(ORIOError, Stop)() = 0;
204 STDMETHOD_(ORIOError, GetChannels)(LONG *numInput, LONG *numOutput) = 0;
205 STDMETHOD_(ORIOError, GetLatencies)(LONG *inputLatency, LONG *outputLatency) = 0;
206 STDMETHOD_(ORIOError, GetBufferSize)(LONG *minSize, LONG *maxSize, LONG *preferredSize, LONG *granularity) = 0;
207 STDMETHOD_(ORIOError, CanSampleRate)(double srate) = 0;
208 STDMETHOD_(ORIOError, GetSampleRate)(double *srate) = 0;
209 STDMETHOD_(ORIOError, SetSampleRate)(double srate) = 0;
210 STDMETHOD_(ORIOError, GetClockSources)(ORIOClockSource *clocks, LONG *numSources) = 0;
211 STDMETHOD_(ORIOError, SetClockSource)(LONG index) = 0;
212 STDMETHOD_(ORIOError, GetSamplePosition)(ORIO64Bit *splPos, ORIO64Bit *tstampNS) = 0;
213 STDMETHOD_(ORIOError, GetChannelInfo)(ORIOChannelInfo *info) = 0;
214 STDMETHOD_(ORIOError, CreateBuffers)(ORIOBufferInfo *infos, LONG numInfos, LONG bufferSize, ORIOCallbacks *callbacks) = 0;
215 STDMETHOD_(ORIOError, DisposeBuffers)() = 0;
216 STDMETHOD_(ORIOError, ControlPanel)() = 0;
217 STDMETHOD_(ORIOError, Future)(LONG selector, void *opt) = 0;
218 STDMETHOD_(ORIOError, OutputReady)() = 0;
220 ORIOiface() = default;
221 ORIOiface(const ORIOiface&) = delete;
222 auto operator=(const ORIOiface&) -> ORIOiface& = delete;
223 ~ORIOiface() = delete;
225 #ifdef __GNUC__
226 _Pragma("GCC diagnostic pop")
227 #endif
229 namespace {
231 using namespace std::string_view_literals;
232 using std::chrono::nanoseconds;
233 using std::chrono::milliseconds;
234 using std::chrono::seconds;
237 struct DeviceEntry {
238 std::string mDrvName;
239 CLSID mDrvGuid{};
242 std::vector<DeviceEntry> gDeviceList;
245 struct KeyCloser {
246 void operator()(HKEY key) { RegCloseKey(key); }
248 using KeyPtr = std::unique_ptr<std::remove_pointer_t<HKEY>,KeyCloser>;
250 [[nodiscard]]
251 auto PopulateDeviceList() -> HRESULT
253 auto regbase = KeyPtr{};
254 auto res = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\ASIO", 0, KEY_READ,
255 al::out_ptr(regbase));
256 if(res != ERROR_SUCCESS)
258 ERR("Error opening HKLM\\Software\\ASIO: %ld\n", res);
259 return E_NOINTERFACE;
262 auto numkeys = DWORD{};
263 auto maxkeylen = DWORD{};
264 res = RegQueryInfoKeyW(regbase.get(), nullptr, nullptr, nullptr, &numkeys, &maxkeylen, nullptr,
265 nullptr, nullptr, nullptr, nullptr, nullptr);
266 if(res != ERROR_SUCCESS)
268 ERR("Error querying HKLM\\Software\\ASIO info: %ld\n", res);
269 return E_FAIL;
272 /* maxkeylen is the max number of unicode characters a subkey is. A unicode
273 * character can occupy two WCHARs, so ensure there's enough space for them
274 * and the null char.
276 auto keyname = std::vector<WCHAR>(maxkeylen*2 + 1);
277 for(DWORD i{0};i < numkeys;++i)
279 auto namelen = static_cast<DWORD>(keyname.size());
280 res = RegEnumKeyExW(regbase.get(), i, keyname.data(), &namelen, nullptr, nullptr, nullptr,
281 nullptr);
282 if(res != ERROR_SUCCESS)
284 ERR("Error querying HKLM\\Software\\ASIO subkey %lu: %ld\n", i, res);
285 continue;
287 if(namelen == 0)
289 ERR("HKLM\\Software\\ASIO subkey %lu is blank?\n", i);
290 continue;
292 auto subkeyname = wstr_to_utf8({keyname.data(), namelen});
294 auto subkey = KeyPtr{};
295 res = RegOpenKeyExW(regbase.get(), keyname.data(), 0, KEY_READ, al::out_ptr(subkey));
296 if(res != ERROR_SUCCESS)
298 ERR("Error opening HKLM\\Software\\ASIO\\%s: %ld\n", subkeyname.c_str(), res);
299 continue;
302 auto idstr = std::array<WCHAR,48>{};
303 auto readsize = DWORD{idstr.size()*sizeof(WCHAR)};
304 res = RegGetValueW(subkey.get(), L"", L"CLSID", RRF_RT_REG_SZ, nullptr, idstr.data(),
305 &readsize);
306 if(res != ERROR_SUCCESS)
308 ERR("Failed to read HKLM\\Software\\ASIO\\%s\\CLSID: %ld\n", subkeyname.c_str(), res);
309 continue;
311 idstr.back() = 0;
313 auto guid = CLSID{};
314 if(auto hr = CLSIDFromString(idstr.data(), &guid); FAILED(hr))
316 ERR("Failed to parse CLSID \"%s\": 0x%08lx\n", wstr_to_utf8(idstr.data()).c_str(), hr);
317 continue;
320 /* The CLSID is also used for the IID. */
321 auto iface = ComPtr<ORIOiface>{};
322 auto hr = CoCreateInstance(guid, nullptr, CLSCTX_INPROC_SERVER, guid, al::out_ptr(iface));
323 if(SUCCEEDED(hr))
325 #ifndef ALSOFT_UWP
326 if(!iface->Init(GetForegroundWindow()))
327 #else
328 if(!iface->Init(nullptr))
329 #endif
331 ERR("Failed to initialize %s\n", subkeyname.c_str());
332 continue;
334 auto drvname = std::array<char,32>{};
335 iface->GetDriverName(drvname);
336 auto drvver = iface->GetDriverVersion();
338 auto &entry = gDeviceList.emplace_back();
339 entry.mDrvName = drvname.data();
340 entry.mDrvGuid = guid;
342 TRACE("Got %s v%ld, CLSID {%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}\n",
343 entry.mDrvName.c_str(), drvver, guid.Data1, guid.Data2, guid.Data3, guid.Data4[0],
344 guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5],
345 guid.Data4[6], guid.Data4[7]);
347 else
348 ERR("Failed to create %s instance for CLSID {%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}: 0x%08lx\n",
349 subkeyname.c_str(), guid.Data1, guid.Data2, guid.Data3, guid.Data4[0],
350 guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5],
351 guid.Data4[6], guid.Data4[7], hr);
354 return S_OK;
358 enum class MsgType {
359 OpenDevice,
360 ResetDevice,
361 StartDevice,
362 StopDevice,
363 CloseDevice,
365 QuitThread
368 constexpr const char *GetMessageTypeName(MsgType type) noexcept
370 switch(type)
372 case MsgType::OpenDevice: return "Open Device";
373 case MsgType::ResetDevice: return "Reset Device";
374 case MsgType::StartDevice: return "Start Device";
375 case MsgType::StopDevice: return "Stop Device";
376 case MsgType::CloseDevice: return "Close Device";
377 case MsgType::QuitThread: break;
379 return "";
383 /* Proxy interface used by the message handler, to ensure COM objects are used
384 * on a thread where COM is initialized.
386 struct OtherIOProxy {
387 OtherIOProxy() = default;
388 OtherIOProxy(const OtherIOProxy&) = delete;
389 OtherIOProxy(OtherIOProxy&&) = delete;
390 virtual ~OtherIOProxy() = default;
392 void operator=(const OtherIOProxy&) = delete;
393 void operator=(OtherIOProxy&&) = delete;
395 virtual HRESULT openProxy(std::string_view name) = 0;
396 virtual void closeProxy() = 0;
398 virtual HRESULT resetProxy() = 0;
399 virtual HRESULT startProxy() = 0;
400 virtual void stopProxy() = 0;
402 struct Msg {
403 MsgType mType;
404 OtherIOProxy *mProxy;
405 std::string_view mParam;
406 std::promise<HRESULT> mPromise;
408 explicit operator bool() const noexcept { return mType != MsgType::QuitThread; }
410 static inline std::deque<Msg> mMsgQueue;
411 static inline std::mutex mMsgQueueLock;
412 static inline std::condition_variable mMsgQueueCond;
414 auto pushMessage(MsgType type, std::string_view param={}) -> std::future<HRESULT>
416 auto promise = std::promise<HRESULT>{};
417 auto future = std::future<HRESULT>{promise.get_future()};
419 auto msglock = std::lock_guard{mMsgQueueLock};
420 mMsgQueue.emplace_back(Msg{type, this, param, std::move(promise)});
422 mMsgQueueCond.notify_one();
423 return future;
426 static auto popMessage() -> Msg
428 auto lock = std::unique_lock{mMsgQueueLock};
429 mMsgQueueCond.wait(lock, []{return !mMsgQueue.empty();});
430 auto msg = Msg{std::move(mMsgQueue.front())};
431 mMsgQueue.pop_front();
432 return msg;
435 static void messageHandler(std::promise<HRESULT> *promise);
438 void OtherIOProxy::messageHandler(std::promise<HRESULT> *promise)
440 TRACE("Starting COM message thread\n");
442 auto com = ComWrapper{COINIT_APARTMENTTHREADED};
443 if(!com)
445 WARN("Failed to initialize COM: 0x%08lx\n", com.status());
446 promise->set_value(com.status());
447 return;
450 auto hr = PopulateDeviceList();
451 if(FAILED(hr))
453 promise->set_value(hr);
454 return;
457 promise->set_value(S_OK);
458 promise = nullptr;
460 TRACE("Starting message loop\n");
461 while(Msg msg{popMessage()})
463 TRACE("Got message \"%s\" (0x%04x, this=%p, param=\"%.*s\")\n",
464 GetMessageTypeName(msg.mType), static_cast<uint>(msg.mType),
465 static_cast<void*>(msg.mProxy), al::sizei(msg.mParam), msg.mParam.data());
467 switch(msg.mType)
469 case MsgType::OpenDevice:
470 hr = msg.mProxy->openProxy(msg.mParam);
471 msg.mPromise.set_value(hr);
472 continue;
474 case MsgType::ResetDevice:
475 hr = msg.mProxy->resetProxy();
476 msg.mPromise.set_value(hr);
477 continue;
479 case MsgType::StartDevice:
480 hr = msg.mProxy->startProxy();
481 msg.mPromise.set_value(hr);
482 continue;
484 case MsgType::StopDevice:
485 msg.mProxy->stopProxy();
486 msg.mPromise.set_value(S_OK);
487 continue;
489 case MsgType::CloseDevice:
490 msg.mProxy->closeProxy();
491 msg.mPromise.set_value(S_OK);
492 continue;
494 case MsgType::QuitThread:
495 break;
497 ERR("Unexpected message: %u\n", static_cast<uint>(msg.mType));
498 msg.mPromise.set_value(E_FAIL);
500 TRACE("Message loop finished\n");
504 struct OtherIOPlayback final : public BackendBase, OtherIOProxy {
505 OtherIOPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
506 ~OtherIOPlayback() final;
508 void mixerProc();
510 void open(std::string_view name) final;
511 auto openProxy(std::string_view name) -> HRESULT final;
512 void closeProxy() final;
513 auto reset() -> bool final;
514 auto resetProxy() -> HRESULT final;
515 void start() final;
516 auto startProxy() -> HRESULT final;
517 void stop() final;
518 void stopProxy() final;
520 HRESULT mOpenStatus{E_FAIL};
522 std::atomic<bool> mKillNow{true};
523 std::thread mThread;
526 OtherIOPlayback::~OtherIOPlayback()
528 if(SUCCEEDED(mOpenStatus))
529 pushMessage(MsgType::CloseDevice).wait();
532 void OtherIOPlayback::mixerProc()
534 const auto restTime = milliseconds{mDevice->UpdateSize*1000/mDevice->Frequency / 2};
536 SetRTPriority();
537 althrd_setname(GetMixerThreadName());
539 auto done = int64_t{0};
540 auto start = std::chrono::steady_clock::now();
541 while(!mKillNow.load(std::memory_order_acquire)
542 && mDevice->Connected.load(std::memory_order_acquire))
544 auto now = std::chrono::steady_clock::now();
546 /* This converts from nanoseconds to nanosamples, then to samples. */
547 auto avail = int64_t{std::chrono::duration_cast<seconds>((now-start) * mDevice->Frequency).count()};
548 if(avail-done < mDevice->UpdateSize)
550 std::this_thread::sleep_for(restTime);
551 continue;
553 while(avail-done >= mDevice->UpdateSize)
555 mDevice->renderSamples(nullptr, mDevice->UpdateSize, 0u);
556 done += mDevice->UpdateSize;
559 if(done >= mDevice->Frequency)
561 auto s = seconds{done/mDevice->Frequency};
562 start += s;
563 done -= mDevice->Frequency*s.count();
569 void OtherIOPlayback::open(std::string_view name)
571 if(name.empty() && !gDeviceList.empty())
572 name = gDeviceList[0].mDrvName;
573 else
575 constexpr auto prefix = "OpenAL Soft on "sv;
576 if(al::starts_with(name, prefix))
577 name.remove_prefix(prefix.size());
579 auto iter = std::find_if(gDeviceList.cbegin(), gDeviceList.cend(),
580 [name](const DeviceEntry &entry) { return entry.mDrvName == name; });
581 if(iter == gDeviceList.cend())
582 throw al::backend_exception{al::backend_error::NoDevice,
583 "Device name \"%.*s\" not found", al::sizei(name), name.data()};
586 mOpenStatus = pushMessage(MsgType::OpenDevice, name).get();
587 if(FAILED(mOpenStatus))
588 throw al::backend_exception{al::backend_error::DeviceError, "Failed to open \"%.*s\"",
589 al::sizei(name), name.data()};
591 mDevice->DeviceName = "OpenAL Soft on "+std::string{name};
594 auto OtherIOPlayback::openProxy(std::string_view name [[maybe_unused]]) -> HRESULT
596 return S_OK;
599 void OtherIOPlayback::closeProxy()
603 auto OtherIOPlayback::reset() -> bool
605 return SUCCEEDED(pushMessage(MsgType::ResetDevice).get());
608 auto OtherIOPlayback::resetProxy() -> HRESULT
610 setDefaultWFXChannelOrder();
611 return S_OK;
614 void OtherIOPlayback::start()
616 auto hr = pushMessage(MsgType::StartDevice).get();
617 if(FAILED(hr))
618 throw al::backend_exception{al::backend_error::DeviceError,
619 "Failed to start playback: 0x%08lx", hr};
622 auto OtherIOPlayback::startProxy() -> HRESULT
624 try {
625 mKillNow.store(false, std::memory_order_release);
626 mThread = std::thread{std::mem_fn(&OtherIOPlayback::mixerProc), this};
627 return S_OK;
629 catch(std::exception& e) {
630 ERR("Failed to start mixing thread: %s", e.what());
632 return E_FAIL;
635 void OtherIOPlayback::stop()
637 pushMessage(MsgType::StopDevice).wait();
640 void OtherIOPlayback::stopProxy()
642 if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
643 return;
644 mThread.join();
647 } // namespace
650 auto OtherIOBackendFactory::init() -> bool
652 static HRESULT InitResult{E_FAIL};
653 if(FAILED(InitResult)) try
655 auto promise = std::promise<HRESULT>{};
656 auto future = promise.get_future();
658 std::thread{&OtherIOProxy::messageHandler, &promise}.detach();
659 InitResult = future.get();
661 catch(...) {
664 return SUCCEEDED(InitResult);
667 auto OtherIOBackendFactory::querySupport(BackendType type) -> bool
668 { return type == BackendType::Playback; }
670 auto OtherIOBackendFactory::enumerate(BackendType type) -> std::vector<std::string>
672 std::vector<std::string> outnames;
674 switch(type)
676 case BackendType::Playback:
677 std::for_each(gDeviceList.cbegin(), gDeviceList.cend(),
678 [&outnames](const DeviceEntry &entry)
679 { outnames.emplace_back("OpenAL Soft on "+entry.mDrvName); });
680 break;
682 case BackendType::Capture:
683 break;
686 return outnames;
689 auto OtherIOBackendFactory::createBackend(DeviceBase *device, BackendType type) -> BackendPtr
691 if(type == BackendType::Playback)
692 return BackendPtr{new OtherIOPlayback{device}};
693 return nullptr;
696 auto OtherIOBackendFactory::getFactory() -> BackendFactory&
698 static auto factory = OtherIOBackendFactory{};
699 return factory;
702 auto OtherIOBackendFactory::queryEventSupport(alc::EventType, BackendType) -> alc::EventSupport
704 return alc::EventSupport::NoSupport;