707 lines
20 KiB
C++
707 lines
20 KiB
C++
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cinttypes>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <variant>
|
|
|
|
#include <imgui.h>
|
|
#include <miniaudio.h>
|
|
#include <yas/serialize.hpp>
|
|
#include <yas/types/std/string.hpp>
|
|
#include <yas/types/std/variant.hpp>
|
|
|
|
#include "nf7.hh"
|
|
|
|
#include "common/audio_queue.hh"
|
|
#include "common/dir_item.hh"
|
|
#include "common/file_base.hh"
|
|
#include "common/generic_context.hh"
|
|
#include "common/generic_type_info.hh"
|
|
#include "common/life.hh"
|
|
#include "common/logger_ref.hh"
|
|
#include "common/node.hh"
|
|
#include "common/ptr_selector.hh"
|
|
#include "common/yas_audio.hh"
|
|
|
|
|
|
using namespace std::literals;
|
|
|
|
namespace nf7 {
|
|
namespace {
|
|
|
|
class Device final : public nf7::FileBase, public nf7::DirItem, public nf7::Node {
|
|
public:
|
|
static inline const GenericTypeInfo<Device> kType = {
|
|
"Audio/Device", {"nf7::DirItem",}};
|
|
static void UpdateTypeTooltip() noexcept {
|
|
ImGui::TextUnformatted("Manages ring buffer and sends PCM samples to actual device.");
|
|
ImGui::Bullet();
|
|
ImGui::TextUnformatted("requires nf7::audio::Queue with name '_audio' on upper dirs");
|
|
}
|
|
|
|
class Ring;
|
|
class PlaybackLambda;
|
|
class CaptureLambda;
|
|
|
|
using Selector = std::variant<size_t, std::string>;
|
|
struct SelectorVisitor;
|
|
|
|
static ma_device_config defaultConfig() noexcept {
|
|
ma_device_config cfg;
|
|
cfg = ma_device_config_init(ma_device_type_playback);
|
|
cfg.sampleRate = 48000;
|
|
cfg.playback.format = ma_format_f32;
|
|
cfg.playback.channels = 2;
|
|
cfg.capture.format = ma_format_f32;
|
|
cfg.capture.channels = 2;
|
|
return cfg;
|
|
}
|
|
Device(nf7::Env& env, Selector&& sel = size_t{0}, const ma_device_config& cfg = defaultConfig()) noexcept :
|
|
nf7::FileBase(kType, env),
|
|
nf7::DirItem(DirItem::kMenu | DirItem::kTooltip),
|
|
data_(std::make_shared<AsyncData>(*this)),
|
|
selector_(std::move(sel)), cfg_(cfg),
|
|
config_popup_(std::make_shared<ConfigPopup>()) {
|
|
nf7::FileBase::Install(data_->log);
|
|
}
|
|
|
|
Device(nf7::Deserializer& ar) : Device(ar.env()) {
|
|
ar(selector_, cfg_);
|
|
}
|
|
void Serialize(nf7::Serializer& ar) const noexcept override {
|
|
ar(selector_, cfg_);
|
|
}
|
|
std::unique_ptr<nf7::File> Clone(nf7::Env& env) const noexcept override {
|
|
return std::make_unique<Device>(env, Selector {selector_}, cfg_);
|
|
}
|
|
|
|
std::shared_ptr<Lambda> CreateLambda(const std::shared_ptr<Lambda>&) noexcept override;
|
|
std::span<const std::string> GetInputs() const noexcept override;
|
|
std::span<const std::string> GetOutputs() const noexcept override;
|
|
|
|
void Handle(const Event&) noexcept override;
|
|
void Update() noexcept override;
|
|
void UpdateMenu() noexcept override;
|
|
void UpdateTooltip() noexcept override;
|
|
void UpdateNode(Node::Editor&) noexcept override { }
|
|
|
|
static bool UpdateModeSelector(ma_device_type*) noexcept;
|
|
static const ma_device_info* UpdateSelector(Selector*, ma_device_info*, size_t) noexcept;
|
|
static void UpdatePresetSelector(ma_device_config*, const ma_device_info*) noexcept;
|
|
|
|
File::Interface* interface(const std::type_info& t) noexcept override {
|
|
return nf7::InterfaceSelector<nf7::DirItem, nf7::Node>(t).Select(this);
|
|
}
|
|
|
|
private:
|
|
struct AsyncData {
|
|
public:
|
|
AsyncData(nf7::File& f) noexcept :
|
|
log(f), ring(std::make_unique<Ring>()) {
|
|
}
|
|
|
|
nf7::LoggerRef log;
|
|
std::unique_ptr<Ring> ring;
|
|
|
|
std::shared_ptr<nf7::audio::Queue> aq;
|
|
|
|
std::optional<ma_device> dev;
|
|
std::atomic<size_t> busy = 0;
|
|
};
|
|
std::shared_ptr<AsyncData> data_;
|
|
|
|
// persistent params
|
|
Selector selector_;
|
|
ma_device_config cfg_;
|
|
|
|
// ConfigPopup param (must be shared_ptr because saves fetched device list async)
|
|
struct ConfigPopup final : std::enable_shared_from_this<ConfigPopup> {
|
|
bool open = false;
|
|
|
|
// params
|
|
ma_device_config cfg;
|
|
Selector selector;
|
|
|
|
// fetched devices
|
|
std::atomic<bool> fetching = false;
|
|
ma_device_info* devs = nullptr;
|
|
ma_uint32 devs_n = 0;
|
|
|
|
void FetchDevs(File& f, const std::shared_ptr<nf7::audio::Queue>& aq) noexcept {
|
|
const auto mode = cfg.deviceType;
|
|
|
|
fetching = true;
|
|
aq->Push(
|
|
std::make_shared<nf7::GenericContext>(f, "fetching device list"),
|
|
[this, self = shared_from_this(), mode](auto ma) {
|
|
try {
|
|
auto [ptr, n] = Device::FetchDevs(ma, mode);
|
|
devs = ptr;
|
|
devs_n = static_cast<ma_uint32>(n);
|
|
} catch (nf7::Exception&) {
|
|
devs = nullptr;
|
|
devs_n = 0;
|
|
}
|
|
fetching = false;
|
|
});
|
|
}
|
|
};
|
|
std::shared_ptr<ConfigPopup> config_popup_;
|
|
|
|
|
|
void InitDev() noexcept;
|
|
void DeinitDev() noexcept;
|
|
|
|
static std::pair<ma_device_info*, size_t> FetchDevs(ma_context* ctx, ma_device_type mode) {
|
|
ma_device_info* devs = nullptr;
|
|
ma_uint32 num = 0;
|
|
const auto ret =
|
|
mode == ma_device_type_playback?
|
|
ma_context_get_devices(ctx, &devs, &num, nullptr, nullptr):
|
|
mode == ma_device_type_capture?
|
|
ma_context_get_devices(ctx, nullptr, nullptr, &devs, &num):
|
|
throw nf7::Exception("unknown mode");
|
|
if (MA_SUCCESS != ret) {
|
|
throw nf7::Exception("failed to get device list");
|
|
}
|
|
return {devs, num};
|
|
}
|
|
static auto& GetChannels(auto& cfg) noexcept {
|
|
switch (cfg.deviceType) {
|
|
case ma_device_type_playback:
|
|
return cfg.playback.channels;
|
|
case ma_device_type_capture:
|
|
return cfg.capture.channels;
|
|
default:
|
|
std::abort();
|
|
}
|
|
}
|
|
static std::string StringifyPreset(const auto& p) noexcept {
|
|
std::stringstream st;
|
|
st << "f32, " << p.sampleRate << "Hz, " << p.channels << " ch";
|
|
return st.str();
|
|
}
|
|
static std::vector<float> GenerateSineWave(uint32_t srate, uint32_t ch) noexcept {
|
|
std::vector<float> ret;
|
|
ret.resize(srate*ch);
|
|
for (size_t i = 0; i < srate; ++i) {
|
|
const double t = static_cast<double>(i)/static_cast<double>(srate);
|
|
const float v = static_cast<float>(sin(t*200*2*3.14));
|
|
for (size_t j = 0; j < ch; ++j) {
|
|
ret[i*ch + j] = v;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
nf7::Value infoTuple() const noexcept {
|
|
return nf7::Value {std::vector<nf7::Value::TuplePair> {
|
|
{"sampleRate", {static_cast<nf7::Value::Integer>(cfg_.sampleRate)}},
|
|
{"channels", {static_cast<nf7::Value::Integer>(GetChannels(cfg_))}},
|
|
}};
|
|
}
|
|
};
|
|
|
|
class Device::Ring final {
|
|
public:
|
|
static constexpr auto kDur = 3000; /* msecs */
|
|
|
|
Ring() noexcept {
|
|
Reset(1, 1);
|
|
}
|
|
Ring(const Ring&) = delete;
|
|
Ring(Ring&&) = delete;
|
|
Ring& operator=(const Ring&) = delete;
|
|
Ring& operator=(Ring&&) = delete;
|
|
|
|
void Reset(uint32_t srate, uint32_t ch) noexcept {
|
|
std::unique_lock<std::mutex> k(mtx_);
|
|
time_begin_ = time_;
|
|
cursor_ = 0;
|
|
buf_.clear();
|
|
buf_.resize(kDur*srate*ch/1000);
|
|
}
|
|
|
|
// for playback mode: mix samples into this ring
|
|
std::pair<uint64_t, uint64_t> Mix(const float* ptr, size_t n, uint64_t time) noexcept {
|
|
std::unique_lock<std::mutex> k(mtx_);
|
|
if (time < time_) {
|
|
time = time_;
|
|
}
|
|
if (time-time_ > buf_.size()) {
|
|
return {time_+buf_.size(), 0};
|
|
}
|
|
|
|
n = std::min(n, buf_.size());
|
|
if (time+n-time_ > buf_.size()) {
|
|
n = buf_.size() - (time-time_);
|
|
}
|
|
|
|
const size_t offset = (time-time_begin_)%buf_.size();
|
|
for (size_t srci = 0, dsti = offset; srci < n; ++srci, ++dsti) {
|
|
if (dsti >= buf_.size()) dsti = 0;
|
|
buf_[dsti] += ptr[srci];
|
|
}
|
|
return {time+n, n};
|
|
}
|
|
// for playback mode: consume samples in this ring
|
|
void Consume(float* dst, size_t n) noexcept {
|
|
std::unique_lock<std::mutex> k(mtx_);
|
|
for (size_t i = 0; i < n; ++i, ++cursor_) {
|
|
if (cursor_ >= buf_.size()) cursor_ = 0;
|
|
dst[i] = std::exchange(buf_[cursor_], 0.f);
|
|
}
|
|
time_ += n;
|
|
}
|
|
|
|
// for capture mode: append samples to this ring
|
|
void Append(const float* src, size_t n) noexcept {
|
|
std::unique_lock<std::mutex> k(mtx_);
|
|
const size_t vn = std::min(n, buf_.size());
|
|
for (size_t i = 0; i < vn; ++i, ++cursor_) {
|
|
if (cursor_ >= buf_.size()) cursor_ = 0;
|
|
buf_[cursor_] = src[i];
|
|
}
|
|
time_ += n;
|
|
}
|
|
// for capture mode: read samples
|
|
// actual samples are stored as float32 in dst
|
|
uint64_t Peek(std::vector<uint8_t>& dst, uint64_t ptime) noexcept {
|
|
std::unique_lock<std::mutex> k(mtx_);
|
|
const size_t vn = std::min(time_-ptime, buf_.size());
|
|
dst.resize(vn*sizeof(float));
|
|
|
|
float* dstp = reinterpret_cast<float*>(dst.data());
|
|
for (size_t i = 0, dsti = vn, srci = cursor_; i < vn; ++i) {
|
|
if (srci == 0) srci = buf_.size();
|
|
--dsti, --srci;
|
|
dstp[dsti] = buf_[srci];
|
|
}
|
|
return time_;
|
|
}
|
|
|
|
uint64_t time() const noexcept { return time_; }
|
|
|
|
private:
|
|
std::mutex mtx_;
|
|
uint32_t ch_;
|
|
|
|
size_t cursor_ = 0;
|
|
std::vector<float> buf_;
|
|
|
|
uint64_t time_begin_ = 0;
|
|
std::atomic<uint64_t> time_ = 0;
|
|
};
|
|
|
|
class Device::PlaybackLambda final : public nf7::Node::Lambda,
|
|
public std::enable_shared_from_this<Device::PlaybackLambda> {
|
|
public:
|
|
static inline const std::vector<std::string> kInputs = {"get_info", "mix"};
|
|
static inline const std::vector<std::string> kOutputs = {"info", "mixed_size"};
|
|
|
|
PlaybackLambda() = delete;
|
|
PlaybackLambda(Device& f, const std::shared_ptr<Lambda>& parent) noexcept :
|
|
Lambda(f, parent), data_(f.data_), info_(f.infoTuple()) {
|
|
}
|
|
|
|
void Handle(std::string_view name, const nf7::Value& v,
|
|
const std::shared_ptr<Lambda>& caller) noexcept override {
|
|
if (name == "get_info") {
|
|
caller->Handle("info", nf7::Value {info_}, shared_from_this());
|
|
return;
|
|
}
|
|
if (name == "mix") {
|
|
const auto& vec = v.vector();
|
|
const auto ptr = reinterpret_cast<const float*>(vec->data());
|
|
const auto n = vec->size()/sizeof(float);
|
|
|
|
const auto [time, mixed] = data_->ring->Mix(ptr, n, time_);
|
|
time_ = time;
|
|
|
|
const auto ret = static_cast<nf7::Value::Integer>(mixed);
|
|
caller->Handle("mixed_size", ret, shared_from_this());
|
|
return;
|
|
}
|
|
data_->log.Warn("got unknown input");
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<AsyncData> data_;
|
|
nf7::Value info_;
|
|
|
|
uint64_t time_ = 0;
|
|
};
|
|
class Device::CaptureLambda final : public nf7::Node::Lambda,
|
|
std::enable_shared_from_this<Device::CaptureLambda> {
|
|
public:
|
|
static inline const std::vector<std::string> kInputs = {"get_info", "peek"};
|
|
static inline const std::vector<std::string> kOutputs = {"info", "samples"};
|
|
|
|
CaptureLambda() = delete;
|
|
CaptureLambda(Device& f, const std::shared_ptr<Lambda>& parent) noexcept :
|
|
Lambda(f, parent), data_(f.data_), info_(f.infoTuple()) {
|
|
}
|
|
|
|
void Handle(std::string_view name, const nf7::Value&,
|
|
const std::shared_ptr<Lambda>& caller) noexcept override {
|
|
if (name == "get_info") {
|
|
caller->Handle("info", nf7::Value {info_}, shared_from_this());
|
|
return;
|
|
}
|
|
if (name == "peek") {
|
|
std::vector<uint8_t> samples;
|
|
if (time_) {
|
|
time_ = data_->ring->Peek(samples, *time_);
|
|
} else {
|
|
time_ = data_->ring->time();
|
|
}
|
|
caller->Handle("samples", {std::move(samples)}, shared_from_this());
|
|
return;
|
|
}
|
|
data_->log.Warn("got unknown input");
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<AsyncData> data_;
|
|
nf7::Value info_;
|
|
|
|
std::optional<uint64_t> time_;
|
|
};
|
|
std::shared_ptr<Node::Lambda> Device::CreateLambda(const std::shared_ptr<Lambda>& parent) noexcept {
|
|
switch (cfg_.deviceType) {
|
|
case ma_device_type_playback:
|
|
return std::make_shared<Device::PlaybackLambda>(*this, parent);
|
|
case ma_device_type_capture:
|
|
return std::make_shared<Device::CaptureLambda>(*this, parent);
|
|
default:
|
|
std::abort();
|
|
}
|
|
}
|
|
|
|
|
|
struct Device::SelectorVisitor final {
|
|
public:
|
|
SelectorVisitor() = delete;
|
|
SelectorVisitor(ma_device_info* info, size_t n) noexcept : info_(info), n_(n) {
|
|
}
|
|
SelectorVisitor(const SelectorVisitor&) = delete;
|
|
SelectorVisitor(SelectorVisitor&&) = delete;
|
|
SelectorVisitor& operator=(const SelectorVisitor&) = delete;
|
|
SelectorVisitor& operator=(SelectorVisitor&&) = delete;
|
|
|
|
ma_device_info* operator()(const size_t& idx) noexcept {
|
|
return idx < n_? &info_[idx]: nullptr;
|
|
}
|
|
ma_device_info* operator()(const std::string& name) noexcept {
|
|
for (size_t i = 0; i < n_; ++i) {
|
|
auto& d = info_[i];
|
|
if (name == d.name) return &d;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
private:
|
|
ma_device_info* info_;
|
|
size_t n_;
|
|
};
|
|
|
|
|
|
std::span<const std::string> Device::GetInputs() const noexcept {
|
|
switch (cfg_.deviceType) {
|
|
case ma_device_type_playback:
|
|
return PlaybackLambda::kInputs;
|
|
case ma_device_type_capture:
|
|
return CaptureLambda::kInputs;
|
|
default:
|
|
assert(false);
|
|
}
|
|
return {};
|
|
}
|
|
std::span<const std::string> Device::GetOutputs() const noexcept {
|
|
switch (cfg_.deviceType) {
|
|
case ma_device_type_playback:
|
|
return PlaybackLambda::kOutputs;
|
|
case ma_device_type_capture:
|
|
return CaptureLambda::kOutputs;
|
|
default:
|
|
assert(false);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void Device::InitDev() noexcept {
|
|
if (!data_->aq) {
|
|
data_->log.Error("audio queue is missing");
|
|
return;
|
|
}
|
|
|
|
static const auto kPlaybackCallback = [](ma_device* dev, void* out, const void*, ma_uint32 n) {
|
|
auto& ring = *static_cast<Ring*>(dev->pUserData);
|
|
ring.Consume(static_cast<float*>(out), n*dev->playback.channels);
|
|
};
|
|
static const auto kCaptureCallback = [](ma_device* dev, void*, const void* in, ma_uint32 n) {
|
|
auto& ring = *static_cast<Ring*>(dev->pUserData);
|
|
ring.Append(static_cast<const float*>(in), n*dev->capture.channels);
|
|
};
|
|
|
|
++data_->busy;
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*this, "initializing audio device");
|
|
data_->aq->Push(ctx, [d = data_, sel = selector_, cfg = cfg_](auto ma) mutable {
|
|
try {
|
|
if (!ma) {
|
|
throw nf7::Exception("audio task queue is broken");
|
|
}
|
|
if (d->dev) {
|
|
ma_device_uninit(&*d->dev);
|
|
d->dev = std::nullopt;
|
|
}
|
|
|
|
auto [devs, devs_n] = FetchDevs(ma, cfg.deviceType);
|
|
auto dinfo = std::visit(SelectorVisitor {devs, devs_n}, sel);
|
|
if (!dinfo) {
|
|
throw nf7::Exception("missing device");
|
|
}
|
|
cfg.playback.pDeviceID = cfg.capture.pDeviceID = &dinfo->id;
|
|
|
|
cfg.pUserData = d->ring.get();
|
|
cfg.dataCallback =
|
|
cfg.deviceType == ma_device_type_playback? kPlaybackCallback:
|
|
cfg.deviceType == ma_device_type_capture ? kCaptureCallback:
|
|
throw nf7::Exception("unknown mode");
|
|
|
|
d->dev.emplace();
|
|
if (MA_SUCCESS != ma_device_init(ma, &cfg, &*d->dev)) {
|
|
d->dev = std::nullopt;
|
|
throw nf7::Exception("failed to init audio device");
|
|
}
|
|
if (MA_SUCCESS != ma_device_start(&*d->dev)) {
|
|
ma_device_uninit(&*d->dev);
|
|
d->dev = std::nullopt;
|
|
throw nf7::Exception("failed to start device");
|
|
}
|
|
d->ring->Reset(cfg.sampleRate, GetChannels(cfg));
|
|
} catch (nf7::Exception& e) {
|
|
d->log.Error(e.msg());
|
|
}
|
|
--d->busy;
|
|
});
|
|
}
|
|
void Device::DeinitDev() noexcept {
|
|
if (!data_->aq) {
|
|
data_->log.Error("audio queue is missing");
|
|
return;
|
|
}
|
|
|
|
++data_->busy;
|
|
auto ctx = std::make_shared<nf7::GenericContext>(*this, "deleting audio device");
|
|
data_->aq->Push(ctx, [d = data_](auto) {
|
|
if (d->dev) {
|
|
ma_device_uninit(&*d->dev);
|
|
d->dev = std::nullopt;
|
|
}
|
|
--d->busy;
|
|
});
|
|
}
|
|
|
|
|
|
void Device::Handle(const Event& ev) noexcept {
|
|
nf7::FileBase::Handle(ev);
|
|
|
|
switch (ev.type) {
|
|
case Event::kAdd:
|
|
try {
|
|
data_->aq =
|
|
ResolveUpwardOrThrow("_audio").
|
|
interfaceOrThrow<nf7::audio::Queue>().self();
|
|
InitDev();
|
|
} catch (nf7::Exception&) {
|
|
data_->log.Info("audio context is not found");
|
|
}
|
|
return;
|
|
|
|
case Event::kRemove:
|
|
if (data_->aq) {
|
|
DeinitDev();
|
|
}
|
|
data_->aq = nullptr;
|
|
return;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Device::Update() noexcept {
|
|
nf7::FileBase::Update();
|
|
|
|
if (std::exchange(config_popup_->open, false)) {
|
|
ImGui::OpenPopup("ConfigPopup");
|
|
}
|
|
if (ImGui::BeginPopup("ConfigPopup")) {
|
|
auto& p = config_popup_;
|
|
|
|
ImGui::TextUnformatted("Audio/Output");
|
|
if (ImGui::IsWindowAppearing()) {
|
|
p->cfg = cfg_;
|
|
p->selector = selector_;
|
|
|
|
if (data_->aq) {
|
|
p->FetchDevs(*this, data_->aq);
|
|
}
|
|
}
|
|
|
|
if (UpdateModeSelector(&p->cfg.deviceType)) {
|
|
if (data_->aq) {
|
|
p->FetchDevs(*this, data_->aq);
|
|
}
|
|
}
|
|
const ma_device_info* dev = nullptr;
|
|
if (!p->fetching) {
|
|
dev = UpdateSelector(&p->selector, p->devs, p->devs_n);
|
|
} else {
|
|
ImGui::LabelText("device", "fetching list...");
|
|
}
|
|
|
|
UpdatePresetSelector(&p->cfg, dev);
|
|
|
|
static const uint32_t kU32_1 = 1;
|
|
static const uint32_t kU32_16 = 16;
|
|
ImGui::DragScalar("sample rate", ImGuiDataType_U32, &p->cfg.sampleRate, 1, &kU32_1);
|
|
ImGui::DragScalar("channels", ImGuiDataType_U32, &GetChannels(p->cfg), 1, &kU32_1, &kU32_16);
|
|
|
|
if (ImGui::Button("ok")) {
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
cfg_ = p->cfg;
|
|
selector_ = p->selector;
|
|
InitDev();
|
|
Touch();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
void Device::UpdateMenu() noexcept {
|
|
if (cfg_.deviceType == ma_device_type_playback) {
|
|
if (ImGui::MenuItem("simulate sinwave output for 1 sec")) {
|
|
const auto wave = GenerateSineWave(cfg_.sampleRate, cfg_.playback.channels);
|
|
data_->ring->Mix(wave.data(), wave.size(), 0);
|
|
}
|
|
ImGui::Separator();
|
|
}
|
|
if (ImGui::MenuItem("config")) {
|
|
config_popup_->open = true;
|
|
}
|
|
}
|
|
void Device::UpdateTooltip() noexcept {
|
|
const char* mode =
|
|
cfg_.deviceType == ma_device_type_playback? "playback":
|
|
cfg_.deviceType == ma_device_type_capture ? "capture":
|
|
"unknown";
|
|
ImGui::Text("mode : %s", mode);
|
|
ImGui::Text("context : %s", data_->aq ? "ready": "broken");
|
|
ImGui::Text("device : %s", data_->busy? "initializing": data_->dev? "ready": "broken");
|
|
ImGui::Text("channels : %" PRIu32, cfg_.playback.channels);
|
|
ImGui::Text("sample rate: %" PRIu32, cfg_.sampleRate);
|
|
}
|
|
bool Device::UpdateModeSelector(ma_device_type* m) noexcept {
|
|
const char* mode =
|
|
*m == ma_device_type_playback? "playback":
|
|
*m == ma_device_type_capture? "capture":
|
|
"unknown";
|
|
bool ret = false;
|
|
if (ImGui::BeginCombo("mode", mode)) {
|
|
if (ImGui::Selectable("playback", *m == ma_device_type_playback)) {
|
|
*m = ma_device_type_playback;
|
|
ret = true;
|
|
}
|
|
if (ImGui::Selectable("capture", *m == ma_device_type_capture)) {
|
|
*m = ma_device_type_capture;
|
|
ret = true;
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
return ret;
|
|
}
|
|
const ma_device_info* Device::UpdateSelector(
|
|
Selector* sel, ma_device_info* devs, size_t n) noexcept {
|
|
const auto dev = std::visit(SelectorVisitor {devs, n}, *sel);
|
|
|
|
if (ImGui::BeginCombo("device", dev? dev->name: "(missing)")) {
|
|
for (size_t i = 0; i < n; ++i) {
|
|
const auto& d = devs[i];
|
|
const auto str = std::to_string(i)+": "+d.name;
|
|
if (ImGui::Selectable(str.c_str(), &d == dev)) {
|
|
if (std::holds_alternative<std::string>(*sel)) {
|
|
*sel = std::string {d.name};
|
|
} else if (std::holds_alternative<size_t>(*sel)) {
|
|
*sel = i;
|
|
} else {
|
|
assert(false);
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
|
|
bool b = std::holds_alternative<size_t>(*sel);
|
|
if (ImGui::Checkbox("remember device index", &b)) {
|
|
if (b) {
|
|
*sel = dev? static_cast<size_t>(dev - devs): size_t{0};
|
|
} else {
|
|
*sel = std::string {dev? dev->name: ""};
|
|
}
|
|
}
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("true : the device is remembered by its index\n"
|
|
"false: the device is remembered by its name");
|
|
}
|
|
return dev;
|
|
}
|
|
void Device::UpdatePresetSelector(ma_device_config* cfg, const ma_device_info* dev) noexcept {
|
|
auto& srate = cfg->sampleRate;
|
|
auto& ch = GetChannels(*cfg);
|
|
|
|
std::optional<size_t> match_idx = std::nullopt;
|
|
if (dev) {
|
|
for (size_t i = 0; i < dev->nativeDataFormatCount; ++i) {
|
|
const auto& fmt = dev->nativeDataFormats[i];
|
|
if (fmt.format != ma_format_f32) continue;
|
|
|
|
if (fmt.sampleRate == srate && fmt.channels == ch) {
|
|
match_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto preset_name = match_idx?
|
|
StringifyPreset(dev->nativeDataFormats[*match_idx]):
|
|
std::string {"(custom)"};
|
|
if (ImGui::BeginCombo("preset", preset_name.c_str())) {
|
|
if (dev) {
|
|
for (size_t i = 0; i < dev->nativeDataFormatCount; ++i) {
|
|
const auto& fmt = dev->nativeDataFormats[i];
|
|
if (fmt.format != ma_format_f32) continue;
|
|
|
|
const auto name = StringifyPreset(fmt);
|
|
if (ImGui::Selectable(name.c_str(), match_idx == i)) {
|
|
srate = fmt.sampleRate;
|
|
ch = fmt.channels;
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
}
|
|
|
|
}
|
|
} // namespace nf7
|