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