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