1 /*
2  * Copyright 2020-2021 The Music Player Daemon Project
3  * http://www.musicpd.org
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19 
20 #undef NOUSER // COM needs the "MSG" typedef
21 
22 #include "WasapiOutputPlugin.hxx"
23 #include "ForMixer.hxx"
24 #include "AudioClient.hxx"
25 #include "Device.hxx"
26 #include "PropertyStore.hxx"
27 #include "output/OutputAPI.hxx"
28 #include "lib/icu/Win32.hxx"
29 #include "lib/fmt/AudioFormatFormatter.hxx"
30 #include "mixer/MixerList.hxx"
31 #include "output/Error.hxx"
32 #include "pcm/Export.hxx"
33 #include "thread/Cond.hxx"
34 #include "thread/Mutex.hxx"
35 #include "thread/Name.hxx"
36 #include "thread/Thread.hxx"
37 #include "util/AllocatedString.hxx"
38 #include "util/ConstBuffer.hxx"
39 #include "util/Domain.hxx"
40 #include "util/RuntimeError.hxx"
41 #include "util/ScopeExit.hxx"
42 #include "util/StringBuffer.hxx"
43 #include "win32/Com.hxx"
44 #include "win32/ComPtr.hxx"
45 #include "win32/ComWorker.hxx"
46 #include "win32/HResult.hxx"
47 #include "win32/WinEvent.hxx"
48 #include "Log.hxx"
49 #include "config.h"
50 
51 #include <boost/lockfree/spsc_queue.hpp>
52 
53 #include <algorithm>
54 #include <cinttypes>
55 #include <cmath>
56 #include <optional>
57 #include <variant>
58 
59 #include <audioclient.h>
60 #include <initguid.h>
61 #include <functiondiscoverykeys_devpkey.h>
62 #include <mmdeviceapi.h>
63 
64 namespace {
65 static constexpr Domain wasapi_output_domain("wasapi_output");
66 
67 constexpr uint32_t
GetChannelMask(const uint8_t channels)68 GetChannelMask(const uint8_t channels) noexcept
69 {
70 	switch (channels) {
71 	case 1:
72 		return KSAUDIO_SPEAKER_MONO;
73 	case 2:
74 		return KSAUDIO_SPEAKER_STEREO;
75 	case 3:
76 		return SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER;
77 	case 4:
78 		return KSAUDIO_SPEAKER_QUAD;
79 	case 5:
80 		return SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER |
81 		       SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT;
82 	case 6:
83 		return KSAUDIO_SPEAKER_5POINT1;
84 	case 7:
85 		return KSAUDIO_SPEAKER_5POINT1 | SPEAKER_BACK_CENTER;
86 	case 8:
87 		return KSAUDIO_SPEAKER_7POINT1_SURROUND;
88 	default:
89 		gcc_unreachable();
90 	}
91 }
92 
93 template <typename Functor>
94 inline bool
SafeSilenceTry(Functor && functor)95 SafeSilenceTry(Functor &&functor) noexcept
96 {
97 	try {
98 		functor();
99 		return true;
100 	} catch (...) {
101 		return false;
102 	}
103 }
104 
105 std::vector<WAVEFORMATEXTENSIBLE>
GetFormats(const AudioFormat & audio_format)106 GetFormats(const AudioFormat &audio_format) noexcept
107 {
108 #ifdef ENABLE_DSD
109 	if (audio_format.format == SampleFormat::DSD) {
110 		AudioFormat dop_format = audio_format;
111 		PcmExport::Params params;
112 		params.dsd_mode = PcmExport::DsdMode::DOP;
113 		dop_format.sample_rate =
114 			params.CalcOutputSampleRate(audio_format.sample_rate);
115 		dop_format.format = SampleFormat::S24_P32;
116 		return GetFormats(dop_format);
117 	}
118 #endif
119 	std::vector<WAVEFORMATEXTENSIBLE> Result;
120 	if (audio_format.format == SampleFormat::S24_P32) {
121 		Result.resize(2);
122 		Result[0].Format.wBitsPerSample = 24;
123 		Result[0].Samples.wValidBitsPerSample = 24;
124 		Result[1].Format.wBitsPerSample = 32;
125 		Result[1].Samples.wValidBitsPerSample = 24;
126 	} else {
127 		Result.resize(1);
128 		Result[0].Format.wBitsPerSample = audio_format.GetSampleSize() * 8;
129 		Result[0].Samples.wValidBitsPerSample = audio_format.GetSampleSize() * 8;
130 	}
131 	const DWORD mask = GetChannelMask(audio_format.channels);
132 	const GUID guid = audio_format.format == SampleFormat::FLOAT
133 				  ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
134 				  : KSDATAFORMAT_SUBTYPE_PCM;
135 	for (auto &device_format : Result) {
136 		device_format.dwChannelMask = mask;
137 		device_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
138 		device_format.Format.nChannels = audio_format.channels;
139 		device_format.Format.nSamplesPerSec = audio_format.sample_rate;
140 		device_format.Format.cbSize =
141 			sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
142 		device_format.SubFormat = guid;
143 		device_format.Format.nBlockAlign = device_format.Format.nChannels *
144 						   device_format.Format.wBitsPerSample /
145 						   8;
146 		device_format.Format.nAvgBytesPerSec =
147 			audio_format.sample_rate * device_format.Format.nBlockAlign;
148 	}
149 	return Result;
150 }
151 
152 #ifdef ENABLE_DSD
153 void
SetDSDFallback(AudioFormat & audio_format)154 SetDSDFallback(AudioFormat &audio_format) noexcept
155 {
156 	audio_format.format = SampleFormat::FLOAT;
157 	audio_format.sample_rate = 384000;
158 }
159 #endif
160 
161 } // namespace
162 
163 class WasapiOutputThread {
164 	Thread thread{BIND_THIS_METHOD(Work)};
165 	WinEvent event;
166 	WinEvent data_poped;
167 	IAudioClient &client;
168 	ComPtr<IAudioRenderClient> render_client;
169 	const UINT32 frame_size;
170 	const UINT32 buffer_size_in_frames;
171 	const bool is_exclusive;
172 
173 	/**
174 	 * This flag is only used by the calling thread
175 	 * (i.e. #OutputThread), and specifies whether the
176 	 * WasapiOutputThread has been told to play via Play().  This
177 	 * variable is somewhat redundant because we already have
178 	 * "state", but using this variable saves some overhead for
179 	 * atomic operations.
180 	 */
181 	bool playing = false;
182 
183 	bool started = false;
184 
185 	std::atomic_bool cancel = false;
186 
187 	std::atomic_bool empty = true;
188 
189 	enum class Status : uint32_t { FINISH, PLAY, PAUSE };
190 
191 	alignas(BOOST_LOCKFREE_CACHELINE_BYTES) std::atomic<Status> status =
192 		Status::PAUSE;
193 	alignas(BOOST_LOCKFREE_CACHELINE_BYTES) struct {
194 		std::atomic_bool occur = false;
195 		std::exception_ptr ptr = nullptr;
196 	} error;
197 	boost::lockfree::spsc_queue<BYTE> spsc_buffer;
198 
199 public:
WasapiOutputThread(IAudioClient & _client,ComPtr<IAudioRenderClient> && _render_client,const UINT32 _frame_size,const UINT32 _buffer_size_in_frames,bool _is_exclusive)200 	WasapiOutputThread(IAudioClient &_client,
201 			   ComPtr<IAudioRenderClient> &&_render_client,
202 			   const UINT32 _frame_size, const UINT32 _buffer_size_in_frames,
203 			   bool _is_exclusive)
204 		:client(_client),
205 		 render_client(std::move(_render_client)), frame_size(_frame_size),
206 		 buffer_size_in_frames(_buffer_size_in_frames), is_exclusive(_is_exclusive),
207 		 spsc_buffer(_buffer_size_in_frames * 4 * _frame_size)
208 	{
209 		SetEventHandle(client, event.handle());
210 		thread.Start();
211 	}
212 
Finish()213 	void Finish() noexcept {
214 		SetStatus(Status::FINISH);
215 		thread.Join();
216 	}
217 
Play()218 	void Play() noexcept {
219 		playing = true;
220 		SetStatus(Status::PLAY);
221 	}
222 
Pause()223 	void Pause() noexcept {
224 		if (!playing)
225 			return;
226 
227 		playing = false;
228 		SetStatus(Status::PAUSE);
229 	}
230 
Push(ConstBuffer<void> input)231 	std::size_t Push(ConstBuffer<void> input) noexcept {
232 		empty.store(false);
233 
234 		std::size_t consumed =
235 			spsc_buffer.push(static_cast<const BYTE *>(input.data),
236 					 input.size);
237 
238 		if (!playing) {
239 			playing = true;
240 			Play();
241 		}
242 
243 		return consumed;
244 	}
245 
246 	/**
247 	 * Check if the buffer is empty, and if not, wait a bit.
248 	 *
249 	 * Throws on error.
250 	 *
251 	 * @return true if the buffer is now empty
252 	 */
Drain()253 	bool Drain() {
254 		if (empty)
255 			return true;
256 
257 		CheckException();
258 		Wait();
259 		CheckException();
260 
261 		return empty;
262 	}
263 
264 	/**
265 	 * Instruct the thread to discard the buffer (and wait for
266 	 * completion).  This needs to be done inside this thread,
267 	 * because only the consumer thread is allowed to do that.
268 	 */
Cancel()269 	void Cancel() noexcept {
270 		cancel.store(true);
271 		event.Set();
272 
273 		while (cancel.load() && !error.occur.load())
274 			Wait();
275 
276 		/* not rethrowing the exception here via
277 		   CheckException() because this method must be
278 		   "noexcept"; the next WasapiOutput::Play() call will
279 		   throw */
280 	}
281 
282 	/**
283 	 * Wait for the thread to finish some work (e.g. until some
284 	 * buffer space becomes available).
285 	 */
Wait()286 	void Wait() noexcept {
287 		data_poped.Wait();
288 	}
289 
InterruptWaiter()290 	void InterruptWaiter() noexcept {
291 		data_poped.Set();
292 	}
293 
CheckException()294 	void CheckException() {
295 		if (error.occur.load()) {
296 			std::rethrow_exception(error.ptr);
297 		}
298 	}
299 
300 private:
SetStatus(Status s)301 	void SetStatus(Status s) noexcept {
302 		status.store(s);
303 		event.Set();
304 	}
305 	void Work() noexcept;
306 };
307 
308 class WasapiOutput final : public AudioOutput {
309 	const bool is_exclusive;
310 	const bool enumerate_devices;
311 #ifdef ENABLE_DSD
312 	const bool dop_setting;
313 #endif
314 
315 	/**
316 	 * Only valid if the output is open.
317 	 */
318 	bool paused;
319 
320 	std::atomic_flag not_interrupted = true;
321 
322 	const std::string device_config;
323 
324 	std::shared_ptr<COMWorker> com_worker;
325 	ComPtr<IMMDevice> device;
326 	ComPtr<IAudioClient> client;
327 	WAVEFORMATEXTENSIBLE device_format;
328 	std::optional<WasapiOutputThread> thread;
329 	std::size_t watermark;
330 	std::optional<PcmExport> pcm_export;
331 
332 public:
333 	static AudioOutput *Create(EventLoop &, const ConfigBlock &block);
334 	WasapiOutput(const ConfigBlock &block);
335 
GetComWorker()336 	auto GetComWorker() noexcept {
337 		// TODO: protect access to the shard_ptr
338 		return com_worker;
339 	}
340 
Enable()341 	void Enable() override {
342 		com_worker = std::make_shared<COMWorker>();
343 
344 		try {
345 			com_worker->Async([&]() { ChooseDevice(); }).get();
346 		} catch (...) {
347 			com_worker.reset();
348 			throw;
349 		}
350 	}
Disable()351 	void Disable() noexcept override {
352 		com_worker->Async([&]() { DoDisable(); }).get();
353 		com_worker.reset();
354 	}
Open(AudioFormat & audio_format)355 	void Open(AudioFormat &audio_format) override {
356 		com_worker->Async([&]() { DoOpen(audio_format); }).get();
357 		paused = false;
358 	}
359 	void Close() noexcept override;
360 	std::chrono::steady_clock::duration Delay() const noexcept override;
361 	size_t Play(const void *chunk, size_t size) override;
362 	void Drain() override;
363 	void Cancel() noexcept override;
364 	bool Pause() override;
365 	void Interrupt() noexcept override;
366 
Exclusive() const367 	constexpr bool Exclusive() const { return is_exclusive; }
FrameSize() const368 	constexpr size_t FrameSize() const { return device_format.Format.nBlockAlign; }
SampleRate() const369 	constexpr size_t SampleRate() const {
370 		return device_format.Format.nSamplesPerSec;
371 	}
372 
373 private:
374 	friend bool wasapi_is_exclusive(WasapiOutput &output) noexcept;
375 	friend IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept;
376 	friend IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept;
377 
378 	void DoDisable() noexcept;
379 	void DoOpen(AudioFormat &audio_format);
380 
381 	void ChooseDevice();
382 	bool TryFormatExclusive(const AudioFormat &audio_format);
383 	void FindExclusiveFormatSupported(AudioFormat &audio_format);
384 	void FindSharedFormatSupported(AudioFormat &audio_format);
385 	static void EnumerateDevices(IMMDeviceEnumerator &enumerator);
386 	static ComPtr<IMMDevice> GetDevice(IMMDeviceEnumerator &enumerator,
387 					   unsigned index);
388 	static ComPtr<IMMDevice> SearchDevice(IMMDeviceEnumerator &enumerator,
389 					      std::string_view name);
390 };
391 
392 WasapiOutput &
wasapi_output_downcast(AudioOutput & output)393 wasapi_output_downcast(AudioOutput &output) noexcept
394 {
395 	return static_cast<WasapiOutput &>(output);
396 }
397 
398 bool
wasapi_is_exclusive(WasapiOutput & output)399 wasapi_is_exclusive(WasapiOutput &output) noexcept
400 {
401 	return output.is_exclusive;
402 }
403 
404 std::shared_ptr<COMWorker>
wasapi_output_get_com_worker(WasapiOutput & output)405 wasapi_output_get_com_worker(WasapiOutput &output) noexcept
406 {
407 	return output.GetComWorker();
408 }
409 
410 IMMDevice *
wasapi_output_get_device(WasapiOutput & output)411 wasapi_output_get_device(WasapiOutput &output) noexcept
412 {
413 	return output.device.get();
414 }
415 
416 IAudioClient *
wasapi_output_get_client(WasapiOutput & output)417 wasapi_output_get_client(WasapiOutput &output) noexcept
418 {
419 	return output.client.get();
420 }
421 
422 inline void
Work()423 WasapiOutputThread::Work() noexcept
424 try {
425 	SetThreadName("Wasapi Output Worker");
426 	LogDebug(wasapi_output_domain, "Working thread started");
427 	COM com;
428 
429 	AtScopeExit(this) {
430 		if (started) {
431 			try {
432 				Stop(client);
433 			} catch (...) {
434 				LogError(std::current_exception());
435 			}
436 		}
437 	};
438 
439 	while (true) {
440 		event.Wait();
441 
442 		if (cancel.load()) {
443 			spsc_buffer.consume_all([](auto &&) {});
444 			cancel.store(false);
445 			empty.store(true);
446 			InterruptWaiter();
447 		}
448 
449 		Status current_state = status.load();
450 		switch (current_state) {
451 		case Status::FINISH:
452 			LogDebug(wasapi_output_domain,
453 				 "Working thread stopped");
454 			return;
455 
456 		case Status::PAUSE:
457 			if (!started)
458 				/* don't bother starting the
459 				   IAudioClient if we're paused */
460 				continue;
461 
462 			/* stop the IAudioClient while paused; it will
463 			   be restarted as soon as we're asked to
464 			   resume playback */
465 			Stop(client);
466 			started = false;
467 			continue;
468 
469 		case Status::PLAY:
470 			break;
471 		}
472 
473 		UINT32 write_in_frames = buffer_size_in_frames;
474 		DWORD mode = 0;
475 		AtScopeExit(&) {
476 			render_client->ReleaseBuffer(write_in_frames, mode);
477 
478 			if (!started) {
479 				Start(client);
480 				started = true;
481 			}
482 		};
483 
484 		if (!is_exclusive) {
485 			UINT32 data_in_frames =
486 				GetCurrentPaddingFrames(client);
487 			if (data_in_frames >= buffer_size_in_frames) {
488 				continue;
489 			}
490 			write_in_frames -= data_in_frames;
491 		}
492 
493 		BYTE *data;
494 
495 		if (HRESULT result =
496 		    render_client->GetBuffer(write_in_frames, &data);
497 		    FAILED(result)) {
498 			throw MakeHResultError(result, "Failed to get buffer");
499 		}
500 
501 		const UINT32 write_size = write_in_frames * frame_size;
502 		UINT32 new_data_size = 0;
503 		new_data_size = spsc_buffer.pop(data, write_size);
504 		if (new_data_size == 0)
505 			empty.store(true);
506 
507 		std::fill_n(data + new_data_size,
508 			    write_size - new_data_size, 0);
509 		InterruptWaiter();
510 	}
511 } catch (...) {
512 	error.ptr = std::current_exception();
513 	error.occur.store(true);
514 
515 	/* wake up the client thread which may be inside Wait() */
516 	InterruptWaiter();
517 }
518 
519 AudioOutput *
Create(EventLoop &,const ConfigBlock & block)520 WasapiOutput::Create(EventLoop &, const ConfigBlock &block)
521 {
522 	return new WasapiOutput(block);
523 }
524 
WasapiOutput(const ConfigBlock & block)525 WasapiOutput::WasapiOutput(const ConfigBlock &block)
526 	:AudioOutput(FLAG_ENABLE_DISABLE | FLAG_PAUSE),
527 	 is_exclusive(block.GetBlockValue("exclusive", false)),
528 	 enumerate_devices(block.GetBlockValue("enumerate", false)),
529 #ifdef ENABLE_DSD
530 	 dop_setting(block.GetBlockValue("dop", false)),
531 #endif
532 	 device_config(block.GetBlockValue("device", ""))
533 {
534 }
535 
536 /// run inside COMWorkerThread
537 void
DoDisable()538 WasapiOutput::DoDisable() noexcept
539 {
540 	assert(!thread);
541 
542 	device.reset();
543 }
544 
545 /// run inside COMWorkerThread
546 void
DoOpen(AudioFormat & audio_format)547 WasapiOutput::DoOpen(AudioFormat &audio_format)
548 {
549 	client.reset();
550 
551 	if (GetState(*device) != DEVICE_STATE_ACTIVE) {
552 		device.reset();
553 		ChooseDevice();
554 	}
555 
556 	client = Activate<IAudioClient>(*device);
557 
558 	if (audio_format.channels > 8) {
559 		audio_format.channels = 8;
560 	}
561 
562 #ifdef ENABLE_DSD
563 	if (!dop_setting && audio_format.format == SampleFormat::DSD) {
564 		SetDSDFallback(audio_format);
565 	}
566 #endif
567 	if (Exclusive()) {
568 		FindExclusiveFormatSupported(audio_format);
569 	} else {
570 		FindSharedFormatSupported(audio_format);
571 	}
572 	bool require_export = audio_format.format == SampleFormat::S24_P32;
573 #ifdef ENABLE_DSD
574 	require_export |= audio_format.format == SampleFormat::DSD;
575 #endif
576 	if (require_export) {
577 		PcmExport::Params params;
578 #ifdef ENABLE_DSD
579 		params.dsd_mode = PcmExport::DsdMode::NONE;
580 		if (audio_format.format == SampleFormat::DSD) {
581 			params.dsd_mode = PcmExport::DsdMode::DOP;
582 		}
583 #endif
584 		params.shift8 = false;
585 		params.pack24 = false;
586 		if (device_format.Format.wBitsPerSample == 32 &&
587 		    device_format.Samples.wValidBitsPerSample == 24) {
588 			params.shift8 = true;
589 		}
590 		if (device_format.Format.wBitsPerSample == 24) {
591 			params.pack24 = true;
592 		}
593 		FmtDebug(wasapi_output_domain, "Packing data: shift8={} pack24={}",
594 			 params.shift8, params.pack24);
595 		pcm_export.emplace();
596 		pcm_export->Open(audio_format.format, audio_format.channels, params);
597 	}
598 
599 	using s = std::chrono::seconds;
600 	using ms = std::chrono::milliseconds;
601 	using ns = std::chrono::nanoseconds;
602 	using hundred_ns = std::chrono::duration<uint64_t, std::ratio<1, 10000000>>;
603 
604 	// The unit in REFERENCE_TIME is hundred nanoseconds
605 	REFERENCE_TIME default_device_period, min_device_period;
606 
607 	if (HRESULT result =
608 		    client->GetDevicePeriod(&default_device_period, &min_device_period);
609 	    FAILED(result)) {
610 		throw MakeHResultError(result, "Unable to get device period");
611 	}
612 	FmtDebug(wasapi_output_domain,
613 		 "Default device period: {} ns, Minimum device period: "
614 		 "{} ns",
615 		 ns(hundred_ns(default_device_period)).count(),
616 		 ns(hundred_ns(min_device_period)).count());
617 
618 	REFERENCE_TIME buffer_duration;
619 	if (Exclusive()) {
620 		buffer_duration = default_device_period;
621 	} else {
622 		const REFERENCE_TIME align = hundred_ns(ms(50)).count();
623 		buffer_duration = (align / default_device_period) * default_device_period;
624 	}
625 	FmtDebug(wasapi_output_domain, "Buffer duration: {} ns",
626 		 ns(hundred_ns(buffer_duration)).count());
627 
628 	if (Exclusive()) {
629 		if (HRESULT result = client->Initialize(
630 			    AUDCLNT_SHAREMODE_EXCLUSIVE,
631 			    AUDCLNT_STREAMFLAGS_EVENTCALLBACK, buffer_duration,
632 			    buffer_duration,
633 			    reinterpret_cast<WAVEFORMATEX *>(&device_format), nullptr);
634 		    FAILED(result)) {
635 			if (result == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
636 				// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize
637 				UINT32 buffer_size_in_frames =
638 					GetBufferSizeInFrames(*client);
639 				buffer_duration =
640 					std::ceil(double(buffer_size_in_frames *
641 							 hundred_ns(s(1)).count()) /
642 						  SampleRate());
643 				FmtDebug(wasapi_output_domain,
644 					 "Aligned buffer duration: {} ns",
645 					 ns(hundred_ns(buffer_duration)).count());
646 				client.reset();
647 				client = Activate<IAudioClient>(*device);
648 				result = client->Initialize(
649 					AUDCLNT_SHAREMODE_EXCLUSIVE,
650 					AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
651 					buffer_duration, buffer_duration,
652 					reinterpret_cast<WAVEFORMATEX *>(&device_format),
653 					nullptr);
654 			}
655 
656 			if (FAILED(result)) {
657 				throw MakeHResultError(result, "Unable to initialize audio client");
658 			}
659 		}
660 	} else {
661 		if (HRESULT result = client->Initialize(
662 			    AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
663 			    buffer_duration, 0,
664 			    reinterpret_cast<WAVEFORMATEX *>(&device_format), nullptr);
665 		    FAILED(result)) {
666 			throw MakeHResultError(result,
667 					       "Unable to initialize audio client");
668 		}
669 	}
670 
671 	auto render_client = GetService<IAudioRenderClient>(*client);
672 
673 	const UINT32 buffer_size_in_frames = GetBufferSizeInFrames(*client);
674 
675 	watermark = buffer_size_in_frames * 3 * FrameSize();
676 	thread.emplace(*client, std::move(render_client), FrameSize(),
677 		       buffer_size_in_frames, is_exclusive);
678 
679 	paused = false;
680 }
681 
682 void
Close()683 WasapiOutput::Close() noexcept
684 {
685 	assert(thread);
686 
687 	try {
688 		thread->CheckException();
689 	} catch (...) {
690 		LogError(wasapi_output_domain, "exception while stopping");
691 	}
692 	thread->Finish();
693 	com_worker->Async([&]() {
694 		thread.reset();
695 		client.reset();
696 	}).get();
697 	pcm_export.reset();
698 }
699 
700 std::chrono::steady_clock::duration
Delay() const701 WasapiOutput::Delay() const noexcept
702 {
703 	if (paused) {
704 		// idle while paused
705 		return std::chrono::seconds(1);
706 	}
707 
708 	return std::chrono::steady_clock::duration::zero();
709 }
710 
711 size_t
Play(const void * chunk,size_t size)712 WasapiOutput::Play(const void *chunk, size_t size)
713 {
714 	assert(thread);
715 
716 	paused = false;
717 
718 	not_interrupted.test_and_set();
719 
720 	ConstBuffer<void> input(chunk, size);
721 	if (pcm_export) {
722 		input = pcm_export->Export(input);
723 	}
724 	if (input.empty())
725 		return size;
726 
727 	do {
728 		const size_t consumed_size = thread->Push({input.data, input.size});
729 
730 		if (consumed_size == 0) {
731 			thread->Wait();
732 			thread->CheckException();
733 			if (!not_interrupted.test_and_set()) {
734 				throw AudioOutputInterrupted{};
735 			}
736 			continue;
737 		}
738 
739 		thread->CheckException();
740 
741 		if (pcm_export) {
742 			return pcm_export->CalcInputSize(consumed_size);
743 		}
744 		return consumed_size;
745 	} while (true);
746 }
747 
748 bool
Pause()749 WasapiOutput::Pause()
750 {
751 	paused = true;
752 	thread->Pause();
753 	thread->CheckException();
754 	return true;
755 }
756 
757 void
Interrupt()758 WasapiOutput::Interrupt() noexcept
759 {
760 	if (thread) {
761 		not_interrupted.clear();
762 		thread->InterruptWaiter();
763 	}
764 }
765 
766 void
Drain()767 WasapiOutput::Drain()
768 {
769 	assert(thread);
770 
771 	not_interrupted.test_and_set();
772 
773 	while (!thread->Drain()) {
774 		if (!not_interrupted.test_and_set())
775 			throw AudioOutputInterrupted{};
776 	}
777 
778 	/* TODO: this needs to wait until the hardware has really
779 	   finished playing */
780 }
781 
782 void
Cancel()783 WasapiOutput::Cancel() noexcept
784 {
785 	assert(thread);
786 
787 	thread->Cancel();
788 }
789 
790 /// run inside COMWorkerThread
791 void
ChooseDevice()792 WasapiOutput::ChooseDevice()
793 {
794 	ComPtr<IMMDeviceEnumerator> enumerator;
795 	enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
796 				    CLSCTX_INPROC_SERVER);
797 
798 	if (enumerate_devices) {
799 		try {
800 			EnumerateDevices(*enumerator);
801 		} catch (...) {
802 			LogError(std::current_exception());
803 		}
804 	}
805 
806 	if (!device_config.empty()) {
807 		unsigned int id;
808 		if (!SafeSilenceTry([this, &id]() { id = std::stoul(device_config); })) {
809 			device = SearchDevice(*enumerator, device_config);
810 			if (!device)
811 				throw FormatRuntimeError("Device '%s' not found",
812 							 device_config.c_str());
813 		} else
814 			device = GetDevice(*enumerator, id);
815 	} else {
816 		device = GetDefaultAudioEndpoint(*enumerator);
817 	}
818 }
819 
820 /// run inside COMWorkerThread
821 bool
TryFormatExclusive(const AudioFormat & audio_format)822 WasapiOutput::TryFormatExclusive(const AudioFormat &audio_format)
823 {
824 	for (auto test_format : GetFormats(audio_format)) {
825 		HRESULT result = client->IsFormatSupported(
826 			AUDCLNT_SHAREMODE_EXCLUSIVE,
827 			reinterpret_cast<WAVEFORMATEX *>(&test_format), nullptr);
828 		const auto result_string = std::string(HRESULTToString(result));
829 		FmtDebug(wasapi_output_domain, "Trying {} {} {}-{} (exclusive) -> {}",
830 			 audio_format, test_format.Format.nSamplesPerSec,
831 			 test_format.Format.wBitsPerSample,
832 			 test_format.Samples.wValidBitsPerSample,
833 			 result_string);
834 		if (SUCCEEDED(result)) {
835 			device_format = test_format;
836 			return true;
837 		}
838 
839 		if (result == AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED)
840 			throw std::runtime_error("Exclusive mode not allowed");
841 	}
842 	return false;
843 }
844 
845 /// run inside COMWorkerThread
846 void
FindExclusiveFormatSupported(AudioFormat & audio_format)847 WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format)
848 {
849 	for (uint8_t channels : {0, 2, 6, 8, 7, 1, 4, 5, 3}) {
850 		if (audio_format.channels == channels) {
851 			continue;
852 		}
853 		if (channels == 0) {
854 			channels = audio_format.channels;
855 		}
856 		auto old_channels = std::exchange(audio_format.channels, channels);
857 #ifdef ENABLE_DSD
858 		bool was_dsd = false;
859 		if (audio_format.format == SampleFormat::DSD) {
860 			if (dop_setting && TryFormatExclusive(audio_format)) {
861 				return;
862 			}
863 			was_dsd = true;
864 			SetDSDFallback(audio_format);
865 		}
866 #endif
867 		for (uint32_t rate : {0, 384000, 352800, 192000, 176400, 96000, 88200,
868 				      48000, 44100, 32000, 22050, 16000, 11025, 8000}) {
869 			if (audio_format.sample_rate <= rate) {
870 				continue;
871 			}
872 			if (rate == 0) {
873 				rate = audio_format.sample_rate;
874 			}
875 			auto old_rate = std::exchange(audio_format.sample_rate, rate);
876 			for (SampleFormat format : {
877 				     SampleFormat::UNDEFINED,
878 				     SampleFormat::S32,
879 				     SampleFormat::S24_P32,
880 				     SampleFormat::S16,
881 				     SampleFormat::S8,
882 			     }) {
883 				if (audio_format.format == format) {
884 					continue;
885 				}
886 				if (format == SampleFormat::UNDEFINED) {
887 					format = audio_format.format;
888 				}
889 				auto old_format =
890 					std::exchange(audio_format.format, format);
891 				if (TryFormatExclusive(audio_format)) {
892 					return;
893 				}
894 				audio_format.format = old_format;
895 			}
896 			audio_format.sample_rate = old_rate;
897 		}
898 #ifdef ENABLE_DSD
899 		if (was_dsd) {
900 			audio_format.format = SampleFormat::DSD;
901 		}
902 #endif
903 		audio_format.channels = old_channels;
904 	}
905 }
906 
907 /// run inside COMWorkerThread
908 void
FindSharedFormatSupported(AudioFormat & audio_format)909 WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format)
910 {
911 	HRESULT result;
912 
913 	// In shared mode, different sample rate is always unsupported.
914 	auto mixer_format = GetMixFormat(*client);
915 
916 	audio_format.sample_rate = mixer_format->nSamplesPerSec;
917 	device_format = GetFormats(audio_format).front();
918 
919 	ComHeapPtr<WAVEFORMATEXTENSIBLE> closest_format;
920 	result = client->IsFormatSupported(
921 		AUDCLNT_SHAREMODE_SHARED,
922 		reinterpret_cast<WAVEFORMATEX *>(&device_format),
923 		closest_format.AddressCast<WAVEFORMATEX>());
924 	{
925 		const auto result_string = std::string(HRESULTToString(result));
926 		FmtDebug(wasapi_output_domain, "Trying {} {} {}-{} (shared) -> {}",
927 			 audio_format, device_format.Format.nSamplesPerSec,
928 			 device_format.Format.wBitsPerSample,
929 			 device_format.Samples.wValidBitsPerSample,
930 			 result_string);
931 	}
932 
933 	if (FAILED(result) && result != AUDCLNT_E_UNSUPPORTED_FORMAT) {
934 		throw MakeHResultError(result, "IsFormatSupported failed");
935 	}
936 
937 	switch (result) {
938 	case S_OK:
939 		break;
940 	case AUDCLNT_E_UNSUPPORTED_FORMAT:
941 	default:
942 		// Trying channels fallback.
943 		audio_format.channels = mixer_format->nChannels;
944 
945 		device_format = GetFormats(audio_format).front();
946 
947 		result = client->IsFormatSupported(
948 			AUDCLNT_SHAREMODE_SHARED,
949 			reinterpret_cast<WAVEFORMATEX *>(&device_format),
950 			closest_format.AddressCast<WAVEFORMATEX>());
951 		{
952 			const auto result_string = std::string(HRESULTToString(result));
953 			FmtDebug(wasapi_output_domain,
954 				 "Trying {} {} {}-{} (shared) -> {}",
955 				 audio_format,
956 				 device_format.Format.nSamplesPerSec,
957 				 device_format.Format.wBitsPerSample,
958 				 device_format.Samples.wValidBitsPerSample,
959 				 result_string);
960 		}
961 		if (FAILED(result)) {
962 			throw MakeHResultError(result, "Format is not supported");
963 		}
964 		break;
965 	case S_FALSE:
966 		if (closest_format->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
967 			device_format = *closest_format;
968 		} else {
969 			device_format.Samples.wValidBitsPerSample =
970 				device_format.Format.wBitsPerSample;
971 			device_format.Format = closest_format->Format;
972 			switch (std::exchange(device_format.Format.wFormatTag,
973 					      WAVE_FORMAT_EXTENSIBLE)) {
974 			case WAVE_FORMAT_PCM:
975 				device_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
976 				break;
977 			case WAVE_FORMAT_IEEE_FLOAT:
978 				device_format.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
979 				break;
980 			default:
981 				gcc_unreachable();
982 			}
983 		}
984 		break;
985 	}
986 
987 	// Copy closest match back to audio_format.
988 	audio_format.channels = device_format.Format.nChannels;
989 	audio_format.sample_rate = device_format.Format.nSamplesPerSec;
990 	if (device_format.SubFormat == KSDATAFORMAT_SUBTYPE_PCM) {
991 		switch (device_format.Format.wBitsPerSample) {
992 		case 8:
993 			audio_format.format = SampleFormat::S8;
994 			break;
995 		case 16:
996 			audio_format.format = SampleFormat::S16;
997 			break;
998 		case 32:
999 			audio_format.format =
1000 				device_format.Samples.wValidBitsPerSample == 32
1001 					? SampleFormat::S32
1002 					: SampleFormat::S24_P32;
1003 			break;
1004 		}
1005 	} else if (device_format.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
1006 		audio_format.format = SampleFormat::FLOAT;
1007 	}
1008 }
1009 
1010 /// run inside COMWorkerThread
1011 void
EnumerateDevices(IMMDeviceEnumerator & enumerator)1012 WasapiOutput::EnumerateDevices(IMMDeviceEnumerator &enumerator)
1013 {
1014 	const auto device_collection = EnumAudioEndpoints(enumerator);
1015 
1016 	const UINT count = GetCount(*device_collection);
1017 	for (UINT i = 0; i < count; ++i) {
1018 		const auto enumerated_device = Item(*device_collection, i);
1019 
1020 		const auto property_store =
1021 			OpenPropertyStore(*enumerated_device);
1022 
1023 		auto name = GetString(*property_store,
1024 				      PKEY_Device_FriendlyName);
1025 		if (name == nullptr)
1026 			continue;
1027 
1028 		FmtNotice(wasapi_output_domain,
1029 			  "Device \"{}\" \"{}\"", i, name);
1030 	}
1031 }
1032 
1033 /// run inside COMWorkerThread
1034 ComPtr<IMMDevice>
GetDevice(IMMDeviceEnumerator & enumerator,unsigned index)1035 WasapiOutput::GetDevice(IMMDeviceEnumerator &enumerator, unsigned index)
1036 {
1037 	const auto device_collection = EnumAudioEndpoints(enumerator);
1038 	return Item(*device_collection, index);
1039 }
1040 
1041 /// run inside COMWorkerThread
1042 ComPtr<IMMDevice>
SearchDevice(IMMDeviceEnumerator & enumerator,std::string_view name)1043 WasapiOutput::SearchDevice(IMMDeviceEnumerator &enumerator,
1044 			   std::string_view name)
1045 {
1046 	const auto device_collection = EnumAudioEndpoints(enumerator);
1047 
1048 	const UINT count = GetCount(*device_collection);
1049 	for (UINT i = 0; i < count; ++i) {
1050 		auto d = Item(*device_collection, i);
1051 
1052 		const auto property_store = OpenPropertyStore(*d);
1053 		auto n = GetString(*property_store, PKEY_Device_FriendlyName);
1054 		if (n != nullptr && name.compare(n) == 0)
1055 			return d;
1056 	}
1057 
1058 	return nullptr;
1059 }
1060 
1061 static bool
wasapi_output_test_default_device()1062 wasapi_output_test_default_device()
1063 {
1064 	return true;
1065 }
1066 
1067 const struct AudioOutputPlugin wasapi_output_plugin = {
1068 	"wasapi",
1069 	wasapi_output_test_default_device,
1070 	WasapiOutput::Create,
1071 	&wasapi_mixer_plugin,
1072 };
1073