diff --git a/CMakeLists.txt b/CMakeLists.txt index 526c8c10..cbd46256 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,14 @@ file(GLOB_RECURSE ABADDON_SOURCES list(FILTER ABADDON_SOURCES EXCLUDE REGEX ".*notifier_gio\\.cpp$") list(FILTER ABADDON_SOURCES EXCLUDE REGEX ".*notifier_fallback\\.cpp$") +if (NOT ENABLE_VOICE) + list(FILTER ABADDON_SOURCES EXCLUDE REGEX "src/audio/voice/.*") +endif () + +if (NOT (ENABLE_VOICE OR ENABLE_NOTIFICATION_SOUNDS)) + list(FILTER ABADDON_SOURCES EXCLUDE REGEX "src/audio/.*") +endif () + add_executable(abaddon ${ABADDON_SOURCES}) target_include_directories(abaddon PUBLIC ${PROJECT_SOURCE_DIR}/src) target_include_directories(abaddon PUBLIC ${PROJECT_BINARY_DIR}) @@ -220,3 +228,10 @@ set(ABADDON_COMPILER_DEFS "" CACHE STRING "Additional compiler definitions") foreach (COMPILER_DEF IN LISTS ABADDON_COMPILER_DEFS) target_compile_definitions(abaddon PRIVATE "${COMPILER_DEF}") endforeach () + +# LTO breaks on Windows +# Due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=108383 +# And other weirdness +if (NOT WIN32) + set_property(TARGET abaddon PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) +endif () diff --git a/README.md b/README.md index b1d1a35b..706e7ed1 100644 --- a/README.md +++ b/README.md @@ -338,10 +338,11 @@ For example, memory_db would be set by adding `memory_db = true` under the line #### voice -| Setting | Type | Default | Description | -|------------|--------|------------------------------------|----------------------------------------------------------------------------------------------------------------------------| -| `vad` | string | rnnoise if enabled, gate otherwise | Method used for voice activity detection. Changeable in UI | -| `backends` | string | empty | Change backend priority when initializing miniaudio: `wasapi;dsound;winmm;coreaudio;sndio;audio4;oss;pulseaudio;alsa;jack` | +| Setting | Type | Default | Description | +|--------------------|---------|------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| `vad` | string | rnnoise if enabled, gate otherwise | Method used for voice activity detection. Changeable in UI | +| `backends` | string | empty | Change backend priority when initializing miniaudio: `wasapi;dsound;winmm;coreaudio;sndio;audio4;oss;pulseaudio;alsa;jack` | +| `separate_sources` | boolean | false | Spawn separate audio sources for each user in voice call, useful for multitrack recording on Linux | #### windows diff --git a/res/res/sound/voice_connected.mp3 b/res/res/sound/voice_connected.mp3 new file mode 100644 index 00000000..3e030fac Binary files /dev/null and b/res/res/sound/voice_connected.mp3 differ diff --git a/res/res/sound/voice_deafened.mp3 b/res/res/sound/voice_deafened.mp3 new file mode 100644 index 00000000..81681232 Binary files /dev/null and b/res/res/sound/voice_deafened.mp3 differ diff --git a/res/res/sound/voice_disconnected.mp3 b/res/res/sound/voice_disconnected.mp3 new file mode 100644 index 00000000..c58e9ed4 Binary files /dev/null and b/res/res/sound/voice_disconnected.mp3 differ diff --git a/res/res/sound/voice_muted.mp3 b/res/res/sound/voice_muted.mp3 new file mode 100644 index 00000000..b87de43c Binary files /dev/null and b/res/res/sound/voice_muted.mp3 differ diff --git a/res/res/sound/voice_undeafened.mp3 b/res/res/sound/voice_undeafened.mp3 new file mode 100644 index 00000000..797c56ec Binary files /dev/null and b/res/res/sound/voice_undeafened.mp3 differ diff --git a/res/res/sound/voice_unmuted.mp3 b/res/res/sound/voice_unmuted.mp3 new file mode 100644 index 00000000..fd784228 Binary files /dev/null and b/res/res/sound/voice_unmuted.mp3 differ diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 653327cd..0b20ebeb 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -7,7 +7,6 @@ #include #include #include "platform.hpp" -#include "audio/manager.hpp" #include "discord/discord.hpp" #include "dialogs/token.hpp" #include "dialogs/confirm.hpp" @@ -50,12 +49,16 @@ void macOSThemeChangedCallback(CFNotificationCenterRef center, void *observer, C #pragma comment(lib, "crypt32.lib") #endif +#ifdef WITH_MINIAUDIO +#include "audio/manager.hpp" +#endif + Abaddon::Abaddon() : m_settings(Platform::FindConfigFile()) , m_discord(GetSettings().UseMemoryDB) // stupid but easy , m_emojis(GetResPath("/emojis.db")) -#ifdef WITH_VOICE - , m_audio(GetSettings().Backends) +#ifdef WITH_MINIAUDIO + , m_audio(GetSettings().Backends, m_discord) #endif { LoadFromSettings(); @@ -81,10 +84,6 @@ Abaddon::Abaddon() #ifdef WITH_VOICE m_discord.signal_voice_connected().connect(sigc::mem_fun(*this, &Abaddon::OnVoiceConnected)); m_discord.signal_voice_disconnected().connect(sigc::mem_fun(*this, &Abaddon::OnVoiceDisconnected)); - m_discord.signal_voice_speaking().connect([this](const VoiceSpeakingData &m) { - spdlog::get("voice")->debug("{} SSRC: {}", m.UserID, m.SSRC); - m_audio.AddSSRC(m.SSRC); - }); #endif m_discord.signal_channel_accessibility_changed().connect([this](Snowflake id, bool accessible) { @@ -103,7 +102,7 @@ Abaddon::Abaddon() } #ifdef WITH_VOICE - m_audio.SetVADMethod(GetSettings().VAD); + m_audio.GetVoice().GetCapture().GetEffects().SetVADMethod(GetSettings().VAD); #endif } @@ -487,19 +486,18 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) { #ifdef WITH_VOICE void Abaddon::OnVoiceConnected() { - m_audio.StartCaptureDevice(); ShowVoiceWindow(); } void Abaddon::OnVoiceDisconnected() { - m_audio.StopCaptureDevice(); - m_audio.RemoveAllSSRCs(); if (m_voice_window != nullptr) { m_voice_window->close(); } } void Abaddon::ShowVoiceWindow() { + using SystemSound = AbaddonClient::Audio::SystemAudio::SystemSound; + if (m_voice_window != nullptr) return; auto *wnd = new VoiceWindow(m_discord.GetVoiceChannelID()); @@ -507,23 +505,53 @@ void Abaddon::ShowVoiceWindow() { wnd->signal_mute().connect([this](bool is_mute) { m_discord.SetVoiceMuted(is_mute); - m_audio.SetCapture(!is_mute); + m_audio.GetVoice().GetCapture().SetActive(!is_mute); + + auto sound = is_mute ? SystemSound::VoiceMuted : SystemSound::VoiceUnmuted; + m_audio.GetSystem().PlaySound(sound); }); wnd->signal_deafen().connect([this](bool is_deaf) { m_discord.SetVoiceDeafened(is_deaf); - m_audio.SetPlayback(!is_deaf); + m_audio.GetVoice().GetPlayback().SetActive(!is_deaf); + + auto sound = is_deaf ? SystemSound::VoiceDeafened : SystemSound::VoiceUndeafened; + m_audio.GetSystem().PlaySound(sound); }); wnd->signal_mute_user_cs().connect([this](Snowflake id, bool is_mute) { if (const auto ssrc = m_discord.GetSSRCOfUser(id); ssrc.has_value()) { - m_audio.SetMuteSSRC(*ssrc, is_mute); + m_audio.GetVoice().GetPlayback().GetClientStore().SetClientMute(*ssrc, is_mute); } }); wnd->signal_user_volume_changed().connect([this](Snowflake id, double volume) { auto &vc = m_discord.GetVoiceClient(); vc.SetUserVolume(id, volume); + + if (const auto ssrc = m_discord.GetSSRCOfUser(id); ssrc.has_value()) { + m_audio.GetVoice().GetPlayback().GetClientStore().SetClientVolume(*ssrc, volume); + } + }); + + wnd->signal_playback_device_changed().connect([this](const Gtk::TreeModel::iterator &iter) { + auto device_id = m_audio.GetDevices().GetPlaybackDeviceIDFromModel(iter); + if (!device_id) { + spdlog::get("audio")->error("Requested ID from iterator is invalid"); + return; + } + + m_audio.GetVoice().GetPlayback().SetPlaybackDevice(*device_id); + }); + + wnd->signal_capture_device_changed().connect([this](const Gtk::TreeModel::iterator &iter) { + auto device_id = m_audio.GetDevices().GetCaptureDeviceIDFromModel(iter); + if (!device_id) { + spdlog::get("audio")->error("Requested ID from iterator is invalid"); + return; + } + + m_audio.GetVoice().GetCapture().SetCaptureDevice(*device_id); }); wnd->set_position(Gtk::WIN_POS_CENTER); @@ -1129,7 +1157,7 @@ EmojiResource &Abaddon::GetEmojis() { return m_emojis; } -#ifdef WITH_VOICE +#ifdef WITH_MINIAUDIO AudioManager &Abaddon::GetAudio() { return m_audio; } diff --git a/src/abaddon.hpp b/src/abaddon.hpp index 6093523f..1402b81c 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -12,12 +12,13 @@ #include "imgmanager.hpp" #include "emojis.hpp" #include "notifications/notifications.hpp" + +#ifdef WITH_MINIAUDIO #include "audio/manager.hpp" +#endif #define APP_TITLE "Abaddon" -class AudioManager; - class Abaddon { private: Abaddon(); @@ -72,7 +73,7 @@ class Abaddon { ImageManager &GetImageManager(); EmojiResource &GetEmojis(); -#ifdef WITH_VOICE +#ifdef WITH_MINIAUDIO AudioManager &GetAudio(); #endif @@ -174,8 +175,11 @@ class Abaddon { ImageManager m_img_mgr; EmojiResource m_emojis; -#ifdef WITH_VOICE +#ifdef WITH_MINIAUDIO AudioManager m_audio; +#endif + +#ifdef WITH_VOICE Gtk::Window *m_voice_window = nullptr; #endif diff --git a/src/audio/audio_device.cpp b/src/audio/audio_device.cpp new file mode 100644 index 00000000..67d838fb --- /dev/null +++ b/src/audio/audio_device.cpp @@ -0,0 +1,90 @@ +#include "audio_device.hpp" + +namespace AbaddonClient::Audio { + +AudioDevice::AudioDevice(Context &context, ma_device_config &&config, std::optional &&device_id) noexcept : + m_context(context), + m_config(std::move(config)), + m_device_id(std::move(device_id)) +{ + SyncDeviceID(); +} + +bool AudioDevice::Start() noexcept { + if (m_started) { + return true; + } + + m_device = Miniaudio::MaDevice::Create(m_context.GetRaw(), m_config); + if (!m_device) { + return false; + } + + m_started = m_device->Start(); + if (!m_started) { + m_device.reset(); + } + + return m_started; +} + +bool AudioDevice::Stop() noexcept { + if (!m_started) { + return true; + } + + m_started = !m_device->Stop(); + + // If we're still running something went wrong + if (m_started) { + return false; + } + + m_device.reset(); + return true; +} + +bool AudioDevice::ChangeDevice(const ma_device_id &device_id) noexcept { + m_device_id = device_id; + + return RefreshDevice(); +} + +void AudioDevice::SyncDeviceID() noexcept { + if (!m_device_id) { + return; + } + + auto& device_id = *m_device_id; + + switch (m_config.deviceType) { + case ma_device_type_playback: { + m_config.playback.pDeviceID = &device_id; + } break; + + case ma_device_type_capture: { + m_config.capture.pDeviceID = &device_id; + } break; + + case ma_device_type_duplex: { + m_config.playback.pDeviceID = &device_id; + m_config.capture.pDeviceID = &device_id; + } + + case ma_device_type_loopback: { + m_config.capture.pDeviceID = &device_id; + } + } +} + +bool AudioDevice::RefreshDevice() noexcept { + m_device.reset(); + if (m_started) { + m_started = false; + return Start(); + } + + return true; +} + +} diff --git a/src/audio/audio_device.hpp b/src/audio/audio_device.hpp new file mode 100644 index 00000000..42877a24 --- /dev/null +++ b/src/audio/audio_device.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "context.hpp" + +#include "miniaudio/ma_device.hpp" + +namespace AbaddonClient::Audio { + +class AudioDevice { +public: + AudioDevice(Context& context, ma_device_config &&config, std::optional &&device_id) noexcept; + + bool Start() noexcept; + bool Stop() noexcept; + + bool ChangeDevice(const ma_device_id &device_id) noexcept; +private: + void SyncDeviceID() noexcept; + bool RefreshDevice() noexcept; + + bool m_started = false; + + Context &m_context; + std::optional m_device; + + ma_device_config m_config; + std::optional m_device_id; +}; + +} diff --git a/src/audio/audio_engine.cpp b/src/audio/audio_engine.cpp new file mode 100644 index 00000000..2689abe5 --- /dev/null +++ b/src/audio/audio_engine.cpp @@ -0,0 +1,53 @@ +#include "audio_engine.hpp" + +#include +#include + +namespace AbaddonClient::Audio { + +AudioEngine::AudioEngine(Context &context) noexcept : + m_context(context) {} + +AudioEngine::~AudioEngine() noexcept { + StopTimer(); +} + +bool AudioEngine::PlaySound(std::string_view file_path) noexcept { + StopTimer(); + + const auto result = m_engine.LockScope([this, &file_path](std::optional &engine) { + if (!engine) { + engine = Miniaudio::MaEngine::Create(m_context.GetEngineConfig()); + if (!engine) { + return false; + } + } + + return engine->PlaySound(file_path); + }); + + StartTimer(); + return result; +} + +void AudioEngine::StartTimer() noexcept { + // NOTE: I am not using g_timeout_add_seconds_once here since we want to own the source and destroy it manually + // g_source_remove throws an error on destroyed source + m_timer_source = g_timeout_source_new_seconds(5); + + g_source_set_callback(m_timer_source, GSourceFunc(AudioEngine::TimeoutEngine), this, nullptr); + g_source_attach(m_timer_source, Glib::MainContext::get_default()->gobj()); +} + +void AudioEngine::StopTimer() noexcept { + if (m_timer_source != nullptr) { + g_source_destroy(m_timer_source); + g_source_unref(m_timer_source); + } +} + +void AudioEngine::TimeoutEngine(AudioEngine &engine) noexcept { + engine.m_engine.Lock()->reset(); +} + +} diff --git a/src/audio/audio_engine.hpp b/src/audio/audio_engine.hpp new file mode 100644 index 00000000..8f1ccc46 --- /dev/null +++ b/src/audio/audio_engine.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include +#include + +#include "misc/mutex.hpp" + +#include "audio/context.hpp" +#include "miniaudio/ma_engine.hpp" + +namespace AbaddonClient::Audio { + +class AudioEngine { +public: + AudioEngine(Context& context) noexcept; + ~AudioEngine() noexcept; + + bool PlaySound(std::string_view file_path) noexcept; +private: + void StartTimer() noexcept; + void StopTimer() noexcept; + + static void TimeoutEngine(AudioEngine &engine) noexcept; + + Context &m_context; + Mutex> m_engine; + + GSource* m_timer_source; +}; + +} diff --git a/src/audio/context.cpp b/src/audio/context.cpp new file mode 100644 index 00000000..c0e4e787 --- /dev/null +++ b/src/audio/context.cpp @@ -0,0 +1,88 @@ +#include "context.hpp" + +#include + +namespace AbaddonClient::Audio { + +Context::Context(Miniaudio::MaContext &&context) noexcept : + m_context(std::move(context)) +{ + PopulateDevices(); + FindDefaultDevices(); +} + +std::optional Context::Create(ma_context_config &&config, ConstSlice backends) noexcept { + auto context = Miniaudio::MaContext::Create(std::move(config), backends); + if (!context) { + return std::nullopt; + } + + return std::move(*context); +} + +ma_engine_config Context::GetEngineConfig() noexcept { + auto config = ma_engine_config_init(); + config.pContext = &m_context->GetInternal(); + + return config; +} + +ConstSlice Context::GetPlaybackDevices() noexcept { + return m_playback_devices; +} + +ConstSlice Context::GetCaptureDevices() noexcept { + return m_capture_devices; +} + +std::optional Context::GetActivePlaybackID() noexcept { + return m_active_playback_id; +} + +std::optional Context::GetActiveCaptureID() noexcept { + return m_active_capture_id; +} + +Miniaudio::MaContext& Context::GetRaw() noexcept { + return *m_context; +} + +void Context::PopulateDevices() noexcept { + auto result = m_context->GetDevices(); + if (!result) { + return; + } + + auto& playback_devices = result->first; + auto& capture_devices = result->second; + + m_playback_devices.reserve(playback_devices.size()); + m_capture_devices.reserve(capture_devices.size()); + + m_playback_devices.assign(playback_devices.begin(), playback_devices.end()); + m_capture_devices.assign(capture_devices.begin(), capture_devices.end()); +} + +void Context::FindDefaultDevices() noexcept { + for (auto& playback : m_playback_devices) { + if (playback.isDefault) { + m_active_playback_id = playback.id; + } + } + + for (auto& capture : m_capture_devices) { + if (capture.isDefault) { + m_active_capture_id = capture.id; + } + } + + if (!m_active_playback_id) { + spdlog::get("audio")->warn("No default playback device found"); + } + + if (!m_active_capture_id) { + spdlog::get("audio")->warn("No default capture device found"); + } +} + +} diff --git a/src/audio/context.hpp b/src/audio/context.hpp new file mode 100644 index 00000000..dd4d3a1f --- /dev/null +++ b/src/audio/context.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "miniaudio/ma_context.hpp" + +namespace AbaddonClient::Audio { + +class Context { +public: + Context(Miniaudio::MaContext &&context) noexcept; + static std::optional Create(ma_context_config &&config, ConstSlice backends) noexcept; + + ma_engine_config GetEngineConfig() noexcept; + + ConstSlice GetPlaybackDevices() noexcept; + ConstSlice GetCaptureDevices() noexcept; + + std::optional GetActivePlaybackID() noexcept; + std::optional GetActiveCaptureID() noexcept; + + Miniaudio::MaContext& GetRaw() noexcept; + +private: + + void PopulateDevices() noexcept; + void FindDefaultDevices() noexcept; + + std::optional m_active_playback_id; + std::optional m_active_capture_id; + + std::vector m_playback_devices; + std::vector m_capture_devices; + + std::optional m_context; +}; + + +} diff --git a/src/audio/devices.cpp b/src/audio/devices.cpp index dfb71640..515974d3 100644 --- a/src/audio/devices.cpp +++ b/src/audio/devices.cpp @@ -1,5 +1,3 @@ -#ifdef WITH_VOICE - // clang-format off #include "devices.hpp" @@ -13,15 +11,15 @@ AudioDevices::AudioDevices() , m_capture(Gtk::ListStore::create(m_capture_columns)) { } -Glib::RefPtr AudioDevices::GetPlaybackDeviceModel() { +Glib::RefPtr AudioDevices::GetPlaybackDeviceModel() const { return m_playback; } -Glib::RefPtr AudioDevices::GetCaptureDeviceModel() { +Glib::RefPtr AudioDevices::GetCaptureDeviceModel() const { return m_capture; } -void AudioDevices::SetDevices(ma_device_info *pPlayback, ma_uint32 playback_count, ma_device_info *pCapture, ma_uint32 capture_count) { +void AudioDevices::SetDevices(const ma_device_info *pPlayback, ma_uint32 playback_count, const ma_device_info *pCapture, ma_uint32 capture_count) { m_playback->clear(); for (ma_uint32 i = 0; i < playback_count; i++) { @@ -33,7 +31,7 @@ void AudioDevices::SetDevices(ma_device_info *pPlayback, ma_uint32 playback_coun if (d.isDefault) { m_default_playback_iter = row; - SetActivePlaybackDevice(row); + SetActivePlaybackDeviceIter(row); } } @@ -48,7 +46,7 @@ void AudioDevices::SetDevices(ma_device_info *pPlayback, ma_uint32 playback_coun if (d.isDefault) { m_default_capture_iter = row; - SetActiveCaptureDevice(row); + SetActiveCaptureDeviceIter(row); } } @@ -78,34 +76,34 @@ std::optional AudioDevices::GetCaptureDeviceIDFromModel(const Gtk: } std::optional AudioDevices::GetDefaultPlayback() const { - if (m_default_playback_iter) { - return static_cast((*m_default_playback_iter)[m_playback_columns.DeviceID]); - } - - return std::nullopt; + return GetPlaybackDeviceIDFromModel(m_default_playback_iter); } std::optional AudioDevices::GetDefaultCapture() const { - if (m_default_capture_iter) { - return static_cast((*m_default_capture_iter)[m_capture_columns.DeviceID]); - } + return GetCaptureDeviceIDFromModel(m_default_capture_iter); +} - return std::nullopt; +std::optional AudioDevices::GetActivePlayback() const { + return GetPlaybackDeviceIDFromModel(m_active_playback_iter); +} + +std::optional AudioDevices::GetActiveCapture() const { + return GetCaptureDeviceIDFromModel(m_active_capture_iter); } -void AudioDevices::SetActivePlaybackDevice(const Gtk::TreeModel::iterator &iter) { +void AudioDevices::SetActivePlaybackDeviceIter(const Gtk::TreeModel::iterator &iter) { m_active_playback_iter = iter; } -void AudioDevices::SetActiveCaptureDevice(const Gtk::TreeModel::iterator &iter) { +void AudioDevices::SetActiveCaptureDeviceIter(const Gtk::TreeModel::iterator &iter) { m_active_capture_iter = iter; } -Gtk::TreeModel::iterator AudioDevices::GetActivePlaybackDevice() { +Gtk::TreeModel::iterator AudioDevices::GetActivePlaybackDeviceIter() const { return m_active_playback_iter; } -Gtk::TreeModel::iterator AudioDevices::GetActiveCaptureDevice() { +Gtk::TreeModel::iterator AudioDevices::GetActiveCaptureDeviceIter() const { return m_active_capture_iter; } @@ -118,4 +116,3 @@ AudioDevices::CaptureColumns::CaptureColumns() { add(Name); add(DeviceID); } -#endif diff --git a/src/audio/devices.hpp b/src/audio/devices.hpp index c83bdb41..d42c5625 100644 --- a/src/audio/devices.hpp +++ b/src/audio/devices.hpp @@ -1,5 +1,4 @@ #pragma once -#ifdef WITH_VOICE // clang-format off @@ -13,10 +12,7 @@ class AudioDevices { public: AudioDevices(); - Glib::RefPtr GetPlaybackDeviceModel(); - Glib::RefPtr GetCaptureDeviceModel(); - - void SetDevices(ma_device_info *pPlayback, ma_uint32 playback_count, ma_device_info *pCapture, ma_uint32 capture_count); + void SetDevices(const ma_device_info *pPlayback, ma_uint32 playback_count, const ma_device_info *pCapture, ma_uint32 capture_count); [[nodiscard]] std::optional GetPlaybackDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const; [[nodiscard]] std::optional GetCaptureDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const; @@ -24,11 +20,17 @@ class AudioDevices { [[nodiscard]] std::optional GetDefaultPlayback() const; [[nodiscard]] std::optional GetDefaultCapture() const; - void SetActivePlaybackDevice(const Gtk::TreeModel::iterator &iter); - void SetActiveCaptureDevice(const Gtk::TreeModel::iterator &iter); + [[nodiscard]] std::optional GetActivePlayback() const; + [[nodiscard]] std::optional GetActiveCapture() const; + + void SetActivePlaybackDeviceIter(const Gtk::TreeModel::iterator &iter); + void SetActiveCaptureDeviceIter(const Gtk::TreeModel::iterator &iter); + + [[nodiscard]] Gtk::TreeModel::iterator GetActivePlaybackDeviceIter() const; + [[nodiscard]] Gtk::TreeModel::iterator GetActiveCaptureDeviceIter() const; - Gtk::TreeModel::iterator GetActivePlaybackDevice(); - Gtk::TreeModel::iterator GetActiveCaptureDevice(); + [[nodiscard]] Glib::RefPtr GetPlaybackDeviceModel() const; + [[nodiscard]] Glib::RefPtr GetCaptureDeviceModel() const; private: class PlaybackColumns : public Gtk::TreeModel::ColumnRecord { @@ -55,4 +57,3 @@ class AudioDevices { Gtk::TreeModel::iterator m_active_capture_iter; Gtk::TreeModel::iterator m_default_capture_iter; }; -#endif diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index d32c5e20..0cd63d00 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -1,55 +1,4 @@ -#ifdef WITH_VOICE -// clang-format off - -#ifdef _WIN32 - #include -#endif - #include "manager.hpp" -#include "abaddon.hpp" -#include -#include -#include -#include -#include -#include -// clang-format on - -void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount) { - AudioManager *mgr = reinterpret_cast(pDevice->pUserData); - if (mgr == nullptr) return; - std::lock_guard _(mgr->m_mutex); - - auto *pOutputF32 = static_cast(pOutput); - for (auto &[ssrc, pair] : mgr->m_sources) { - double volume = 1.0; - if (const auto vol_it = mgr->m_volume_ssrc.find(ssrc); vol_it != mgr->m_volume_ssrc.end()) { - volume = vol_it->second; - } - auto &buf = pair.first; - const size_t n = std::min(static_cast(buf.size()), static_cast(frameCount * 2ULL)); - for (size_t i = 0; i < n; i++) { - pOutputF32[i] += volume * buf[i] / 32768.F; - } - buf.erase(buf.begin(), buf.begin() + n); - } -} - -void capture_data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount) { - auto *mgr = reinterpret_cast(pDevice->pUserData); - if (mgr == nullptr) return; - - mgr->OnCapturedPCM(static_cast(pInput), frameCount); - - /* - * You can simply increment it by 480 in UDPSocket::SendEncrypted but this is wrong - * The timestamp is supposed to be strictly linear eg. if there's discontinuous - * transmission for 1 second then the timestamp should be 48000 greater than the - * last packet. So it's incremented here because this is fired 100x per second - * and is always called in sync with UDPSocket::SendEncrypted - */ - mgr->m_rtp_timestamp += 480; -} void mgr_log_callback(void *pUserData, ma_uint32 level, const char *pMessage) { auto *log = static_cast(pUserData); @@ -75,351 +24,39 @@ void mgr_log_callback(void *pUserData, ma_uint32 level, const char *pMessage) { g_free(msg); } -AudioManager::AudioManager(const Glib::ustring &backends_string) - : m_log(spdlog::stdout_color_mt("miniaudio")) { - m_ok = true; - - ma_log_init(nullptr, &m_ma_log); - ma_log_register_callback(&m_ma_log, ma_log_callback_init(mgr_log_callback, m_log.get())); - -#ifdef WITH_RNNOISE - RNNoiseInitialize(); -#endif - - int err; - m_encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_VOIP, &err); - if (err != OPUS_OK) { - spdlog::get("audio")->error("failed to initialize opus encoder: {}", err); - m_ok = false; - return; - } - opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(64000)); - +AudioManager::AudioManager(const Glib::ustring &backends_string, DiscordClient &discord) + : m_log(spdlog::stdout_color_mt("miniaudio")), + m_ma_log(AbaddonClient::Audio::Miniaudio::MaLog::Create()) +{ auto ctx_cfg = ma_context_config_init(); - ctx_cfg.pLog = &m_ma_log; - ma_backend *pBackends = nullptr; - ma_uint32 backendCount = 0; + if (m_ma_log) { + m_ma_log->RegisterCallback(ma_log_callback_init(mgr_log_callback, m_log.get())); + ctx_cfg.pLog = &m_ma_log->GetInternal(); + } - std::vector backends_vec; + std::vector backends; if (!backends_string.empty()) { spdlog::get("audio")->debug("Using backends list: {}", std::string(backends_string)); - backends_vec = ParseBackendsList(backends_string); - pBackends = backends_vec.data(); - backendCount = static_cast(backends_vec.size()); + backends = ParseBackendsList(backends_string); } - if (ma_context_init(pBackends, backendCount, &ctx_cfg, &m_context) != MA_SUCCESS) { - spdlog::get("audio")->error("failed to initialize context"); - m_ok = false; + m_context = AbaddonClient::Audio::Context::Create(std::move(ctx_cfg), backends); + + if (!m_context) { return; } - const auto backend_name = ma_get_backend_name(m_context.backend); - spdlog::get("audio")->info("Audio backend: {}", backend_name); - Enumerate(); - m_playback_config = ma_device_config_init(ma_device_type_playback); - m_playback_config.playback.format = ma_format_f32; - m_playback_config.playback.channels = 2; - m_playback_config.sampleRate = 48000; - m_playback_config.dataCallback = data_callback; - m_playback_config.pUserData = this; - - if (const auto playback_id = m_devices.GetDefaultPlayback(); playback_id.has_value()) { - m_playback_id = *playback_id; - m_playback_config.playback.pDeviceID = &m_playback_id; - - if (auto code = ma_device_init(&m_context, &m_playback_config, &m_playback_device); code != MA_SUCCESS) { - spdlog::get("audio")->error("failed to initialize playback device (code: {})", static_cast(code)); - m_ok = false; - return; - } + m_system.emplace(*m_context); - if (auto code = ma_device_start(&m_playback_device); code != MA_SUCCESS) { - spdlog::get("audio")->error("failed to start playback (code: {})", static_cast(code)); - ma_device_uninit(&m_playback_device); - m_ok = false; - return; - } - - char playback_device_name[MA_MAX_DEVICE_NAME_LENGTH + 1]; - ma_device_get_name(&m_playback_device, ma_device_type_playback, playback_device_name, sizeof(playback_device_name), nullptr); - spdlog::get("audio")->info("using {} as playback device", playback_device_name); - } - - m_capture_config = ma_device_config_init(ma_device_type_capture); - m_capture_config.capture.format = ma_format_s16; - m_capture_config.capture.channels = 2; - m_capture_config.sampleRate = 48000; - m_capture_config.periodSizeInFrames = 480; - m_capture_config.dataCallback = capture_data_callback; - m_capture_config.pUserData = this; - - if (const auto capture_id = m_devices.GetDefaultCapture(); capture_id.has_value()) { - m_capture_id = *capture_id; - m_capture_config.capture.pDeviceID = &m_capture_id; - - if (auto code = ma_device_init(&m_context, &m_capture_config, &m_capture_device); code != MA_SUCCESS) { - spdlog::get("audio")->error("failed to initialize capture device (code: {})", static_cast(code)); - m_ok = false; - return; - } - - char capture_device_name[MA_MAX_DEVICE_NAME_LENGTH + 1]; - ma_device_get_name(&m_capture_device, ma_device_type_capture, capture_device_name, sizeof(capture_device_name), nullptr); - spdlog::get("audio")->info("using {} as capture device", capture_device_name); - } - - Glib::signal_timeout().connect(sigc::mem_fun(*this, &AudioManager::DecayVolumeMeters), 40); -} - -AudioManager::~AudioManager() { - ma_device_uninit(&m_playback_device); - ma_device_uninit(&m_capture_device); - ma_context_uninit(&m_context); - RemoveAllSSRCs(); - -#ifdef WITH_RNNOISE - RNNoiseUninitialize(); +#if WITH_VOICE + m_voice.emplace(*m_context, discord); + m_system->BindToVoice(discord); #endif -} - -void AudioManager::AddSSRC(uint32_t ssrc) { - std::lock_guard _(m_mutex); - int error; - if (m_sources.find(ssrc) == m_sources.end()) { - auto *decoder = opus_decoder_create(48000, 2, &error); - m_sources.insert(std::make_pair(ssrc, std::make_pair(std::deque {}, decoder))); - } -} -void AudioManager::RemoveSSRC(uint32_t ssrc) { - std::lock_guard _(m_mutex); - if (auto it = m_sources.find(ssrc); it != m_sources.end()) { - opus_decoder_destroy(it->second.second); - m_sources.erase(it); - } -} - -void AudioManager::RemoveAllSSRCs() { - spdlog::get("audio")->info("removing all ssrc"); - std::lock_guard _(m_mutex); - for (auto &[ssrc, pair] : m_sources) { - opus_decoder_destroy(pair.second); - } - m_sources.clear(); -} - -void AudioManager::SetOpusBuffer(uint8_t *ptr) { - m_opus_buffer = ptr; -} - -void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { - if (!m_should_playback || ma_device_get_state(&m_playback_device) != ma_device_state_started) return; - - std::lock_guard _(m_mutex); - if (m_muted_ssrcs.find(ssrc) != m_muted_ssrcs.end()) return; - - static std::array pcm; - if (auto it = m_sources.find(ssrc); it != m_sources.end()) { - int decoded = opus_decode(it->second.second, data.data(), static_cast(data.size()), pcm.data(), 120 * 48, 0); - if (decoded <= 0) { - } else { - UpdateReceiveVolume(ssrc, pcm.data(), decoded); - auto &buf = it->second.first; - buf.insert(buf.end(), pcm.begin(), pcm.begin() + decoded * 2); - } - } -} - -void AudioManager::StartCaptureDevice() { - if (ma_device_start(&m_capture_device) != MA_SUCCESS) { - spdlog::get("audio")->error("Failed to start capture device"); - } -} - -void AudioManager::StopCaptureDevice() { - if (ma_device_stop(&m_capture_device) != MA_SUCCESS) { - spdlog::get("audio")->error("Failed to stop capture device"); - } -} - -void AudioManager::SetPlaybackDevice(const Gtk::TreeModel::iterator &iter) { - spdlog::get("audio")->debug("Setting new playback device"); - - const auto device_id = m_devices.GetPlaybackDeviceIDFromModel(iter); - if (!device_id) { - spdlog::get("audio")->error("Requested ID from iterator is invalid"); - return; - } - - m_devices.SetActivePlaybackDevice(iter); - - m_playback_id = *device_id; - - ma_device_uninit(&m_playback_device); - - m_playback_config = ma_device_config_init(ma_device_type_playback); - m_playback_config.playback.format = ma_format_f32; - m_playback_config.playback.channels = 2; - m_playback_config.playback.pDeviceID = &m_playback_id; - m_playback_config.sampleRate = 48000; - m_playback_config.dataCallback = data_callback; - m_playback_config.pUserData = this; - - if (ma_device_init(&m_context, &m_playback_config, &m_playback_device) != MA_SUCCESS) { - spdlog::get("audio")->error("Failed to initialize new device"); - return; - } - - if (ma_device_start(&m_playback_device) != MA_SUCCESS) { - spdlog::get("audio")->error("Failed to start new device"); - return; - } -} - -void AudioManager::SetCaptureDevice(const Gtk::TreeModel::iterator &iter) { - spdlog::get("audio")->debug("Setting new capture device"); - - const auto device_id = m_devices.GetCaptureDeviceIDFromModel(iter); - if (!device_id) { - spdlog::get("audio")->error("Requested ID from iterator is invalid"); - return; - } - - m_devices.SetActiveCaptureDevice(iter); - - m_capture_id = *device_id; - - ma_device_uninit(&m_capture_device); - - m_capture_config = ma_device_config_init(ma_device_type_capture); - m_capture_config.capture.format = ma_format_s16; - m_capture_config.capture.channels = 2; - m_capture_config.capture.pDeviceID = &m_capture_id; - m_capture_config.sampleRate = 48000; - m_capture_config.periodSizeInFrames = 480; - m_capture_config.dataCallback = capture_data_callback; - m_capture_config.pUserData = this; - - if (ma_device_init(&m_context, &m_capture_config, &m_capture_device) != MA_SUCCESS) { - spdlog::get("audio")->error("Failed to initialize new device"); - return; - } - - // technically this should probably try and check old state but if you are in the window to change it then you are connected - if (ma_device_start(&m_capture_device) != MA_SUCCESS) { - spdlog::get("audio")->error("Failed to start new device"); - return; - } -} - -void AudioManager::SetCapture(bool capture) { - m_should_capture = capture; -} - -void AudioManager::SetPlayback(bool playback) { - m_should_playback = playback; -} - -void AudioManager::SetCaptureGate(double gate) { - m_capture_gate = gate; -} - -void AudioManager::SetCaptureGain(double gain) { - m_capture_gain = gain; -} - -double AudioManager::GetCaptureGate() const noexcept { - return m_capture_gate; -} - -double AudioManager::GetCaptureGain() const noexcept { - return m_capture_gain; -} - -void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { - std::lock_guard _(m_mutex); - if (mute) { - m_muted_ssrcs.insert(ssrc); - } else { - m_muted_ssrcs.erase(ssrc); - } -} - -void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { - std::lock_guard _(m_mutex); - m_volume_ssrc[ssrc] = volume; -} - -double AudioManager::GetVolumeSSRC(uint32_t ssrc) const { - std::lock_guard _(m_mutex); - if (const auto iter = m_volume_ssrc.find(ssrc); iter != m_volume_ssrc.end()) { - return iter->second; - } - return 1.0; -} - -void AudioManager::SetEncodingApplication(int application) { - std::lock_guard _(m_enc_mutex); - int prev_bitrate = 64000; - if (int err = opus_encoder_ctl(m_encoder, OPUS_GET_BITRATE(&prev_bitrate)); err != OPUS_OK) { - spdlog::get("audio")->error("Failed to get old bitrate when reinitializing: {}", err); - } - opus_encoder_destroy(m_encoder); - int err = 0; - m_encoder = opus_encoder_create(48000, 2, application, &err); - if (err != OPUS_OK) { - spdlog::get("audio")->critical("opus_encoder_create failed: {}", err); - return; - } - - if (int err = opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(prev_bitrate)); err != OPUS_OK) { - spdlog::get("audio")->error("Failed to set bitrate when reinitializing: {}", err); - } -} - -int AudioManager::GetEncodingApplication() { - std::lock_guard _(m_enc_mutex); - int temp = OPUS_APPLICATION_VOIP; - if (int err = opus_encoder_ctl(m_encoder, OPUS_GET_APPLICATION(&temp)); err != OPUS_OK) { - spdlog::get("audio")->error("opus_encoder_ctl(OPUS_GET_APPLICATION) failed: {}", err); - } - return temp; -} - -void AudioManager::SetSignalHint(int signal) { - std::lock_guard _(m_enc_mutex); - if (int err = opus_encoder_ctl(m_encoder, OPUS_SET_SIGNAL(signal)); err != OPUS_OK) { - spdlog::get("audio")->error("opus_encoder_ctl(OPUS_SET_SIGNAL) failed: {}", err); - } -} - -int AudioManager::GetSignalHint() { - std::lock_guard _(m_enc_mutex); - int temp = OPUS_AUTO; - if (int err = opus_encoder_ctl(m_encoder, OPUS_GET_SIGNAL(&temp)); err != OPUS_OK) { - spdlog::get("audio")->error("opus_encoder_ctl(OPUS_GET_SIGNAL) failed: {}", err); - } - return temp; -} - -void AudioManager::SetBitrate(int bitrate) { - std::lock_guard _(m_enc_mutex); - if (int err = opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(bitrate)); err != OPUS_OK) { - spdlog::get("audio")->error("opus_encoder_ctl(OPUS_SET_BITRATE) failed: {}", err); - } -} - -int AudioManager::GetBitrate() { - std::lock_guard _(m_enc_mutex); - int temp = 64000; - if (int err = opus_encoder_ctl(m_encoder, OPUS_GET_BITRATE(&temp)); err != OPUS_OK) { - spdlog::get("audio")->error("opus_encoder_ctl(OPUS_GET_BITRATE) failed: {}", err); - } - return temp; + m_ok = true; } void AudioManager::Enumerate() { @@ -430,223 +67,27 @@ void AudioManager::Enumerate() { spdlog::get("audio")->debug("Enumerating devices"); - if (ma_context_get_devices( - &m_context, - &pPlaybackDeviceInfo, - &playbackDeviceCount, - &pCaptureDeviceInfo, - &captureDeviceCount) != MA_SUCCESS) { - spdlog::get("audio")->error("Failed to enumerate devices"); - return; - } - - spdlog::get("audio")->debug("Found {} playback devices and {} capture devices", playbackDeviceCount, captureDeviceCount); - - m_devices.SetDevices(pPlaybackDeviceInfo, playbackDeviceCount, pCaptureDeviceInfo, captureDeviceCount); -} - -void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { - if (m_opus_buffer == nullptr || !m_should_capture) return; - - const double gain = m_capture_gain; - - std::vector new_pcm(pcm, pcm + frames * 2); - for (auto &val : new_pcm) { - const int32_t unclamped = static_cast(val * gain); - val = std::clamp(unclamped, INT16_MIN, INT16_MAX); - } - - if (m_mix_mono) { - for (size_t i = 0; i < frames * 2; i += 2) { - const int sample_L = new_pcm[i]; - const int sample_R = new_pcm[i + 1]; - const int16_t mixed = static_cast((sample_L + sample_R) / 2); - new_pcm[i] = mixed; - new_pcm[i + 1] = mixed; - } - } - - UpdateCaptureVolume(new_pcm.data(), frames); - - static std::array denoised_L; - static std::array denoised_R; - - bool m_rnnoise_passed = false; -#ifdef WITH_RNNOISE - if (m_vad_method == VADMethod::RNNoise || m_enable_noise_suppression) { - m_rnnoise_passed = CheckVADRNNoise(new_pcm.data(), denoised_L.data(), denoised_R.data()); - } -#endif - - switch (m_vad_method) { - case VADMethod::Gate: - if (!CheckVADVoiceGate()) return; - break; -#ifdef WITH_RNNOISE - case VADMethod::RNNoise: - if (!m_rnnoise_passed) return; - break; -#endif - } - - m_enc_mutex.lock(); - int payload_len = -1; - - if (m_enable_noise_suppression) { - static std::array denoised_interleaved; - for (size_t i = 0; i < 480; i++) { - denoised_interleaved[i * 2] = static_cast(denoised_L[i]); - } - for (size_t i = 0; i < 480; i++) { - denoised_interleaved[i * 2 + 1] = static_cast(denoised_R[i]); - } - payload_len = opus_encode(m_encoder, denoised_interleaved.data(), 480, static_cast(m_opus_buffer), 1275); - } else { - payload_len = opus_encode(m_encoder, new_pcm.data(), 480, static_cast(m_opus_buffer), 1275); - } - - m_enc_mutex.unlock(); - if (payload_len < 0) { - spdlog::get("audio")->error("encoding error: {}", payload_len); - } else { - m_signal_opus_packet.emit(payload_len); - } -} - -void AudioManager::UpdateReceiveVolume(uint32_t ssrc, const int16_t *pcm, int frames) { - std::lock_guard _(m_vol_mtx); - - auto &meter = m_volumes[ssrc]; - for (int i = 0; i < frames * 2; i += 2) { - const int amp = std::abs(pcm[i]); - meter = std::max(meter, std::abs(amp) / 32768.0); - } -} - -void AudioManager::UpdateCaptureVolume(const int16_t *pcm, ma_uint32 frames) { - for (ma_uint32 i = 0; i < frames * 2; i += 2) { - const int amp = std::abs(pcm[i]); - m_capture_peak_meter = std::max(m_capture_peak_meter.load(std::memory_order_relaxed), amp); - } -} - -bool AudioManager::DecayVolumeMeters() { - m_capture_peak_meter -= 600; - if (m_capture_peak_meter < 0) m_capture_peak_meter = 0; - - const auto x = m_vad_prob.load() - 0.05f; - m_vad_prob.store(x < 0.0f ? 0.0f : x); + const auto playback_devices = m_context->GetPlaybackDevices(); + const auto capture_devices = m_context->GetCaptureDevices(); - std::lock_guard _(m_vol_mtx); + spdlog::get("audio")->info("Found {} playback devices and {} capture devices", playback_devices.size(), capture_devices.size()); - for (auto &[ssrc, meter] : m_volumes) { - meter -= 0.01; - if (meter < 0.0) meter = 0.0; - } - - return true; -} - -bool AudioManager::CheckVADVoiceGate() { - return m_capture_peak_meter / 32768.0 > m_capture_gate; + m_devices.SetDevices( + playback_devices.data(), + playback_devices.size(), + capture_devices.data(), + capture_devices.size() + ); } -#ifdef WITH_RNNOISE -bool AudioManager::CheckVADRNNoise(const int16_t *pcm, float *denoised_left, float *denoised_right) { - // use left channel for vad, only denoise right if noise suppression enabled - std::unique_lock _(m_rnn_mutex); - - static float rnnoise_input[480]; - for (size_t i = 0; i < 480; i++) { - rnnoise_input[i] = static_cast(pcm[i * 2]); - } - m_vad_prob = std::max(m_vad_prob.load(), rnnoise_process_frame(m_rnnoise[0], denoised_left, rnnoise_input)); - - if (m_enable_noise_suppression) { - for (size_t i = 0; i < 480; i++) { - rnnoise_input[i] = static_cast(pcm[i * 2 + 1]); - } - rnnoise_process_frame(m_rnnoise[1], denoised_right, rnnoise_input); - } - - return m_vad_prob > m_prob_threshold; -} - -void AudioManager::RNNoiseInitialize() { - spdlog::get("audio")->debug("Initializing RNNoise"); - RNNoiseUninitialize(); - std::unique_lock _(m_rnn_mutex); - m_rnnoise[0] = rnnoise_create(nullptr); - m_rnnoise[1] = rnnoise_create(nullptr); - const auto expected = rnnoise_get_frame_size(); - if (expected != 480) { - spdlog::get("audio")->warn("RNNoise expects a frame count other than 480"); - } -} - -void AudioManager::RNNoiseUninitialize() { - if (m_rnnoise[0] != nullptr) { - spdlog::get("audio")->debug("Uninitializing RNNoise"); - std::unique_lock _(m_rnn_mutex); - rnnoise_destroy(m_rnnoise[0]); - rnnoise_destroy(m_rnnoise[1]); - m_rnnoise[0] = nullptr; - m_rnnoise[1] = nullptr; - } -} -#endif - bool AudioManager::OK() const { return m_ok; } -double AudioManager::GetCaptureVolumeLevel() const noexcept { - return m_capture_peak_meter / 32768.0; -} - -double AudioManager::GetSSRCVolumeLevel(uint32_t ssrc) const noexcept { - std::lock_guard _(m_vol_mtx); - if (const auto it = m_volumes.find(ssrc); it != m_volumes.end()) { - return it->second; - } - return 0.0; -} - AudioDevices &AudioManager::GetDevices() { return m_devices; } -uint32_t AudioManager::GetRTPTimestamp() const noexcept { - return m_rtp_timestamp; -} - -void AudioManager::SetVADMethod(const std::string &method) { - spdlog::get("audio")->debug("Setting VAD method to {}", method); - if (method == "gate") { - SetVADMethod(VADMethod::Gate); - } else if (method == "rnnoise") { -#ifdef WITH_RNNOISE - SetVADMethod(VADMethod::RNNoise); -#else - SetVADMethod(VADMethod::Gate); - spdlog::get("audio")->error("Tried to set RNNoise VAD method with support disabled"); -#endif - } else { - SetVADMethod(VADMethod::Gate); - spdlog::get("audio")->error("Tried to set unknown VAD method {}", method); - } -} - -void AudioManager::SetVADMethod(VADMethod method) { - const auto method_int = static_cast(method); - spdlog::get("audio")->debug("Setting VAD method to enum {}", method_int); - m_vad_method = method; -} - -AudioManager::VADMethod AudioManager::GetVADMethod() const { - return m_vad_method; -} - std::vector AudioManager::ParseBackendsList(const Glib::ustring &list) { auto regex = Glib::Regex::create(";"); const std::vector split = regex->split(list); @@ -669,38 +110,30 @@ std::vector AudioManager::ParseBackendsList(const Glib::ustring &lis return backends; } -#ifdef WITH_RNNOISE -float AudioManager::GetCurrentVADProbability() const { - return m_vad_prob; -} - -double AudioManager::GetRNNProbThreshold() const { - return m_prob_threshold; +AbaddonClient::Audio::Context& AudioManager::GetContext() noexcept { + return *m_context; } -void AudioManager::SetRNNProbThreshold(double value) { - m_prob_threshold = value; +const AbaddonClient::Audio::Context& AudioManager::GetContext() const noexcept { + return *m_context; } -void AudioManager::SetSuppressNoise(bool value) { - m_enable_noise_suppression = value; +AbaddonClient::Audio::SystemAudio& AudioManager::GetSystem() noexcept { + return *m_system; } -bool AudioManager::GetSuppressNoise() const { - return m_enable_noise_suppression; +const AbaddonClient::Audio::SystemAudio& AudioManager::GetSystem() const noexcept { + return *m_system; } -#endif -void AudioManager::SetMixMono(bool value) { - m_mix_mono = value; -} +#ifdef WITH_VOICE -bool AudioManager::GetMixMono() const { - return m_mix_mono; +AbaddonClient::Audio::VoiceAudio& AudioManager::GetVoice() noexcept { + return *m_voice; } -AudioManager::type_signal_opus_packet AudioManager::signal_opus_packet() { - return m_signal_opus_packet; +const AbaddonClient::Audio::VoiceAudio& AudioManager::GetVoice() const noexcept { + return *m_voice; } #endif diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 5716fc55..59ece484 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -1,179 +1,59 @@ #pragma once -#ifdef WITH_VOICE + // clang-format off -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include - -#ifdef WITH_RNNOISE -#include -#endif +#include #include "devices.hpp" -// clang-format on - -class AudioManager { -public: - AudioManager(const Glib::ustring &backends_string); - ~AudioManager(); - - void AddSSRC(uint32_t ssrc); - void RemoveSSRC(uint32_t ssrc); - void RemoveAllSSRCs(); - - void SetOpusBuffer(uint8_t *ptr); - void FeedMeOpus(uint32_t ssrc, const std::vector &data); - - void StartCaptureDevice(); - void StopCaptureDevice(); - void SetPlaybackDevice(const Gtk::TreeModel::iterator &iter); - void SetCaptureDevice(const Gtk::TreeModel::iterator &iter); +#include "audio/context.hpp" - void SetCapture(bool capture); - void SetPlayback(bool playback); - - void SetCaptureGate(double gate); - void SetCaptureGain(double gain); - double GetCaptureGate() const noexcept; - double GetCaptureGain() const noexcept; - - void SetMuteSSRC(uint32_t ssrc, bool mute); - void SetVolumeSSRC(uint32_t ssrc, double volume); - double GetVolumeSSRC(uint32_t ssrc) const; +#ifdef WITH_VOICE +#include "voice/voice_audio.hpp" +#endif - void SetEncodingApplication(int application); - int GetEncodingApplication(); - void SetSignalHint(int signal); - int GetSignalHint(); - void SetBitrate(int bitrate); - int GetBitrate(); +#include "system/system_audio.hpp" - void Enumerate(); +#include "miniaudio/ma_log.hpp" - bool OK() const; +// clang-format on - double GetCaptureVolumeLevel() const noexcept; - double GetSSRCVolumeLevel(uint32_t ssrc) const noexcept; +class AudioManager { +public: + AudioManager(const Glib::ustring &backends_string, DiscordClient &discord); AudioDevices &GetDevices(); - uint32_t GetRTPTimestamp() const noexcept; - - enum class VADMethod { - Gate, - RNNoise, - }; + AbaddonClient::Audio::Context& GetContext() noexcept; + const AbaddonClient::Audio::Context& GetContext() const noexcept; - void SetVADMethod(const std::string &method); - void SetVADMethod(VADMethod method); - VADMethod GetVADMethod() const; + AbaddonClient::Audio::SystemAudio& GetSystem() noexcept; + const AbaddonClient::Audio::SystemAudio& GetSystem() const noexcept; - static std::vector ParseBackendsList(const Glib::ustring &list); - -#ifdef WITH_RNNOISE - float GetCurrentVADProbability() const; - double GetRNNProbThreshold() const; - void SetRNNProbThreshold(double value); - void SetSuppressNoise(bool value); - bool GetSuppressNoise() const; +#if WITH_VOICE + AbaddonClient::Audio::VoiceAudio& GetVoice() noexcept; + const AbaddonClient::Audio::VoiceAudio& GetVoice() const noexcept; #endif - void SetMixMono(bool value); - bool GetMixMono() const; + bool OK() const; private: - void OnCapturedPCM(const int16_t *pcm, ma_uint32 frames); - - void UpdateReceiveVolume(uint32_t ssrc, const int16_t *pcm, int frames); - void UpdateCaptureVolume(const int16_t *pcm, ma_uint32 frames); - std::atomic m_capture_peak_meter = 0; - - bool DecayVolumeMeters(); - - bool CheckVADVoiceGate(); - -#ifdef WITH_RNNOISE - bool CheckVADRNNoise(const int16_t *pcm, float *denoised_left, float *denoised_right); - - void RNNoiseInitialize(); - void RNNoiseUninitialize(); -#endif - - friend void data_callback(ma_device *, void *, const void *, ma_uint32); - friend void capture_data_callback(ma_device *, void *, const void *, ma_uint32); - - std::thread m_thread; - - bool m_ok; - - // playback - ma_device m_playback_device; - ma_device_config m_playback_config; - ma_device_id m_playback_id; - // capture - ma_device m_capture_device; - ma_device_config m_capture_config; - ma_device_id m_capture_id; - - ma_context m_context; - - mutable std::mutex m_mutex; - mutable std::mutex m_enc_mutex; - -#ifdef WITH_RNNOISE - mutable std::mutex m_rnn_mutex; -#endif - - std::unordered_map, OpusDecoder *>> m_sources; - - OpusEncoder *m_encoder; - - uint8_t *m_opus_buffer = nullptr; - - std::atomic m_should_capture = true; - std::atomic m_should_playback = true; - - std::atomic m_capture_gate = 0.0; - std::atomic m_capture_gain = 1.0; - std::atomic m_prob_threshold = 0.5; - std::atomic m_vad_prob = 0.0; - std::atomic m_enable_noise_suppression = false; - std::atomic m_mix_mono = false; - - std::unordered_set m_muted_ssrcs; - std::unordered_map m_volume_ssrc; - - mutable std::mutex m_vol_mtx; - std::unordered_map m_volumes; + void Enumerate(); + static std::vector ParseBackendsList(const Glib::ustring &list); AudioDevices m_devices; - VADMethod m_vad_method; -#ifdef WITH_RNNOISE - DenoiseState *m_rnnoise[2]; -#endif - std::atomic m_rtp_timestamp = 0; + std::optional m_context; + std::optional m_ma_log; - ma_log m_ma_log; std::shared_ptr m_log; -public: - using type_signal_opus_packet = sigc::signal; - type_signal_opus_packet signal_opus_packet(); +#ifdef WITH_VOICE + std::optional m_voice; +#endif -private: - type_signal_opus_packet m_signal_opus_packet; + std::optional m_system; + + bool m_ok = false; }; -#endif diff --git a/src/audio/miniaudio/ma_context.cpp b/src/audio/miniaudio/ma_context.cpp new file mode 100644 index 00000000..14f970d6 --- /dev/null +++ b/src/audio/miniaudio/ma_context.cpp @@ -0,0 +1,45 @@ +#include "ma_context.hpp" + +#include + +namespace AbaddonClient::Audio::Miniaudio { + +MaContext::MaContext(ContextPtr &&context) noexcept : + m_context(std::move(context)) {} + +std::optional MaContext::Create(ma_context_config &&config, ConstSlice backends) noexcept { + ContextPtr context = ContextPtr(new ma_context); + + const auto result = ma_context_init(backends.data(), backends.size(), &config, context.get()); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to create context: {}", ma_result_description(result)); + return std::nullopt; + } + + return MaContext(std::move(context)); +} + +std::optional MaContext::GetDevices() noexcept { + ma_device_info* playback_device_infos; + ma_uint32 playback_device_count; + + ma_device_info* capture_device_infos; + ma_uint32 capture_device_count; + + const auto result = ma_context_get_devices(m_context.get(), &playback_device_infos, &playback_device_count, &capture_device_infos, &capture_device_count); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to get devices information: {}", ma_result_description(result)); + return std::nullopt; + } + + const auto playback_info = PlaybackDeviceInfo(playback_device_infos, playback_device_count); + const auto capture_info = CaptureDeviceInfo(capture_device_infos, capture_device_count); + + return DeviceInfo(playback_info, capture_info); +} + +ma_context& MaContext::GetInternal() noexcept { + return *m_context; +} + +} diff --git a/src/audio/miniaudio/ma_context.hpp b/src/audio/miniaudio/ma_context.hpp new file mode 100644 index 00000000..4d441bc4 --- /dev/null +++ b/src/audio/miniaudio/ma_context.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include + +#include "misc/slice.hpp" + +namespace AbaddonClient::Audio::Miniaudio { + +class MaContext { +public: + using PlaybackDeviceInfo = ConstSlice; + using CaptureDeviceInfo = ConstSlice; + using DeviceInfo = std::pair; + + static std::optional Create(ma_context_config &&config, ConstSlice backends) noexcept; + + std::optional GetDevices() noexcept; + + ma_context& GetInternal() noexcept; + +private: + struct ContextDeleter { + void operator()(ma_context* ptr) noexcept { + ma_context_uninit(ptr); + } + }; + + // Put ma_context behind pointer to allow moving. + // miniaudio expects ma_context reference to be valid at all times + // Moving it to other location would cause memory corruption + using ContextPtr = std::unique_ptr; + MaContext(ContextPtr &&context) noexcept; + + ContextPtr m_context; +}; + +} diff --git a/src/audio/miniaudio/ma_device.cpp b/src/audio/miniaudio/ma_device.cpp new file mode 100644 index 00000000..cc0345e6 --- /dev/null +++ b/src/audio/miniaudio/ma_device.cpp @@ -0,0 +1,46 @@ +#include "ma_device.hpp" + +#include + +namespace AbaddonClient::Audio::Miniaudio { + +MaDevice::MaDevice(DevicePtr &&device) noexcept : + m_device(std::move(device)) {} + +std::optional MaDevice::Create(MaContext &context, ma_device_config &config) noexcept { + DevicePtr device = DevicePtr(new ma_device); + + const auto result = ma_device_init(&context.GetInternal(), &config, device.get()); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to create MaDevice: {}", ma_result_description(result)); + return std::nullopt; + } + + return MaDevice(std::move(device)); +} + +bool MaDevice::Start() noexcept { + const auto result = ma_device_start(m_device.get()); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to start device: {}", ma_result_description(result)); + return false; + } + + return true; +} + +bool MaDevice::Stop() noexcept { + const auto result = ma_device_stop(m_device.get()); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to stop device: {}", ma_result_description(result)); + return false; + } + + return true; +} + +ma_device& MaDevice::GetInternal() noexcept { + return *m_device; +} + +} diff --git a/src/audio/miniaudio/ma_device.hpp b/src/audio/miniaudio/ma_device.hpp new file mode 100644 index 00000000..41910032 --- /dev/null +++ b/src/audio/miniaudio/ma_device.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include + +#include "ma_context.hpp" + +namespace AbaddonClient::Audio::Miniaudio { + +class MaDevice { +public: + static std::optional Create(MaContext &context, ma_device_config &config) noexcept; + + bool Start() noexcept; + bool Stop() noexcept; + + ma_device& GetInternal() noexcept; + +private: + struct DeviceDeleter { + void operator()(ma_device* ptr) noexcept { + ma_device_uninit(ptr); + } + }; + + // Put ma_device behind pointer to allow moving + // miniaudio expects ma_device reference to be valid at all times. + // Moving it to other location would cause memory corruption + using DevicePtr = std::unique_ptr; + MaDevice(DevicePtr &&device) noexcept; + + DevicePtr m_device; +}; + +} diff --git a/src/audio/miniaudio/ma_engine.cpp b/src/audio/miniaudio/ma_engine.cpp new file mode 100644 index 00000000..5286eb32 --- /dev/null +++ b/src/audio/miniaudio/ma_engine.cpp @@ -0,0 +1,58 @@ +#include "ma_engine.hpp" + +#include + +#include + +namespace AbaddonClient::Audio::Miniaudio { + +MaEngine::MaEngine(EnginePtr &&engine) noexcept : + m_engine(std::move(engine)) {} + +std::optional MaEngine::Create(ma_engine_config &&config) noexcept { + EnginePtr engine = EnginePtr(new ma_engine); + + const auto result = ma_engine_init(&config, engine.get()); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to create engine: {}", ma_result_description(result)); + return std::nullopt; + } + + return MaEngine(std::move(engine)); +} + +bool MaEngine::Start() noexcept { + const auto result = ma_engine_start(m_engine.get()); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to start engine: {}", ma_result_description(result)); + return false; + } + + return true; +} + +bool MaEngine::Stop() noexcept { + const auto result = ma_engine_stop(m_engine.get()); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to stop engine: {}", ma_result_description(result)); + return false; + } + + return true; +} + +bool MaEngine::PlaySound(std::string_view file_path) noexcept { + const auto result = ma_engine_play_sound(m_engine.get(), file_path.data(), nullptr); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to play sound at {}: {}", file_path.data(), ma_result_description(result)); + return false; + } + + return true; +} + +ma_engine& MaEngine::GetInternal() noexcept { + return *m_engine; +} + +} diff --git a/src/audio/miniaudio/ma_engine.hpp b/src/audio/miniaudio/ma_engine.hpp new file mode 100644 index 00000000..509dc492 --- /dev/null +++ b/src/audio/miniaudio/ma_engine.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include + +namespace AbaddonClient::Audio::Miniaudio { + +class MaEngine { +public: + static std::optional Create(ma_engine_config &&config) noexcept; + + bool Start() noexcept; + bool Stop() noexcept; + + bool PlaySound(std::string_view file_path) noexcept; + + ma_engine& GetInternal() noexcept; + +private: + struct EngineDeleter { + void operator()(ma_engine* ptr) noexcept { + ma_engine_uninit(ptr); + } + }; + + // Put ma_engine behind pointer to allow moving. + // miniaudio expects ma_engine reference to be valid at all times + // Moving it to other location would cause memory corruption + using EnginePtr = std::unique_ptr; + MaEngine(EnginePtr &&engine) noexcept; + + EnginePtr m_engine; +}; + +} diff --git a/src/audio/miniaudio/ma_log.cpp b/src/audio/miniaudio/ma_log.cpp new file mode 100644 index 00000000..33481dbe --- /dev/null +++ b/src/audio/miniaudio/ma_log.cpp @@ -0,0 +1,32 @@ +#include "ma_log.hpp" + +#include + +#include + +namespace AbaddonClient::Audio::Miniaudio { + +MaLog::MaLog(LogPtr &&log) noexcept : + m_log(std::move(log)) {} + +std::optional MaLog::Create() noexcept { + LogPtr log = LogPtr(new ma_log); + + const auto result = ma_log_init(nullptr, log.get()); + if (result != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to create log: {}", ma_result_description(result)); + return std::nullopt; + } + + return MaLog(std::move(log)); +} + +bool MaLog::RegisterCallback(ma_log_callback callback) noexcept { + return ma_log_register_callback(m_log.get(), callback) == MA_SUCCESS; +} + +ma_log& MaLog::GetInternal() noexcept { + return *m_log; +} + +} diff --git a/src/audio/miniaudio/ma_log.hpp b/src/audio/miniaudio/ma_log.hpp new file mode 100644 index 00000000..ec67699b --- /dev/null +++ b/src/audio/miniaudio/ma_log.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include + +namespace AbaddonClient::Audio::Miniaudio { + +class MaLog { +public: + static std::optional Create() noexcept; + + bool RegisterCallback(ma_log_callback callback) noexcept; + + ma_log& GetInternal() noexcept; +private: + struct LogDeleter { + void operator()(ma_log* ptr) noexcept { + ma_log_uninit(ptr); + } + }; + + using LogPtr = std::unique_ptr; + MaLog(LogPtr &&log) noexcept; + + LogPtr m_log; +}; + +} diff --git a/src/audio/miniaudio/ma_pcm_rb.cpp b/src/audio/miniaudio/ma_pcm_rb.cpp new file mode 100644 index 00000000..ba1a9948 --- /dev/null +++ b/src/audio/miniaudio/ma_pcm_rb.cpp @@ -0,0 +1,99 @@ +#include "ma_pcm_rb.hpp" + +#include + +#include + +namespace AbaddonClient::Audio::Miniaudio { + +MaPCMRingBuffer::MaPCMRingBuffer(RingBufferPtr &&ringbuffer, uint32_t channels) noexcept : + m_ringbuffer(std::move(ringbuffer)), + m_channels(channels) {} + + +std::optional +MaPCMRingBuffer::Create(uint32_t channels, uint32_t buffer_size_in_frames) noexcept { + auto ringbuffer_ptr = RingBufferPtr(new ma_pcm_rb); + + const auto result = ma_pcm_rb_init(ma_format_f32, channels, buffer_size_in_frames, nullptr, nullptr, ringbuffer_ptr.get()); + if (result != MA_SUCCESS) { + spdlog::get("voice")->error("Failed to create MaPCMRingBuffer: {}", ma_result_description(result)); + return std::nullopt; + } + + return MaPCMRingBuffer(std::move(ringbuffer_ptr), channels); +} + + +void MaPCMRingBuffer::Read(OutputBuffer output) noexcept { + const auto total_frames = output.size() / m_channels; + + uint32_t read = 0; + uint32_t tries = 0; + + // Try twice in case of wrap around + while (read < total_frames && tries < 2) { + read += DoRead(output, read, total_frames); + tries++; + } +} + +void MaPCMRingBuffer::Write(InputBuffer input) noexcept { + const auto total_frames = input.size() / m_channels; + + uint32_t written = 0; + uint32_t tries = 0; + + // Try twice in case of wrap around + while (written < total_frames && tries < 2) { + written += DoWrite(input, written, total_frames); + tries++; + } +} + +void MaPCMRingBuffer::Clear() noexcept { + ma_pcm_rb_reset(m_ringbuffer.get()); +} + +uint32_t MaPCMRingBuffer::GetAvailableReadFrames() noexcept { + return ma_pcm_rb_available_read(m_ringbuffer.get()); +} + +uint32_t MaPCMRingBuffer::GetAvailableWriteFrames() noexcept { + return ma_pcm_rb_available_write(m_ringbuffer.get()); +} + + +uint32_t MaPCMRingBuffer::DoRead(OutputBuffer output, uint32_t read_frames, uint32_t total_frames) noexcept { + auto frames = total_frames - read_frames; + float* read_ptr; + + ma_pcm_rb_acquire_read(m_ringbuffer.get(), &frames, reinterpret_cast(&read_ptr)); + + const auto output_ptr = output.begin() + read_frames * m_channels; + const auto samples = frames * m_channels; + + std::copy_n(read_ptr, samples, output_ptr); + + ma_pcm_rb_commit_read(m_ringbuffer.get(), frames); + + return frames; +} + +uint32_t MaPCMRingBuffer::DoWrite(InputBuffer input, uint32_t written_frames, uint32_t total_frames) noexcept { + auto frames = total_frames - written_frames; + float* write_ptr; + + ma_pcm_rb_acquire_write(m_ringbuffer.get(), &frames, reinterpret_cast(&write_ptr)); + + const auto input_ptr = input.begin() + written_frames * m_channels; + const auto samples = frames * m_channels; + + std::copy_n(input_ptr, samples, write_ptr); + + ma_pcm_rb_commit_write(m_ringbuffer.get(), frames); + + return frames; +} + +} diff --git a/src/audio/miniaudio/ma_pcm_rb.hpp b/src/audio/miniaudio/ma_pcm_rb.hpp new file mode 100644 index 00000000..b53fec67 --- /dev/null +++ b/src/audio/miniaudio/ma_pcm_rb.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +#include + +#include "audio/utils.hpp" + +namespace AbaddonClient::Audio::Miniaudio { + +class MaPCMRingBuffer { +public: + static std::optional Create(uint32_t channels, uint32_t buffer_size_in_frames) noexcept; + + void Read(OutputBuffer output) noexcept; + void Write(InputBuffer input) noexcept; + void Clear() noexcept; + + uint32_t GetAvailableReadFrames() noexcept; + uint32_t GetAvailableWriteFrames() noexcept; +private: + uint32_t DoRead(OutputBuffer output, uint32_t read_frames, uint32_t total_frames) noexcept; + uint32_t DoWrite(InputBuffer input, uint32_t written_frames, uint32_t total_frames) noexcept; + + struct RingBufferDeleter { + void operator()(ma_pcm_rb* ptr) noexcept { + ma_pcm_rb_uninit(ptr); + } + }; + + using RingBufferPtr = std::unique_ptr; + MaPCMRingBuffer(RingBufferPtr &&ringbuffer, uint32_t channel) noexcept; + + RingBufferPtr m_ringbuffer; + + const uint32_t m_channels; +}; + +} diff --git a/src/audio/system/system_audio.cpp b/src/audio/system/system_audio.cpp new file mode 100644 index 00000000..ba524b3e --- /dev/null +++ b/src/audio/system/system_audio.cpp @@ -0,0 +1,80 @@ +#include "system_audio.hpp" + +#include "abaddon.hpp" + +namespace AbaddonClient::Audio { + +SystemAudio::SystemAudio(Context &context) noexcept : + m_engine(context) {} + +std::string_view SystemAudio::GetSoundPath(SystemSound sound) noexcept { + switch (sound) { +#ifdef ENABLE_NOTIFICATION_SOUNDS + case SystemSound::Notification: { + return "/sound/message.mp3"; + } +#endif + +#ifdef WITH_VOICE + case SystemSound::VoiceConnected: { + return "/sound/voice_connected.mp3"; + } + case SystemSound::VoiceDisconnected: { + return "/sound/voice_disconnected.mp3"; + } + case SystemSound::VoiceMuted: { + return "/sound/voice_muted.mp3"; + } + case SystemSound::VoiceUnmuted: { + return "/sound/voice_unmuted.mp3"; + } + case SystemSound::VoiceDeafened: { + return "/sound/voice_deafened.mp3"; + } + case SystemSound::VoiceUndeafened: { + return "/sound/voice_undeafened.mp3"; + } +#endif + } + + return ""; +} + +void SystemAudio::PlaySound(SystemSound sound) noexcept { + const auto path = Abaddon::Get().GetResPath() + GetSoundPath(sound).data(); + m_engine.PlaySound(path); +} + +#ifdef WITH_VOICE + +void SystemAudio::BindToVoice(DiscordClient &discord) noexcept { + discord.signal_voice_user_connect() + .connect(sigc::mem_fun(*this, &SystemAudio::OnVoiceUserConnect)); + + discord.signal_voice_user_disconnect() + .connect(sigc::mem_fun(*this, &SystemAudio::OnVoiceUserDisconnect)); +} + +void SystemAudio::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) noexcept { + if (!IsCurrentVoiceChannel(channel_id)) { + return; + } + + PlaySound(SystemSound::VoiceConnected); +} + +void SystemAudio::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) noexcept { + if (!IsCurrentVoiceChannel(channel_id)) { + return; + } + + PlaySound(SystemSound::VoiceDisconnected); +} + +bool SystemAudio::IsCurrentVoiceChannel(Snowflake channel_id) noexcept { + return Abaddon::Get().GetDiscordClient().GetVoiceChannelID() == channel_id; +} + +#endif + +} diff --git a/src/audio/system/system_audio.hpp b/src/audio/system/system_audio.hpp new file mode 100644 index 00000000..ef9269dd --- /dev/null +++ b/src/audio/system/system_audio.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include + +#include "audio/audio_engine.hpp" +#include "discord/discord.hpp" +#include "discord/snowflake.hpp" + +namespace AbaddonClient::Audio { + +class SystemAudio : public sigc::trackable { +public: + enum SystemSound { +#ifdef ENABLE_NOTIFICATION_SOUNDS + Notification, +#endif + +#ifdef WITH_VOICE + VoiceConnected, + VoiceDisconnected, + VoiceMuted, + VoiceUnmuted, + VoiceDeafened, + VoiceUndeafened, +#endif + }; + + SystemAudio(Context &context) noexcept; + +#ifdef WITH_VOICE + void BindToVoice(DiscordClient &discord) noexcept; +#endif + + std::string_view GetSoundPath(SystemSound sound) noexcept; + void PlaySound(SystemSound sound) noexcept; + +private: + +#ifdef WITH_VOICE + void OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) noexcept; + void OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) noexcept; + + static bool IsCurrentVoiceChannel(Snowflake channel_id) noexcept; +#endif + + AudioEngine m_engine; +}; + +} diff --git a/src/audio/utils.hpp b/src/audio/utils.hpp new file mode 100644 index 00000000..85019e87 --- /dev/null +++ b/src/audio/utils.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include "misc/slice.hpp" + +using InputBuffer = ConstSlice; +using OutputBuffer = Slice; + +namespace AbaddonClient::Audio { + +class AudioUtils { +public: + AudioUtils() = delete; + ~AudioUtils() = delete; + + static void ApplyGain(OutputBuffer buffer, float gain) noexcept { + for (auto &sample : buffer) { + sample *= gain; + } + } + + static void ClampToFloatRange(OutputBuffer buffer) noexcept { + for (auto& sample : buffer) { + sample = std::clamp(sample, -1.0f, 1.0f); + } + } + + static void MixStereoToMono(OutputBuffer buffer) noexcept { + for (auto iter = buffer.begin(); iter < buffer.end() - 2; iter += 2) { + const auto mixed = std::reduce(iter, iter + 2, 0.0f) / 2.0f; + std::fill(iter, iter + 2, mixed); + } + } + + static void MixBuffers(InputBuffer first, OutputBuffer second) noexcept { + std::transform(first.begin(), first.end(), second.begin(), second.begin(), std::plus()); + } + +}; + +} diff --git a/src/audio/voice/capture/constants.hpp b/src/audio/voice/capture/constants.hpp new file mode 100644 index 00000000..ae2b8eb9 --- /dev/null +++ b/src/audio/voice/capture/constants.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include + +constexpr ma_format CAPTURE_FORMAT = ma_format_f32; +constexpr uint32_t CAPTURE_SAMPLE_RATE = 48000; +constexpr uint32_t CAPTURE_CHANNELS = 2; +constexpr uint32_t CAPTURE_FRAME_SIZE = 480; +constexpr size_t CAPTURE_BUFFER_SIZE = CAPTURE_FRAME_SIZE * CAPTURE_CHANNELS; + +using CaptureBuffer = std::array; + +constexpr size_t OPUS_LATENCY_TENTH_MS = 10000 * CAPTURE_FRAME_SIZE / CAPTURE_SAMPLE_RATE; +static_assert +( + OPUS_LATENCY_TENTH_MS == 25 || + OPUS_LATENCY_TENTH_MS == 50 || + OPUS_LATENCY_TENTH_MS == 100 || + OPUS_LATENCY_TENTH_MS == 200 || + OPUS_LATENCY_TENTH_MS == 400 || + OPUS_LATENCY_TENTH_MS == 600, + "Opus latency should be either 2.5, 5, 10, 20, 40 or 60 ms" +); diff --git a/src/audio/voice/capture/effects/gate.cpp b/src/audio/voice/capture/effects/gate.cpp new file mode 100644 index 00000000..79d7dd37 --- /dev/null +++ b/src/audio/voice/capture/effects/gate.cpp @@ -0,0 +1,9 @@ +#include "gate.hpp" + +namespace AbaddonClient::Audio::Voice::Capture::Effects { + +bool Gate::PassesVAD(InputBuffer buffer, float current_peak) const noexcept { + return current_peak > VADThreshold; +} + +} diff --git a/src/audio/voice/capture/effects/gate.hpp b/src/audio/voice/capture/effects/gate.hpp new file mode 100644 index 00000000..9bdf3516 --- /dev/null +++ b/src/audio/voice/capture/effects/gate.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "audio/utils.hpp" + +namespace AbaddonClient::Audio::Voice::Capture::Effects { + +class Gate { +public: + bool PassesVAD(InputBuffer buffer, float current_peak) const noexcept; + + std::atomic VADThreshold; +}; + +} diff --git a/src/audio/voice/capture/effects/noise.cpp b/src/audio/voice/capture/effects/noise.cpp new file mode 100644 index 00000000..dda3338f --- /dev/null +++ b/src/audio/voice/capture/effects/noise.cpp @@ -0,0 +1,71 @@ +#include "noise.hpp" + +namespace AbaddonClient::Audio::Voice::Capture::Effects { + +bool Noise::PassesVAD(InputBuffer buffer) noexcept { + // Use first channel for VAD, only denoise the rest if noise suppression is enabled + const auto prob = m_channels.Lock()[0].DenoiseChannel(buffer, 0); + + m_peak_meter.SetPeak(prob); + m_denoised_first_channel = true; + + return prob > VADThreshold; +} + +void Noise::Denoise(OutputBuffer buffer) noexcept { + auto start = 0; + if (m_denoised_first_channel) { + m_denoised_first_channel = false; + start = 1; + } + + // Denoise required channels + auto channels = m_channels.Lock(); + for (size_t channel = start; channel < channels->size(); channel++) { + auto& channel_buffer = channels[channel]; + + channel_buffer.DenoiseChannel(buffer, channel); + } + + // Write them back + for (size_t channel = 0; channel < channels->size(); channel++) { + auto& channel_buffer = channels[channel]; + + channel_buffer.WriteChannel(buffer, channel); + } +} + +PeakMeter& Noise::GetPeakMeter() noexcept { + return m_peak_meter; +} + +const PeakMeter& Noise::GetPeakMeter() const noexcept { + return m_peak_meter; +} + +NoiseBuffer::NoiseBuffer() noexcept : + m_state(RNNoisePtr(rnnoise_create(NULL))) {} + +float NoiseBuffer::DenoiseChannel(InputBuffer buffer, size_t channel) noexcept { + ChannelBuffer input_channel; + + // Copy interleaved samples from a specific channel + for (size_t i = 0; i < m_channel.size(); i++) { + const auto offset = channel + (i * CAPTURE_CHANNELS); + + // RNNoise expects samples to fall in range of 16-bit PCM, but as float. Weird + input_channel[i] = buffer[offset] * INT16_MAX; + } + + const auto prob = rnnoise_process_frame(m_state.get(), m_channel.data(), input_channel.data()); + return prob; +} + +void NoiseBuffer::WriteChannel(OutputBuffer buffer, size_t channel) noexcept { + for (size_t i = 0; i < m_channel.size(); i++) { + const auto offset = channel + (i * CAPTURE_CHANNELS); + buffer[offset] = m_channel[i] / INT16_MAX; // Convert to f32 sample range + } +} + +} diff --git a/src/audio/voice/capture/effects/noise.hpp b/src/audio/voice/capture/effects/noise.hpp new file mode 100644 index 00000000..b8824795 --- /dev/null +++ b/src/audio/voice/capture/effects/noise.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include + +#include "audio/voice/capture/constants.hpp" +#include "audio/voice/peak_meter/peak_meter.hpp" +#include "misc/mutex.hpp" + +namespace AbaddonClient::Audio::Voice::Capture::Effects { + +class NoiseBuffer { +public: + NoiseBuffer() noexcept; + + float DenoiseChannel(InputBuffer buffer, size_t channel) noexcept; + void WriteChannel(OutputBuffer buffer, size_t channel) noexcept; + +private: + struct RNNoiseDeleter { + void operator()(DenoiseState* ptr) noexcept { + rnnoise_destroy(ptr); + } + }; + + using RNNoisePtr = std::unique_ptr; + using ChannelBuffer = std::array; + + RNNoisePtr m_state; + ChannelBuffer m_channel; +}; + +class Noise { +public: + bool PassesVAD(InputBuffer buffer) noexcept; + void Denoise(OutputBuffer buffer) noexcept; + + PeakMeter& GetPeakMeter() noexcept; + const PeakMeter& GetPeakMeter() const noexcept; + + std::atomic VADThreshold = 0.5; +private: + using NoiseArray = std::array; + + Mutex m_channels; + PeakMeter m_peak_meter; + + bool m_denoised_first_channel; +}; + +}; diff --git a/src/audio/voice/capture/voice_capture.cpp b/src/audio/voice/capture/voice_capture.cpp new file mode 100644 index 00000000..9513cf61 --- /dev/null +++ b/src/audio/voice/capture/voice_capture.cpp @@ -0,0 +1,167 @@ +#include "voice_capture.hpp" +#include "constants.hpp" + +#include + +namespace AbaddonClient::Audio::Voice { + +using OpusEncoder = Opus::OpusEncoder; +using EncoderSettings = OpusEncoder::EncoderSettings; +using SignalHint = OpusEncoder::SignalHint; +using EncodingApplication = OpusEncoder::EncodingApplication; + +void capture_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { + auto capture = static_cast(pDevice->pUserData); + if (capture == nullptr) { + return; + } + + const auto buffer = InputBuffer(static_cast(pInput), CAPTURE_BUFFER_SIZE); + capture->OnAudioCapture(buffer); +} + +VoiceCapture::VoiceCapture(Context &context) noexcept : + m_device(context, GetDeviceConfig(), context.GetActiveCaptureID()) {} + +void VoiceCapture::Start() noexcept { + StartEncoder(); + if (!m_encoder.Lock()->has_value()) { + return; + } + + m_device.Start(); +} + +void VoiceCapture::Stop() noexcept { + m_device.Stop(); + m_encoder.Lock()->reset(); +} + +void VoiceCapture::SetActive(bool active) noexcept { + m_active = active; +} + +void VoiceCapture::SetCaptureDevice(const ma_device_id &device_id) noexcept { + spdlog::get("voice")->info("Setting capture device"); + + const auto success = m_device.ChangeDevice(device_id); + if (!success) { + spdlog::get("voice")->error("Failed to set capture device"); + } +} + +ma_device_config VoiceCapture::GetDeviceConfig() noexcept { + auto config = ma_device_config_init(ma_device_type_capture); + config.capture.format = CAPTURE_FORMAT; + config.sampleRate = CAPTURE_SAMPLE_RATE; + config.capture.channels = CAPTURE_CHANNELS; + config.pulse.pStreamNameCapture = "Abaddon (Capture)"; + + config.periodSizeInFrames = CAPTURE_FRAME_SIZE; + config.performanceProfile = ma_performance_profile_low_latency; + + config.dataCallback = capture_callback; + config.pUserData = this; + + return config; +} + +void VoiceCapture::StartEncoder() noexcept { + EncoderSettings settings; + settings.sample_rate = CAPTURE_SAMPLE_RATE; + settings.channels = CAPTURE_CHANNELS; + settings.bitrate = 64000; + settings.signal_hint = SignalHint::Auto; + settings.application = EncodingApplication::VOIP; + + auto encoder = OpusEncoder::Create(settings); + if (!encoder) { + return; + } + + m_encoder.Lock()->emplace(std::move(*encoder)); +} + +void VoiceCapture::OnAudioCapture(InputBuffer input) noexcept { + if (!m_active) { + return; + } + + CaptureBuffer buffer; + std::copy(input.begin(), input.end(), buffer.begin()); + + ApplyEffects(buffer); + if (ApplyNoise(buffer)) { + EncodeAndSend(buffer); + } +} + +void VoiceCapture::ApplyEffects(CaptureBuffer &buffer) noexcept { + if (MixMono) { + AudioUtils::MixStereoToMono(buffer); + } + + AudioUtils::ApplyGain(buffer, Gain); + m_peak_meter.UpdatePeak(buffer); +} + +bool VoiceCapture::ApplyNoise(CaptureBuffer &buffer) noexcept { + if(!m_effects.PassesVAD(buffer, m_peak_meter.GetPeak())) { + return false; + } + + if (SuppressNoise) { + m_effects.Denoise(buffer); + } + + return true; +} + +void VoiceCapture::EncodeAndSend(const CaptureBuffer &buffer) noexcept { + std::vector opus; + opus.resize(1275); + + const auto bytes = m_encoder.Lock()->value().Encode(buffer, opus, CAPTURE_FRAME_SIZE); + opus.resize(bytes); + + if (bytes > 0) { + m_signal.emit(opus); + } + + m_rtp_timestamp += CAPTURE_FRAME_SIZE; +} + +Capture::VoiceEffects& VoiceCapture::GetEffects() noexcept { + return m_effects; +} + +const Capture::VoiceEffects& VoiceCapture::GetEffects() const noexcept { + return m_effects; +} + +PeakMeter& VoiceCapture::GetPeakMeter() noexcept { + return m_peak_meter; +} + +const PeakMeter& VoiceCapture::GetPeakMeter() const noexcept { + return m_peak_meter; +} + +MutexGuard> VoiceCapture::GetEncoder() noexcept { + return m_encoder.Lock(); +} + +const MutexGuard> VoiceCapture::GetEncoder() const noexcept { + return m_encoder.Lock(); +} + +VoiceCapture::CaptureSignal VoiceCapture::GetCaptureSignal() const noexcept { + return m_signal; +} + +uint32_t VoiceCapture::GetRTPTimestamp() const noexcept { + return m_rtp_timestamp; +} + + +} diff --git a/src/audio/voice/capture/voice_capture.hpp b/src/audio/voice/capture/voice_capture.hpp new file mode 100644 index 00000000..10f5f37f --- /dev/null +++ b/src/audio/voice/capture/voice_capture.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +#include "audio/audio_device.hpp" +#include "audio/context.hpp" +#include "audio/utils.hpp" + +#include "audio/voice/opus/opus_encoder.hpp" +#include "audio/voice/peak_meter/peak_meter.hpp" + +#include "voice_effects.hpp" + +namespace AbaddonClient::Audio::Voice { + +class VoiceCapture { +public: + using CaptureSignal = sigc::signal&)>; + + VoiceCapture(Context &context) noexcept; + + void Start() noexcept; + void Stop() noexcept; + + void SetActive(bool active) noexcept; + void SetCaptureDevice(const ma_device_id &device_id) noexcept; + + Capture::VoiceEffects& GetEffects() noexcept; + const Capture::VoiceEffects& GetEffects() const noexcept; + + MutexGuard> GetEncoder() noexcept; + const MutexGuard> GetEncoder() const noexcept; + + PeakMeter& GetPeakMeter() noexcept; + const PeakMeter& GetPeakMeter() const noexcept; + + CaptureSignal GetCaptureSignal() const noexcept; + uint32_t GetRTPTimestamp() const noexcept; + + std::atomic Gain = 1.0f; + std::atomic MixMono = false; + std::atomic SuppressNoise = false; +private: + ma_device_config GetDeviceConfig() noexcept; + void StartEncoder() noexcept; + + void OnAudioCapture(const InputBuffer input_buffer) noexcept; + + void ApplyEffects(CaptureBuffer &buffer) noexcept; + bool ApplyNoise(CaptureBuffer &buffer) noexcept; + void EncodeAndSend(const CaptureBuffer &buffer) noexcept; + + Capture::VoiceEffects m_effects; + PeakMeter m_peak_meter; + + AudioDevice m_device; + Mutex> m_encoder; + CaptureSignal m_signal; + + // TODO: Ideally this should not be here + // RTP should be handled by VoiceClient + std::atomic m_rtp_timestamp = 0; + std::atomic m_active = true; + + friend void capture_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount); +}; + +} diff --git a/src/audio/voice/capture/voice_effects.cpp b/src/audio/voice/capture/voice_effects.cpp new file mode 100644 index 00000000..83768a34 --- /dev/null +++ b/src/audio/voice/capture/voice_effects.cpp @@ -0,0 +1,98 @@ +#include "voice_effects.hpp" + +#include + +namespace AbaddonClient::Audio::Voice::Capture { + +bool VoiceEffects::PassesVAD(InputBuffer buffer, float current_peak) noexcept { + switch (m_vad_method) { + case VADMethod::Gate: { + return m_gate.PassesVAD(buffer, current_peak); + } +#ifdef WITH_RNNOISE + case VADMethod::RNNoise: { + return m_noise.PassesVAD(buffer); + } +#endif + } + + return true; +}; + +void VoiceEffects::Denoise(OutputBuffer buffer) noexcept { +#ifdef WITH_RNNOISE + m_noise.Denoise(buffer); +#endif +} + +void VoiceEffects::SetCurrentThreshold(float threshold) noexcept { + switch (m_vad_method) { + case VADMethod::Gate: { + m_gate.VADThreshold = threshold; + } break; +#ifdef WITH_RNNOISE + case VADMethod::RNNoise: { + m_noise.VADThreshold = threshold; + } break; +#endif + } +} + +float VoiceEffects::GetCurrentThreshold() const noexcept { + switch (m_vad_method) { + case VADMethod::Gate: { + return m_gate.VADThreshold; + } break; +#ifdef WITH_RNNOISE + case VADMethod::RNNoise: { + return m_noise.VADThreshold; + } break; +#endif + } + + return 0.0f; +} + +void VoiceEffects::SetVADMethod(const std::string &method) noexcept { + if (method == "gate") { + m_vad_method = VADMethod::Gate; + } +#ifdef WITH_RNNOISE + else if (method == "rnnoise") { + m_vad_method = VADMethod::RNNoise; + } +#endif + else { + spdlog::get("voice")->error("Tried to set non-existent VAD method: {}", method); + } +} + +void VoiceEffects::SetVADMethod(VADMethod method) noexcept { + m_vad_method = method; +} + +VADMethod VoiceEffects::GetVADMethod() const noexcept { + return m_vad_method; +} + +Effects::Gate& VoiceEffects::GetGate() noexcept { + return m_gate; +} + +const Effects::Gate& VoiceEffects::GetGate() const noexcept { + return m_gate; +} + +#ifdef WITH_RNNOISE + +Effects::Noise& VoiceEffects::GetNoise() noexcept { + return m_noise; +} + +const Effects::Noise& VoiceEffects::GetNoise() const noexcept { + return m_noise; +} + +#endif + +} diff --git a/src/audio/voice/capture/voice_effects.hpp b/src/audio/voice/capture/voice_effects.hpp new file mode 100644 index 00000000..218e0a1b --- /dev/null +++ b/src/audio/voice/capture/voice_effects.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "audio/utils.hpp" + +#include "effects/gate.hpp" + +#ifdef WITH_RNNOISE +#include "effects/noise.hpp" +#endif + +namespace AbaddonClient::Audio::Voice::Capture { + +enum class VADMethod { + Gate, +#ifdef WITH_RNNOISE + RNNoise +#endif +}; + +class VoiceEffects { +public: + bool PassesVAD(InputBuffer buffer, float current_volume) noexcept; + void Denoise(OutputBuffer buffer) noexcept; + + void SetCurrentThreshold(float threshold) noexcept; + float GetCurrentThreshold() const noexcept; + + void SetVADMethod(const std::string &method) noexcept; + void SetVADMethod(VADMethod method) noexcept; + VADMethod GetVADMethod() const noexcept; + + Effects::Gate& GetGate() noexcept; + const Effects::Gate& GetGate() const noexcept; + +#ifdef WITH_RNNOISE + Effects::Noise& GetNoise() noexcept; + const Effects::Noise& GetNoise() const noexcept; +#endif + + VADMethod m_vad_method; +private: + Effects::Gate m_gate; + +#ifdef WITH_RNNOISE + Effects::Noise m_noise; +#endif +}; + +}; diff --git a/src/audio/voice/opus/opus_decoder.cpp b/src/audio/voice/opus/opus_decoder.cpp new file mode 100644 index 00000000..5a46936f --- /dev/null +++ b/src/audio/voice/opus/opus_decoder.cpp @@ -0,0 +1,33 @@ +#include "opus_decoder.hpp" + +#include + +namespace AbaddonClient::Audio::Voice::Opus { + +OpusDecoder::OpusDecoder(DecoderPtr encoder, DecoderSettings settings) noexcept : + m_encoder(std::move(encoder)) {} + +std::optional +OpusDecoder::Create(const DecoderSettings settings) noexcept { + int error; + const auto decoder = opus_decoder_create(settings.sample_rate, settings.channels, &error); + + if (error != OPUS_OK) { + spdlog::get("voice")->error("Failed to create opus decoder: {}", opus_strerror(error)); + return std::nullopt; + } + + auto decoder_ptr = DecoderPtr(decoder); + return OpusDecoder(std::move(decoder_ptr), settings); +} + +int OpusDecoder::Decode(OpusInput opus, OutputBuffer output, const int frame_size) noexcept { + const auto frames = opus_decode_float(m_encoder.get(), opus.data(), opus.size(), output.data(), frame_size, 0); + if (frames < 0) { + spdlog::get("voice")->error("Opus decoder error: {}", opus_strerror(frames)); + } + + return frames; +} + +} diff --git a/src/audio/voice/opus/opus_decoder.hpp b/src/audio/voice/opus/opus_decoder.hpp new file mode 100644 index 00000000..f7ed0b24 --- /dev/null +++ b/src/audio/voice/opus/opus_decoder.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include + +#include "audio/utils.hpp" + +namespace AbaddonClient::Audio::Voice::Opus { + +using OpusInput = ConstSlice; + +class OpusDecoder { +public: + struct DecoderSettings { + int32_t sample_rate = 48000; + int channels = 2; + }; + + static std::optional Create(DecoderSettings settings) noexcept; + + int Decode(OpusInput opus, OutputBuffer pcm, int frame_size) noexcept; +private: + struct DecoderDeleter { + void operator()(::OpusDecoder* ptr) noexcept { + opus_decoder_destroy(ptr); + } + }; + + using DecoderPtr = std::unique_ptr<::OpusDecoder, DecoderDeleter>; + OpusDecoder(DecoderPtr encoder, DecoderSettings settings) noexcept; + + DecoderPtr m_encoder; +}; + +} diff --git a/src/audio/voice/opus/opus_encoder.cpp b/src/audio/voice/opus/opus_encoder.cpp new file mode 100644 index 00000000..e1f77856 --- /dev/null +++ b/src/audio/voice/opus/opus_encoder.cpp @@ -0,0 +1,106 @@ +#include "opus_encoder.hpp" + +#include + +namespace AbaddonClient::Audio::Voice::Opus { + +using EncoderSettings = OpusEncoder::EncoderSettings; +using EncodingApplication = OpusEncoder::EncodingApplication; +using SignalHint = OpusEncoder::SignalHint; + +OpusEncoder::OpusEncoder(EncoderPtr encoder, EncoderSettings settings) noexcept : + m_encoder(std::move(encoder)), + m_sample_rate(settings.sample_rate), + m_channels(settings.channels), + m_bitrate(settings.bitrate), + m_signal_hint(settings.signal_hint), + m_application(settings.application) +{ + SetBitrate(m_bitrate); + SetSignalHint(m_signal_hint); +} + +std::optional +OpusEncoder::Create(EncoderSettings settings) noexcept { + int error; + const auto encoder = opus_encoder_create(settings.sample_rate, settings.channels, static_cast(settings.application), &error); + + if (error != OPUS_OK) { + spdlog::get("voice")->error("Cannot create opus encoder: {}", opus_strerror(error)); + return std::nullopt; + } + + auto encoder_ptr = EncoderPtr(encoder); + return OpusEncoder(std::move(encoder_ptr), settings); +} + +int OpusEncoder::Encode(InputBuffer &&input, OpusOutput &&output, int frame_size) noexcept { + const auto bytes = opus_encode_float(m_encoder.get(), input.data(), frame_size, output.data(), output.size()); + if (bytes < 0) { + spdlog::get("voice")->error("Opus encoder error: {}", opus_strerror(bytes)); + } + + return bytes; +} + +void OpusEncoder::ResetState() noexcept { + OpusCTL("OPUS_RESET_STATE", OPUS_RESET_STATE); +} + +void OpusEncoder::SetBitrate(int32_t bitrate) noexcept { + const auto success = OpusCTL("OPUS_SET_BITRATE", OPUS_SET_BITRATE(bitrate)); + + if (success) { + m_bitrate = bitrate; + } +} + +void OpusEncoder::SetSignalHint(SignalHint hint) noexcept { + const auto hint_int = static_cast(hint); + const auto success = OpusCTL("OPUS_SET_SIGNAL", OPUS_SET_SIGNAL(hint_int)); + + if (success) { + m_signal_hint = hint; + } +} + +void OpusEncoder::SetEncodingApplication(EncodingApplication application) noexcept { + int error; + const auto encoder = opus_encoder_create(m_sample_rate, m_channels, static_cast(application), &error); + + if (error != OPUS_OK) { + spdlog::get("voice")->error("Cannot change encoding application: {}", opus_strerror(error)); + return; + } + + m_encoder.reset(encoder); + m_application = application; + + SetBitrate(m_bitrate); + SetSignalHint(m_signal_hint); +} + +int32_t OpusEncoder::GetBitrate() const noexcept { + return m_bitrate; +} + +EncodingApplication OpusEncoder::GetEncodingApplication() const noexcept { + return m_application; +} + +SignalHint OpusEncoder::GetSignalHint() const noexcept { + return m_signal_hint; +} + +template +bool OpusEncoder::OpusCTL(std::string_view request, Args&& ...args) noexcept { + auto error = opus_encoder_ctl(m_encoder.get(), std::forward(args)...); + if (error != OPUS_OK) { + spdlog::get("voice")->error("Opus encoder CTL error ({}): {}", request, opus_strerror(error)); + return false; + } + + return true; +} + +} diff --git a/src/audio/voice/opus/opus_encoder.hpp b/src/audio/voice/opus/opus_encoder.hpp new file mode 100644 index 00000000..d777cafb --- /dev/null +++ b/src/audio/voice/opus/opus_encoder.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "audio/utils.hpp" + +namespace AbaddonClient::Audio::Voice::Opus { + +using OpusOutput = Slice; + +class OpusEncoder { +public: + enum class EncodingApplication { + Audio = OPUS_APPLICATION_AUDIO, + LowDelay = OPUS_APPLICATION_RESTRICTED_LOWDELAY, + VOIP = OPUS_APPLICATION_VOIP + }; + + enum class SignalHint { + Auto = OPUS_AUTO, + Voice = OPUS_SIGNAL_VOICE, + Music = OPUS_SIGNAL_MUSIC + }; + + struct EncoderSettings { + int32_t sample_rate = 48000; + int channels = 2; + int32_t bitrate = 64000; + SignalHint signal_hint = SignalHint::Auto; + EncodingApplication application = EncodingApplication::VOIP; + }; + + static std::optional Create(EncoderSettings settings) noexcept; + + int Encode(InputBuffer &&audio, OpusOutput &&opus, int frame_size) noexcept; + void ResetState() noexcept; + + void SetBitrate(int32_t bitrate) noexcept; + void SetSignalHint(SignalHint hint) noexcept; + void SetEncodingApplication(EncodingApplication application) noexcept; + + int32_t GetBitrate() const noexcept; + SignalHint GetSignalHint() const noexcept; + EncodingApplication GetEncodingApplication() const noexcept; +private: + struct EncoderDeleter { + void operator()(::OpusEncoder* ptr) noexcept { + opus_encoder_destroy(ptr); + } + }; + + using EncoderPtr = std::unique_ptr<::OpusEncoder, EncoderDeleter>; + OpusEncoder(EncoderPtr encoder, EncoderSettings settings) noexcept; + + template + bool OpusCTL(std::string_view request, Args&& ...args) noexcept; + + EncoderPtr m_encoder; + + const int32_t m_sample_rate; + const int m_channels; + + int32_t m_bitrate; + SignalHint m_signal_hint; + EncodingApplication m_application; +}; + +} diff --git a/src/audio/voice/peak_meter/peak_meter.cpp b/src/audio/voice/peak_meter/peak_meter.cpp new file mode 100644 index 00000000..8d087533 --- /dev/null +++ b/src/audio/voice/peak_meter/peak_meter.cpp @@ -0,0 +1,35 @@ +#include "peak_meter.hpp" + +#include + +namespace AbaddonClient::Audio::Voice { + +PeakMeter::PeakMeter() noexcept { + Glib::signal_timeout().connect(sigc::mem_fun(*this, &PeakMeter::Decay), 40); +} + +void PeakMeter::UpdatePeak(InputBuffer buffer) noexcept { + // Cache to prevent atomic operations in the loop + float peak = m_peak; + + for (const auto& sample: buffer) { + peak = std::max(peak, std::abs(sample)); + } + + m_peak = peak; +} + +void PeakMeter::SetPeak(float peak) noexcept { + m_peak = std::max(m_peak.load(), peak); +} + +float PeakMeter::GetPeak() const noexcept { + return m_peak; +} + +bool PeakMeter::Decay() noexcept { + m_peak = std::max(m_peak - 0.05f, 0.0f); + return true; +} + +} diff --git a/src/audio/voice/peak_meter/peak_meter.hpp b/src/audio/voice/peak_meter/peak_meter.hpp new file mode 100644 index 00000000..954121f4 --- /dev/null +++ b/src/audio/voice/peak_meter/peak_meter.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "audio/utils.hpp" + +namespace AbaddonClient::Audio::Voice { + +class PeakMeter { +public: + PeakMeter() noexcept; + + void UpdatePeak(InputBuffer buffer) noexcept; + + void SetPeak(float peak) noexcept; + float GetPeak() const noexcept; +private: + bool Decay() noexcept; + + std::atomic m_peak = 0; +}; + +}; diff --git a/src/audio/voice/playback/client.cpp b/src/audio/voice/playback/client.cpp new file mode 100644 index 00000000..2f7e7e1f --- /dev/null +++ b/src/audio/voice/playback/client.cpp @@ -0,0 +1,99 @@ +#include "client.hpp" +#include "constants.hpp" + +#include "abaddon.hpp" + +namespace AbaddonClient::Audio::Voice::Playback { + +void client_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { + const auto playback_active = Abaddon::Get().GetAudio().GetVoice().GetPlayback().GetActive(); + if (!playback_active) { + return; + } + + auto client = static_cast(pDevice->pUserData); + auto buffer = OutputBuffer(static_cast(pOutput), frameCount * RTP_CHANNELS); + + client->WriteAudio(buffer); + AudioUtils::ClampToFloatRange(buffer); +} + +Client::Client(Context &context, Opus::OpusDecoder &&decoder, VoiceBuffer &&buffer, DecodePool &decode_pool) noexcept : + m_device(context, GetDeviceConfig(), context.GetActivePlaybackID()), + m_decoder( std::make_shared< Mutex >(std::move(decoder)) ), + m_buffer( std::make_shared( std::move(buffer) )), + m_decode_pool(decode_pool) +{ + if (Abaddon::Get().GetSettings().SeparateSources) { + m_device.Start(); + } +} + +void Client::Decode(std::vector &&rtp) noexcept { + if (m_muted) { + return; + } + + auto decode_data = DecodePool::DecodeData { + std::move(rtp), + m_decoder, + m_buffer, + }; + + m_decode_pool.Decode(std::move(decode_data)); +} + +void Client::WriteAudio(OutputBuffer buffer) noexcept { + if (m_muted) { + return; + } + + m_buffer->Read(buffer); + AudioUtils::ApplyGain(buffer, Volume); + + m_peak_meter.UpdatePeak(buffer); +} + +void Client::ClearBuffer() noexcept { + m_buffer->Clear(); +} + +void Client::SetMuted(bool muted) noexcept { + if (muted) { + // Clear the buffer to prevent residue samples playing back later + ClearBuffer(); + } + + m_muted = muted; +} + +bool Client::GetMuted() const noexcept { + return m_muted; +} + +PeakMeter& Client::GetPeakMeter() noexcept { + return m_peak_meter; +} + +const PeakMeter& Client::GetPeakMeter() const noexcept { + return m_peak_meter; +} + +ma_device_config Client::GetDeviceConfig() noexcept { + auto config = ma_device_config_init(ma_device_type_playback); + config.playback.format = RTP_FORMAT; + config.sampleRate = RTP_SAMPLE_RATE; + config.playback.channels = RTP_CHANNELS; + config.pulse.pStreamNamePlayback = "Abaddon (User)"; + + config.noClip = true; + config.noFixedSizedCallback = true; + config.performanceProfile = ma_performance_profile_low_latency; + + config.dataCallback = client_callback; + config.pUserData = this; + + return config; +} + +} diff --git a/src/audio/voice/playback/client.hpp b/src/audio/voice/playback/client.hpp new file mode 100644 index 00000000..55392cdd --- /dev/null +++ b/src/audio/voice/playback/client.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "audio/voice/opus/opus_decoder.hpp" +#include "audio/voice/peak_meter/peak_meter.hpp" + +#include "audio/audio_device.hpp" +#include "audio/context.hpp" + +#include "decode_pool.hpp" + +namespace AbaddonClient::Audio::Voice::Playback { + +class Client { +public: + Client(Context &context, Opus::OpusDecoder &&decoder, VoiceBuffer &&buffer, DecodePool &decode_pool) noexcept; + + void Decode(std::vector &&rtp) noexcept; + void WriteAudio(OutputBuffer output) noexcept; + + void ClearBuffer() noexcept; + + void SetMuted(bool muted) noexcept; + bool GetMuted() const noexcept; + + PeakMeter& GetPeakMeter() noexcept; + const PeakMeter& GetPeakMeter() const noexcept; + + std::atomic Volume = 1.0; + +private: + ma_device_config GetDeviceConfig() noexcept; + + AudioDevice m_device; + + SharedDecoder m_decoder; + SharedBuffer m_buffer; + + DecodePool &m_decode_pool; + PeakMeter m_peak_meter; + + std::atomic m_muted = false; + + friend void client_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount); +}; + +} diff --git a/src/audio/voice/playback/client_store.cpp b/src/audio/voice/playback/client_store.cpp new file mode 100644 index 00000000..68ccc782 --- /dev/null +++ b/src/audio/voice/playback/client_store.cpp @@ -0,0 +1,148 @@ +#include "client_store.hpp" +#include "constants.hpp" + +#include + +#include "abaddon.hpp" + +namespace AbaddonClient::Audio::Voice::Playback { + +void ClientStore::AddClient(ClientID id) noexcept { + auto clients = m_clients.Lock(); + + if (clients->find(id) != clients->end()) { + spdlog::get("voice")->error("Tried to add an existing client: {}", id); + return; + }; + + Opus::OpusDecoder::DecoderSettings settings; + settings.sample_rate = RTP_SAMPLE_RATE; + settings.channels = RTP_CHANNELS; + + auto decoder = Opus::OpusDecoder::Create(settings); + if (!decoder) { + return; + } + + auto buffer = VoiceBuffer::Create(RTP_CHANNELS, RTP_OPUS_FRAME_SIZE * 4, 1024); + if (!buffer) { + return; + } + + m_decode_pool.AddDecoder(); + + auto &context = Abaddon::Get().GetAudio().GetContext(); + + clients->emplace( + std::piecewise_construct, + std::forward_as_tuple(id), + std::forward_as_tuple(context, std::move(*decoder), std::move(*buffer), m_decode_pool) + ); +} + +void ClientStore::RemoveClient(ClientID id) noexcept { + auto clients = m_clients.Lock(); + clients->erase(id); + + // Keep the decoder count below client count + // Doesn't make sense to have more + if (m_decode_pool.GetDecoderCount() > clients->size()) { + m_decode_pool.RemoveDecoder(); + } +} + +void ClientStore::Clear() noexcept { + m_decode_pool.ClearDecoders(); + m_clients.Lock()->clear(); +} + +void ClientStore::Decode(ClientID id, std::vector &&data) noexcept { + auto clients = m_clients.Lock(); + auto client = clients->find(id); + + if (client == clients->end()) { + spdlog::get("voice")->error("Tried to decode Opus data for missing client: {}", id); + return; + } + + client->second.Decode(std::move(data)); +} + +void ClientStore::WriteMixed(OutputBuffer buffer) noexcept { + auto clients = m_clients.Lock(); + + // Reusing per client buffer, + // miniaudio doesn't provide a way to know the maximum buffer size in advance + // so we have to resize it dynamically. + // This shouldn't have a big performance hit as the capacity will settle on some value eventually + + for (auto& [it, client] : clients) { + m_client_buffer.resize(buffer.size()); + + client.WriteAudio(m_client_buffer); + AudioUtils::MixBuffers(m_client_buffer, buffer); + + m_client_buffer.clear(); + } +} + +void ClientStore::SetClientVolume(ClientID id, float volume) noexcept { + auto clients = m_clients.Lock(); + auto client = clients->find(id); + + if (client != clients->end()) { + client->second.Volume = volume; + } +} + +void ClientStore::ClearAllBuffers() noexcept { + auto clients = m_clients.Lock(); + + for (auto& [_, client] : clients) { + client.ClearBuffer(); + } +} + +void ClientStore::SetClientMute(ClientID id, bool muted) noexcept { + auto clients = m_clients.Lock(); + auto client = clients->find(id); + + if (client != clients->end()) { + client->second.SetMuted(muted); + } +} + +float ClientStore::GetClientVolume(ClientID id) const noexcept { + const auto clients = m_clients.Lock(); + const auto client = clients->find(id); + + if (client != clients->end()) { + return client->second.Volume; + } + + return 1.0; +} + +bool ClientStore::GetClientMute(ClientID id) const noexcept { + const auto clients = m_clients.Lock(); + const auto client = clients->find(id); + + if (client != clients->end()) { + return client->second.GetMuted(); + } + + return false; +} + +float ClientStore::GetClientPeakVolume(ClientID id) const noexcept { + const auto clients = m_clients.Lock(); + const auto client = clients->find(id); + + if (client != clients->end()) { + return client->second.GetPeakMeter().GetPeak(); + } + + return 0.0f; +} + +} diff --git a/src/audio/voice/playback/client_store.hpp b/src/audio/voice/playback/client_store.hpp new file mode 100644 index 00000000..a7d7c81b --- /dev/null +++ b/src/audio/voice/playback/client_store.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include "audio/utils.hpp" +#include "misc/mutex.hpp" + +#include "client.hpp" +#include "decode_pool.hpp" + +// This needs to be forward declared for friend declaration +namespace AbaddonClient::Audio::Voice { + class VoicePlayback; +} + +namespace AbaddonClient::Audio::Voice::Playback { + + +class ClientStore { +public: + using ClientID = uint32_t; + + void AddClient(ClientID id) noexcept; + void RemoveClient(ClientID id) noexcept; + void Clear() noexcept; + + void ClearAllBuffers() noexcept; + + void SetClientVolume(ClientID id, float volume) noexcept; + void SetClientMute(ClientID id, bool muted) noexcept; + + float GetClientVolume(ClientID id) const noexcept; + bool GetClientMute(ClientID id) const noexcept; + + float GetClientPeakVolume(ClientID id) const noexcept; + +private: + using ClientMap = std::unordered_map; + + // Keep these two private and expose through VoicePlayback + void Decode(ClientID id, std::vector &&data) noexcept; + void WriteMixed(OutputBuffer buffer) noexcept; + + DecodePool m_decode_pool; + Mutex m_clients; + + std::vector m_client_buffer; + + friend Voice::VoicePlayback; +}; + +} diff --git a/src/audio/voice/playback/constants.hpp b/src/audio/voice/playback/constants.hpp new file mode 100644 index 00000000..5d0199e2 --- /dev/null +++ b/src/audio/voice/playback/constants.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +constexpr ma_format RTP_FORMAT = ma_format_f32; +constexpr uint32_t RTP_SAMPLE_RATE = 48000; +constexpr uint32_t RTP_CHANNELS = 2; +constexpr uint32_t RTP_OPUS_FRAME_SIZE = 5760; +constexpr uint32_t RTP_OPUS_MAX_BUFFER_SIZE = RTP_OPUS_FRAME_SIZE * RTP_CHANNELS; diff --git a/src/audio/voice/playback/decode_pool.cpp b/src/audio/voice/playback/decode_pool.cpp new file mode 100644 index 00000000..7e12dcf8 --- /dev/null +++ b/src/audio/voice/playback/decode_pool.cpp @@ -0,0 +1,60 @@ +#include "decode_pool.hpp" +#include "constants.hpp" + +#include + +namespace AbaddonClient::Audio::Voice::Playback { + + + +DecodePool::DecodePool() noexcept : + m_pool(Pool(&DecodePool::DecodeThread, 20)) {} + +void DecodePool::Decode(DecodeData &&decode_data) noexcept { + m_pool.SendToPool(std::move(decode_data)); +} + +void DecodePool::AddDecoder() noexcept { + m_pool.AddThread(); +} + +void DecodePool::RemoveDecoder() noexcept { + m_pool.RemoveThread(); +} + +void DecodePool::ClearDecoders() noexcept { + m_pool.Clear(); +} + +size_t DecodePool::GetDecoderCount() const noexcept { + return m_pool.GetThreadCount(); +} + +void DecodePool::DecodeThread(Channel &channel) noexcept { + while (true) { + auto message = channel.Recv(); + if (std::holds_alternative(message)) { + break; + } + + auto&& decode_message = std::get(message); + OnDecodeMessage(std::move(decode_message)); + } +} + +void DecodePool::OnDecodeMessage(DecodeData &&message) noexcept { + auto& [opus, decoder, buffer] = message; + + static std::array pcm; + + int frames = decoder->Lock()->Decode(opus, pcm, RTP_OPUS_FRAME_SIZE); + if (frames < 0) { + return; + } + + // Take only what we've got from the decoder + auto pcm_written = OutputBuffer(pcm.data(), frames * RTP_CHANNELS); + buffer->Write(pcm_written); +} + +} diff --git a/src/audio/voice/playback/decode_pool.hpp b/src/audio/voice/playback/decode_pool.hpp new file mode 100644 index 00000000..cf2b625b --- /dev/null +++ b/src/audio/voice/playback/decode_pool.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "audio/voice/opus/opus_decoder.hpp" +#include "voice_buffer.hpp" + +#include "misc/threadpool.hpp" + +namespace AbaddonClient::Audio::Voice::Playback { + +using SharedDecoder = std::shared_ptr>; +using SharedBuffer = std::shared_ptr; + +class DecodePool { +public: + DecodePool() noexcept; + + struct DecodeData { + std::vector opus; + + SharedDecoder decoder; + SharedBuffer buffer; + }; + + void Decode(DecodeData &&decode_data) noexcept; + + void AddDecoder() noexcept; + void RemoveDecoder() noexcept; + void ClearDecoders() noexcept; + + size_t GetDecoderCount() const noexcept; + +private: + static void DecodeThread(Channel> &channel) noexcept; + static void OnDecodeMessage(DecodeData &&decode_data) noexcept; + + using Pool = ThreadPool; + + Pool m_pool; +}; + +} diff --git a/src/audio/voice/playback/voice_buffer.cpp b/src/audio/voice/playback/voice_buffer.cpp new file mode 100644 index 00000000..4d0accc5 --- /dev/null +++ b/src/audio/voice/playback/voice_buffer.cpp @@ -0,0 +1,35 @@ +#include "voice_buffer.hpp" + +namespace AbaddonClient::Audio::Voice::Playback { + +VoiceBuffer::VoiceBuffer(Miniaudio::MaPCMRingBuffer &&ringbuffer, uint32_t buffer_frames) noexcept : + m_ringbuffer(std::move(ringbuffer)), + m_buffer_frames(buffer_frames) {} + +std::optional VoiceBuffer::Create(uint32_t channels, uint32_t buffer_size_in_frames, uint32_t buffer_frames) noexcept { + auto ringbuffer = Miniaudio::MaPCMRingBuffer::Create(channels, buffer_size_in_frames); + + if (!ringbuffer) { + return std::nullopt; + } + + return VoiceBuffer(std::move(*ringbuffer), buffer_frames); +} + +void VoiceBuffer::Read(OutputBuffer output) noexcept { + if (m_ringbuffer.GetAvailableReadFrames() < m_buffer_frames) { + return; + } + + m_ringbuffer.Read(output); +} + +void VoiceBuffer::Write(InputBuffer input) noexcept { + m_ringbuffer.Write(input); +} + +void VoiceBuffer::Clear() noexcept { + m_ringbuffer.Clear(); +} + +} diff --git a/src/audio/voice/playback/voice_buffer.hpp b/src/audio/voice/playback/voice_buffer.hpp new file mode 100644 index 00000000..473c2b99 --- /dev/null +++ b/src/audio/voice/playback/voice_buffer.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "audio/miniaudio/ma_pcm_rb.hpp" + +namespace AbaddonClient::Audio::Voice::Playback { + +class VoiceBuffer { +public: + static std::optional Create(uint32_t channels, uint32_t buffer_size_in_frames, uint32_t buffer_frames) noexcept; + + void Read(OutputBuffer output) noexcept; + void Write(InputBuffer input) noexcept; + void Clear() noexcept; + +private: + VoiceBuffer(Miniaudio::MaPCMRingBuffer &&ringbuffer, uint32_t buffer_frames) noexcept; + + Miniaudio::MaPCMRingBuffer m_ringbuffer; + const uint32_t m_buffer_frames; +}; + +} diff --git a/src/audio/voice/playback/voice_playback.cpp b/src/audio/voice/playback/voice_playback.cpp new file mode 100644 index 00000000..a22d3689 --- /dev/null +++ b/src/audio/voice/playback/voice_playback.cpp @@ -0,0 +1,115 @@ +#include "voice_playback.hpp" +#include "constants.hpp" + +#include "abaddon.hpp" + +namespace AbaddonClient::Audio::Voice { + +void playback_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { + auto playback = static_cast(pDevice->pUserData); + if (playback == nullptr) { + return; + } + + auto buffer = OutputBuffer(static_cast(pOutput), frameCount * RTP_CHANNELS); + playback->OnAudioPlayback(buffer); +} + +VoicePlayback::VoicePlayback(Context &context, DiscordClient &discord) noexcept : + m_device(context, GetDeviceConfig(), context.GetActivePlaybackID()), + m_voice_client(discord.GetVoiceClient()) +{ + m_voice_client.signal_speaking() + .connect(sigc::mem_fun(*this, &VoicePlayback::OnUserSpeaking)); + + discord.signal_voice_user_disconnect() + .connect(sigc::mem_fun(*this, &VoicePlayback::OnUserDisconnect)); +} + +void VoicePlayback::OnOpusData(ClientID id, std::vector &&data) noexcept { + if (m_active) { + m_clients.Decode(id, std::move(data)); + } +} + +void VoicePlayback::OnAudioPlayback(OutputBuffer buffer) noexcept { + if (m_active) { + m_clients.WriteMixed(buffer); + AudioUtils::ClampToFloatRange(buffer); // Clamp it at the end + } +} + +void VoicePlayback::OnUserSpeaking(const VoiceSpeakingData &speaking_data) noexcept { + m_clients.AddClient(speaking_data.SSRC); + + const auto volume = m_voice_client.GetUserVolume(speaking_data.UserID); + m_clients.SetClientVolume(speaking_data.SSRC, volume); +} + +void VoicePlayback::OnUserDisconnect(Snowflake user_id, Snowflake channel_id) noexcept { + const auto ssrc = m_voice_client.GetSSRCOfUser(user_id); + if (ssrc) { + m_clients.RemoveClient(*ssrc); + } +} + +void VoicePlayback::Start() noexcept { + if (Abaddon::Get().GetSettings().SeparateSources) { + return; + } + + m_device.Start(); +} + +void VoicePlayback::Stop() noexcept { + m_device.Stop(); +} + +void VoicePlayback::SetActive(bool active) noexcept { + if (!active) { + // Clear all buffers to prevent residue samples playing back later + m_clients.ClearAllBuffers(); + } + + m_active = active; +} + +bool VoicePlayback::GetActive() const noexcept { + return m_active; +} + +void VoicePlayback::SetPlaybackDevice(const ma_device_id &device_id) noexcept { + spdlog::get("voice")->info("Setting playback device"); + + const auto success = m_device.ChangeDevice(device_id); + if (!success) { + spdlog::get("voice")->error("Failed to set playback device"); + } +} + +Playback::ClientStore& VoicePlayback::GetClientStore() noexcept { + return m_clients; +} + +const Playback::ClientStore& VoicePlayback::GetClientStore() const noexcept { + return m_clients; +} + +ma_device_config VoicePlayback::GetDeviceConfig() noexcept { + auto config = ma_device_config_init(ma_device_type_playback); + config.playback.format = RTP_FORMAT; + config.sampleRate = RTP_SAMPLE_RATE; + config.playback.channels = RTP_CHANNELS; + config.pulse.pStreamNamePlayback = "Abaddon (Voice)"; + + config.noClip = true; + config.noFixedSizedCallback = true; + config.performanceProfile = ma_performance_profile_low_latency; + + config.dataCallback = playback_callback; + config.pUserData = this; + + return config; +} + +} diff --git a/src/audio/voice/playback/voice_playback.hpp b/src/audio/voice/playback/voice_playback.hpp new file mode 100644 index 00000000..2f3f7439 --- /dev/null +++ b/src/audio/voice/playback/voice_playback.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include "discord/snowflake.hpp" +#include "discord/discord.hpp" +#include "discord/voiceclient.hpp" + +#include "audio/audio_device.hpp" +#include "audio/context.hpp" +#include "audio/utils.hpp" + +#include "client_store.hpp" + +namespace AbaddonClient::Audio::Voice { + +class VoicePlayback : public sigc::trackable { +public: + using ClientID = Playback::ClientStore::ClientID; + + VoicePlayback(Context &context, DiscordClient &discord) noexcept; + + void OnOpusData(ClientID id, std::vector &&data) noexcept; + + void Start() noexcept; + void Stop() noexcept; + + void SetActive(bool active) noexcept; + bool GetActive() const noexcept; + + void SetPlaybackDevice(const ma_device_id &device_id) noexcept; + + Playback::ClientStore& GetClientStore() noexcept; + const Playback::ClientStore& GetClientStore() const noexcept; + +private: + void OnAudioPlayback(OutputBuffer buffer) noexcept; + + void OnUserSpeaking(const VoiceSpeakingData &speaking_data) noexcept; + void OnUserDisconnect(Snowflake user_id, Snowflake channel_id) noexcept; + + ma_device_config GetDeviceConfig() noexcept; + + DiscordVoiceClient &m_voice_client; + + AudioDevice m_device; + Playback::ClientStore m_clients; + + std::atomic m_active = true; + + friend void playback_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount); +}; + +}; diff --git a/src/audio/voice/voice_audio.cpp b/src/audio/voice/voice_audio.cpp new file mode 100644 index 00000000..07fb171b --- /dev/null +++ b/src/audio/voice/voice_audio.cpp @@ -0,0 +1,47 @@ +#include "voice_audio.hpp" + +namespace AbaddonClient::Audio { + +using VoicePlayback = Voice::VoicePlayback; +using VoiceCapture = Voice::VoiceCapture; + +VoiceAudio::VoiceAudio(Context &context, DiscordClient &discord) noexcept : + m_playback(context, discord), + m_capture(context) +{ + auto& voice_client = discord.GetVoiceClient(); + + voice_client.signal_connected() + .connect(sigc::mem_fun(*this, &VoiceAudio::Start)); + + voice_client.signal_disconnected() + .connect(sigc::mem_fun(*this, &VoiceAudio::Stop)); +} + +void VoiceAudio::Start() noexcept { + m_playback.Start(); + m_capture.Start(); +} + +void VoiceAudio::Stop() noexcept { + m_playback.Stop(); + m_capture.Stop(); +} + +VoicePlayback& VoiceAudio::GetPlayback() noexcept { + return m_playback; +} + +const VoicePlayback& VoiceAudio::GetPlayback() const noexcept { + return m_playback; +} + +VoiceCapture& VoiceAudio::GetCapture() noexcept { + return m_capture; +} + +const VoiceCapture& VoiceAudio::GetCapture() const noexcept { + return m_capture; +} + +} diff --git a/src/audio/voice/voice_audio.hpp b/src/audio/voice/voice_audio.hpp new file mode 100644 index 00000000..0ed165f3 --- /dev/null +++ b/src/audio/voice/voice_audio.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include "discord/discord.hpp" + +#include "audio/context.hpp" + +#include "capture/voice_capture.hpp" +#include "playback/voice_playback.hpp" + +namespace AbaddonClient::Audio { + +class VoiceAudio : public sigc::trackable { +public: + using VoicePlayback = Voice::VoicePlayback; + using VoiceCapture = Voice::VoiceCapture; + + VoiceAudio(Context &context, DiscordClient &discord) noexcept; + + void Start() noexcept; + void Stop() noexcept; + + VoicePlayback& GetPlayback() noexcept; + const VoicePlayback& GetPlayback() const noexcept; + + VoiceCapture& GetCapture() noexcept; + const VoiceCapture& GetCapture() const noexcept; + +private: + VoiceCapture m_capture; + VoicePlayback m_playback; +}; + +} diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index f021e49f..ec309566 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -50,8 +50,7 @@ void UDPSocket::SetSSRC(uint32_t ssrc) { void UDPSocket::SendEncrypted(const uint8_t *data, size_t len) { m_sequence++; - const uint32_t timestamp = Abaddon::Get().GetAudio().GetRTPTimestamp(); - + const uint32_t timestamp = Abaddon::Get().GetAudio().GetVoice().GetCapture().GetRTPTimestamp(); std::vector rtp(12 + len + crypto_secretbox_MACBYTES, 0); rtp[0] = 0x80; // ver 2 rtp[1] = 0x78; // payload type 0x78 @@ -152,11 +151,10 @@ DiscordVoiceClient::DiscordVoiceClient() // idle or else singleton deadlock Glib::signal_idle().connect_once([this]() { - auto &audio = Abaddon::Get().GetAudio(); - audio.SetOpusBuffer(m_opus_buffer.data()); - audio.signal_opus_packet().connect([this](int payload_size) { + auto &capture = Abaddon::Get().GetAudio().GetVoice().GetCapture(); + capture.GetCaptureSignal().connect([this](const std::vector &opus) { if (IsConnected()) { - m_udp.SendEncrypted(m_opus_buffer.data(), payload_size); + m_udp.SendEncrypted(opus.data(), opus.size()); } }); }); @@ -222,9 +220,6 @@ void DiscordVoiceClient::SetUserID(Snowflake id) { void DiscordVoiceClient::SetUserVolume(Snowflake id, float volume) { m_user_volumes[id] = volume; - if (const auto ssrc = GetSSRCOfUser(id); ssrc.has_value()) { - Abaddon::Get().GetAudio().SetVolumeSSRC(*ssrc, volume); - } } [[nodiscard]] float DiscordVoiceClient::GetUserVolume(Snowflake id) const { @@ -347,7 +342,7 @@ void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) { // set volume if already set but ssrc just found if (const auto iter = m_user_volumes.find(d.UserID); iter != m_user_volumes.end()) { if (m_ssrc_map.find(d.UserID) == m_ssrc_map.end()) { - Abaddon::Get().GetAudio().SetVolumeSSRC(d.SSRC, iter->second); + Abaddon::Get().GetAudio().GetVoice().GetPlayback().GetClientStore().SetClientVolume(d.SSRC, iter->second); } } @@ -484,13 +479,16 @@ void DiscordVoiceClient::OnUDPData(std::vector data) { (data[11] << 0); static std::array nonce = {}; std::memcpy(nonce.data(), data.data(), 12); - if (crypto_secretbox_open_easy(payload, payload, data.size() - 12, nonce.data(), m_secret_key.data())) { - // spdlog::get("voice")->trace("UDP payload decryption failure"); - } else { - size_t opus_offset = GetPayloadOffset(data.data(), data.size()); - payload = data.data() + opus_offset; - Abaddon::Get().GetAudio().FeedMeOpus(ssrc, { payload, payload + data.size() - opus_offset - crypto_box_MACBYTES }); + + const int error = crypto_secretbox_open_easy(payload, payload, data.size() - 12, nonce.data(), m_secret_key.data()); + if (error) { + return; } + + const size_t opus_offset = GetPayloadOffset(data.data(), data.size()); + payload = data.data() + opus_offset; + + Abaddon::Get().GetAudio().GetVoice().GetPlayback().OnOpusData(ssrc, { payload, payload + data.size() - opus_offset - crypto_box_MACBYTES }); } void DiscordVoiceClient::OnDispatch() { diff --git a/src/misc/channel.hpp b/src/misc/channel.hpp new file mode 100644 index 00000000..a3e9de51 --- /dev/null +++ b/src/misc/channel.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include "mutex.hpp" + +// Channel with bounded queue size +// Discards the data if the queue is full +template +class Channel { +public: + Channel(size_t capacity) noexcept : m_capacity(capacity) {} + + [[nodiscard]] T Recv() noexcept { + auto queue = m_queue.Lock(); + + if (queue->empty()) { + queue.cond_wait(m_condition_var); + } + + auto data = std::move(queue->front()); + queue->pop_front(); + + return data; + }; + + void Send(T &&data) noexcept { + auto queue = m_queue.Lock(); + + if (queue->size() == m_capacity) { + return; + } + + queue->push_back(std::forward(data)); + m_condition_var.notify_one(); + } + + void ClearQueue() { + m_queue.Lock()->clear(); + } + +private: + Mutex> m_queue; + size_t m_capacity; + + std::condition_variable m_condition_var; +}; diff --git a/src/misc/mutex.hpp b/src/misc/mutex.hpp new file mode 100644 index 00000000..1f45a1be --- /dev/null +++ b/src/misc/mutex.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include + +#include + +// RAII style mutex guard +// Unlocks mutex upon destruction +template +class MutexGuard { +public: + MutexGuard(T &object, std::unique_lock &&lock) : + m_object(object), + m_lock(std::move(lock)) {} + + MutexGuard(MutexGuard &&other) noexcept : + m_lock(std::move(other.m_lock)), + m_object(other.m_object) {} + + // Only implement arrow operator + // Dereference operator would expose the inner object + T* operator->() noexcept { + return &m_object; + } + + const T* operator->() const noexcept { + return &m_object; + } + + void cond_wait(std::condition_variable &condition_var) noexcept { + condition_var.wait(m_lock); + } + + // Provide methods for container types + auto& operator[](size_t index) noexcept { + return m_object[index]; + } + + const auto& operator[](size_t index) const noexcept { + return m_object[index]; + } + + auto begin() noexcept { + return m_object.begin(); + } + + auto end() noexcept { + return m_object.end(); + } + + auto begin() const noexcept { + return m_object.begin(); + } + + auto end() const noexcept { + return m_object.end(); + } + +private: + std::unique_lock m_lock; + T &m_object; +}; + +// RAII style mutex +// Owns the provided object +// Use Lock with MutexGuard or LockScope with closure to access protected object +template +class Mutex { + +public: + Mutex(T&& object) : m_object(std::forward(object)) {} + + template + Mutex(Args&&... args) : m_object( T(std::forward(args)...) ) {} + + Mutex(const Mutex&) = delete; + + template + auto LockScope(C closure) noexcept { + std::scoped_lock lock(m_mutex); + return closure(m_object); + } + + template + auto LockScope(C closure) const noexcept { + std::scoped_lock lock(m_mutex); + return closure(m_object); + } + + [[nodiscard]] MutexGuard Lock() noexcept { + std::unique_lock lock(m_mutex); + return MutexGuard(m_object, std::move(lock)); + } + + // Make sure to return const MutexGuard here + [[nodiscard]] const MutexGuard Lock() const noexcept { + std::unique_lock lock(m_mutex); + return MutexGuard(m_object, std::move(lock)); + } + +private: + mutable std::mutex m_mutex; + mutable T m_object; // Needs to be mutable for const to work +}; diff --git a/src/misc/slice.hpp b/src/misc/slice.hpp new file mode 100644 index 00000000..fbb83ac2 --- /dev/null +++ b/src/misc/slice.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include + +// C++20 span-like slice +// T has to be contigious array-like initialized structure with size of "size * sizeof(T)" +template +class Slice { +public: + using iterator = T*; + using const_iterator = const T*; + + Slice(T* ref, size_t size) : m_ref(ref), m_size(size) {} + Slice(T &ref, size_t size) : m_ref(&ref), m_size(size) {} + + template + Slice(std::array &array) : m_ref(array.data()), m_size(SIZE) {} + + Slice(std::vector &vec) : m_ref(vec.data()), m_size(vec.size()) {} + + T& operator[](size_t i) noexcept { + return m_ref[i]; + } + + const T& operator[](size_t i) const noexcept { + return m_ref[i]; + } + + constexpr iterator begin() noexcept { + return iterator(m_ref); + } + + constexpr const_iterator begin() const noexcept { + return const_iterator(m_ref); + } + + constexpr const_iterator cbegin() const noexcept { + return begin(); + } + + constexpr iterator end() noexcept { + return iterator(m_ref + m_size); + } + + constexpr const_iterator end() const noexcept { + return const_iterator(m_ref + m_size); + } + + constexpr const_iterator cend() const noexcept { + return end(); + } + + constexpr T* data() noexcept { + return m_ref; + } + + constexpr const T* data() const noexcept { + return m_ref; + } + + constexpr size_t size() const noexcept { + return m_size; + } +private: + T* m_ref; + const size_t m_size; +}; + +template +class ConstSlice { +public: + using iterator = const T*; + + ConstSlice(const T* ref, size_t size) : m_ref(ref), m_size(size) {} + ConstSlice(const T& ref, size_t size) : m_ref(&ref), m_size(size) {} + ConstSlice(Slice view) : m_ref(view.data()), m_size(view.size()) {} + + template + ConstSlice(const std::array &array) : m_ref(array.data()), m_size(SIZE) {} + + ConstSlice(const std::vector &vec) : m_ref(vec.data()), m_size(vec.size()) {} + + const T& operator[](size_t i) const noexcept { + return m_ref[i]; + } + + constexpr iterator begin() const noexcept { + return iterator(m_ref); + } + + constexpr iterator end() const noexcept { + return iterator(m_ref + m_size); + } + + constexpr const T* data() const noexcept { + return m_ref; + } + + constexpr size_t size() const noexcept { + return m_size; + } +private: + const T* m_ref; + const size_t m_size; +}; diff --git a/src/misc/threadpool.hpp b/src/misc/threadpool.hpp new file mode 100644 index 00000000..3649558e --- /dev/null +++ b/src/misc/threadpool.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include "thread" +#include "variant" + +#include "channel.hpp" + +using Terminate = std::monostate; + +template +class ThreadPool { +public: + using ThreadMessage = std::variant; + + ThreadPool(Callable callable, size_t channel_capacity) noexcept : + m_callable(std::move(callable)), + m_channel(Channel(channel_capacity)) + { + auto concurrency = std::thread::hardware_concurrency() / 2; + if (concurrency == 0) { + concurrency = 2; + } + + MaxThreads = concurrency; + } + + ~ThreadPool() noexcept { + Clear(); + } + + void AddThread() noexcept { + if (GetThreadCount() == MaxThreads) { + return; + } + + m_threads.emplace(m_threads.end(), m_callable, std::ref(m_channel)); + m_threads.back().detach(); + } + + void RemoveThread() { + if (m_threads.empty()) { + return; + } + + m_channel.Send(Terminate()); + m_threads.pop_back(); + } + + void Clear() { + const auto thread_count = GetThreadCount(); + + // Clear the queue and send termination messages to remaining threads + m_channel.ClearQueue(); + for (int i = 0; i < thread_count; i++) { + m_channel.Send(Terminate()); + } + + m_threads.clear(); + } + + size_t GetThreadCount() const noexcept { + return m_threads.size(); + } + + void SendToPool(ThreadData &&data) { + m_channel.Send(std::move(data)); + } + + size_t MaxThreads; + +private: + std::vector m_threads; + Channel m_channel; + Callable m_callable; +}; diff --git a/src/notifications/notifier.hpp b/src/notifications/notifier.hpp index cd3eb71d..f0a20306 100644 --- a/src/notifications/notifier.hpp +++ b/src/notifications/notifier.hpp @@ -2,20 +2,8 @@ #include #include -#ifdef ENABLE_NOTIFICATION_SOUNDS - #include -#endif - class Notifier { public: - Notifier(); - ~Notifier(); - void Notify(const Glib::ustring &id, const Glib::ustring &title, const Glib::ustring &text, const Glib::ustring &default_action, const std::string &icon_path); void Withdraw(const Glib::ustring &id); - -private: -#ifdef ENABLE_NOTIFICATION_SOUNDS - ma_engine m_engine; -#endif }; diff --git a/src/notifications/notifier_fallback.cpp b/src/notifications/notifier_fallback.cpp index 8478fc7d..45a62aa6 100644 --- a/src/notifications/notifier_fallback.cpp +++ b/src/notifications/notifier_fallback.cpp @@ -7,24 +7,13 @@ maybe it can be LoadLibrary'd in :s */ -Notifier::Notifier() { -#ifdef ENABLE_NOTIFICATION_SOUNDS - if (ma_engine_init(nullptr, &m_engine) != MA_SUCCESS) { - printf("failed to initialize miniaudio engine\n"); - } -#endif -} - -Notifier::~Notifier() { -#ifdef ENABLE_NOTIFICATION_SOUNDS - ma_engine_uninit(&m_engine); -#endif -} - void Notifier::Notify(const Glib::ustring &id, const Glib::ustring &title, const Glib::ustring &text, const Glib::ustring &default_action, const std::string &icon_path) { #ifdef ENABLE_NOTIFICATION_SOUNDS - if (Abaddon::Get().GetSettings().NotificationsPlaySound) { - ma_engine_play_sound(&m_engine, Abaddon::Get().GetResPath("/sound/message.mp3").c_str(), nullptr); + using SystemSound = AbaddonClient::Audio::SystemAudio::SystemSound; + + auto& abaddon = Abaddon::Get(); + if (abaddon.GetSettings().NotificationsPlaySound) { + abaddon.GetAudio().GetSystem().PlaySound(SystemSound::Notification); } #endif } diff --git a/src/notifications/notifier_gio.cpp b/src/notifications/notifier_gio.cpp index 2708407e..30ed7d06 100644 --- a/src/notifications/notifier_gio.cpp +++ b/src/notifications/notifier_gio.cpp @@ -4,20 +4,6 @@ #include "abaddon.hpp" -Notifier::Notifier() { -#ifdef ENABLE_NOTIFICATION_SOUNDS - if (ma_engine_init(nullptr, &m_engine) != MA_SUCCESS) { - printf("failed to initialize miniaudio engine\n"); - } -#endif -} - -Notifier::~Notifier() { -#ifdef ENABLE_NOTIFICATION_SOUNDS - ma_engine_uninit(&m_engine); -#endif -} - void Notifier::Notify(const Glib::ustring &id, const Glib::ustring &title, const Glib::ustring &text, const Glib::ustring &default_action, const std::string &icon_path) { auto n = Gio::Notification::create(title); n->set_body(text); @@ -35,8 +21,11 @@ void Notifier::Notify(const Glib::ustring &id, const Glib::ustring &title, const g_object_unref(file); #ifdef ENABLE_NOTIFICATION_SOUNDS - if (Abaddon::Get().GetSettings().NotificationsPlaySound) { - ma_engine_play_sound(&m_engine, Abaddon::Get().GetResPath("/sound/message.mp3").c_str(), nullptr); + using SystemSound = AbaddonClient::Audio::SystemAudio::SystemSound; + + auto& abaddon = Abaddon::Get(); + if (abaddon.GetSettings().NotificationsPlaySound) { + abaddon.GetAudio().GetSystem().PlaySound(SystemSound::Notification); } #endif } diff --git a/src/platform.cpp b/src/platform.cpp index 726655be..2ce6d2a0 100644 --- a/src/platform.cpp +++ b/src/platform.cpp @@ -1,4 +1,5 @@ #include "platform.hpp" +#include "util.hpp" #include #include #include @@ -203,15 +204,15 @@ std::string Platform::FindConfigFile() { if (mkdir(homefolder_path, 0755) == 0) { spdlog::get("discord")->warn("created Application Support dir"); } - + char home_path[PATH_MAX]; snprintf(home_path, sizeof(home_path), "%s/%s", homefolder_path, "/abaddon.ini"); - + return home_path; } std::string Platform::FindStateCacheFolder() { - + passwd *home = getpwuid(getuid()); const char *homeDir = home->pw_dir; diff --git a/src/settings.cpp b/src/settings.cpp index e9b6fc76..3618d1a6 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -130,6 +130,7 @@ void SettingsManager::DefineSettings() { AddSetting("voice", "vad", "gate"s, &Settings::VAD); #endif AddSetting("voice", "backends", ""s, &Settings::Backends); + AddSetting("voice", "separate_sources", false, &Settings::SeparateSources); } void SettingsManager::ReadSettings() { @@ -208,4 +209,4 @@ void SettingsManager::Close() { spdlog::get("ui")->error("Failed to save settings Keyfile: {}", e.what().c_str()); } } -} \ No newline at end of file +} diff --git a/src/settings.hpp b/src/settings.hpp index 5805452b..608dfff0 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -52,6 +52,7 @@ class SettingsManager { // [voice] std::string VAD; std::string Backends; + bool SeparateSources; // [windows] bool HideConsole; diff --git a/src/windows/voice/voicewindow.cpp b/src/windows/voice/voicewindow.cpp index e59705a2..5aefba0b 100644 --- a/src/windows/voice/voicewindow.cpp +++ b/src/windows/voice/voicewindow.cpp @@ -63,29 +63,18 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_vad_param.set_range(0.0, 100.0); m_vad_param.set_value_pos(Gtk::POS_LEFT); m_vad_param.signal_value_changed().connect([this]() { - auto &audio = Abaddon::Get().GetAudio(); const double val = m_vad_param.get_value() * 0.01; - switch (audio.GetVADMethod()) { - case AudioManager::VADMethod::Gate: - audio.SetCaptureGate(val); - m_vad_value.SetTick(val); - break; -#ifdef WITH_RNNOISE - case AudioManager::VADMethod::RNNoise: - audio.SetRNNProbThreshold(val); - m_vad_value.SetTick(val); - break; -#endif - }; + Abaddon::Get().GetAudio().GetVoice().GetCapture() + .GetEffects().SetCurrentThreshold(val); }); UpdateVADParamValue(); m_capture_gain.set_range(0.0, 200.0); m_capture_gain.set_value_pos(Gtk::POS_LEFT); - m_capture_gain.set_value(audio.GetCaptureGain() * 100.0); + m_capture_gain.set_value(audio.GetVoice().GetCapture().Gain * 100.0); m_capture_gain.signal_value_changed().connect([this]() { const double val = m_capture_gain.get_value() / 100.0; - Abaddon::Get().GetAudio().SetCaptureGain(val); + Abaddon::Get().GetAudio().GetVoice().GetCapture().Gain = val; }); m_vad_combo.set_valign(Gtk::ALIGN_END); @@ -107,22 +96,22 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) #endif } m_vad_combo.signal_changed().connect([this]() { - auto &audio = Abaddon::Get().GetAudio(); const auto id = m_vad_combo.get_active_id(); + auto &abaddon = Abaddon::Get(); - audio.SetVADMethod(id); - Abaddon::Get().GetSettings().VAD = id; + abaddon.GetAudio().GetVoice().GetCapture().GetEffects().SetVADMethod(id); + abaddon.GetSettings().VAD = id; UpdateVADParamValue(); }); - m_noise_suppression.set_active(audio.GetSuppressNoise()); + m_noise_suppression.set_active(audio.GetVoice().GetCapture().SuppressNoise); m_noise_suppression.signal_toggled().connect([this]() { - Abaddon::Get().GetAudio().SetSuppressNoise(m_noise_suppression.get_active()); + Abaddon::Get().GetAudio().GetVoice().GetCapture().SuppressNoise = m_noise_suppression.get_active(); }); - m_mix_mono.set_active(audio.GetMixMono()); + m_mix_mono.set_active(audio.GetVoice().GetCapture().MixMono); m_mix_mono.signal_toggled().connect([this]() { - Abaddon::Get().GetAudio().SetMixMono(m_mix_mono.get_active()); + Abaddon::Get().GetAudio().GetVoice().GetCapture().MixMono = m_mix_mono.get_active(); }); m_disconnect.signal_clicked().connect([this]() { @@ -134,13 +123,13 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_playback_combo.set_hexpand(true); m_playback_combo.set_halign(Gtk::ALIGN_FILL); m_playback_combo.set_model(audio.GetDevices().GetPlaybackDeviceModel()); - if (const auto iter = audio.GetDevices().GetActivePlaybackDevice()) { + if (const auto iter = audio.GetDevices().GetActivePlaybackDeviceIter()) { m_playback_combo.set_active(iter); } m_playback_combo.pack_start(*playback_renderer); m_playback_combo.add_attribute(*playback_renderer, "text", 0); m_playback_combo.signal_changed().connect([this]() { - Abaddon::Get().GetAudio().SetPlaybackDevice(m_playback_combo.get_active()); + m_signal_playback_device_changed.emit(m_playback_combo.get_active()); }); auto *capture_renderer = Gtk::make_managed(); @@ -148,13 +137,13 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_capture_combo.set_hexpand(true); m_capture_combo.set_halign(Gtk::ALIGN_FILL); m_capture_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetCaptureDeviceModel()); - if (const auto iter = Abaddon::Get().GetAudio().GetDevices().GetActiveCaptureDevice()) { + if (const auto iter = Abaddon::Get().GetAudio().GetDevices().GetActiveCaptureDeviceIter()) { m_capture_combo.set_active(iter); } m_capture_combo.pack_start(*capture_renderer); m_capture_combo.add_attribute(*capture_renderer, "text", 0); m_capture_combo.signal_changed().connect([this]() { - Abaddon::Get().GetAudio().SetCaptureDevice(m_capture_combo.get_active()); + m_signal_capture_device_changed.emit(m_capture_combo.get_active()); }); m_menu_bar.append(m_menu_view); @@ -164,7 +153,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) auto *window = new VoiceSettingsWindow; const auto cb = [this](double gain) { m_capture_gain.set_value(gain * 100.0); - Abaddon::Get().GetAudio().SetCaptureGain(gain); + Abaddon::Get().GetAudio().GetVoice().GetCapture().Gain = gain; }; window->signal_gain().connect(sigc::track_obj(cb, *this)); window->show(); @@ -319,14 +308,20 @@ void VoiceWindow::TryDeleteRow(Snowflake id) { } bool VoiceWindow::UpdateVoiceMeters() { - auto &audio = Abaddon::Get().GetAudio(); - switch (audio.GetVADMethod()) { - case AudioManager::VADMethod::Gate: - m_vad_value.SetVolume(audio.GetCaptureVolumeLevel()); + using VADMethod = AbaddonClient::Audio::Voice::Capture::VADMethod; + + auto& voice = Abaddon::Get().GetAudio().GetVoice(); + auto& playback = voice.GetPlayback(); + auto& capture = voice.GetCapture(); + auto& effects = capture.GetEffects(); + + switch (effects.GetVADMethod()) { + case VADMethod::Gate: + m_vad_value.SetVolume(capture.GetPeakMeter().GetPeak()); break; #ifdef WITH_RNNOISE - case AudioManager::VADMethod::RNNoise: - m_vad_value.SetVolume(audio.GetCurrentVADProbability()); + case VADMethod::RNNoise: + m_vad_value.SetVolume(effects.GetNoise().GetPeakMeter().GetPeak()); break; #endif } @@ -335,7 +330,7 @@ bool VoiceWindow::UpdateVoiceMeters() { const auto ssrc = Abaddon::Get().GetDiscordClient().GetSSRCOfUser(id); if (ssrc.has_value()) { if (auto *speaker_row = dynamic_cast(row)) { - speaker_row->SetVolumeMeter(audio.GetSSRCVolumeLevel(*ssrc)); + speaker_row->SetVolumeMeter(playback.GetClientStore().GetClientPeakVolume(*ssrc)); } } } @@ -343,17 +338,8 @@ bool VoiceWindow::UpdateVoiceMeters() { } void VoiceWindow::UpdateVADParamValue() { - auto &audio = Abaddon::Get().GetAudio(); - switch (audio.GetVADMethod()) { - case AudioManager::VADMethod::Gate: - m_vad_param.set_value(audio.GetCaptureGate() * 100.0); - break; -#ifdef WITH_RNNOISE - case AudioManager::VADMethod::RNNoise: - m_vad_param.set_value(audio.GetRNNProbThreshold() * 100.0); - break; -#endif - } + auto &effects = Abaddon::Get().GetAudio().GetVoice().GetCapture().GetEffects(); + m_vad_param.set_value(effects.GetCurrentThreshold() * 100.0); } void VoiceWindow::UpdateStageCommand() { @@ -445,4 +431,12 @@ VoiceWindow::type_signal_mute_user_cs VoiceWindow::signal_mute_user_cs() { VoiceWindow::type_signal_user_volume_changed VoiceWindow::signal_user_volume_changed() { return m_signal_user_volume_changed; } + +VoiceWindow::type_signal_playback_device_changed VoiceWindow::signal_playback_device_changed() { + return m_signal_playback_device_changed; +} + +VoiceWindow::type_signal_capture_device_changed VoiceWindow::signal_capture_device_changed() { + return m_signal_capture_device_changed; +} #endif diff --git a/src/windows/voice/voicewindow.hpp b/src/windows/voice/voicewindow.hpp index 05033d9b..9adcbc7a 100644 --- a/src/windows/voice/voicewindow.hpp +++ b/src/windows/voice/voicewindow.hpp @@ -103,15 +103,24 @@ class VoiceWindow : public Gtk::Window { using type_signal_mute_user_cs = sigc::signal; using type_signal_user_volume_changed = sigc::signal; + using type_signal_playback_device_changed = sigc::signal; + using type_signal_capture_device_changed = sigc::signal; + type_signal_mute signal_mute(); type_signal_deafen signal_deafen(); type_signal_mute_user_cs signal_mute_user_cs(); type_signal_user_volume_changed signal_user_volume_changed(); + type_signal_playback_device_changed signal_playback_device_changed(); + type_signal_capture_device_changed signal_capture_device_changed(); + private: type_signal_mute m_signal_mute; type_signal_deafen m_signal_deafen; type_signal_mute_user_cs m_signal_mute_user_cs; type_signal_user_volume_changed m_signal_user_volume_changed; + + type_signal_playback_device_changed m_signal_playback_device_changed; + type_signal_capture_device_changed m_signal_capture_device_changed; }; #endif diff --git a/src/windows/voicesettingswindow.cpp b/src/windows/voicesettingswindow.cpp index 90d07172..395a7843 100644 --- a/src/windows/voicesettingswindow.cpp +++ b/src/windows/voicesettingswindow.cpp @@ -10,6 +10,9 @@ // clang-format on +using SignalHint = AbaddonClient::Audio::Voice::Opus::OpusEncoder::SignalHint; +using EncodingApplication = AbaddonClient::Audio::Voice::Opus::OpusEncoder::EncodingApplication; + VoiceSettingsWindow::VoiceSettingsWindow() : m_main(Gtk::ORIENTATION_VERTICAL) { get_style_context()->add_class("app-window"); @@ -25,25 +28,27 @@ VoiceSettingsWindow::VoiceSettingsWindow() "Music - Optimize for non-voice signals incl. music\n" "Restricted - Optimize for non-voice, low latency. Not recommended"); - const auto mode = Abaddon::Get().GetAudio().GetEncodingApplication(); - if (mode == OPUS_APPLICATION_VOIP) { + const auto mode = Abaddon::Get().GetAudio().GetVoice().GetCapture().GetEncoder()->value().GetEncodingApplication(); + + if (mode == EncodingApplication::VOIP) { m_encoding_mode.set_active(0); - } else if (mode == OPUS_APPLICATION_AUDIO) { + } else if (mode == EncodingApplication::Audio) { m_encoding_mode.set_active(1); - } else if (mode == OPUS_APPLICATION_RESTRICTED_LOWDELAY) { + } else if (mode == EncodingApplication::LowDelay) { m_encoding_mode.set_active(2); } m_encoding_mode.signal_changed().connect([this]() { const auto mode = m_encoding_mode.get_active_text(); - auto &audio = Abaddon::Get().GetAudio(); + auto &encoder = Abaddon::Get().GetAudio().GetVoice().GetCapture().GetEncoder()->value(); + if (mode == "Voice") { - audio.SetEncodingApplication(OPUS_APPLICATION_VOIP); + encoder.SetEncodingApplication(EncodingApplication::VOIP); } else if (mode == "Music") { spdlog::get("audio")->debug("music/audio"); - audio.SetEncodingApplication(OPUS_APPLICATION_AUDIO); + encoder.SetEncodingApplication(EncodingApplication::Audio); } else if (mode == "Restricted") { - audio.SetEncodingApplication(OPUS_APPLICATION_RESTRICTED_LOWDELAY); + encoder.SetEncodingApplication(EncodingApplication::LowDelay); } }); @@ -56,24 +61,28 @@ VoiceSettingsWindow::VoiceSettingsWindow() "Voice - Tell Opus it's a voice signal\n" "Music - Tell Opus it's a music signal"); - const auto signal = Abaddon::Get().GetAudio().GetSignalHint(); - if (signal == OPUS_AUTO) { + const auto signal = Abaddon::Get().GetAudio().GetVoice().GetCapture().GetEncoder()->value().GetSignalHint(); + if (signal == SignalHint::Auto) { m_signal.set_active(0); - } else if (signal == OPUS_SIGNAL_VOICE) { + } else if (signal == SignalHint::Voice) { m_signal.set_active(1); - } else if (signal == OPUS_SIGNAL_MUSIC) { + } else if (signal == SignalHint::Music) { m_signal.set_active(2); } m_signal.signal_changed().connect([this]() { const auto signal = m_signal.get_active_text(); - auto &audio = Abaddon::Get().GetAudio(); + auto encoder = Abaddon::Get().GetAudio().GetVoice().GetCapture().GetEncoder(); + if (!encoder->has_value()) { + return; + } + if (signal == "Auto") { - audio.SetSignalHint(OPUS_AUTO); + encoder->value().SetSignalHint(SignalHint::Auto); } else if (signal == "Voice") { - audio.SetSignalHint(OPUS_SIGNAL_VOICE); + encoder->value().SetSignalHint(SignalHint::Voice); } else if (signal == "Music") { - audio.SetSignalHint(OPUS_SIGNAL_MUSIC); + encoder->value().SetSignalHint(SignalHint::Music); } }); @@ -91,7 +100,7 @@ VoiceSettingsWindow::VoiceSettingsWindow() m_bitrate.set_range(0.0, 100.0); m_bitrate.set_value_pos(Gtk::POS_TOP); - m_bitrate.set_value(bitrate_scale_r(Abaddon::Get().GetAudio().GetBitrate())); + m_bitrate.set_value(bitrate_scale_r(Abaddon::Get().GetAudio().GetVoice().GetCapture().GetEncoder()->value().GetBitrate())); m_bitrate.signal_format_value().connect([this, bitrate_scale](double value) { const auto scaled = bitrate_scale(value); if (value <= 99.9) { @@ -103,16 +112,18 @@ VoiceSettingsWindow::VoiceSettingsWindow() m_bitrate.signal_value_changed().connect([this, bitrate_scale]() { const auto value = m_bitrate.get_value(); const auto scaled = bitrate_scale(value); + + auto& encoder = Abaddon::Get().GetAudio().GetVoice().GetCapture().GetEncoder()->value(); if (value <= 99.9) { - Abaddon::Get().GetAudio().SetBitrate(static_cast(scaled)); + encoder.SetBitrate(static_cast(scaled)); } else { - Abaddon::Get().GetAudio().SetBitrate(OPUS_BITRATE_MAX); + encoder.SetBitrate(MAX_BITRATE); } }); m_gain.set_increments(1.0, 5.0); m_gain.set_range(0.0, 6969696969.0); - m_gain.set_value(Abaddon::Get().GetAudio().GetCaptureGain() * 100.0); + m_gain.set_value(Abaddon::Get().GetAudio().GetVoice().GetCapture().Gain * 100.0); const auto cb = [this]() { spdlog::get("ui")->warn("emit"); m_signal_gain.emit(m_gain.get_value() / 100.0);