() add Audio/Output
This commit is contained in:
parent
0baa6d37f7
commit
2cd49511d6
@ -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)
|
||||
|
||||
@ -47,6 +47,7 @@ target_sources(nf7
|
||||
common/aggregate_command.hh
|
||||
common/async_buffer.hh
|
||||
common/async_buffer_adaptor.hh
|
||||
common/audio.hh
|
||||
common/buffer.hh
|
||||
common/dir.hh
|
||||
common/dir_item.hh
|
||||
@ -57,6 +58,7 @@ target_sources(nf7
|
||||
common/generic_memento.hh
|
||||
common/generic_type_info.hh
|
||||
common/generic_watcher.hh
|
||||
common/gui_audio.hh
|
||||
common/gui_dnd.hh
|
||||
common/gui_file.hh
|
||||
common/gui_node.hh
|
||||
@ -84,6 +86,7 @@ target_sources(nf7
|
||||
common/task.hh
|
||||
common/value.hh
|
||||
common/wait_queue.hh
|
||||
common/yas_audio.hh
|
||||
common/yas_imgui.hh
|
||||
common/yas_imnodes.hh
|
||||
common/yas_nf7.hh
|
||||
@ -91,6 +94,7 @@ target_sources(nf7
|
||||
|
||||
$<$<PLATFORM_ID:Linux>:common/native_file_unix.cc>
|
||||
|
||||
file/audio_output.cc
|
||||
file/luajit_context.cc
|
||||
file/luajit_node.cc
|
||||
file/luajit_obj.cc
|
||||
@ -111,6 +115,7 @@ target_link_libraries(nf7
|
||||
implot
|
||||
linalg.h
|
||||
luajit
|
||||
miniaudio
|
||||
source_location
|
||||
yas
|
||||
)
|
||||
|
96
common/audio.hh
Normal file
96
common/audio.hh
Normal file
@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
#include <miniaudio.h>
|
||||
#include <yas/serialize.hpp>
|
||||
|
||||
#include "nf7.hh"
|
||||
|
||||
|
||||
namespace nf7::audio {
|
||||
|
||||
using Format = ma_format;
|
||||
constexpr auto kF32 = ma_format_f32;
|
||||
constexpr auto kS32 = ma_format_s32;
|
||||
constexpr auto kS24 = ma_format_s24;
|
||||
constexpr auto kS16 = ma_format_s16;
|
||||
constexpr auto kU8 = ma_format_u8;
|
||||
|
||||
inline const char* StringifyFormat(Format fmt) noexcept {
|
||||
switch (fmt) {
|
||||
case kF32: return "f32";
|
||||
case kS32: return "s32";
|
||||
case kS24: return "s24";
|
||||
case kS16: return "s16";
|
||||
case kU8 : return "u8";
|
||||
default : return "unknown";
|
||||
}
|
||||
}
|
||||
inline Format ParseFormat(std::string_view v) {
|
||||
if (v == "f32") return kF32;
|
||||
if (v == "s32") return kS32;
|
||||
if (v == "s24") return kS24;
|
||||
if (v == "s16") return kS16;
|
||||
if (v == "u8" ) return kU8;
|
||||
throw nf7::DeserializeException("unknown audio format");
|
||||
}
|
||||
|
||||
|
||||
class Output : public nf7::File::Interface {
|
||||
public:
|
||||
class Mixer;
|
||||
|
||||
Output() = default;
|
||||
|
||||
virtual std::unique_ptr<Mixer> CreateMixer() noexcept = 0;
|
||||
};
|
||||
|
||||
class Output::Mixer {
|
||||
public:
|
||||
Mixer() = default;
|
||||
virtual ~Mixer() = default;
|
||||
Mixer(const Mixer&) = delete;
|
||||
Mixer(Mixer&&) = delete;
|
||||
Mixer& operator=(const Mixer&) = delete;
|
||||
Mixer& operator=(Mixer&&) = delete;
|
||||
|
||||
// returns a number of samples actually mixed
|
||||
virtual size_t Mix(const uint8_t* ptr, Format fmt, size_t ch, size_t n) noexcept = 0;
|
||||
};
|
||||
|
||||
|
||||
// use with std::visit
|
||||
struct DeviceSelector final {
|
||||
public:
|
||||
using Variant = std::variant<size_t, std::string>;
|
||||
|
||||
DeviceSelector() = delete;
|
||||
DeviceSelector(const ma_device_info* info, size_t n) noexcept : info_(info), n_(n) {
|
||||
}
|
||||
DeviceSelector(const DeviceSelector&) = delete;
|
||||
DeviceSelector(DeviceSelector&&) = delete;
|
||||
DeviceSelector& operator=(const DeviceSelector&) = delete;
|
||||
DeviceSelector& operator=(DeviceSelector&&) = delete;
|
||||
|
||||
const ma_device_info* operator()(const size_t& idx) noexcept {
|
||||
return idx < n_? &info_[idx]: nullptr;
|
||||
}
|
||||
const ma_device_info* operator()(const std::string& name) noexcept {
|
||||
for (size_t i = 0; i < n_; ++i) {
|
||||
const auto& d = info_[i];
|
||||
if (name == d.name) return &d;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
const ma_device_info* info_;
|
||||
size_t n_;
|
||||
};
|
||||
|
||||
} // namespace nf7::audio
|
100
common/gui_audio.hh
Normal file
100
common/gui_audio.hh
Normal file
@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <miniaudio.h>
|
||||
|
||||
#include "common/audio.hh"
|
||||
|
||||
|
||||
namespace nf7::gui {
|
||||
|
||||
template <ma_device_type kType>
|
||||
struct AudioDeviceConfig {
|
||||
public:
|
||||
AudioDeviceConfig() = delete;
|
||||
AudioDeviceConfig(ma_device_config cfg) noexcept : cfg_(cfg) {
|
||||
}
|
||||
AudioDeviceConfig(const AudioDeviceConfig&) = delete;
|
||||
AudioDeviceConfig(AudioDeviceConfig&&) = delete;
|
||||
AudioDeviceConfig& operator=(const AudioDeviceConfig&) = delete;
|
||||
AudioDeviceConfig& operator=(AudioDeviceConfig&&) = delete;
|
||||
|
||||
void Update(const ma_device_info* dev) noexcept {
|
||||
auto& info = sampleInfo();
|
||||
|
||||
std::optional<size_t> preset_idx = std::nullopt;
|
||||
for (size_t i = 0; i < dev->nativeDataFormatCount; ++i) {
|
||||
const auto& nfmt = dev->nativeDataFormats[i];
|
||||
const bool match =
|
||||
nfmt.sampleRate == cfg_.sampleRate &&
|
||||
nfmt.format == info.format &&
|
||||
nfmt.channels == info.channels;
|
||||
if (match) {
|
||||
preset_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const std::string preset_name = preset_idx?
|
||||
StringifyPreset(dev->nativeDataFormats[*preset_idx]): "(custom)";
|
||||
if (ImGui::BeginCombo("preset", preset_name.c_str())) {
|
||||
for (size_t i = 0; i < dev->nativeDataFormatCount; ++i) {
|
||||
const auto& nfmt = dev->nativeDataFormats[i];
|
||||
if (ImGui::Selectable(StringifyPreset(nfmt).c_str(), preset_idx && i == *preset_idx)) {
|
||||
cfg_.sampleRate = nfmt.sampleRate;
|
||||
info.format = nfmt.format;
|
||||
info.channels = nfmt.channels;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
static const uint32_t kU32_1 = 1;
|
||||
static const uint32_t kU32_16 = 16;
|
||||
ImGui::DragScalar("sample rate", ImGuiDataType_U32, &cfg_.sampleRate, 1, &kU32_1);
|
||||
UpdateFormatCombo("format", &sampleInfo().format);
|
||||
ImGui::DragScalar("channels", ImGuiDataType_U32, &sampleInfo().channels, 1, &kU32_1, &kU32_16);
|
||||
}
|
||||
|
||||
const ma_device_config& entity() const noexcept { return cfg_; }
|
||||
|
||||
private:
|
||||
ma_device_config cfg_;
|
||||
|
||||
|
||||
auto& sampleInfo() noexcept {
|
||||
if constexpr (kType == ma_device_type_playback) {
|
||||
return cfg_.playback;
|
||||
} else if constexpr (kType == ma_device_type_capture) {
|
||||
return cfg_.capture;
|
||||
}
|
||||
}
|
||||
|
||||
static void UpdateFormatCombo(const char* id, ma_format* out) noexcept {
|
||||
if (ImGui::BeginCombo(id, audio::StringifyFormat(*out))) {
|
||||
UpdateFormatSelection(out, ma_format_f32);
|
||||
UpdateFormatSelection(out, ma_format_s32);
|
||||
UpdateFormatSelection(out, ma_format_s24);
|
||||
UpdateFormatSelection(out, ma_format_s16);
|
||||
UpdateFormatSelection(out, ma_format_u8);
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
static void UpdateFormatSelection(ma_format* out, ma_format fmt) noexcept {
|
||||
if (ImGui::Selectable(audio::StringifyFormat(fmt), *out == fmt)) *out = fmt;
|
||||
}
|
||||
|
||||
static std::string StringifyPreset(const auto& preset) noexcept {
|
||||
std::stringstream st;
|
||||
st << preset.sampleRate << "Hz, ";
|
||||
st << preset.channels << "ch, ";
|
||||
st << audio::StringifyFormat(preset.format) << ", ";
|
||||
return st.str();
|
||||
}
|
||||
};
|
||||
using AudioPlaybackDeviceConfig = AudioDeviceConfig<ma_device_type_playback>;
|
||||
using AudioCaptureDeviceConfig = AudioDeviceConfig<ma_device_type_capture>;
|
||||
|
||||
} // namespace nf7::gui
|
68
common/yas_audio.hh
Normal file
68
common/yas_audio.hh
Normal file
@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#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,
|
||||
nf7::audio::Format> {
|
||||
public:
|
||||
template <typename Archive>
|
||||
static Archive& save(Archive& ar, nf7::audio::Format fmt) {
|
||||
ar(nf7::audio::StringifyFormat(fmt));
|
||||
return ar;
|
||||
}
|
||||
template <typename Archive>
|
||||
static Archive& load(Archive& ar, nf7::audio::Format& fmt) {
|
||||
std::string str;
|
||||
ar(str);
|
||||
fmt = nf7::audio::ParseFormat(str);
|
||||
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.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
|
258
file/audio_output.cc
Normal file
258
file/audio_output.cc
Normal file
@ -0,0 +1,258 @@
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
#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.hh"
|
||||
#include "common/dir_item.hh"
|
||||
#include "common/generic_type_info.hh"
|
||||
#include "common/gui_audio.hh"
|
||||
#include "common/ptr_selector.hh"
|
||||
#include "common/yas_audio.hh"
|
||||
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace nf7 {
|
||||
namespace {
|
||||
|
||||
class Output final : public nf7::File, public nf7::DirItem, public nf7::audio::Output {
|
||||
public:
|
||||
static inline const GenericTypeInfo<Output> kType = {"Audio/Output", {"DirItem",}};
|
||||
|
||||
using Selector = audio::DeviceSelector::Variant;
|
||||
|
||||
class Ring;
|
||||
class Mixer;
|
||||
|
||||
|
||||
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;
|
||||
return cfg;
|
||||
}
|
||||
Output(Env& env, Selector&& sel = size_t{0}, const ma_device_config& cfg = defaultConfig()) noexcept
|
||||
try : File(kType, env), nf7::DirItem(DirItem::kMenu | DirItem::kTooltip),
|
||||
ring_(std::make_shared<Ring>()), selector_(std::move(sel)), cfg_(cfg) {
|
||||
ctx_.emplace();
|
||||
if (MA_SUCCESS != ma_context_init(nullptr, 0, nullptr, &*ctx_)) {
|
||||
ctx_ = std::nullopt;
|
||||
throw nf7::Exception("context initialization failed");
|
||||
}
|
||||
FetchDevs();
|
||||
} catch (nf7::Exception& e) {
|
||||
msg_ = e.msg();
|
||||
}
|
||||
~Output() noexcept {
|
||||
if (dev_) {
|
||||
ma_device_uninit(&*dev_);
|
||||
}
|
||||
if (ctx_) {
|
||||
ma_context_uninit(&*ctx_);
|
||||
}
|
||||
}
|
||||
|
||||
Output(Env& env, Deserializer& ar) : Output(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<Output>(env, Selector {selector_}, cfg_);
|
||||
}
|
||||
|
||||
std::unique_ptr<audio::Output::Mixer> CreateMixer() noexcept override;
|
||||
|
||||
void Update() noexcept override;
|
||||
void UpdateMenu() noexcept override;
|
||||
void UpdateTooltip() noexcept override;
|
||||
|
||||
File::Interface* interface(const std::type_info& t) noexcept override {
|
||||
return nf7::InterfaceSelector<nf7::DirItem, nf7::audio::Output>(t).Select(this);
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Ring> ring_;
|
||||
std::string msg_;
|
||||
|
||||
std::optional<ma_context> ctx_;
|
||||
ma_device_info* devs_ = nullptr;
|
||||
ma_uint32 devs_cnt_ = 0;
|
||||
|
||||
std::optional<ma_device> dev_;
|
||||
|
||||
const char* popup_ = nullptr;
|
||||
|
||||
// persistent params
|
||||
Selector selector_;
|
||||
ma_device_config cfg_;
|
||||
|
||||
|
||||
void FetchDevs() noexcept
|
||||
try {
|
||||
if (!ctx_) throw nf7::Exception("contex is broken");
|
||||
|
||||
const auto ret = ma_context_get_devices(&*ctx_, &devs_, &devs_cnt_, nullptr, nullptr);
|
||||
if (MA_SUCCESS != ret) {
|
||||
throw nf7::Exception("failed to get playback device list");
|
||||
}
|
||||
} catch (nf7::Exception& e) {
|
||||
msg_ = e.msg();
|
||||
devs_ = nullptr;
|
||||
devs_cnt_ = 0;
|
||||
}
|
||||
|
||||
void InitDev(ma_device_config cfg) noexcept
|
||||
try {
|
||||
if (!ctx_) throw nf7::Exception("context is broken");
|
||||
|
||||
auto d = std::visit(audio::DeviceSelector {devs_, devs_cnt_}, selector_);
|
||||
if (!d) {
|
||||
throw nf7::Exception("device missing");
|
||||
}
|
||||
|
||||
if (dev_) {
|
||||
ma_device_uninit(&*dev_);
|
||||
dev_ = std::nullopt;
|
||||
}
|
||||
|
||||
dev_.emplace();
|
||||
cfg.dataCallback = DataCallback;
|
||||
cfg.pUserData = this;
|
||||
if (MA_SUCCESS != ma_device_init(&*ctx_, &cfg, &*dev_)) {
|
||||
throw nf7::Exception("failed to init device");
|
||||
}
|
||||
|
||||
if (MA_SUCCESS != ma_device_start(&*dev_)) {
|
||||
ma_device_uninit(&*dev_);
|
||||
throw nf7::Exception("failed to start device");
|
||||
}
|
||||
cfg_ = cfg;
|
||||
|
||||
} catch (nf7::Exception& e) {
|
||||
dev_ = std::nullopt;
|
||||
msg_ = e.msg();
|
||||
}
|
||||
|
||||
static void DataCallback(ma_device*, void*, const void*, ma_uint32) noexcept;
|
||||
};
|
||||
|
||||
class Output::Ring final {
|
||||
public:
|
||||
Ring() = default;
|
||||
Ring(const Ring&) = delete;
|
||||
Ring(Ring&&) = delete;
|
||||
Ring& operator=(const Ring&) = delete;
|
||||
Ring& operator=(Ring&&) = delete;
|
||||
|
||||
private:
|
||||
std::mutex mtx_;
|
||||
};
|
||||
void Output::DataCallback(ma_device* dev, void* out, const void* in, ma_uint32 n) noexcept {
|
||||
auto& self = *static_cast<Output*>(dev->pUserData);
|
||||
(void) self;
|
||||
(void) out;
|
||||
(void) in;
|
||||
(void) n;
|
||||
}
|
||||
|
||||
class Output::Mixer final : public nf7::audio::Output::Mixer {
|
||||
public:
|
||||
Mixer() = delete;
|
||||
Mixer(const std::shared_ptr<Ring>& ring) noexcept : ring_(ring) {
|
||||
}
|
||||
Mixer(const Mixer&) = delete;
|
||||
Mixer(Mixer&&) = delete;
|
||||
Mixer& operator=(const Mixer&) = delete;
|
||||
Mixer& operator=(Mixer&&) = delete;
|
||||
|
||||
size_t Mix(const uint8_t* ptr, audio::Format fmt, size_t ch, size_t n) noexcept override {
|
||||
(void) ptr;
|
||||
(void) fmt;
|
||||
(void) ch;
|
||||
(void) n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::weak_ptr<Ring> ring_;
|
||||
};
|
||||
std::unique_ptr<audio::Output::Mixer> Output::CreateMixer() noexcept {
|
||||
return std::make_unique<Output::Mixer>(ring_);
|
||||
}
|
||||
|
||||
|
||||
void Output::Update() noexcept {
|
||||
if (const auto popup = std::exchange(popup_, nullptr)) {
|
||||
ImGui::OpenPopup(popup);
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopup("ConfigPopup")) {
|
||||
static const ma_device_info* dev;
|
||||
static bool save_index;
|
||||
|
||||
static std::optional<gui::AudioPlaybackDeviceConfig> cfg;
|
||||
|
||||
ImGui::TextUnformatted("Audio/Output");
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
dev = std::visit(audio::DeviceSelector {devs_, devs_cnt_}, selector_);
|
||||
save_index = std::holds_alternative<size_t>(selector_);
|
||||
cfg.emplace(cfg_);
|
||||
}
|
||||
|
||||
if (ImGui::BeginCombo("device", dev? dev->name: "(missing)")) {
|
||||
for (size_t i = 0; i < devs_cnt_; ++i) {
|
||||
const auto& d = devs_[i];
|
||||
if (ImGui::Selectable(d.name, &d == dev)) {
|
||||
dev = &d;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::Checkbox("save device index", &save_index);
|
||||
|
||||
ImGui::BeginDisabled(!dev);
|
||||
cfg->Update(dev);
|
||||
if (ImGui::Button("ok")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
|
||||
if (save_index) {
|
||||
selector_ = static_cast<size_t>(dev-devs_);
|
||||
} else {
|
||||
selector_ = dev->name;
|
||||
}
|
||||
InitDev(cfg->entity());
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
void Output::UpdateMenu() noexcept {
|
||||
if (ImGui::MenuItem("config")) {
|
||||
popup_ = "ConfigPopup";
|
||||
}
|
||||
}
|
||||
void Output::UpdateTooltip() noexcept {
|
||||
ImGui::Text("context : %s", ctx_? "ready": "broken");
|
||||
ImGui::Text("device : %s", dev_? "ready": "broken");
|
||||
ImGui::Text("format : %s", audio::StringifyFormat(cfg_.playback.format));
|
||||
ImGui::Text("channels : %" PRIu32, cfg_.playback.channels);
|
||||
ImGui::Text("sample rate: %" PRIu32, cfg_.sampleRate);
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace nf7
|
Loading…
Reference in New Issue
Block a user