HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
win32_device_interface.hpp
1// Copyright Take Vos 2022.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "audio_direction.hpp"
8#include "audio_format_range.hpp"
9#include "../utility/utility.hpp"
10#include "../telemetry/telemetry.hpp"
11#include "../macros.hpp"
12#include <string>
13#include <coroutine>
14
15hi_export_module(hikogui.audio.win32_device_interface);
16
17hi_export namespace hi { inline namespace v1 {
18
19hi_export class win32_device_interface {
20public:
21 win32_device_interface(std::string device_name) : _device_name(std::move(device_name)), _handle(INVALID_HANDLE_VALUE)
22 {
23 auto device_name_ = hi::to_wstring(_device_name);
24
25 _handle = CreateFileW(device_name_.c_str(), FILE_SHARE_READ | FILE_SHARE_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
26 if (_handle == INVALID_HANDLE_VALUE) {
27 throw io_error(std::format("Could not open win32_device_interface {}: {}", _device_name, get_last_error_message()));
28 }
29 }
30
32 {
33 if (_handle != INVALID_HANDLE_VALUE) {
34 if (not CloseHandle(_handle)) {
35 hi_log_error("Could not close win32_device_interface {}: {}", _device_name, get_last_error_message());
36 }
37 }
38 }
39
40 [[nodiscard]] ULONG pin_count() const noexcept
41 {
42 try {
43 return wide_cast<ULONG>(get_pin_property<DWORD>(0, KSPROPERTY_PIN_CTYPES));
44 } catch (std::exception const& e) {
45 hi_log_error("Could not get pin-count on device {}: {}", _device_name, e.what());
46 return 0;
47 }
48 }
49
50 [[nodiscard]] std::string pin_name(ULONG pin_nr) const noexcept
51 {
52 try {
53 return get_pin_property<std::string>(pin_nr, KSPROPERTY_PIN_NAME);
54 } catch (std::exception const& e) {
55 hi_log_error("Could not get pin-name on device {}: {}", _device_name, e.what());
56 return std::string{"<unknown pin>"};
57 }
58 }
59
60 [[nodiscard]] generator<ULONG> find_streaming_pins(audio_direction direction) const noexcept
61 {
62 auto const num_pins = pin_count();
63 for (ULONG pin_nr = 0; pin_nr != num_pins; ++pin_nr) {
64 if (is_streaming_pin(pin_nr, direction)) {
65 co_yield pin_nr;
66 }
67 }
68 }
69
70 [[nodiscard]] GUID pin_category(ULONG pin_nr) const noexcept
71 {
72 try {
73 return get_pin_property<GUID>(0, KSPROPERTY_PIN_CATEGORY);
74 } catch (std::exception const& e) {
75 hi_log_error("Could not get pin-category on device {}: {}", _device_name, e.what());
76 return {};
77 }
78 }
79
80 [[nodiscard]] KSPIN_COMMUNICATION pin_communication(ULONG pin_nr) const noexcept
81 {
82 try {
83 return get_pin_property<KSPIN_COMMUNICATION>(0, KSPROPERTY_PIN_COMMUNICATION);
84 } catch (std::exception const& e) {
85 hi_log_error("Could not get pin-communication on device {}: {}", _device_name, e.what());
86 return KSPIN_COMMUNICATION_NONE;
87 }
88 }
89
90 [[nodiscard]] generator<audio_format_range> get_format_ranges(ULONG pin_nr) const noexcept
91 {
92 for (auto *format_range : get_pin_properties<KSDATARANGE>(pin_nr, KSPROPERTY_PIN_DATARANGES)) {
93 if (IsEqualGUID(format_range->MajorFormat, KSDATAFORMAT_TYPE_AUDIO)) {
94 auto has_int = false;
95 auto has_float = false;
96 if (IsEqualGUID(format_range->SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) {
97 has_int = true;
98 } else if (IsEqualGUID(format_range->SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) {
99 has_float = true;
100 } else if (IsEqualGUID(format_range->SubFormat, KSDATAFORMAT_SUBTYPE_WILDCARD)) {
101 has_int = true;
102 has_float = true;
103 } else {
104 // The scarlett returns KSDATAFORMAT_SUBTYPE_ANALOGUE for one of the pins,
105 // we can't filter scarlet for the proper streaming-pin.
106 continue;
107 }
108
109 auto const *format_range_ = reinterpret_cast<KSDATARANGE_AUDIO const *>(format_range);
110 if (format_range_->MinimumBitsPerSample > 64) {
111 hi_log_error(
112 "Bad KSDATARANGE_AUDIO MinimumBitsPerSample == {} for device {}",
113 format_range_->MinimumBitsPerSample,
114 _device_name);
115 continue;
116 }
117 if (format_range_->MaximumBitsPerSample > 64) {
118 hi_log_error(
119 "Bad KSDATARANGE_AUDIO MaximumBitsPerSample == {} for device {}",
120 format_range_->MaximumBitsPerSample,
121 _device_name);
122 continue;
123 }
124 if (format_range_->MinimumBitsPerSample > format_range_->MaximumBitsPerSample) {
125 hi_log_error(
126 "Bad KSDATARANGE_AUDIO MinimumBitsPerSample == {}, MaximumBitsPerSample {} for device {}",
127 format_range_->MinimumBitsPerSample,
128 format_range_->MaximumBitsPerSample,
129 _device_name);
130 continue;
131 }
132
133 if (format_range_->MaximumChannels > std::numeric_limits<uint16_t>::max()) {
134 hi_log_error(
135 "Bad KSDATARANGE_AUDIO MaximumChannels == {} for device {}",
136 format_range_->MaximumChannels,
137 _device_name);
138 continue;
139 }
140
141 if (format_range_->MinimumSampleFrequency > format_range_->MaximumSampleFrequency) {
142 hi_log_error(
143 "Bad KSDATARANGE_AUDIO MinimumSampleFrequency == {}, MaximumSampleFrequency {} for device {}",
144 format_range_->MinimumSampleFrequency,
145 format_range_->MaximumSampleFrequency,
146 _device_name);
147 continue;
148 }
149
150 auto const num_bits_first = format_range_->MinimumBitsPerSample;
151 auto const num_bits_last = format_range_->MaximumBitsPerSample;
152 auto const num_channels = narrow_cast<uint16_t>(format_range_->MaximumChannels);
153 auto const min_sample_rate = narrow_cast<uint32_t>(format_range_->MinimumSampleFrequency);
154 auto const max_sample_rate = narrow_cast<uint32_t>(format_range_->MaximumSampleFrequency);
155
156 // There are only very few sample-formats that a device will actually support, therefor
157 // the audio-format-range discretized them. Very likely the audio device driver will be lying.
158 for (auto num_bits = num_bits_first; num_bits <= num_bits_last; ++num_bits) {
159 auto const num_bytes = narrow_cast<uint8_t>((num_bits + 7) / 8);
160 if (has_int) {
161 auto const num_minor_bits = narrow_cast<uint8_t>(num_bits - 1);
162 auto const sample_format = pcm_format{false, std::endian::native, true, num_bytes, 0, num_minor_bits};
163 co_yield audio_format_range{
164 sample_format, num_channels, min_sample_rate, max_sample_rate, surround_mode::none};
165 }
166 if (has_float and num_bits == 32) {
167 auto const sample_format = pcm_format{true, std::endian::native, true, num_bytes, 8, 23};
168 co_yield audio_format_range{
169 sample_format, num_channels, min_sample_rate, max_sample_rate, surround_mode::none};
170 }
171 }
172 }
173 }
174 }
175
183 [[nodiscard]] generator<audio_format_range> get_format_ranges(audio_direction direction) const noexcept
184 {
185 for (auto pin_nr : find_streaming_pins(direction)) {
186 for (auto const& range : get_format_ranges(pin_nr)) {
187 co_yield range;
188 }
189 }
190 }
191
192 template<typename T>
193 [[nodiscard]] generator<T const *> get_pin_properties(ULONG pin_id, KSPROPERTY_PIN property) const
194 {
195 auto r = get_pin_property_data(pin_id, property);
196 auto *ptr = r.get();
197
198 auto *header = std::launder(reinterpret_cast<KSMULTIPLE_ITEM const *>(ptr));
199
200 auto const expected_size = header->Count * sizeof(T) + sizeof(KSMULTIPLE_ITEM);
201 if (header->Size != expected_size) {
202 throw io_error("KSMULTIPLE_ITEM header corrupt");
203 }
204
205 ptr += sizeof(KSMULTIPLE_ITEM);
206 for (auto i = 0_uz; i != header->Count; ++i) {
207 co_yield std::launder(reinterpret_cast<T const *>(ptr));
208 ptr += sizeof(T);
209 }
210 }
211
216 template<>
217 [[nodiscard]] generator<KSDATARANGE const *> get_pin_properties(ULONG pin_id, KSPROPERTY_PIN property) const
218 {
219 auto r = get_pin_property_data(pin_id, property);
220 auto *ptr = r.get();
221
222 auto *header = std::launder(reinterpret_cast<KSMULTIPLE_ITEM const *>(ptr));
223
224 ptr += sizeof(KSMULTIPLE_ITEM);
225 for (auto i = 0_uz; i != header->Count; ++i) {
226 auto *item = std::launder(reinterpret_cast<KSDATARANGE const *>(ptr));
227 co_yield item;
228
229 ptr += item->FormatSize;
230 }
231 }
232
233private:
234 std::string _device_name;
235 HANDLE _handle;
236
237 [[nodiscard]] std::string get_pin_property_string(ULONG pin_id, KSPROPERTY_PIN property) const;
238
239 template<typename T>
240 [[nodiscard]] T get_pin_property(ULONG pin_id, KSPROPERTY_PIN property) const
241 {
242 auto property_info = KSP_PIN{};
243 property_info.Property.Set = KSPROPSETID_Pin;
244 property_info.Property.Id = property;
245 property_info.Property.Flags = KSPROPERTY_TYPE_GET;
246 property_info.PinId = pin_id;
247 property_info.Reserved = 0;
248
249 DWORD r_size;
250 T r;
251 if (not DeviceIoControl(_handle, IOCTL_KS_PROPERTY, &property_info, sizeof(KSP_PIN), &r, sizeof(T), &r_size, NULL)) {
253 }
254
255 if (r_size != sizeof(T)) {
256 throw io_error("Unexpected return size");
257 }
258
259 return r;
260 }
261
262 std::unique_ptr<char[]> get_pin_property_data(ULONG pin_id, KSPROPERTY_PIN property) const
263 {
264 auto property_info = KSP_PIN{};
265 property_info.Property.Set = KSPROPSETID_Pin;
266 property_info.Property.Id = property;
267 property_info.Property.Flags = KSPROPERTY_TYPE_GET;
268 property_info.PinId = pin_id;
269 property_info.Reserved = 0;
270
271 DWORD r_size;
272 if (DeviceIoControl(_handle, IOCTL_KS_PROPERTY, &property_info, sizeof(KSP_PIN), NULL, 0, &r_size, NULL)) {
273 throw io_error("Unexpected return size 0");
274 }
275 if (GetLastError() != ERROR_MORE_DATA) {
276 throw io_error(get_last_error_message());
277 }
278
279 if (r_size < sizeof(KSMULTIPLE_ITEM)) {
280 throw io_error("Unexpected return size");
281 }
282
283 auto r = std::make_unique<char[]>(r_size);
284 if (not DeviceIoControl(_handle, IOCTL_KS_PROPERTY, &property_info, sizeof(KSP_PIN), r.get(), r_size, &r_size, NULL)) {
285 throw io_error(get_last_error_message());
286 }
287
288 if (r_size < sizeof(KSMULTIPLE_ITEM)) {
289 throw io_error("Incomplete header read");
290 }
291
292 auto *header = std::launder(reinterpret_cast<KSMULTIPLE_ITEM *>(r.get()));
293 if (r_size < header->Size) {
294 throw io_error("Incomplete read");
295 }
296
297 return r;
298 }
299
300 template<>
301 [[nodiscard]] std::string get_pin_property<std::string>(ULONG pin_id, KSPROPERTY_PIN property) const
302 {
303 auto property_info = KSP_PIN{};
304 property_info.Property.Set = KSPROPSETID_Pin;
305 property_info.Property.Id = property;
306 property_info.Property.Flags = KSPROPERTY_TYPE_GET;
307 property_info.PinId = pin_id;
308 property_info.Reserved = 0;
309
310 DWORD r_size;
311 if (DeviceIoControl(_handle, IOCTL_KS_PROPERTY, &property_info, sizeof(KSP_PIN), NULL, 0, &r_size, NULL)) {
312 return std::string{};
313 }
314
315 if (GetLastError() != ERROR_MORE_DATA) {
316 throw io_error(get_last_error_message());
317 }
318
319 if (r_size % 2 != 0) {
320 throw io_error("Expected even number of bytes in return value.");
321 }
322
323 auto r = std::wstring(r_size / sizeof(wchar_t{}) - 1, wchar_t{});
324 hi_assert(r_size == (r.size() + 1) * sizeof(wchar_t{}));
325
326 if (not DeviceIoControl(_handle, IOCTL_KS_PROPERTY, &property_info, sizeof(KSP_PIN), r.data(), r_size, &r_size, NULL)) {
327 throw io_error(get_last_error_message());
328 }
329
330 return hi::to_string(r);
331 }
332
333 [[nodiscard]] bool is_streaming_interface(ULONG pin_nr) const noexcept
334 {
335 try {
336 for (auto *identifier : get_pin_properties<KSIDENTIFIER>(pin_nr, KSPROPERTY_PIN_INTERFACES)) {
337 if (not IsEqualGUID(identifier->Set, KSINTERFACESETID_Standard)) {
338 continue;
339 }
340 if (identifier->Id == KSINTERFACE_STANDARD_STREAMING or identifier->Id == KSINTERFACE_STANDARD_LOOPED_STREAMING) {
341 return true;
342 }
343 }
344 } catch (std::exception const& e) {
345 hi_log_error("Could not get pin-interface property for {} pin_nr {}: {}", _device_name, pin_nr, e.what());
346 }
347 return false;
348 }
349
350 [[nodiscard]] bool is_standerdio_medium(ULONG pin_nr) const noexcept
351 {
352 try {
353 for (auto const& identifier : get_pin_properties<KSIDENTIFIER>(pin_nr, KSPROPERTY_PIN_MEDIUMS)) {
354 if (not IsEqualGUID(identifier->Set, KSMEDIUMSETID_Standard)) {
355 continue;
356 }
357 if (identifier->Id == KSMEDIUM_STANDARD_DEVIO) {
358 return true;
359 }
360 }
361 } catch (std::exception const& e) {
362 hi_log_error("Could not get pin-medium property for {} pin_nr {}: {}", _device_name, pin_nr, e.what());
363 }
364 return false;
365 }
366
367 [[nodiscard]] bool is_streaming_pin(ULONG pin_nr, audio_direction direction) const noexcept
368 {
369 // Check if this is a streaming-pin.
370 if (not is_streaming_interface(pin_nr)) {
371 return false;
372 }
373
374 if (not is_standerdio_medium(pin_nr)) {
375 return false;
376 }
377
378 // Check if the dataflow direction of the pin is in the opposite-direction of the end-point.
379 switch (get_pin_property<KSPIN_DATAFLOW>(pin_nr, KSPROPERTY_PIN_DATAFLOW)) {
380 case KSPIN_DATAFLOW_OUT:
381 if (direction != audio_direction::input and direction != audio_direction::bidirectional) {
382 return false;
383 }
384 break;
385 case KSPIN_DATAFLOW_IN:
386 if (direction != audio_direction::output and direction != audio_direction::bidirectional) {
387 return false;
388 }
389 break;
390 default:
391 hi_no_default();
392 }
393
394 // Modern device drivers seem no longer to support directly streaming samples through this API, therefor those pins
395 // can no longer communicate at all, but can still be interrogated for the audio formats it supports.
396 // The Scarlett 2i2 has streaming pins that can be interrogated but are KSPIN_COMMUNICATION_NONE.
397 // Therefor the old examples on the web that check for KSPROPERTY_PIN_COMMUNICATION are no longer valid.
398
399 return true;
400 }
401};
402
403}} // namespace hi::inline v1
constexpr std::wstring to_wstring(std::u32string_view rhs) noexcept
Conversion from UTF-32 to wide-string (UTF-16/32).
Definition to_string.hpp:160
The HikoGUI namespace.
Definition array_generic.hpp:20
hi_export std::string get_last_error_message()
Get the OS error message from the last error received on this thread.
Definition exception_win32_impl.hpp:30
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
Definition audio_format_range.hpp:17
Definition pcm_format.hpp:18
Definition win32_device_interface.hpp:19
generator< audio_format_range > get_format_ranges(audio_direction direction) const noexcept
Get all the audio formats supported by this device.
Definition win32_device_interface.hpp:183
generator< KSDATARANGE const * > get_pin_properties(ULONG pin_id, KSPROPERTY_PIN property) const
Definition win32_device_interface.hpp:217
Exception thrown during I/O on an error.
Definition exception_intf.hpp:173
T move(T... args)
T what(T... args)