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>
61 #include "althrd_setname.h"
63 #include "core/converter.h"
64 #include "core/device.h"
65 #include "core/helpers.h"
66 #include "core/logging.h"
70 /* A custom C++ interface that should be capable of interoperating with ASIO
73 enum class ORIOError
: LONG
{
85 /* A 64-bit integer or double, which has the most significant 32-bit word first. */
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
{
129 struct ORIOClockSource
{
134 std::array
<char,32> mName
;
137 struct ORIOChannelInfo
{
142 ORIOSampleType mSampleType
;
143 std::array
<char,32> mName
;
146 struct ORIOBufferInfo
{
149 std::array
<void*,2> mBuffers
;
155 ORIO64Bit mSystemTime
;
156 ORIO64Bit mSamplePosition
;
159 std::array
<char,12> mReserved
;
163 ORIO64Bit mTimeCodeSamples
;
165 std::array
<char,64> mFuture
;
168 std::array
<LONG
,4> mReserved
;
174 #define ORIO_CALLBACK CALLBACK
176 #define ORIO_CALLBACK
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.
190 _Pragma("GCC diagnostic push")
191 _Pragma("GCC diagnostic ignored \"-Wnon-virtual-dtor\"")
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;
226 _Pragma("GCC diagnostic pop")
231 using namespace std::string_view_literals
;
232 using std::chrono::nanoseconds
;
233 using std::chrono::milliseconds
;
234 using std::chrono::seconds
;
238 std::string mDrvName
;
242 std::vector
<DeviceEntry
> gDeviceList
;
246 void operator()(HKEY key
) { RegCloseKey(key
); }
248 using KeyPtr
= std::unique_ptr
<std::remove_pointer_t
<HKEY
>,KeyCloser
>;
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
);
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
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,
282 if(res
!= ERROR_SUCCESS
)
284 ERR("Error querying HKLM\\Software\\ASIO subkey %lu: %ld\n", i
, res
);
289 ERR("HKLM\\Software\\ASIO subkey %lu is blank?\n", i
);
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
);
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(),
306 if(res
!= ERROR_SUCCESS
)
308 ERR("Failed to read HKLM\\Software\\ASIO\\%s\\CLSID: %ld\n", subkeyname
.c_str(), res
);
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
);
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
));
326 if(!iface
->Init(GetForegroundWindow()))
328 if(!iface
->Init(nullptr))
331 ERR("Failed to initialize %s\n", subkeyname
.c_str());
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]);
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
);
368 constexpr const char *GetMessageTypeName(MsgType type
) noexcept
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;
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;
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();
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();
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
};
445 WARN("Failed to initialize COM: 0x%08lx\n", com
.status());
446 promise
->set_value(com
.status());
450 auto hr
= PopulateDeviceList();
453 promise
->set_value(hr
);
457 promise
->set_value(S_OK
);
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());
469 case MsgType::OpenDevice
:
470 hr
= msg
.mProxy
->openProxy(msg
.mParam
);
471 msg
.mPromise
.set_value(hr
);
474 case MsgType::ResetDevice
:
475 hr
= msg
.mProxy
->resetProxy();
476 msg
.mPromise
.set_value(hr
);
479 case MsgType::StartDevice
:
480 hr
= msg
.mProxy
->startProxy();
481 msg
.mPromise
.set_value(hr
);
484 case MsgType::StopDevice
:
485 msg
.mProxy
->stopProxy();
486 msg
.mPromise
.set_value(S_OK
);
489 case MsgType::CloseDevice
:
490 msg
.mProxy
->closeProxy();
491 msg
.mPromise
.set_value(S_OK
);
494 case MsgType::QuitThread
:
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
;
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
;
516 auto startProxy() -> HRESULT final
;
518 void stopProxy() final
;
520 HRESULT mOpenStatus
{E_FAIL
};
522 std::atomic
<bool> mKillNow
{true};
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};
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
);
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
};
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
;
575 auto iter
= std::find_if(gDeviceList
.cbegin(), gDeviceList
.cend(),
576 [name
](const DeviceEntry
&entry
) { return entry
.mDrvName
== name
; });
577 if(iter
== gDeviceList
.cend())
578 throw al::backend_exception
{al::backend_error::NoDevice
,
579 "Device name \"%.*s\" not found", al::sizei(name
), name
.data()};
582 mOpenStatus
= pushMessage(MsgType::OpenDevice
, name
).get();
583 if(FAILED(mOpenStatus
))
584 throw al::backend_exception
{al::backend_error::DeviceError
, "Failed to open \"%.*s\"",
585 al::sizei(name
), name
.data()};
590 auto OtherIOPlayback::openProxy(std::string_view name
[[maybe_unused
]]) -> HRESULT
595 void OtherIOPlayback::closeProxy()
599 auto OtherIOPlayback::reset() -> bool
601 return SUCCEEDED(pushMessage(MsgType::ResetDevice
).get());
604 auto OtherIOPlayback::resetProxy() -> HRESULT
606 setDefaultWFXChannelOrder();
610 void OtherIOPlayback::start()
612 auto hr
= pushMessage(MsgType::StartDevice
).get();
614 throw al::backend_exception
{al::backend_error::DeviceError
,
615 "Failed to start playback: 0x%08lx", hr
};
618 auto OtherIOPlayback::startProxy() -> HRESULT
621 mKillNow
.store(false, std::memory_order_release
);
622 mThread
= std::thread
{std::mem_fn(&OtherIOPlayback::mixerProc
), this};
625 catch(std::exception
& e
) {
626 ERR("Failed to start mixing thread: %s", e
.what());
631 void OtherIOPlayback::stop()
633 pushMessage(MsgType::StopDevice
).wait();
636 void OtherIOPlayback::stopProxy()
638 if(mKillNow
.exchange(true, std::memory_order_acq_rel
) || !mThread
.joinable())
646 auto OtherIOBackendFactory::init() -> bool
648 static HRESULT InitResult
{E_FAIL
};
649 if(FAILED(InitResult
)) try
651 auto promise
= std::promise
<HRESULT
>{};
652 auto future
= promise
.get_future();
654 std::thread
{&OtherIOProxy::messageHandler
, &promise
}.detach();
655 InitResult
= future
.get();
660 return SUCCEEDED(InitResult
);
663 auto OtherIOBackendFactory::querySupport(BackendType type
) -> bool
664 { return type
== BackendType::Playback
; }
666 auto OtherIOBackendFactory::enumerate(BackendType type
) -> std::vector
<std::string
>
668 std::vector
<std::string
> outnames
;
672 case BackendType::Playback
:
673 std::for_each(gDeviceList
.cbegin(), gDeviceList
.cend(),
674 [&outnames
](const DeviceEntry
&entry
) { outnames
.emplace_back(entry
.mDrvName
); });
677 case BackendType::Capture
:
684 auto OtherIOBackendFactory::createBackend(DeviceBase
*device
, BackendType type
) -> BackendPtr
686 if(type
== BackendType::Playback
)
687 return BackendPtr
{new OtherIOPlayback
{device
}};
691 auto OtherIOBackendFactory::getFactory() -> BackendFactory
&
693 static auto factory
= OtherIOBackendFactory
{};
697 auto OtherIOBackendFactory::queryEventSupport(alc::EventType
, BackendType
) -> alc::EventSupport
699 return alc::EventSupport::NoSupport
;