Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor audio handling #277

Open
wants to merge 42 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b33704b
Refactor voice audio handling
ryze312 Mar 8, 2024
cf64646
Apply suggestions
ryze312 Mar 11, 2024
eb1451a
Implement RAII mutex
ryze312 May 12, 2024
8e1d681
Implement slice
ryze312 May 12, 2024
0992139
Implement data channel
ryze312 May 12, 2024
555a3cd
Implement thread pool
ryze312 May 12, 2024
dbef57a
Fix RemoveSSRC never getting called
ryze312 May 12, 2024
9b550ed
Reimplement audio engine
ryze312 May 12, 2024
b7151f4
Sync with upstream
ryze312 May 12, 2024
39011b6
Fix typo: u_int8_t -> uint8_t
ryze312 May 12, 2024
145057f
Make constructors public for in-place constructor to work
ryze312 May 13, 2024
f4679b4
Apply suggestions
ryze312 May 21, 2024
c59c496
Fix switching devices
ryze312 May 22, 2024
6221b60
Use functors for deleters
ryze312 May 22, 2024
896714b
Split VoiceBuffer miniaudio ringbuffer
ryze312 May 22, 2024
f235bcd
Use VoiceClient and DiscordClient signals in VoiceAudio
ryze312 May 22, 2024
631eb03
Wrap ma_log
ryze312 May 28, 2024
09cff96
Remove no longer used stuff in AudioManager
ryze312 May 29, 2024
dffe44e
Clean up AudioManager and ifdef conditions, expose VoiceAudio
ryze312 Jun 2, 2024
486fec9
Make PeakMeter decay itself
ryze312 Jun 2, 2024
3d89d24
Wrap MaEngine
ryze312 Jun 2, 2024
2a1150a
Implement SystemAudio
ryze312 Jun 2, 2024
5e0b8f6
Make notification device start on demand
ryze312 Jun 2, 2024
cb44e05
More voice sounds
ryze312 Jun 3, 2024
030f5da
Why snake case?
ryze312 Jun 3, 2024
bc4b6d1
Add voice sounds
ryze312 Jun 3, 2024
72e2248
Play connect/disconnect sounds only for the current channel
ryze312 Jun 3, 2024
c5fbe39
Don't copy in capture signal
ryze312 Jun 3, 2024
d9373a2
i can't type
ryze312 Jun 3, 2024
a762623
i can't type 2
ryze312 Jun 3, 2024
0558de1
Make ringbuffer more readable and use std::copy_n
ryze312 Jun 4, 2024
0930bd7
Fix wrong max bitrate and default EncodingApplication
ryze312 Jun 4, 2024
bd45254
Implement separate sources for voice
ryze312 Jun 4, 2024
9d5eb73
Fix includes, include audio sources only when needed
ryze312 Jun 11, 2024
12e701c
Disable LTO on Windows
ryze312 Jun 12, 2024
98306b2
Document separate_sources option in README
ryze312 Jun 12, 2024
2d96b38
Add missing headers
ryze312 Jun 13, 2024
c3cf374
Merge branch 'master' into audio_refactor
ryze312 Jun 13, 2024
06fb2a9
Update RTP stripping
ryze312 Jul 11, 2024
8d4384e
Fix left denoised channel not being written
ryze312 Jul 11, 2024
d6ae366
Sync with upstream
ryze312 Jul 11, 2024
e48e487
Remove duplicate definition of GetPayloadOffset
ryze312 Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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 ()
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Binary file added res/res/sound/voice_connected.mp3
Binary file not shown.
Binary file added res/res/sound/voice_deafened.mp3
Binary file not shown.
Binary file added res/res/sound/voice_disconnected.mp3
Binary file not shown.
Binary file added res/res/sound/voice_muted.mp3
Binary file not shown.
Binary file added res/res/sound/voice_undeafened.mp3
Binary file not shown.
Binary file added res/res/sound/voice_unmuted.mp3
Binary file not shown.
58 changes: 43 additions & 15 deletions src/abaddon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
#include <algorithm>
#include <gtkmm.h>
#include "platform.hpp"
#include "audio/manager.hpp"
#include "discord/discord.hpp"
#include "dialogs/token.hpp"
#include "dialogs/confirm.hpp"
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -103,7 +102,7 @@ Abaddon::Abaddon()
}

#ifdef WITH_VOICE
m_audio.SetVADMethod(GetSettings().VAD);
m_audio.GetVoice().GetCapture().GetEffects().SetVADMethod(GetSettings().VAD);
#endif
}

Expand Down Expand Up @@ -487,43 +486,72 @@ 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());
m_voice_window = wnd;

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);
Expand Down Expand Up @@ -1129,7 +1157,7 @@ EmojiResource &Abaddon::GetEmojis() {
return m_emojis;
}

#ifdef WITH_VOICE
#ifdef WITH_MINIAUDIO
AudioManager &Abaddon::GetAudio() {
return m_audio;
}
Expand Down
12 changes: 8 additions & 4 deletions src/abaddon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -72,7 +73,7 @@ class Abaddon {
ImageManager &GetImageManager();
EmojiResource &GetEmojis();

#ifdef WITH_VOICE
#ifdef WITH_MINIAUDIO
AudioManager &GetAudio();
#endif

Expand Down Expand Up @@ -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

Expand Down
90 changes: 90 additions & 0 deletions src/audio/audio_device.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "audio_device.hpp"

namespace AbaddonClient::Audio {

AudioDevice::AudioDevice(Context &context, ma_device_config &&config, std::optional<ma_device_id> &&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;
}

}
30 changes: 30 additions & 0 deletions src/audio/audio_device.hpp
Original file line number Diff line number Diff line change
@@ -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<ma_device_id> &&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<Miniaudio::MaDevice> m_device;

ma_device_config m_config;
std::optional<ma_device_id> m_device_id;
};

}
53 changes: 53 additions & 0 deletions src/audio/audio_engine.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "audio_engine.hpp"

#include <glibmm/main.h>
#include <spdlog/spdlog.h>

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<Miniaudio::MaEngine> &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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im guessing this is meant to kill the engine if no sounds are played for 5 seconds? might it just be simpler to keep it around anyways? or maybe we can just call ma_engine_stop to avoid having to create it over and over.
also im pretty sure there are glibmm bindings for this but thats not a big deal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to match the behavior of the official client, it spawns source as needed with the timeout of 5 seconds. ma_engine_stop seems to just stop the device's callbacks internally, it still keeps the source intact and connected.

glibmm has SignalTimeout::connect_seconds_once, but it doesn't seem to provide a way to destroy/reset the timeout.


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();
}

}
Loading
Loading