Check that there are ASIO devices to initialize OtherIO
[openal-soft.git] / alc / backends / otherio.cpp
blob2a4afb933cb7f13540121b35c95df9b40e4e8d41
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 "alstring.h"
60 #include "althrd_setname.h"
61 #include "comptr.h"
62 #include "core/converter.h"
63 #include "core/device.h"
64 #include "core/helpers.h"
65 #include "core/logging.h"
66 #include "strutils.h"
68 namespace {
70 using namespace std::string_view_literals;
71 using std::chrono::nanoseconds;
72 using std::chrono::milliseconds;
73 using std::chrono::seconds;
76 struct DeviceEntry {
77 std::string mDrvName;
78 CLSID mDrvGuid{};
81 std::vector<DeviceEntry> gDeviceList;
84 struct KeyCloser {
85 void operator()(HKEY key) { RegCloseKey(key); }
87 using KeyPtr = std::unique_ptr<std::remove_pointer_t<HKEY>,KeyCloser>;
89 [[nodiscard]]
90 auto PopulateDeviceList() -> HRESULT
92 auto regbase = KeyPtr{};
93 auto res = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\ASIO", 0, KEY_READ,
94 al::out_ptr(regbase));
95 if(res != ERROR_SUCCESS)
97 ERR("Error opening HKLM\\Software\\ASIO: %ld\n", res);
98 return E_NOINTERFACE;
101 auto numkeys = DWORD{};
102 auto maxkeylen = DWORD{};
103 res = RegQueryInfoKeyW(regbase.get(), nullptr, nullptr, nullptr, &numkeys, &maxkeylen, nullptr,
104 nullptr, nullptr, nullptr, nullptr, nullptr);
105 if(res != ERROR_SUCCESS)
107 ERR("Error querying HKLM\\Software\\ASIO info: %ld\n", res);
108 return E_FAIL;
111 /* maxkeylen is the max number of unicode characters a subkey is. A unicode
112 * character can occupy two WCHARs, so ensure there's enough space for them
113 * and the null char.
115 auto keyname = std::vector<WCHAR>(maxkeylen*2 + 1);
116 for(DWORD i{0};i < numkeys;++i)
118 auto namelen = static_cast<DWORD>(keyname.size());
119 res = RegEnumKeyExW(regbase.get(), i, keyname.data(), &namelen, nullptr, nullptr, nullptr,
120 nullptr);
121 if(res != ERROR_SUCCESS)
123 ERR("Error querying HKLM\\Software\\ASIO subkey %lu: %ld\n", i, res);
124 continue;
126 if(namelen == 0)
128 ERR("HKLM\\Software\\ASIO subkey %lu is blank?\n", i);
129 continue;
131 auto subkeyname = wstr_to_utf8({keyname.data(), namelen});
133 auto subkey = KeyPtr{};
134 res = RegOpenKeyExW(regbase.get(), keyname.data(), 0, KEY_READ, al::out_ptr(subkey));
135 if(res != ERROR_SUCCESS)
137 ERR("Error opening HKLM\\Software\\ASIO\\%s: %ld\n", subkeyname.c_str(), res);
138 continue;
141 auto idstr = std::array<WCHAR,48>{};
142 auto readsize = DWORD{idstr.size()*sizeof(WCHAR)};
143 res = RegGetValueW(subkey.get(), L"", L"CLSID", RRF_RT_REG_SZ, nullptr, idstr.data(),
144 &readsize);
145 if(res != ERROR_SUCCESS)
147 ERR("Failed to read HKLM\\Software\\ASIO\\%s\\CLSID: %ld\n", subkeyname.c_str(), res);
148 continue;
150 idstr.back() = 0;
152 auto guid = CLSID{};
153 if(auto hr = CLSIDFromString(idstr.data(), &guid); FAILED(hr))
155 ERR("Failed to parse CLSID \"%s\": 0x%08lx\n", wstr_to_utf8(idstr.data()).c_str(), hr);
156 continue;
159 /* The CLSID is also used for the IID. */
160 auto iface = ComPtr<IUnknown>{};
161 auto hr = CoCreateInstance(guid, nullptr, CLSCTX_INPROC_SERVER, guid, al::out_ptr(iface));
162 if(SUCCEEDED(hr))
164 auto &entry = gDeviceList.emplace_back();
165 entry.mDrvName = std::move(subkeyname);
166 entry.mDrvGuid = guid;
168 TRACE("Got driver %s, CLSID {%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}\n",
169 entry.mDrvName.c_str(), guid.Data1, guid.Data2, guid.Data3, guid.Data4[0],
170 guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5],
171 guid.Data4[6], guid.Data4[7]);
173 else
174 ERR("Failed to create %s instance for CLSID {%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}: 0x%08lx\n",
175 subkeyname.c_str(), guid.Data1, guid.Data2, guid.Data3, guid.Data4[0],
176 guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5],
177 guid.Data4[6], guid.Data4[7], hr);
180 return S_OK;
184 enum class MsgType {
185 OpenDevice,
186 ResetDevice,
187 StartDevice,
188 StopDevice,
189 CloseDevice,
191 QuitThread
194 constexpr const char *GetMessageTypeName(MsgType type) noexcept
196 switch(type)
198 case MsgType::OpenDevice: return "Open Device";
199 case MsgType::ResetDevice: return "Reset Device";
200 case MsgType::StartDevice: return "Start Device";
201 case MsgType::StopDevice: return "Stop Device";
202 case MsgType::CloseDevice: return "Close Device";
203 case MsgType::QuitThread: break;
205 return "";
209 /* Proxy interface used by the message handler, to ensure COM objects are used
210 * on a thread where COM is initialized.
212 struct OtherIOProxy {
213 OtherIOProxy() = default;
214 OtherIOProxy(const OtherIOProxy&) = delete;
215 OtherIOProxy(OtherIOProxy&&) = delete;
216 virtual ~OtherIOProxy() = default;
218 void operator=(const OtherIOProxy&) = delete;
219 void operator=(OtherIOProxy&&) = delete;
221 virtual HRESULT openProxy(std::string_view name) = 0;
222 virtual void closeProxy() = 0;
224 virtual HRESULT resetProxy() = 0;
225 virtual HRESULT startProxy() = 0;
226 virtual void stopProxy() = 0;
228 struct Msg {
229 MsgType mType;
230 OtherIOProxy *mProxy;
231 std::string_view mParam;
232 std::promise<HRESULT> mPromise;
234 explicit operator bool() const noexcept { return mType != MsgType::QuitThread; }
236 static inline std::deque<Msg> mMsgQueue;
237 static inline std::mutex mMsgQueueLock;
238 static inline std::condition_variable mMsgQueueCond;
240 auto pushMessage(MsgType type, std::string_view param) -> std::future<HRESULT>
242 auto promise = std::promise<HRESULT>{};
243 auto future = std::future<HRESULT>{promise.get_future()};
245 auto msglock = std::lock_guard{mMsgQueueLock};
246 mMsgQueue.emplace_back(Msg{type, this, param, std::move(promise)});
248 mMsgQueueCond.notify_one();
249 return future;
252 static auto popMessage() -> Msg
254 auto lock = std::unique_lock{mMsgQueueLock};
255 mMsgQueueCond.wait(lock, []{return !mMsgQueue.empty();});
256 auto msg = Msg{std::move(mMsgQueue.front())};
257 mMsgQueue.pop_front();
258 return msg;
261 static void messageHandler(std::promise<HRESULT> *promise);
264 void OtherIOProxy::messageHandler(std::promise<HRESULT> *promise)
266 TRACE("Starting COM message thread\n");
268 auto com = ComWrapper{COINIT_APARTMENTTHREADED};
269 if(!com)
271 WARN("Failed to initialize COM: 0x%08lx\n", com.status());
272 promise->set_value(com.status());
273 return;
276 auto hr = PopulateDeviceList();
277 if(FAILED(hr))
279 promise->set_value(hr);
280 return;
283 promise->set_value(S_OK);
284 promise = nullptr;
286 TRACE("Starting message loop\n");
287 while(Msg msg{popMessage()})
289 TRACE("Got message \"%s\" (0x%04x, this=%p, param=\"%.*s\")\n",
290 GetMessageTypeName(msg.mType), static_cast<uint>(msg.mType),
291 static_cast<void*>(msg.mProxy), al::sizei(msg.mParam), msg.mParam.data());
293 switch(msg.mType)
295 case MsgType::OpenDevice:
296 hr = msg.mProxy->openProxy(msg.mParam);
297 msg.mPromise.set_value(hr);
298 continue;
300 case MsgType::ResetDevice:
301 hr = msg.mProxy->resetProxy();
302 msg.mPromise.set_value(hr);
303 continue;
305 case MsgType::StartDevice:
306 hr = msg.mProxy->startProxy();
307 msg.mPromise.set_value(hr);
308 continue;
310 case MsgType::StopDevice:
311 msg.mProxy->stopProxy();
312 msg.mPromise.set_value(S_OK);
313 continue;
315 case MsgType::CloseDevice:
316 msg.mProxy->closeProxy();
317 msg.mPromise.set_value(S_OK);
318 continue;
320 case MsgType::QuitThread:
321 break;
323 ERR("Unexpected message: %u\n", static_cast<uint>(msg.mType));
324 msg.mPromise.set_value(E_FAIL);
326 TRACE("Message loop finished\n");
330 struct OtherIOPlayback final : public BackendBase, OtherIOProxy {
331 OtherIOPlayback(DeviceBase *device) noexcept : BackendBase{device} { }
332 ~OtherIOPlayback() final;
334 void mixerProc();
336 void open(std::string_view name) final;
337 auto openProxy(std::string_view name) -> HRESULT final;
338 void closeProxy() final;
339 auto reset() -> bool final;
340 auto resetProxy() -> HRESULT final;
341 void start() final;
342 auto startProxy() -> HRESULT final;
343 void stop() final;
344 void stopProxy() final;
346 HRESULT mOpenStatus{E_FAIL};
348 std::atomic<bool> mKillNow{true};
349 std::thread mThread;
352 OtherIOPlayback::~OtherIOPlayback()
354 if(SUCCEEDED(mOpenStatus))
355 pushMessage(MsgType::CloseDevice, {}).wait();
358 void OtherIOPlayback::mixerProc()
360 const auto restTime = milliseconds{mDevice->UpdateSize*1000/mDevice->Frequency / 2};
362 SetRTPriority();
363 althrd_setname(GetMixerThreadName());
365 auto done = int64_t{0};
366 auto start = std::chrono::steady_clock::now();
367 while(!mKillNow.load(std::memory_order_acquire)
368 && mDevice->Connected.load(std::memory_order_acquire))
370 auto now = std::chrono::steady_clock::now();
372 /* This converts from nanoseconds to nanosamples, then to samples. */
373 auto avail = int64_t{std::chrono::duration_cast<seconds>((now-start) * mDevice->Frequency).count()};
374 if(avail-done < mDevice->UpdateSize)
376 std::this_thread::sleep_for(restTime);
377 continue;
379 while(avail-done >= mDevice->UpdateSize)
381 mDevice->renderSamples(nullptr, mDevice->UpdateSize, 0u);
382 done += mDevice->UpdateSize;
385 if(done >= mDevice->Frequency)
387 auto s = seconds{done/mDevice->Frequency};
388 start += s;
389 done -= mDevice->Frequency*s.count();
395 void OtherIOPlayback::open(std::string_view name)
397 if(name.empty() && !gDeviceList.empty())
398 name = gDeviceList[0].mDrvName;
399 else
401 constexpr auto prefix = "OpenAL Soft on "sv;
402 if(al::starts_with(name, prefix))
403 name.remove_prefix(prefix.size());
405 auto iter = std::find_if(gDeviceList.cbegin(), gDeviceList.cend(),
406 [name](const DeviceEntry &entry) { return entry.mDrvName == name; });
407 if(iter == gDeviceList.cend())
408 throw al::backend_exception{al::backend_error::NoDevice,
409 "Device name \"%.*s\" not found", al::sizei(name), name.data()};
412 mOpenStatus = pushMessage(MsgType::OpenDevice, name).get();
413 if(FAILED(mOpenStatus))
414 throw al::backend_exception{al::backend_error::DeviceError, "Failed to open \"%.*s\"",
415 al::sizei(name), name.data()};
417 mDevice->DeviceName = "OpenAL Soft on "+std::string{name};
420 auto OtherIOPlayback::openProxy(std::string_view name [[maybe_unused]]) -> HRESULT
422 return S_OK;
425 void OtherIOPlayback::closeProxy()
429 auto OtherIOPlayback::reset() -> bool
431 return SUCCEEDED(pushMessage(MsgType::ResetDevice, {}).get());
434 auto OtherIOPlayback::resetProxy() -> HRESULT
436 setDefaultWFXChannelOrder();
437 return S_OK;
440 void OtherIOPlayback::start()
442 auto hr = pushMessage(MsgType::StartDevice, {}).get();
443 if(FAILED(hr))
444 throw al::backend_exception{al::backend_error::DeviceError,
445 "Failed to start playback: 0x%08lx", hr};
448 auto OtherIOPlayback::startProxy() -> HRESULT
450 try {
451 mKillNow.store(false, std::memory_order_release);
452 mThread = std::thread{std::mem_fn(&OtherIOPlayback::mixerProc), this};
453 return S_OK;
455 catch(std::exception& e) {
456 ERR("Failed to start mixing thread: %s", e.what());
458 return E_FAIL;
461 void OtherIOPlayback::stop()
463 pushMessage(MsgType::StopDevice, {}).wait();
466 void OtherIOPlayback::stopProxy()
468 if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
469 return;
470 mThread.join();
473 } // namespace
476 auto OtherIOBackendFactory::init() -> bool
478 static HRESULT InitResult{E_FAIL};
479 if(FAILED(InitResult)) try
481 auto promise = std::promise<HRESULT>{};
482 auto future = promise.get_future();
484 std::thread{&OtherIOProxy::messageHandler, &promise}.detach();
485 InitResult = future.get();
487 catch(...) {
490 return SUCCEEDED(InitResult);
493 auto OtherIOBackendFactory::querySupport(BackendType type) -> bool
494 { return type == BackendType::Playback; }
496 auto OtherIOBackendFactory::enumerate(BackendType type) -> std::vector<std::string>
498 std::vector<std::string> outnames;
500 switch(type)
502 case BackendType::Playback:
503 std::for_each(gDeviceList.cbegin(), gDeviceList.cend(),
504 [&outnames](const DeviceEntry &entry)
505 { outnames.emplace_back("OpenAL Soft on "+entry.mDrvName); });
506 break;
508 case BackendType::Capture:
509 break;
512 return outnames;
515 auto OtherIOBackendFactory::createBackend(DeviceBase *device, BackendType type) -> BackendPtr
517 if(type == BackendType::Playback)
518 return BackendPtr{new OtherIOPlayback{device}};
519 return nullptr;
522 auto OtherIOBackendFactory::getFactory() -> BackendFactory&
524 static auto factory = OtherIOBackendFactory{};
525 return factory;
528 auto OtherIOBackendFactory::queryEventSupport(alc::EventType, BackendType) -> alc::EventSupport
530 return alc::EventSupport::NoSupport;