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
25 #define WIN32_LEAN_AND_MEAN
35 #include <devpropdef.h>
40 #ifndef _WAVEFORMATEXTENSIBLE_
48 #include <condition_variable>
55 #include <string_view>
60 #include "althrd_setname.h"
62 #include "core/converter.h"
63 #include "core/device.h"
64 #include "core/helpers.h"
65 #include "core/logging.h"
70 using namespace std::string_view_literals
;
71 using std::chrono::nanoseconds
;
72 using std::chrono::milliseconds
;
73 using std::chrono::seconds
;
81 std::vector
<DeviceEntry
> gDeviceList
;
85 void operator()(HKEY key
) { RegCloseKey(key
); }
87 using KeyPtr
= std::unique_ptr
<std::remove_pointer_t
<HKEY
>,KeyCloser
>;
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
);
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
);
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
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,
121 if(res
!= ERROR_SUCCESS
)
123 ERR("Error querying HKLM\\Software\\ASIO subkey %lu: %ld\n", i
, res
);
128 ERR("HKLM\\Software\\ASIO subkey %lu is blank?\n", i
);
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
);
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(),
145 if(res
!= ERROR_SUCCESS
)
147 ERR("Failed to read HKLM\\Software\\ASIO\\%s\\CLSID: %ld\n", subkeyname
.c_str(), res
);
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
);
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
));
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]);
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
);
194 constexpr const char *GetMessageTypeName(MsgType type
) noexcept
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;
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;
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();
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();
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
};
271 WARN("Failed to initialize COM: 0x%08lx\n", com
.status());
272 promise
->set_value(com
.status());
276 auto hr
= PopulateDeviceList();
279 promise
->set_value(hr
);
283 promise
->set_value(S_OK
);
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());
295 case MsgType::OpenDevice
:
296 hr
= msg
.mProxy
->openProxy(msg
.mParam
);
297 msg
.mPromise
.set_value(hr
);
300 case MsgType::ResetDevice
:
301 hr
= msg
.mProxy
->resetProxy();
302 msg
.mPromise
.set_value(hr
);
305 case MsgType::StartDevice
:
306 hr
= msg
.mProxy
->startProxy();
307 msg
.mPromise
.set_value(hr
);
310 case MsgType::StopDevice
:
311 msg
.mProxy
->stopProxy();
312 msg
.mPromise
.set_value(S_OK
);
315 case MsgType::CloseDevice
:
316 msg
.mProxy
->closeProxy();
317 msg
.mPromise
.set_value(S_OK
);
320 case MsgType::QuitThread
:
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
;
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
;
342 auto startProxy() -> HRESULT final
;
344 void stopProxy() final
;
346 HRESULT mOpenStatus
{E_FAIL
};
348 std::atomic
<bool> mKillNow
{true};
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};
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
);
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
};
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
;
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
425 void OtherIOPlayback::closeProxy()
429 auto OtherIOPlayback::reset() -> bool
431 return SUCCEEDED(pushMessage(MsgType::ResetDevice
, {}).get());
434 auto OtherIOPlayback::resetProxy() -> HRESULT
436 setDefaultWFXChannelOrder();
440 void OtherIOPlayback::start()
442 auto hr
= pushMessage(MsgType::StartDevice
, {}).get();
444 throw al::backend_exception
{al::backend_error::DeviceError
,
445 "Failed to start playback: 0x%08lx", hr
};
448 auto OtherIOPlayback::startProxy() -> HRESULT
451 mKillNow
.store(false, std::memory_order_release
);
452 mThread
= std::thread
{std::mem_fn(&OtherIOPlayback::mixerProc
), this};
455 catch(std::exception
& e
) {
456 ERR("Failed to start mixing thread: %s", e
.what());
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())
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();
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
;
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
); });
508 case BackendType::Capture
:
515 auto OtherIOBackendFactory::createBackend(DeviceBase
*device
, BackendType type
) -> BackendPtr
517 if(type
== BackendType::Playback
)
518 return BackendPtr
{new OtherIOPlayback
{device
}};
522 auto OtherIOBackendFactory::getFactory() -> BackendFactory
&
524 static auto factory
= OtherIOBackendFactory
{};
528 auto OtherIOBackendFactory::queryEventSupport(alc::EventType
, BackendType
) -> alc::EventSupport
530 return alc::EventSupport::NoSupport
;