() add Audio/Output

This commit is contained in:
falsycat 2022-07-03 13:27:35 +09:00
parent 0baa6d37f7
commit 2cd49511d6
5 changed files with 528 additions and 1 deletions

View File

@ -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
View 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
View 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
View 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
View 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