add Audio/IO
This commit is contained in:
parent
fbca935907
commit
f7eeae3d83
@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.18)
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
|
||||
|
||||
# ---- configuration ----
|
||||
project(nf7 CXX)
|
||||
project(nf7 C CXX)
|
||||
|
||||
option(NF7_STATIC "link all libs statically" ON)
|
||||
|
||||
@ -86,6 +86,7 @@ target_sources(nf7
|
||||
common/thread.hh
|
||||
common/value.hh
|
||||
common/wait_queue.hh
|
||||
common/yas_audio.hh
|
||||
common/yas_imgui.hh
|
||||
common/yas_imnodes.hh
|
||||
common/yas_nf7.hh
|
||||
@ -94,6 +95,7 @@ target_sources(nf7
|
||||
$<$<PLATFORM_ID:Linux>:common/native_file_unix.cc>
|
||||
|
||||
file/audio_context.cc
|
||||
file/audio_io.cc
|
||||
file/luajit_context.cc
|
||||
file/luajit_node.cc
|
||||
file/luajit_obj.cc
|
||||
@ -114,6 +116,7 @@ target_link_libraries(nf7
|
||||
implot
|
||||
linalg.h
|
||||
luajit
|
||||
miniaudio
|
||||
source_location
|
||||
yas
|
||||
)
|
||||
|
86
common/yas_audio.hh
Normal file
86
common/yas_audio.hh
Normal file
@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include <miniaudio.h>
|
||||
#include <yas/serialize.hpp>
|
||||
|
||||
#include "nf7.hh"
|
||||
|
||||
|
||||
namespace yas::detail {
|
||||
|
||||
template <size_t F>
|
||||
struct serializer<
|
||||
type_prop::not_a_fundamental,
|
||||
ser_case::use_internal_serializer,
|
||||
F,
|
||||
ma_device_type> {
|
||||
public:
|
||||
template <typename Archive>
|
||||
static Archive& save(Archive& ar, const ma_device_type& t) {
|
||||
switch (t) {
|
||||
case ma_device_type_playback:
|
||||
ar("playback");
|
||||
break;
|
||||
case ma_device_type_capture:
|
||||
ar("capture");
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
return ar;
|
||||
}
|
||||
template <typename Archive>
|
||||
static Archive& load(Archive& ar, ma_device_type& t) {
|
||||
std::string v;
|
||||
ar(v);
|
||||
if (v == "playback") {
|
||||
t = ma_device_type_playback;
|
||||
} else if (v == "capture") {
|
||||
t = ma_device_type_capture;
|
||||
} else {
|
||||
throw nf7::DeserializeException("unknown device type");
|
||||
}
|
||||
return ar;
|
||||
}
|
||||
};
|
||||
|
||||
template <size_t F>
|
||||
struct serializer<
|
||||
type_prop::not_a_fundamental,
|
||||
ser_case::use_internal_serializer,
|
||||
F,
|
||||
ma_device_config> {
|
||||
public:
|
||||
template <typename Archive>
|
||||
static Archive& save(Archive& ar, const ma_device_config& v) {
|
||||
serialize(ar, v);
|
||||
return ar;
|
||||
}
|
||||
template <typename Archive>
|
||||
static Archive& load(Archive& ar, ma_device_config& v) {
|
||||
serialize(ar, v);
|
||||
if (v.sampleRate == 0) {
|
||||
throw nf7::DeserializeException("invalid sample rate");
|
||||
}
|
||||
return ar;
|
||||
}
|
||||
|
||||
private:
|
||||
static void serialize(auto& ar, auto& v) {
|
||||
ar(v.deviceType);
|
||||
ar(v.sampleRate);
|
||||
if (v.deviceType == ma_device_type_playback) {
|
||||
ar(v.playback.format);
|
||||
ar(v.playback.channels);
|
||||
} else if (v.deviceType == ma_device_type_capture) {
|
||||
ar(v.capture.format);
|
||||
ar(v.capture.channels);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace yas::detail
|
@ -23,10 +23,7 @@ class AudioContext final : public nf7::File, public nf7::DirItem {
|
||||
|
||||
class Queue;
|
||||
|
||||
AudioContext(Env& env) noexcept :
|
||||
File(kType, env), DirItem(DirItem::kMenu | DirItem::kTooltip),
|
||||
q_(std::make_shared<Queue>(env)) {
|
||||
}
|
||||
AudioContext(Env&) noexcept;
|
||||
|
||||
AudioContext(Env& env, Deserializer&) : AudioContext(env) {
|
||||
}
|
||||
@ -88,8 +85,6 @@ class AudioContext::Queue final : public nf7::audio::Queue,
|
||||
|
||||
Queue() = delete;
|
||||
Queue(Env& env) : env_(&env), th_(std::make_shared<Thread>(env, Runner {*this})) {
|
||||
auto ctx = std::make_shared<nf7::GenericContext>(*env_, 0, "creating ma_context");
|
||||
env.ExecMain(ctx, [this, ctx]() { Init(ctx); });
|
||||
}
|
||||
~Queue() noexcept {
|
||||
th_->Push(
|
||||
@ -102,6 +97,20 @@ class AudioContext::Queue final : public nf7::audio::Queue,
|
||||
Queue& operator=(const Queue&) = delete;
|
||||
Queue& operator=(Queue&&) = delete;
|
||||
|
||||
void Init(Env& env) noexcept {
|
||||
th_->Push(
|
||||
std::make_shared<nf7::GenericContext>(env, 0, "creating ma_context"),
|
||||
[this, self = shared_from_this()](auto) {
|
||||
auto ctx = std::make_shared<ma_context>();
|
||||
if (MA_SUCCESS == ma_context_init(nullptr, 0, nullptr, ctx.get())) {
|
||||
ctx_ = std::move(ctx);
|
||||
state_ = kReady;
|
||||
} else {
|
||||
state_ = kBroken;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Push(const std::shared_ptr<nf7::Context>& ctx, Task&& task) noexcept override {
|
||||
th_->Push(ctx, std::move(task));
|
||||
}
|
||||
@ -116,22 +125,12 @@ class AudioContext::Queue final : public nf7::audio::Queue,
|
||||
|
||||
std::atomic<State> state_ = kInitializing;
|
||||
std::shared_ptr<ma_context> ctx_;
|
||||
|
||||
|
||||
void Init(const std::shared_ptr<nf7::Context>& ctx) noexcept {
|
||||
th_->Push(
|
||||
ctx,
|
||||
[this, self = shared_from_this()](auto) {
|
||||
auto ctx = std::make_shared<ma_context>();
|
||||
if (MA_SUCCESS == ma_context_init(nullptr, 0, nullptr, ctx.get())) {
|
||||
ctx_ = std::move(ctx);
|
||||
state_ = kReady;
|
||||
} else {
|
||||
state_ = kBroken;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
AudioContext::AudioContext(Env& env) noexcept :
|
||||
File(kType, env), DirItem(DirItem::kMenu | DirItem::kTooltip),
|
||||
q_(std::make_shared<Queue>(env)) {
|
||||
q_->Init(env);
|
||||
}
|
||||
|
||||
|
||||
void AudioContext::Update() noexcept {
|
||||
|
683
file/audio_io.cc
Normal file
683
file/audio_io.cc
Normal file
@ -0,0 +1,683 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#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/generic_context.hh"
|
||||
#include "common/generic_type_info.hh"
|
||||
#include "common/lambda.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 IO final : public nf7::File, public nf7::DirItem, public nf7::Node {
|
||||
public:
|
||||
static inline const GenericTypeInfo<IO> kType = {"Audio/IO", {"DirItem",}};
|
||||
|
||||
class Ring;
|
||||
class PlaybackLambda;
|
||||
class CaptureLambda;
|
||||
|
||||
using DeviceSelector = std::variant<size_t, std::string>;
|
||||
struct DeviceSelectorVisitor;
|
||||
|
||||
static ma_device_config defaultConfig() noexcept {
|
||||
ma_device_config cfg;
|
||||
cfg = ma_device_config_init(ma_device_type_playback);
|
||||
cfg.sampleRate = 480000;
|
||||
cfg.playback.format = ma_format_f32;
|
||||
cfg.playback.channels = 2;
|
||||
cfg.capture.format = ma_format_f32;
|
||||
cfg.capture.channels = 2;
|
||||
return cfg;
|
||||
}
|
||||
IO(Env& env,
|
||||
DeviceSelector&& sel = size_t{0},
|
||||
const ma_device_config& cfg = defaultConfig()) noexcept :
|
||||
File(kType, env), nf7::DirItem(DirItem::kMenu | DirItem::kTooltip),
|
||||
data_(std::make_shared<Data>()),
|
||||
selector_(std::move(sel)), cfg_(cfg) {
|
||||
}
|
||||
|
||||
IO(Env& env, Deserializer& ar) : IO(env) {
|
||||
ar(selector_, cfg_);
|
||||
}
|
||||
void Serialize(Serializer& ar) const noexcept override {
|
||||
ar(selector_, cfg_);
|
||||
}
|
||||
std::unique_ptr<nf7::File> Clone(nf7::Env& env) const noexcept override {
|
||||
return std::make_unique<IO>(env, DeviceSelector {selector_}, cfg_);
|
||||
}
|
||||
|
||||
std::shared_ptr<nf7::Lambda> CreateLambda() 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 { }
|
||||
|
||||
File::Interface* interface(const std::type_info& t) noexcept override {
|
||||
return nf7::InterfaceSelector<nf7::DirItem, nf7::Node>(t).Select(this);
|
||||
}
|
||||
|
||||
private:
|
||||
const char* popup_ = nullptr;
|
||||
|
||||
struct Data {
|
||||
public:
|
||||
Data() noexcept : 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<Data> data_;
|
||||
|
||||
// persistent params
|
||||
DeviceSelector selector_;
|
||||
ma_device_config cfg_;
|
||||
|
||||
// ConfigPopup param
|
||||
struct ConfigPopup final : std::enable_shared_from_this<ConfigPopup> {
|
||||
ma_device_config cfg;
|
||||
DeviceSelector selector;
|
||||
|
||||
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] = IO::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;
|
||||
void BuildNode() 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;
|
||||
}
|
||||
|
||||
|
||||
static bool UpdateModeSelector(ma_device_type*) noexcept;
|
||||
static const ma_device_info* UpdateDeviceSelector(DeviceSelector*, ma_device_info*, size_t) noexcept;
|
||||
static void UpdatePresetSelector(ma_device_config*, const ma_device_info*) noexcept;
|
||||
};
|
||||
|
||||
class IO::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
|
||||
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();
|
||||
|
||||
const size_t vn = std::min(n, buf_.size());
|
||||
const size_t offset = (time-time_begin_)%buf_.size();
|
||||
for (size_t srci = 0, dsti = offset; srci < vn; ++srci, ++dsti) {
|
||||
if (dsti >= buf_.size()) dsti = 0;
|
||||
buf_[dsti] += ptr[srci];
|
||||
}
|
||||
return time+vn;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
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 IO::PlaybackLambda final : public nf7::Lambda,
|
||||
public std::enable_shared_from_this<IO::PlaybackLambda> {
|
||||
public:
|
||||
static inline const std::vector<std::string> kInputs = {"samples"};
|
||||
static inline const std::vector<std::string> kOutputs = {"sample_count"};
|
||||
|
||||
enum {
|
||||
kInSamples = 0,
|
||||
kOutSampleCount = 0,
|
||||
};
|
||||
|
||||
PlaybackLambda() = delete;
|
||||
PlaybackLambda(IO& owner) noexcept : data_(owner.data_) {
|
||||
}
|
||||
|
||||
void Handle(size_t idx, nf7::Value&& v, const std::shared_ptr<nf7::Lambda>& caller) noexcept override
|
||||
try {
|
||||
switch (idx) {
|
||||
case kInSamples: {
|
||||
const auto& vec = v.vector();
|
||||
const auto ptr = reinterpret_cast<const float*>(vec->data());
|
||||
const auto n = vec->size()/sizeof(float);
|
||||
|
||||
auto ptime = time_;
|
||||
time_ = data_->ring->Mix(ptr, n, time_);
|
||||
if (time_ < ptime) ptime = time_;
|
||||
|
||||
caller->Handle(kOutSampleCount, static_cast<nf7::Value::Integer>(time_-ptime), shared_from_this());
|
||||
} break;
|
||||
default:
|
||||
throw nf7::Exception("got unknown input");
|
||||
}
|
||||
} catch (nf7::Exception& e) {
|
||||
data_->log.Warn(e.msg());
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Data> data_;
|
||||
|
||||
uint64_t time_ = 0;
|
||||
};
|
||||
class IO::CaptureLambda final : public nf7::Lambda,
|
||||
std::enable_shared_from_this<IO::CaptureLambda> {
|
||||
public:
|
||||
static inline const std::vector<std::string> kInputs = {"start", "peek"};
|
||||
static inline const std::vector<std::string> kOutputs = {"samples"};
|
||||
|
||||
enum {
|
||||
kInStart = 0,
|
||||
kInPeek = 1,
|
||||
|
||||
kOutSamples = 0,
|
||||
};
|
||||
|
||||
CaptureLambda() = delete;
|
||||
CaptureLambda(IO& owner) noexcept : data_(owner.data_) {
|
||||
}
|
||||
|
||||
void Handle(size_t idx, nf7::Value&&, const std::shared_ptr<nf7::Lambda>& caller) noexcept override
|
||||
try {
|
||||
switch (idx) {
|
||||
case kInStart:
|
||||
time_ = data_->ring->time();
|
||||
break;
|
||||
case kInPeek: {
|
||||
if (!time_) throw nf7::Exception("not started");
|
||||
std::vector<uint8_t> samples;
|
||||
time_ = data_->ring->Peek(samples, *time_);
|
||||
caller->Handle(kOutSamples, nf7::Value {std::move(samples)}, shared_from_this());
|
||||
} break;
|
||||
default:
|
||||
throw nf7::Exception("got unknown input");
|
||||
}
|
||||
} catch (nf7::Exception& e) {
|
||||
data_->log.Warn(e.msg());
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Data> data_;
|
||||
|
||||
std::optional<uint64_t> time_;
|
||||
};
|
||||
std::shared_ptr<nf7::Lambda> IO::CreateLambda() noexcept {
|
||||
switch (cfg_.deviceType) {
|
||||
case ma_device_type_playback:
|
||||
return std::make_shared<IO::PlaybackLambda>(*this);
|
||||
case ma_device_type_capture:
|
||||
return std::make_shared<IO::CaptureLambda>(*this);
|
||||
default:
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct IO::DeviceSelectorVisitor final {
|
||||
public:
|
||||
DeviceSelectorVisitor() = delete;
|
||||
DeviceSelectorVisitor(ma_device_info* info, size_t n) noexcept : info_(info), n_(n) {
|
||||
}
|
||||
DeviceSelectorVisitor(const DeviceSelectorVisitor&) = delete;
|
||||
DeviceSelectorVisitor(DeviceSelectorVisitor&&) = delete;
|
||||
DeviceSelectorVisitor& operator=(const DeviceSelectorVisitor&) = delete;
|
||||
DeviceSelectorVisitor& operator=(DeviceSelectorVisitor&&) = 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_;
|
||||
};
|
||||
|
||||
|
||||
void IO::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(DeviceSelectorVisitor {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 IO::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 IO::BuildNode() noexcept {
|
||||
switch (cfg_.deviceType) {
|
||||
case ma_device_type_playback:
|
||||
nf7::Node::input_ = PlaybackLambda::kInputs;
|
||||
nf7::Node::output_ = PlaybackLambda::kOutputs;
|
||||
break;
|
||||
case ma_device_type_capture:
|
||||
nf7::Node::input_ = CaptureLambda::kInputs;
|
||||
nf7::Node::output_ = CaptureLambda::kOutputs;
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
nf7::File::Touch();
|
||||
}
|
||||
|
||||
|
||||
void IO::Handle(const Event& ev) noexcept {
|
||||
switch (ev.type) {
|
||||
case Event::kAdd:
|
||||
data_->log.SetUp(*this);
|
||||
try {
|
||||
data_->aq =
|
||||
ResolveUpwardOrThrow("_audio").
|
||||
interfaceOrThrow<nf7::audio::Queue>().self();
|
||||
InitDev();
|
||||
BuildNode();
|
||||
} catch (nf7::Exception&) {
|
||||
data_->log.Info("audio context is not found");
|
||||
}
|
||||
return;
|
||||
|
||||
case Event::kRemove:
|
||||
if (data_->aq) {
|
||||
DeinitDev();
|
||||
}
|
||||
data_->aq = nullptr;
|
||||
data_->log.TearDown();
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void IO::Update() noexcept {
|
||||
if (const auto popup = std::exchange(popup_, nullptr)) {
|
||||
ImGui::OpenPopup(popup);
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopup("ConfigPopup")) {
|
||||
auto& p = config_popup_;
|
||||
|
||||
ImGui::TextUnformatted("Audio/Output");
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
if (!p) {
|
||||
p = std::make_shared<ConfigPopup>();
|
||||
}
|
||||
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 = UpdateDeviceSelector(&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();
|
||||
BuildNode();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
void IO::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")) {
|
||||
popup_ = "ConfigPopup";
|
||||
}
|
||||
}
|
||||
void IO::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 IO::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* IO::UpdateDeviceSelector(
|
||||
DeviceSelector* sel, ma_device_info* devs, size_t n) noexcept {
|
||||
const auto dev = std::visit(DeviceSelectorVisitor {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 IO::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
|
9
main.cc
9
main.cc
@ -1,5 +1,6 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@ -74,10 +75,11 @@ class Env final : public nf7::Env {
|
||||
}
|
||||
|
||||
alive_ = false;
|
||||
cv_.notify_one();
|
||||
async_.Notify();
|
||||
|
||||
cv_.notify_one();
|
||||
main_thread_.join();
|
||||
|
||||
async_.Notify();
|
||||
for (auto& th : async_threads_) th.join();
|
||||
|
||||
::Env::Pop();
|
||||
@ -275,7 +277,8 @@ class Env final : public nf7::Env {
|
||||
} catch (Exception&) {
|
||||
// TODO: how to handle?
|
||||
}
|
||||
async_.Wait();
|
||||
if (!alive_) break;
|
||||
async_.WaitFor(1s);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user