Loading...
Searching...
No Matches
GenericMiniAudioInterface.hpp
1#pragma once
2#include <Audio/AudioInterface.hpp>
3#include <Audio/Settings/Model.hpp>
4#include <Audio/Settings/View.hpp>
5
6#include <score/command/Dispatchers/SettingsCommandDispatcher.hpp>
7#include <score/tools/Bind.hpp>
8#include <score/widgets/SignalUtils.hpp>
9
10#include <ossia/detail/config.hpp>
11
12#include <ossia/audio/miniaudio_protocol.hpp>
13
14#include <QComboBox>
15#include <QCoreApplication>
16#include <QFormLayout>
17
18#include <version>
19#if OSSIA_ENABLE_MINIAUDIO
20
21namespace Audio
22{
23struct MiniAudioCard
24{
25 MiniAudioCard() = default;
26 MiniAudioCard(const MiniAudioCard&) = default;
27 MiniAudioCard& operator=(const MiniAudioCard&) = default;
28 MiniAudioCard(MiniAudioCard&&) noexcept = default;
29 MiniAudioCard& operator=(MiniAudioCard&&) noexcept = default;
30 MiniAudioCard(QString n, ma_device_type t, ma_device_id id)
31 : name{n}
32 , type{t}
33 , id{id}
34 {
35 }
36 QString name;
37 ma_device_type type{};
38 ma_device_id id{};
39 ma_device_info info{};
40};
41
42class GenericMiniAudioFactory
43 : public QObject
44 , public AudioFactory
45{
46 std::shared_ptr<ossia::miniaudio_context> m_context;
47 bool m_scanned{};
48
49public:
50 std::vector<MiniAudioCard> devices;
51
52 virtual ~GenericMiniAudioFactory() override { }
53
54 std::shared_ptr<ossia::miniaudio_context> acquireContext()
55 {
56 if(!m_context)
57 {
58 m_context = std::make_shared<ossia::miniaudio_context>();
59 auto cfg = ma_context_config_init();
60 cfg.threadPriority = ma_thread_priority_realtime;
61 cfg.threadStackSize = 8388608;
62 ma_context_init(nullptr, 0, &cfg, &m_context->context);
63 }
64
65 return m_context;
66 }
67
68 virtual bool compareDeviceId(const ma_device_id& id, const QString& str) const noexcept
69 = 0;
70 virtual bool
71 compareDeviceId(const ma_device_id& id, std::string_view str) const noexcept
72 = 0;
73 virtual void setDeviceId(ma_device_id& id, const QString& str) const noexcept = 0;
74 virtual QString deviceIdToString(const ma_device_id& id) const noexcept = 0;
75
78 virtual void normalizeDeviceId(ma_device_id& id) const noexcept { }
79
81 virtual ma_share_mode shareMode() const noexcept { return ma_share_mode_shared; }
82
83 // When true, the factory exposes an extra "Default device" entry mapped to
84 // the OS default device. The selection is stored as the "Default" sentinel
85 // and make_engine passes a NULL miniaudio device id, so the backend follows
86 // the OS default (with automatic rerouting on WASAPI) while running.
87 virtual bool followsDefaultDevice() const noexcept { return false; }
88
89 static constexpr int DefaultDeviceItemData = -1;
90
91 int defaultDeviceChannels(ma_device_type tp) const noexcept
92 {
93 for(const auto& d : devices)
94 {
95 if(d.type == tp && d.info.isDefault && d.info.nativeDataFormatCount > 0)
96 return d.info.nativeDataFormats[0].channels;
97 }
98 return 2;
99 }
100
101 void
102 initialize(Audio::Settings::Model& set, const score::ApplicationContext& ctx) override
103 {
104 ensureScanned();
105 // set.setDefaultIn(2);
106 // set.setDefaultOut(2);
107 // set.setRate(48000);
108 // set.setBufferSize(256);
109 // set.changed();
110
111 // Backends that can follow the OS default (WASAPI) default to that mode when
112 // nothing has been configured yet, so playback/capture track the Windows
113 // mixer default output/input out of the box.
114 if(followsDefaultDevice() && set.getCardOut().isEmpty())
115 set.setCardOut("Default");
116 if(followsDefaultDevice() && set.getCardIn().isEmpty())
117 set.setCardIn("Default");
118
119 const MiniAudioCard* user_in{};
120 const MiniAudioCard* user_out{};
121
122 const MiniAudioCard* default_in{};
123 const MiniAudioCard* default_out{};
124
125 const MiniAudioCard* first_viable_in{};
126 const MiniAudioCard* first_viable_out{};
127
128 // qDebug(Q_FUNC_INFO);
129 // qDebug() << "looking for in:" << set.getCardIn();
130 // qDebug() << "looking for out:" << set.getCardOut();
131 for(auto& d : devices)
132 {
133 // qDebug() << "has device : " << d.info.name << d.info.nativeDataFormatCount;
134 {
135 for(int i = 0; i < d.info.nativeDataFormatCount; i++)
136 {
137 // auto fmt = d.info.nativeDataFormats[i];
138 // qDebug() << "Format: " << magic_enum::enum_name(fmt.format) << fmt.channels
139 // << fmt.flags << fmt.sampleRate;
140 }
141 }
142 if(d.info.nativeDataFormatCount > 0 && d.info.nativeDataFormats[0].channels > 0)
143 {
144 // qDebug() << "device id:" << d.id.coreaudio
145 // << d.info.nativeDataFormats[0].channels;
146
147 if(d.type & ma_device_type::ma_device_type_capture)
148 {
149 if(compareDeviceId(d.id, set.getCardIn()))
150 user_in = &d;
151 // qDebug() << "capture" << d.info.isDefault;
152 first_viable_in = &d;
153 if(d.info.isDefault)
154 default_in = &d;
155 }
156 if(d.type & ma_device_type::ma_device_type_playback)
157 {
158 if(compareDeviceId(d.id, set.getCardOut()))
159 user_out = &d;
160 // qDebug() << "playback" << d.info.isDefault;
161 first_viable_out = &d;
162 if(d.info.isDefault)
163 default_out = &d;
164 }
165 }
166 }
167
168 const MiniAudioCard* card_in = user_in;
169 if(!card_in)
170 card_in = default_in;
171 if(!card_in)
172 card_in = first_viable_in;
173 if(set.getCardIn().startsWith("No device"))
174 card_in = nullptr;
175
176 const MiniAudioCard* card_out = user_out;
177 if(!card_out)
178 card_out = default_out;
179 if(!card_out)
180 card_out = first_viable_out;
181 if(set.getCardOut().startsWith("No device"))
182 card_out = nullptr;
183
184 if(followsDefaultDevice() && set.getCardIn() == "Default")
185 {
186 const MiniAudioCard* ref = default_in ? default_in : first_viable_in;
187 set.setDefaultIn(ref ? ref->info.nativeDataFormats[0].channels : 2);
188 }
189 else if(card_in)
190 {
191 set.setCardIn(deviceIdToString(card_in->id));
192 set.setDefaultIn(card_in->info.nativeDataFormats[0].channels);
193 }
194 else
195 {
196 set.setCardIn(devices.front().name);
197 set.setDefaultIn(0);
198 }
199
200 if(followsDefaultDevice() && set.getCardOut() == "Default")
201 {
202 // Keep the "Default device" sentinel so make_engine passes a NULL device
203 // id and miniaudio follows the OS default output. Derive channels/rate
204 // from the current OS default device.
205 const MiniAudioCard* ref = default_out ? default_out : first_viable_out;
206 if(ref)
207 {
208 set.setDefaultOut(ref->info.nativeDataFormats[0].channels);
209 int current_rate = set.getRate();
210 if(int fmt_count = ref->info.nativeDataFormatCount; fmt_count > 0)
211 {
212 auto* fmts = ref->info.nativeDataFormats;
213 if(std::none_of(fmts, fmts + fmt_count, [current_rate](auto fmt) {
214 return fmt.sampleRate == current_rate;
215 }))
216 {
217 set.setRate(ref->info.nativeDataFormats[0].sampleRate);
218 }
219 }
220 }
221 else
222 {
223 set.setDefaultOut(2);
224 }
225 }
226 else if(card_out)
227 {
228 set.setCardOut(deviceIdToString(card_out->id));
229 set.setDefaultOut(card_out->info.nativeDataFormats[0].channels);
230
231 int current_rate = set.getRate();
232 if(int fmt_count = card_out->info.nativeDataFormatCount; fmt_count > 0)
233 {
234 auto* fmts = card_out->info.nativeDataFormats;
235 if(std::none_of(fmts, fmts + fmt_count, [current_rate](auto fmt) {
236 return fmt.sampleRate == current_rate;
237 }))
238 {
239 set.setRate(card_out->info.nativeDataFormats[0].sampleRate);
240 }
241 }
242 }
243 else
244 {
245 set.setCardOut(devices.front().name);
246 set.setDefaultOut(0);
247 }
248
249 set.changed();
250 }
251
252 void ensureScanned()
253 {
254 if(!m_scanned)
255 rescan();
256 }
257
258 void rescan()
259 {
260 auto ctx = acquireContext();
261 if(!ctx)
262 return;
263 m_scanned = true;
264 devices.clear();
265
266 devices.resize(1);
267 devices[0].name = "No device";
268 memset(&devices[0].id, 0, sizeof(devices[0].id));
269 memset(&devices[0].info, 0, sizeof(devices[0].info));
270
271 setDeviceId(devices[0].id, "No device");
272
273 ma_context_enumerate_devices(
274 &m_context->context,
275 [](ma_context* ctx, ma_device_type dev_type, const ma_device_info* dev_info,
276 void* data) -> ma_bool32 {
277 auto& self = *(GenericMiniAudioFactory*)data;
278 self.devices.emplace_back(dev_info->name, dev_type, dev_info->id);
279 return 1;
280 },
281 this);
282
283 for(std::size_t i = 1; i < devices.size(); i++)
284 {
285 auto& dev = devices[i];
286 // miniaudio looks the id up among the ones it enumerated.
287 ma_context_get_device_info(&m_context->context, dev.type, &dev.id, &dev.info);
288 normalizeDeviceId(dev.id);
289 dev.info.id = dev.id;
290 }
291 }
292
293 std::shared_ptr<ossia::audio_engine> make_engine(
294 const Audio::Settings::Model& set, const score::ApplicationContext& ctx) override
295 {
296 ensureScanned();
297 ma_device_id info_in;
298 ma_device_id info_out;
299 memset(&info_in, 0, sizeof(info_in));
300 memset(&info_out, 0, sizeof(info_out));
301
302 auto card_in = set.getCardIn().toStdString();
303 auto card_out = set.getCardOut().toStdString();
304
305 bool found_in = false, found_out = false;
306 for(auto& dev : this->devices)
307 {
308 if(compareDeviceId(dev.id, card_in))
309 {
310 info_in = dev.id;
311 found_in = true;
312 }
313 if(compareDeviceId(dev.id, card_out))
314 {
315 info_out = dev.id;
316 found_out = true;
317 }
318 }
319
320 // A zero id means the system default device: a device that is not
321 // listed (unplugged, busy) is asked for by name, so that it fails rather
322 // than another device playing instead.
323 auto explicit_device = [](const QString& name) {
324 return !name.isEmpty() && name != "Default" && !name.startsWith("No device");
325 };
326 if(!found_in && explicit_device(set.getCardIn()))
327 setDeviceId(info_in, set.getCardIn());
328 if(!found_out && explicit_device(set.getCardOut()))
329 setDeviceId(info_out, set.getCardOut());
330
331 return std::make_shared<ossia::miniaudio_engine>(
332 acquireContext(), "ossia score", info_in, info_out, set.getDefaultIn(),
333 set.getDefaultOut(), set.getRate(), set.getBufferSize(), shareMode());
334 }
335
336 void setCard(QComboBox* combo, ma_device_type tp, QString val)
337 {
338 // "Default device" sentinel: select the dedicated combo entry.
339 if(followsDefaultDevice() && val == "Default")
340 {
341 for(int i = 0; i < combo->count(); i++)
342 {
343 if(combo->itemData(i).toInt() == DefaultDeviceItemData)
344 {
345 combo->setCurrentIndex(i);
346 return;
347 }
348 }
349 }
350
351 auto dev_it = ossia::find_if(devices, [&](const MiniAudioCard& d) {
352 return compareDeviceId(d.id, val) && d.type == tp;
353 });
354 if(dev_it != devices.end())
355 {
356 int device_index = std::distance(devices.begin(), dev_it);
357 for(int i = 0; i < combo->count(); i++)
358 {
359 if(combo->itemData(i).toInt() == device_index)
360 {
361 combo->setCurrentIndex(i);
362 return;
363 }
364 }
365 }
366
367 combo->setCurrentIndex(0);
368 }
369
370 QWidget* make_settings(
372 score::SettingsCommandDispatcher& m_disp, QWidget* parent) override
373 {
374 ensureScanned();
375 /* Not useful: hotplug does not seem to work
376 struct UpdateWhenVisible : public QWidget
377 {
378 public:
379 using QWidget::QWidget;
380
381 bool event(QEvent* e) override
382 {
383 if(e->type() == QEvent::Show)
384 {
385 auto& self = score::AppContext().interfaces<Audio::AudioFactoryList>();
386 auto cf = static_cast<CoreAudioFactory*>(
387 self.get(CoreAudioFactory::static_concreteKey()));
388 cf->rescan();
389 }
390 return QWidget::event(e);
391 }
392 };
393 */
394
395 auto w = new QWidget{parent};
396 auto lay = new QFormLayout{w};
397
398 auto card_list_in = new QComboBox{w};
399 auto card_list_out = new QComboBox{w};
400
401 // Disabled case
402 card_list_in->addItem(devices.front().name + " (capture)", 0);
403 card_list_out->addItem(devices.front().name + " (playback)", 0);
404
405 // Optional "follow the OS default device" entry (e.g. WASAPI).
406 if(followsDefaultDevice())
407 {
408 card_list_in->addItem(
409 QObject::tr("Default device"), DefaultDeviceItemData);
410 card_list_out->addItem(
411 QObject::tr("Default device"), DefaultDeviceItemData);
412 }
413
414 // qDebug() << Q_FUNC_INFO << "Devices: ";
415 for(std::size_t i = 1; i < devices.size(); i++)
416 {
417
418 auto& dev = devices[i];
419 // qDebug() << i << dev.info.name << dev.id.coreaudio << dev.type;
420 if(dev.info.nativeDataFormatCount > 0)
421 {
422 if(dev.info.nativeDataFormats[0].channels > 0)
423 {
424 if(dev.type == ma_device_type_capture)
425 {
426 card_list_in->addItem(dev.name, (int)i);
427 // qDebug() << "adding to card list in" << i << dev.name;
428 }
429 if(dev.type == ma_device_type_playback)
430 card_list_out->addItem(dev.name, (int)i);
431 }
432 }
433 }
434
435 /*
436 for(int i = 0; i < card_list_in->count(); i++)
437 qDebug() << Q_FUNC_INFO << "card in " << i << card_list_in->itemText(i)
438 << card_list_in->itemData(i);
439 for(int i = 0; i < card_list_out->count(); i++)
440 qDebug() << Q_FUNC_INFO << "card out " << i << card_list_out->itemText(i)
441 << card_list_out->itemData(i);
442 */
443 using Model = Audio::Settings::Model;
444
445 {
446 lay->addRow(QObject::tr("Capture"), card_list_in);
447
448 auto update_dev_in = [this, &m, &m_disp](const MiniAudioCard& dev) {
449 if(!compareDeviceId(dev.id, m.getCardIn()))
450 {
451 m_disp.submitDeferredCommand<Audio::Settings::SetModelCardIn>(
452 m, deviceIdToString(dev.id));
453 m_disp.submitDeferredCommand<Audio::Settings::SetModelDefaultIn>(
454 m, dev.info.nativeDataFormats[0].channels);
455 }
456 };
457
458 auto update_default_in = [this, &m, &m_disp]() {
459 if(m.getCardIn() != "Default")
460 {
461 m_disp.submitDeferredCommand<Audio::Settings::SetModelCardIn>(
462 m, QStringLiteral("Default"));
463 m_disp.submitDeferredCommand<Audio::Settings::SetModelDefaultIn>(
464 m, defaultDeviceChannels(ma_device_type_capture));
465 }
466 };
467
468 QObject::connect(
469 card_list_in, SignalUtils::QComboBox_currentIndexChanged_int(), &v,
470 [this, card_list_in, update_dev_in, update_default_in](int i) {
471 int data = card_list_in->itemData(i).toInt();
472 if(data == DefaultDeviceItemData)
473 update_default_in();
474 else
475 update_dev_in(devices[data]);
476 });
477
478 if(m.getCardIn().isEmpty())
479 {
480 if(!devices.empty())
481 {
482 update_dev_in(devices.front());
483 }
484 }
485 else
486 {
487 setCard(card_list_in, ma_device_type_capture, m.getCardIn());
488 }
489 }
490
491 {
492 lay->addRow(QObject::tr("Playback"), card_list_out);
493
494 auto update_dev_out = [this, &m, &m_disp](const MiniAudioCard& dev) {
495 if(!compareDeviceId(dev.id, m.getCardOut()))
496 {
497 m_disp.submitDeferredCommand<Audio::Settings::SetModelCardOut>(
498 m, deviceIdToString(dev.id));
499 m_disp.submitDeferredCommand<Audio::Settings::SetModelDefaultOut>(
500 m, dev.info.nativeDataFormats[0].channels);
501 }
502 };
503
504 auto update_default_out = [this, &m, &m_disp]() {
505 if(m.getCardOut() != "Default")
506 {
507 m_disp.submitDeferredCommand<Audio::Settings::SetModelCardOut>(
508 m, QStringLiteral("Default"));
509 m_disp.submitDeferredCommand<Audio::Settings::SetModelDefaultOut>(
510 m, defaultDeviceChannels(ma_device_type_playback));
511 }
512 };
513
514 QObject::connect(
515 card_list_out, SignalUtils::QComboBox_currentIndexChanged_int(), &v,
516 [this, card_list_out, update_dev_out, update_default_out](int i) {
517 int data = card_list_out->itemData(i).toInt();
518 if(data == DefaultDeviceItemData)
519 update_default_out();
520 else
521 update_dev_out(devices[data]);
522 });
523
524 if(m.getCardOut().isEmpty())
525 {
526 if(!devices.empty())
527 {
528 update_dev_out(devices.front());
529 }
530 }
531 else
532 {
533 setCard(card_list_out, ma_device_type_playback, m.getCardOut());
534 }
535 }
536
537 addBufferSizeWidget(*w, m, v);
538 addSampleRateWidget(*w, m, v);
539
540 con(m, &Model::changed, w, [this, card_list_in, card_list_out, &m] {
541 setCard(card_list_in, ma_device_type_capture, m.getCardIn());
542 setCard(card_list_out, ma_device_type_playback, m.getCardOut());
543 });
544 return w;
545 }
546};
547
548}
549
550#endif
Definition score-plugin-audio/Audio/Settings/Model.hpp:22
Definition score-plugin-audio/Audio/Settings/View.hpp:19
Definition SettingsCommandDispatcher.hpp:10
Used to access all the application-wide state and structures.
Definition ApplicationContext.hpp:25