497 lines
14 KiB
C++
497 lines
14 KiB
C++
#include <cinttypes>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <typeinfo>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <imgui.h>
|
|
|
|
#include <magic_enum.hpp>
|
|
|
|
#include <miniaudio.h>
|
|
|
|
#include <yaml-cpp/yaml.h>
|
|
|
|
#include <yas/serialize.hpp>
|
|
#include <yas/types/std/string.hpp>
|
|
|
|
#include "nf7.hh"
|
|
|
|
#include "common/audio_queue.hh"
|
|
#include "common/config.hh"
|
|
#include "common/dir_item.hh"
|
|
#include "common/file_base.hh"
|
|
#include "common/future.hh"
|
|
#include "common/generic_config.hh"
|
|
#include "common/generic_context.hh"
|
|
#include "common/generic_memento.hh"
|
|
#include "common/generic_type_info.hh"
|
|
#include "common/gui.hh"
|
|
#include "common/life.hh"
|
|
#include "common/logger_ref.hh"
|
|
#include "common/node.hh"
|
|
#include "common/ptr_selector.hh"
|
|
#include "common/ring_buffer.hh"
|
|
#include "common/value.hh"
|
|
#include "common/yas_enum.hh"
|
|
#include "common/yas_nf7.hh"
|
|
|
|
|
|
using namespace std::literals;
|
|
|
|
namespace nf7 {
|
|
namespace {
|
|
|
|
class Device final : public nf7::FileBase,
|
|
public nf7::GenericConfig, public nf7::DirItem, public nf7::Node {
|
|
public:
|
|
static inline const nf7::GenericTypeInfo<Device> kType = {
|
|
"Audio/Device", {"nf7::DirItem",},
|
|
"provides a ring buffer to send/receive PCM samples"};
|
|
|
|
class Instance;
|
|
class Lambda;
|
|
|
|
enum class Mode {
|
|
Playback, Capture,
|
|
};
|
|
static ma_device_type FromMode(Mode m) {
|
|
return
|
|
m == Mode::Playback? ma_device_type_playback:
|
|
m == Mode::Capture ? ma_device_type_capture:
|
|
throw 0;
|
|
}
|
|
|
|
// the least 4 bits represent size of the type
|
|
enum class Format {
|
|
U8 = 0x11, S16 = 0x22, S32 = 0x24, F32 = 0x34,
|
|
};
|
|
static ma_format FromFormat(Format f) {
|
|
return
|
|
f == Format::U8 ? ma_format_u8 :
|
|
f == Format::S16? ma_format_s16:
|
|
f == Format::S32? ma_format_s32:
|
|
f == Format::F32? ma_format_f32:
|
|
throw 0;
|
|
}
|
|
|
|
struct Data {
|
|
Data() noexcept { }
|
|
std::string Stringify() const noexcept;
|
|
void Parse(const std::string&);
|
|
void serialize(auto& ar) {
|
|
ar(ctxpath, mode, devname, fmt, srate, ch);
|
|
}
|
|
|
|
nf7::File::Path ctxpath = {"$", "_audio"};
|
|
|
|
Mode mode = Mode::Playback;
|
|
std::string devname = "";
|
|
|
|
Format fmt = Format::F32;
|
|
uint32_t srate = 48000;
|
|
uint32_t ch = 1;
|
|
|
|
uint64_t ring_size = 48000*3;
|
|
};
|
|
|
|
Device(nf7::Env& env, Data&& data = {}) noexcept :
|
|
nf7::FileBase(kType, env),
|
|
nf7::GenericConfig(mem_),
|
|
nf7::DirItem(nf7::DirItem::kMenu | nf7::DirItem::kTooltip),
|
|
nf7::Node(nf7::Node::kNone),
|
|
life_(*this), log_(*this), mem_(*this, std::move(data)) {
|
|
mem_.onCommit = mem_.onRestore = [this]() { cache_ = std::nullopt; };
|
|
}
|
|
|
|
Device(nf7::Deserializer& ar) : Device(ar.env()) {
|
|
ar(mem_.data());
|
|
}
|
|
void Serialize(nf7::Serializer& ar) const noexcept override {
|
|
ar(mem_.data());
|
|
}
|
|
std::unique_ptr<nf7::File> Clone(nf7::Env& env) const noexcept override {
|
|
return std::make_unique<Device>(env, Data {mem_.data()});
|
|
}
|
|
|
|
std::shared_ptr<nf7::Node::Lambda> CreateLambda(
|
|
const std::shared_ptr<nf7::Node::Lambda>&) noexcept override;
|
|
nf7::Node::Meta GetMeta() const noexcept override {
|
|
static const std::vector<std::string> kInputs = {};
|
|
return {
|
|
{"info", "mix", "peek"},
|
|
{"result"}
|
|
};
|
|
}
|
|
|
|
nf7::Future<std::shared_ptr<Instance>> Build() noexcept;
|
|
|
|
void UpdateMenu() noexcept override;
|
|
void UpdateTooltip() noexcept override;
|
|
|
|
File::Interface* interface(const std::type_info& t) noexcept override {
|
|
return nf7::InterfaceSelector<
|
|
nf7::Config, nf7::DirItem, nf7::Memento, nf7::Node>(t).Select(this, &mem_);
|
|
}
|
|
|
|
private:
|
|
nf7::Life<Device> life_;
|
|
|
|
nf7::LoggerRef log_;
|
|
|
|
nf7::GenericMemento<Data> mem_;
|
|
|
|
std::optional<nf7::Future<std::shared_ptr<Instance>>> cache_;
|
|
|
|
|
|
static const ma_device_id* ChooseDevice(
|
|
ma_device_info* ptr, ma_uint32 n, std::string_view name, std::string& result) {
|
|
for (ma_uint32 i = 0; i < n; ++i) {
|
|
const auto& d = ptr[i];
|
|
bool choose = false;
|
|
if (name.empty()) {
|
|
if (d.isDefault) {
|
|
choose = true;
|
|
}
|
|
} else {
|
|
if (std::string_view::npos != std::string_view {d.name}.find(name)) {
|
|
choose = true;
|
|
}
|
|
}
|
|
if (choose) {
|
|
result = d.name;
|
|
return &d.id;
|
|
}
|
|
}
|
|
throw nf7::Exception {"no device found"};
|
|
}
|
|
};
|
|
|
|
|
|
class Device::Instance final {
|
|
public:
|
|
// must be called on audio thread
|
|
Instance(const std::shared_ptr<nf7::Context>& ctx,
|
|
const std::shared_ptr<nf7::audio::Queue>& aq,
|
|
ma_context* ma, const Data& d) :
|
|
ctx_(ctx), aq_(aq), data_(d),
|
|
sdata_(std::make_shared<SharedData>(
|
|
magic_enum::enum_integer(d.fmt) & 0xF, d.ring_size)) {
|
|
// get device list
|
|
ma_device_info* pbs;
|
|
ma_uint32 pbn;
|
|
ma_device_info* cps;
|
|
ma_uint32 cpn;
|
|
if (MA_SUCCESS != ma_context_get_devices(ma, &pbs, &pbn, &cps, &cpn)) {
|
|
throw nf7::Exception {"failed to get device list"};
|
|
}
|
|
|
|
// construct device config
|
|
ma_device_config cfg = ma_device_config_init(FromMode(d.mode));
|
|
switch (d.mode) {
|
|
case Mode::Playback:
|
|
cfg.dataCallback = PlaybackCallback;
|
|
cfg.playback.pDeviceID = ChooseDevice(pbs, pbn, d.devname, devname_);
|
|
cfg.playback.format = FromFormat(d.fmt);
|
|
cfg.playback.channels = d.ch;
|
|
break;
|
|
case Mode::Capture:
|
|
cfg.dataCallback = CaptureCallback;
|
|
cfg.capture.pDeviceID = ChooseDevice(cps, cpn, d.devname, devname_);
|
|
cfg.capture.format = FromFormat(d.fmt);
|
|
cfg.capture.channels = d.ch;
|
|
break;
|
|
}
|
|
cfg.sampleRate = d.srate;
|
|
cfg.pUserData = sdata_.get();
|
|
|
|
if (MA_SUCCESS != ma_device_init(ma, &cfg, &sdata_->dev)) {
|
|
throw nf7::Exception {"device init failure"};
|
|
}
|
|
if (MA_SUCCESS != ma_device_start(&sdata_->dev)) {
|
|
ma_device_uninit(&sdata_->dev);
|
|
throw nf7::Exception {"device start failure"};
|
|
}
|
|
}
|
|
~Instance() noexcept {
|
|
aq_->Push(ctx_, [sdata = sdata_](auto) {
|
|
ma_device_uninit(&sdata->dev);
|
|
});
|
|
}
|
|
Instance(const Instance&) = delete;
|
|
Instance(Instance&&) = delete;
|
|
Instance& operator=(const Instance&) = delete;
|
|
Instance& operator=(Instance&&) = delete;
|
|
|
|
std::mutex& mtx() const noexcept { return sdata_->mtx; }
|
|
|
|
nf7::RingBuffer& ring() noexcept { return sdata_->ring; }
|
|
const nf7::RingBuffer& ring() const noexcept { return sdata_->ring; }
|
|
|
|
const std::string& devname() const noexcept { return devname_; }
|
|
Data data() const noexcept { return data_; }
|
|
|
|
private:
|
|
std::shared_ptr<nf7::Context> ctx_;
|
|
std::shared_ptr<nf7::audio::Queue> aq_;
|
|
|
|
std::string devname_;
|
|
Data data_;
|
|
|
|
struct SharedData {
|
|
SharedData(uint64_t a, uint64_t b) noexcept : ring(a, b) {
|
|
}
|
|
mutable std::mutex mtx;
|
|
nf7::RingBuffer ring;
|
|
ma_device dev;
|
|
};
|
|
std::shared_ptr<SharedData> sdata_;
|
|
|
|
static void PlaybackCallback(
|
|
ma_device* dev, void* out, const void*, ma_uint32 n) noexcept {
|
|
auto& sdata = *reinterpret_cast<SharedData*>(dev->pUserData);
|
|
|
|
std::unique_lock<std::mutex> _(sdata.mtx);
|
|
sdata.ring.Take(reinterpret_cast<uint8_t*>(out), n);
|
|
}
|
|
static void CaptureCallback(
|
|
ma_device* dev, void*, const void* in, ma_uint32 n) noexcept {
|
|
auto& sdata = *reinterpret_cast<SharedData*>(dev->pUserData);
|
|
|
|
std::unique_lock<std::mutex> _(sdata.mtx);
|
|
sdata.ring.Write(reinterpret_cast<const uint8_t*>(in), n);
|
|
}
|
|
};
|
|
|
|
|
|
class Device::Lambda final : public nf7::Node::Lambda,
|
|
public std::enable_shared_from_this<Device::Lambda> {
|
|
public:
|
|
Lambda(Device& f, const std::shared_ptr<nf7::Node::Lambda>& parent) noexcept :
|
|
nf7::Node::Lambda(f, parent), f_(f.life_) {
|
|
}
|
|
|
|
void Handle(const nf7::Node::Lambda::Msg& in) noexcept override {
|
|
if (!f_) return;
|
|
f_->Build().
|
|
ThenIf(shared_from_this(), [this, in](auto& inst) {
|
|
if (!f_) return;
|
|
try {
|
|
Exec(in, inst);
|
|
} catch (nf7::Exception& e) {
|
|
f_->log_.Error(e);
|
|
}
|
|
}).
|
|
Catch<nf7::Exception>(shared_from_this(), [this](auto&) {
|
|
if (f_) {
|
|
f_->log_.Warn("skip execution because of device init failure");
|
|
}
|
|
});
|
|
}
|
|
|
|
private:
|
|
nf7::Life<Device>::Ref f_;
|
|
std::weak_ptr<Instance> last_inst_;
|
|
|
|
uint64_t time_ = 0;
|
|
|
|
void Exec(const nf7::Node::Lambda::Msg& in,
|
|
const std::shared_ptr<Instance>& inst) {
|
|
const bool reset = last_inst_.expired();
|
|
last_inst_ = inst;
|
|
|
|
const auto& data = inst->data();
|
|
auto& ring = inst->ring();
|
|
|
|
if (in.name == "info") {
|
|
std::vector<nf7::Value::TuplePair> tup {
|
|
{"format", magic_enum::enum_name(data.fmt)},
|
|
{"srate", static_cast<nf7::Value::Integer>(data.srate)},
|
|
{"ch", static_cast<nf7::Value::Integer>(data.ch)},
|
|
};
|
|
in.sender->Handle("result", std::move(tup), shared_from_this());
|
|
|
|
} else if (in.name == "mix") {
|
|
if (data.mode != Mode::Playback) {
|
|
throw nf7::Exception {"device mode is not playback"};
|
|
}
|
|
const auto& vec = *in.value.vector();
|
|
|
|
std::unique_lock<std::mutex> lock(inst->mtx());
|
|
if (reset) time_ = ring.cur();
|
|
const auto ptime = time_;
|
|
|
|
const auto Mix = [&]<typename T>() {
|
|
time_ = ring.Mix(
|
|
time_, reinterpret_cast<const T*>(vec.data()), vec.size()/sizeof(T));
|
|
};
|
|
switch (data.fmt) {
|
|
case Format::U8 : Mix.operator()<uint8_t>(); break;
|
|
case Format::S16: Mix.operator()<int16_t>(); break;
|
|
case Format::S32: Mix.operator()<int32_t>(); break;
|
|
case Format::F32: Mix.operator()<float>(); break;
|
|
}
|
|
lock.unlock();
|
|
|
|
const auto wrote = (time_-ptime) / data.ch;
|
|
in.sender->Handle(
|
|
"result", static_cast<nf7::Value::Integer>(wrote), shared_from_this());
|
|
|
|
} else if (in.name == "peek") {
|
|
if (data.mode != Mode::Playback) {
|
|
throw nf7::Exception {"device mode is not capture"};
|
|
}
|
|
|
|
const auto expect_read = std::min(
|
|
ring.bufn(), in.value.integer<uint64_t>()*data.ch);
|
|
std::vector<uint8_t> buf(expect_read*ring.unit());
|
|
|
|
std::unique_lock<std::mutex> lock(inst->mtx());
|
|
if (reset) time_ = ring.cur();
|
|
const auto ptime = time_;
|
|
time_ = ring.Peek(time_, buf.data(), expect_read);
|
|
lock.unlock();
|
|
|
|
const auto read = time_ - ptime;
|
|
in.sender->Handle(
|
|
"result", static_cast<nf7::Value::Integer>(read), shared_from_this());
|
|
|
|
} else {
|
|
throw nf7::Exception {"unknown command type: "+in.name};
|
|
}
|
|
}
|
|
};
|
|
std::shared_ptr<nf7::Node::Lambda> Device::CreateLambda(
|
|
const std::shared_ptr<nf7::Node::Lambda>& parent) noexcept {
|
|
return std::make_shared<Device::Lambda>(*this, parent);
|
|
}
|
|
|
|
|
|
nf7::Future<std::shared_ptr<Device::Instance>> Device::Build() noexcept
|
|
try {
|
|
if (cache_) return *cache_;
|
|
|
|
auto ctx = std::make_shared<
|
|
nf7::GenericContext>(*this, "audio device instance builder");
|
|
|
|
auto aq =
|
|
ResolveOrThrow(mem_->ctxpath).
|
|
interfaceOrThrow<nf7::audio::Queue>().self();
|
|
|
|
nf7::Future<std::shared_ptr<Device::Instance>>::Promise pro;
|
|
aq->Push(ctx, [ctx, aq, pro, d = mem_.data()](auto ma) mutable {
|
|
pro.Wrap([&]() { return std::make_shared<Instance>(ctx, aq, ma, d); });
|
|
});
|
|
|
|
cache_ = pro.future().
|
|
Catch<nf7::Exception>([f = nf7::Life<Device>::Ref {life_}](auto& e) {
|
|
if (f) f->log_.Error(e);
|
|
});
|
|
return *cache_;
|
|
} catch (nf7::Exception& e) {
|
|
log_.Error(e);
|
|
return {std::current_exception()};
|
|
}
|
|
|
|
|
|
void Device::UpdateMenu() noexcept {
|
|
if (ImGui::MenuItem("build")) {
|
|
Build();
|
|
}
|
|
}
|
|
void Device::UpdateTooltip() noexcept {
|
|
ImGui::Text("format : %s / %" PRIu32 " ch / %" PRIu32 " Hz",
|
|
magic_enum::enum_name(mem_->fmt).data(), mem_->ch, mem_->srate);
|
|
if (!cache_) {
|
|
ImGui::TextUnformatted("status : unused");
|
|
} else if (cache_->yet()) {
|
|
ImGui::TextUnformatted("status : initializing");
|
|
} else if (cache_->done()) {
|
|
auto& inst = *cache_->value();
|
|
ImGui::TextUnformatted("status : running");
|
|
ImGui::Text("devname: %s", inst.devname().c_str());
|
|
} else if (cache_->error()) {
|
|
ImGui::TextUnformatted("status : invalid");
|
|
try {
|
|
cache_->value();
|
|
} catch (nf7::Exception& e) {
|
|
ImGui::Text("msg : %s", e.msg().c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
std::string Device::Data::Stringify() const noexcept {
|
|
YAML::Emitter st;
|
|
st << YAML::BeginMap;
|
|
st << YAML::Key << "ctxpath";
|
|
st << YAML::Value << ctxpath.Stringify();
|
|
st << YAML::Key << "mode";
|
|
st << YAML::Value << std::string {magic_enum::enum_name(mode)};
|
|
st << YAML::Key << "devname";
|
|
st << YAML::Value << devname << YAML::Comment("leave empty to choose default one");
|
|
st << YAML::Key << "format";
|
|
st << YAML::Value << std::string {magic_enum::enum_name(fmt)};
|
|
st << YAML::Key << "srate";
|
|
st << YAML::Value << srate;
|
|
st << YAML::Key << "ch";
|
|
st << YAML::Value << ch;
|
|
st << YAML::Key << "ring_size";
|
|
st << YAML::Value << ring_size;
|
|
st << YAML::EndMap;
|
|
return {st.c_str(), st.size()};
|
|
}
|
|
void Device::Data::Parse(const std::string& str)
|
|
try {
|
|
const auto yaml = YAML::Load(str);
|
|
Data d;
|
|
d.ctxpath = nf7::File::Path::Parse(yaml["ctxpath"].as<std::string>());
|
|
d.mode = magic_enum::enum_cast<Mode>(yaml["mode"].as<std::string>()).value();
|
|
d.devname = yaml["devname"].as<std::string>();
|
|
d.fmt = magic_enum::enum_cast<Format>(yaml["format"].as<std::string>()).value();
|
|
d.srate = yaml["srate"].as<uint32_t>();
|
|
d.ch = yaml["ch"].as<uint32_t>();
|
|
d.ring_size = yaml["ring_size"].as<uint64_t>();
|
|
|
|
if (d.srate > d.ring_size) {
|
|
throw nf7::Exception {"ring size is too small (must be srate or more)"};
|
|
}
|
|
if (d.srate*10 < d.ring_size) {
|
|
throw nf7::Exception {"ring size is too large (must be srate*10 or less)"};
|
|
}
|
|
|
|
*this = std::move(d);
|
|
} catch (std::bad_optional_access&) {
|
|
throw nf7::Exception {"invalid enum"};
|
|
} catch (YAML::Exception& e) {
|
|
throw nf7::Exception {e.what()};
|
|
}
|
|
|
|
}
|
|
} // namespace nf7
|
|
|
|
|
|
namespace yas::detail {
|
|
|
|
template <size_t F>
|
|
struct serializer<
|
|
yas::detail::type_prop::is_enum,
|
|
yas::detail::ser_case::use_internal_serializer,
|
|
F, nf7::Device::Mode> :
|
|
nf7::EnumSerializer<nf7::Device::Mode> {
|
|
};
|
|
|
|
template <size_t F>
|
|
struct serializer<
|
|
yas::detail::type_prop::is_enum,
|
|
yas::detail::ser_case::use_internal_serializer,
|
|
F, nf7::Device::Format> :
|
|
nf7::EnumSerializer<nf7::Device::Format> {
|
|
};
|
|
|
|
} // namespace yas::detail
|