From 0fa33915da6255cf7460758197eaea7e43353543 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 31 Aug 2022 01:51:02 -0400 Subject: rudimentary voice implementation --- src/abaddon.cpp | 18 ++ src/abaddon.hpp | 5 + src/audio/manager.cpp | 92 ++++++++ src/audio/manager.hpp | 35 +++ src/components/channels.cpp | 36 +++- src/components/channels.hpp | 13 ++ src/components/channelscellrenderer.cpp | 43 ++++ src/components/channelscellrenderer.hpp | 14 ++ src/discord/discord.cpp | 37 ++++ src/discord/discord.hpp | 39 +--- src/discord/objects.cpp | 21 ++ src/discord/objects.hpp | 28 +++ src/discord/voiceclient.cpp | 372 ++++++++++++++++++++++++++++++++ src/discord/voiceclient.hpp | 205 ++++++++++++++++++ src/discord/waiter.hpp | 29 +++ 15 files changed, 955 insertions(+), 32 deletions(-) create mode 100644 src/audio/manager.cpp create mode 100644 src/audio/manager.hpp create mode 100644 src/discord/voiceclient.cpp create mode 100644 src/discord/voiceclient.hpp create mode 100644 src/discord/waiter.hpp (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 343dff7..a3a228d 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -3,6 +3,7 @@ #include #include #include "platform.hpp" +#include "audio/manager.hpp" #include "discord/discord.hpp" #include "dialogs/token.hpp" #include "dialogs/editmessage.hpp" @@ -219,6 +220,14 @@ int Abaddon::StartGTK() { return 1; } + m_audio = std::make_unique(); + if (!m_audio->OK()) { + Gtk::MessageDialog dlg(*m_main_window, "The audio engine could not be initialized!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.set_position(Gtk::WIN_POS_CENTER); + dlg.run(); + return 1; + } + // store must be checked before this can be called m_main_window->UpdateComponents(); @@ -238,6 +247,7 @@ int Abaddon::StartGTK() { m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::bind(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened), true)); m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild)); m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings)); + m_main_window->GetChannelList()->signal_action_join_voice_channel().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinVoiceChannel)); m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage)); m_main_window->GetChatWindow()->signal_action_chat_submit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatInputSubmit)); @@ -898,6 +908,10 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) { window->show(); } +void Abaddon::ActionJoinVoiceChannel(Snowflake channel_id) { + m_discord.ConnectToVoice(channel_id); +} + std::optional Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) { TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window); const auto code = dlg.run(); @@ -937,6 +951,10 @@ EmojiResource &Abaddon::GetEmojis() { return m_emojis; } +AudioManager &Abaddon::GetAudio() { + return *m_audio.get(); +} + int main(int argc, char **argv) { if (std::getenv("ABADDON_NO_FC") == nullptr) Platform::SetupFonts(); diff --git a/src/abaddon.hpp b/src/abaddon.hpp index ab80c46..d67f4ab 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -12,6 +12,8 @@ #define APP_TITLE "Abaddon" +class AudioManager; + class Abaddon { private: Abaddon(); @@ -51,6 +53,7 @@ public: void ActionAddRecipient(Snowflake channel_id); void ActionViewPins(Snowflake channel_id); void ActionViewThreads(Snowflake channel_id); + void ActionJoinVoiceChannel(Snowflake channel_id); std::optional ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr); bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr); @@ -59,6 +62,7 @@ public: ImageManager &GetImageManager(); EmojiResource &GetEmojis(); + AudioManager &GetAudio(); std::string GetDiscordToken() const; bool IsDiscordActive() const; @@ -137,6 +141,7 @@ private: ImageManager m_img_mgr; EmojiResource m_emojis; + std::unique_ptr m_audio; mutable std::mutex m_mutex; Glib::RefPtr m_gtk_app; diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp new file mode 100644 index 0000000..af36327 --- /dev/null +++ b/src/audio/manager.cpp @@ -0,0 +1,92 @@ +#ifdef _WIN32 + #include +#endif + +#include "manager.hpp" +#include +#define MINIAUDIO_IMPLEMENTATION +#include +#include +#include + +const uint8_t *StripRTPExtensionHeader(const uint8_t *buf, int num_bytes, size_t &outlen) { + if (buf[0] == 0xbe && buf[1] == 0xde && num_bytes > 4) { + uint64_t offset = 4 + 4 * ((buf[2] << 8) | buf[3]); + + outlen = num_bytes - offset; + return buf + offset; + } + outlen = num_bytes; + return buf; +} + +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_dumb_mutex); + + const auto buffered_frames = std::min(static_cast(mgr->m_dumb.size() / 2), frameCount); + auto *pOutputCast = static_cast(pOutput); + for (ma_uint32 i = 0; i < buffered_frames * 2; i++) { + pOutputCast[i] = mgr->m_dumb.front(); + mgr->m_dumb.pop(); + } +} + +AudioManager::AudioManager() { + m_ok = true; + + m_device_config = ma_device_config_init(ma_device_type_playback); + m_device_config.playback.format = ma_format_s16; + m_device_config.playback.channels = 2; + m_device_config.sampleRate = 48000; + m_device_config.dataCallback = data_callback; + m_device_config.pUserData = this; + + if (ma_device_init(nullptr, &m_device_config, &m_device) != MA_SUCCESS) { + puts("open playabck fail"); + m_ok = false; + return; + } + + if (ma_device_start(&m_device) != MA_SUCCESS) { + puts("failed to start playback"); + ma_device_uninit(&m_device); + m_ok = false; + return; + } + + int err; + m_opus_decoder = opus_decoder_create(48000, 2, &err); + + m_active = true; + // m_thread = std::thread(&AudioManager::testthread, this); +} + +AudioManager::~AudioManager() { + m_active = false; + ma_device_uninit(&m_device); +} + +void AudioManager::FeedMeOpus(const std::vector &data) { + size_t payload_size = 0; + const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); + static std::array pcm; + int decoded = opus_decode(m_opus_decoder, opus_encoded, static_cast(payload_size), pcm.data(), 120 * 48, 0); + if (decoded <= 0) { + printf("failed decode: %d\n", decoded); + } else { + m_dumb_mutex.lock(); + for (size_t i = 0; i < decoded * 2; i++) { + m_dumb.push(pcm[i]); + } + m_dumb_mutex.unlock(); + } +} + +void AudioManager::testthread() { +} + +bool AudioManager::OK() const { + return m_ok; +} diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp new file mode 100644 index 0000000..ffe7ae9 --- /dev/null +++ b/src/audio/manager.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class AudioManager { +public: + AudioManager(); + ~AudioManager(); + + void FeedMeOpus(const std::vector &data); + + [[nodiscard]] bool OK() const; + +private: + friend void data_callback(ma_device *, void *, const void *, ma_uint32); + + std::atomic m_active; + void testthread(); + std::thread m_thread; + + bool m_ok; + + ma_engine m_engine; + ma_device m_device; + ma_device_config m_device_config; + + std::mutex m_dumb_mutex; + std::queue m_dumb; + + OpusDecoder *m_opus_decoder; +}; diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 497c021..566ebd1 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -36,7 +36,7 @@ ChannelList::ChannelList() const auto type = row[m_columns.m_type]; // text channels should not be allowed to be collapsed // maybe they should be but it seems a little difficult to handle expansion to permit this - if (type != RenderType::TextChannel) { + if (type != RenderType::TextChannel && type != RenderType::VoiceChannel) { if (row[m_columns.m_expanded]) { m_view.collapse_row(path); row[m_columns.m_expanded] = false; @@ -161,6 +161,15 @@ ChannelList::ChannelList() m_menu_channel.append(m_menu_channel_copy_id); m_menu_channel.show_all(); + m_menu_voice_channel_join.signal_activate().connect([this]() { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + printf("join voice: %llu\n", static_cast(id)); + m_signal_action_join_voice_channel.emit(id); + }); + + m_menu_voice_channel.append(m_menu_voice_channel_join); + m_menu_voice_channel.show_all(); + m_menu_dm_copy_id.signal_activate().connect([this] { Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); }); @@ -579,7 +588,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { for (const auto &channel_ : *guild.Channels) { const auto channel = discord.GetChannel(channel_.ID); if (!channel.has_value()) continue; - if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) { + if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) { if (channel->ParentID.has_value()) categories[*channel->ParentID].push_back(*channel); else @@ -607,7 +616,10 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { for (const auto &channel : orphan_channels) { auto channel_row = *m_model->append(guild_row.children()); - channel_row[m_columns.m_type] = RenderType::TextChannel; + if (IsTextChannel(channel.Type)) + channel_row[m_columns.m_type] = RenderType::TextChannel; + else + channel_row[m_columns.m_type] = RenderType::VoiceChannel; channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; @@ -630,7 +642,10 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { for (const auto &channel : channels) { auto channel_row = *m_model->append(cat_row.children()); - channel_row[m_columns.m_type] = RenderType::TextChannel; + if (IsTextChannel(channel.Type)) + channel_row[m_columns.m_type] = RenderType::TextChannel; + else + channel_row[m_columns.m_type] = RenderType::VoiceChannel; channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_sort] = *channel.Position; @@ -856,6 +871,10 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { OnChannelSubmenuPopup(); m_menu_channel.popup_at_pointer(reinterpret_cast(ev)); break; + case RenderType::VoiceChannel: + OnVoiceChannelSubmenuPopup(); + m_menu_voice_channel.popup_at_pointer(reinterpret_cast(ev)); + break; case RenderType::DM: { OnDMSubmenuPopup(); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast(row[m_columns.m_id])); @@ -947,6 +966,9 @@ void ChannelList::OnChannelSubmenuPopup() { m_menu_channel_toggle_mute.set_label("Mute"); } +void ChannelList::OnVoiceChannelSubmenuPopup() { +} + void ChannelList::OnDMSubmenuPopup() { auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; @@ -997,6 +1019,12 @@ ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new } #endif +#ifdef WITH_VOICE +ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() { + return m_signal_action_join_voice_channel; +} +#endif + ChannelList::ModelColumns::ModelColumns() { add(m_type); add(m_id); diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 53a68c9..53afbdc 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -125,6 +125,9 @@ protected: Gtk::MenuItem m_menu_channel_open_tab; #endif + Gtk::Menu m_menu_voice_channel; + Gtk::MenuItem m_menu_voice_channel_join; + Gtk::Menu m_menu_dm; Gtk::MenuItem m_menu_dm_copy_id; Gtk::MenuItem m_menu_dm_close; @@ -145,6 +148,7 @@ protected: void OnGuildSubmenuPopup(); void OnCategorySubmenuPopup(); void OnChannelSubmenuPopup(); + void OnVoiceChannelSubmenuPopup(); void OnDMSubmenuPopup(); void OnThreadSubmenuPopup(); @@ -166,6 +170,11 @@ public: type_signal_action_open_new_tab signal_action_open_new_tab(); #endif +#ifdef WITH_VOICE + using type_signal_action_join_voice_channel = sigc::signal; + type_signal_action_join_voice_channel signal_action_join_voice_channel(); +#endif + type_signal_action_channel_item_select signal_action_channel_item_select(); type_signal_action_guild_leave signal_action_guild_leave(); type_signal_action_guild_settings signal_action_guild_settings(); @@ -178,4 +187,8 @@ private: #ifdef WITH_LIBHANDY type_signal_action_open_new_tab m_signal_action_open_new_tab; #endif + +#ifdef WITH_VOICE + type_signal_action_join_voice_channel m_signal_action_join_voice_channel; +#endif }; diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index 9afce8a..e9c43aa 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -65,6 +65,8 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width); case RenderType::Thread: return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width); + case RenderType::VoiceChannel: + return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width); case RenderType::DMHeader: return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width); case RenderType::DM: @@ -82,6 +84,8 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width); case RenderType::Thread: return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width); + case RenderType::VoiceChannel: + return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width); case RenderType::DMHeader: return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width); case RenderType::DM: @@ -99,6 +103,8 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int & return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height); case RenderType::Thread: return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height); + case RenderType::VoiceChannel: + return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height); case RenderType::DMHeader: return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height); case RenderType::DM: @@ -116,6 +122,8 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height); case RenderType::Thread: return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height); + case RenderType::VoiceChannel: + return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height); case RenderType::DMHeader: return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height); case RenderType::DM: @@ -133,6 +141,8 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr &cr, return render_vfunc_channel(cr, widget, background_area, cell_area, flags); case RenderType::Thread: return render_vfunc_thread(cr, widget, background_area, cell_area, flags); + case RenderType::VoiceChannel: + return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags); case RenderType::DMHeader: return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags); case RenderType::DM: @@ -499,6 +509,39 @@ void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + Gtk::Requisition minimum_size, natural_size; + m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); + + const int text_x = background_area.get_x() + 21; + const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2; + const int text_w = natural_size.width; + const int text_h = natural_size.height; + + Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); + m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#0f0"); + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; +} + // dm header void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { diff --git a/src/components/channelscellrenderer.hpp b/src/components/channelscellrenderer.hpp index e2be9b2..e77cf47 100644 --- a/src/components/channelscellrenderer.hpp +++ b/src/components/channelscellrenderer.hpp @@ -10,6 +10,7 @@ enum class RenderType : uint8_t { Category, TextChannel, Thread, + VoiceChannel, DMHeader, DM, @@ -83,6 +84,19 @@ protected: const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags); +#ifdef WITH_VOICE + // voice channel + void get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_voice_channel(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); +#endif + // dm header void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 561b25b..ed9b999 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1169,6 +1169,16 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI }); } +void DiscordClient::ConnectToVoice(Snowflake channel_id) { + auto channel = GetChannel(channel_id); + if (!channel.has_value() || !channel->GuildID.has_value()) return; + VoiceStateUpdateMessage m; + m.GuildID = *channel->GuildID; + m.ChannelID = channel_id; + m.PreferredRegion = "newark"; + m_websocket.Send(m); +} + void DiscordClient::SetReferringChannel(Snowflake id) { if (!id.IsValid()) { m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me"); @@ -1488,6 +1498,12 @@ void DiscordClient::HandleGatewayMessage(std::string str) { case GatewayEvent::GUILD_MEMBERS_CHUNK: { HandleGatewayGuildMembersChunk(m); } break; + case GatewayEvent::VOICE_STATE_UPDATE: { + HandleGatewayVoiceStateUpdate(m); + } break; + case GatewayEvent::VOICE_SERVER_UPDATE: { + HandleGatewayVoiceServerUpdate(m); + } break; } } break; default: @@ -2098,6 +2114,25 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) { m_store.EndTransaction(); } +void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { + VoiceStateUpdateData data = msg.Data; + if (data.UserID == m_user_data.ID) { + printf("voice session id: %s\n", data.SessionID.c_str()); + m_voice.SetSessionID(data.SessionID); + } +} + +void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) { + VoiceServerUpdateData data = msg.Data; + printf("endpoint: %s\n", data.Endpoint.c_str()); + printf("token: %s\n", data.Token.c_str()); + m_voice.SetEndpoint(data.Endpoint); + m_voice.SetToken(data.Token); + m_voice.SetServerID(data.GuildID); + m_voice.SetUserID(m_user_data.ID); + m_voice.Start(); +} + void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { ReadySupplementalData data = msg.Data; for (const auto &p : data.MergedPresences.Friends) { @@ -2589,6 +2624,8 @@ void DiscordClient::LoadEventMap() { m_event_map["MESSAGE_ACK"] = GatewayEvent::MESSAGE_ACK; m_event_map["USER_GUILD_SETTINGS_UPDATE"] = GatewayEvent::USER_GUILD_SETTINGS_UPDATE; m_event_map["GUILD_MEMBERS_CHUNK"] = GatewayEvent::GUILD_MEMBERS_CHUNK; + m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE; + m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE; } DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 70c2d82..a6eabd9 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -1,9 +1,11 @@ #pragma once -#include "websocket.hpp" +#include "chatsubmitparams.hpp" +#include "waiter.hpp" #include "httpclient.hpp" #include "objects.hpp" #include "store.hpp" -#include "chatsubmitparams.hpp" +#include "voiceclient.hpp" +#include "websocket.hpp" #include #include #include @@ -18,31 +20,6 @@ #undef GetMessage #endif -class HeartbeatWaiter { -public: - template - bool wait_for(std::chrono::duration const &time) const { - std::unique_lock lock(m); - return !cv.wait_for(lock, time, [&] { return terminate; }); - } - - void kill() { - std::unique_lock lock(m); - terminate = true; - cv.notify_all(); - } - - void revive() { - std::unique_lock lock(m); - terminate = false; - } - -private: - mutable std::condition_variable cv; - mutable std::mutex m; - bool terminate = false; -}; - class Abaddon; class DiscordClient { friend class Abaddon; @@ -204,6 +181,8 @@ public: void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot)> &callback); void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot &callback); + void ConnectToVoice(Snowflake channel_id); + void SetReferringChannel(Snowflake id); void SetBuildNumber(uint32_t build_number); @@ -283,6 +262,8 @@ private: void HandleGatewayMessageAck(const GatewayMessage &msg); void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg); void HandleGatewayGuildMembersChunk(const GatewayMessage &msg); + void HandleGatewayVoiceStateUpdate(const GatewayMessage &msg); + void HandleGatewayVoiceServerUpdate(const GatewayMessage &msg); void HandleGatewayReadySupplemental(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg); void HandleGatewayInvalidSession(const GatewayMessage &msg); @@ -338,13 +319,15 @@ private: std::thread m_heartbeat_thread; std::atomic m_last_sequence = -1; std::atomic m_heartbeat_msec = 0; - HeartbeatWaiter m_heartbeat_waiter; + Waiter m_heartbeat_waiter; std::atomic m_heartbeat_acked = true; bool m_reconnecting = false; // reconnecting either to resume or reidentify bool m_wants_resume = false; // reconnecting specifically to resume std::string m_session_id; + DiscordVoiceClient m_voice; + mutable std::mutex m_msg_mutex; Glib::Dispatcher m_msg_dispatch; std::queue m_msg_queue; diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index e43e05a..e4c61c5 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -640,3 +640,24 @@ void from_json(const nlohmann::json &j, GuildMembersChunkData &m) { JS_D("members", m.Members); JS_D("guild_id", m.GuildID); } + +void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) { + j["op"] = GatewayOp::VoiceStateUpdate; + j["d"]["guild_id"] = m.GuildID; + j["d"]["channel_id"] = m.ChannelID; + j["d"]["self_mute"] = m.SelfMute; + j["d"]["self_deaf"] = m.SelfDeaf; + j["d"]["self_video"] = m.SelfVideo; + j["d"]["preferred_region"] = m.PreferredRegion; +} + +void from_json(const nlohmann::json &j, VoiceStateUpdateData &m) { + JS_ON("user_id", m.UserID); + JS_ON("session_id", m.SessionID); +} + +void from_json(const nlohmann::json &j, VoiceServerUpdateData &m) { + JS_D("token", m.Token); + JS_D("guild_id", m.GuildID); + JS_D("endpoint", m.Endpoint); +} diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp index 9db9369..240b4c5 100644 --- a/src/discord/objects.hpp +++ b/src/discord/objects.hpp @@ -100,6 +100,8 @@ enum class GatewayEvent : int { MESSAGE_ACK, USER_GUILD_SETTINGS_UPDATE, GUILD_MEMBERS_CHUNK, + VOICE_STATE_UPDATE, + VOICE_SERVER_UPDATE, }; enum class GatewayCloseCode : uint16_t { @@ -864,3 +866,29 @@ struct GuildMembersChunkData { friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m); }; + +struct VoiceStateUpdateMessage { + Snowflake GuildID; + Snowflake ChannelID; + bool SelfMute = false; + bool SelfDeaf = false; + bool SelfVideo = false; + std::string PreferredRegion; + + friend void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m); +}; + +struct VoiceStateUpdateData { + Snowflake UserID; + std::string SessionID; + + friend void from_json(const nlohmann::json &j, VoiceStateUpdateData &m); +}; + +struct VoiceServerUpdateData { + std::string Token; + Snowflake GuildID; + std::string Endpoint; + + friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m); +}; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp new file mode 100644 index 0000000..162a5a1 --- /dev/null +++ b/src/discord/voiceclient.cpp @@ -0,0 +1,372 @@ +#include "voiceclient.hpp" +#include "json.hpp" +#include +#include "abaddon.hpp" +#include "audio/manager.hpp" + +UDPSocket::UDPSocket() { + m_socket = socket(AF_INET, SOCK_DGRAM, 0); +} + +UDPSocket::~UDPSocket() { + Stop(); +} + +void UDPSocket::Connect(std::string_view ip, uint16_t port) { + std::memset(&m_server, 0, sizeof(m_server)); + m_server.sin_family = AF_INET; + m_server.sin_addr.S_un.S_addr = inet_addr(ip.data()); + m_server.sin_port = htons(port); + bind(m_socket, reinterpret_cast(&m_server), sizeof(m_server)); +} + +void UDPSocket::Run() { + m_running = true; + m_thread = std::thread(&UDPSocket::ReadThread, this); +} + +void UDPSocket::SetSecretKey(std::array key) { + m_secret_key = key; +} + +void UDPSocket::SetSSRC(uint32_t ssrc) { + m_ssrc = ssrc; +} + +void UDPSocket::SendEncrypted(const std::vector &data) { + m_sequence++; + m_timestamp += (48000 / 100) * 2; + + std::vector rtp(12, 0); + rtp[0] = 0x80; // ver 2 + rtp[1] = 0x78; // payload type 0x78 + rtp[2] = (m_sequence >> 8) & 0xFF; + rtp[3] = (m_sequence >> 0) & 0xFF; + rtp[4] = (m_timestamp >> 24) & 0xFF; + rtp[5] = (m_timestamp >> 16) & 0xFF; + rtp[6] = (m_timestamp >> 8) & 0xFF; + rtp[7] = (m_timestamp >> 0) & 0xFF; + rtp[8] = (m_ssrc >> 24) & 0xFF; + rtp[9] = (m_ssrc >> 16) & 0xFF; + rtp[10] = (m_ssrc >> 8) & 0xFF; + rtp[11] = (m_ssrc >> 0) & 0xFF; + + static std::array nonce = {}; + std::memcpy(nonce.data(), rtp.data(), 12); + + std::vector ciphertext(crypto_secretbox_MACBYTES + rtp.size(), 0); + crypto_secretbox_easy(ciphertext.data(), rtp.data(), rtp.size(), nonce.data(), m_secret_key.data()); + rtp.insert(rtp.end(), ciphertext.begin(), ciphertext.end()); + + Send(rtp.data(), rtp.size()); +} + +void UDPSocket::Send(const uint8_t *data, size_t len) { + sendto(m_socket, reinterpret_cast(data), static_cast(len), 0, reinterpret_cast(&m_server), sizeof(m_server)); +} + +std::vector UDPSocket::Receive() { + while (true) { + sockaddr_in from; + int fromlen = sizeof(from); + static std::array buf; + int n = recvfrom(m_socket, reinterpret_cast(buf.data()), sizeof(buf), 0, reinterpret_cast(&from), &fromlen); + if (n < 0) { + return {}; + } else if (from.sin_addr.S_un.S_addr == m_server.sin_addr.S_un.S_addr && from.sin_port == m_server.sin_port) { + return { buf.begin(), buf.begin() + n }; + } + } +} + +void UDPSocket::Stop() { + m_running = false; + shutdown(m_socket, SD_BOTH); + if (m_thread.joinable()) m_thread.join(); +} + +void UDPSocket::ReadThread() { + while (m_running) { + static std::array buf; + sockaddr_in from; + int addrlen = sizeof(from); + int n = recvfrom(m_socket, reinterpret_cast(buf.data()), sizeof(buf), 0, reinterpret_cast(&from), &addrlen); + if (n > 0 && from.sin_addr.S_un.S_addr == m_server.sin_addr.S_un.S_addr && from.sin_port == m_server.sin_port) { + m_signal_data.emit({ buf.begin(), buf.begin() + n }); + } + } +} + +UDPSocket::type_signal_data UDPSocket::signal_data() { + return m_signal_data; +} + +DiscordVoiceClient::DiscordVoiceClient() { + sodium_init(); + + m_ws.signal_open().connect([this]() { + puts("vws open"); + }); + + m_ws.signal_close().connect([this](uint16_t code) { + printf("vws close %u\n", code); + }); + + m_ws.signal_message().connect([this](const std::string &str) { + std::lock_guard _(m_dispatch_mutex); + m_message_queue.push(str); + m_dispatcher.emit(); + }); + + m_udp.signal_data().connect([this](const std::vector &data) { + std::lock_guard _(m_udp_dispatch_mutex); + m_udp_message_queue.push(data); + m_udp_dispatcher.emit(); + }); + + m_dispatcher.connect([this]() { + m_dispatch_mutex.lock(); + if (m_message_queue.empty()) { + m_dispatch_mutex.unlock(); + return; + } + auto msg = std::move(m_message_queue.front()); + m_message_queue.pop(); + m_dispatch_mutex.unlock(); + OnGatewayMessage(msg); + }); + + m_udp_dispatcher.connect([this]() { + m_udp_dispatch_mutex.lock(); + if (m_udp_message_queue.empty()) { + m_udp_dispatch_mutex.unlock(); + return; + } + auto data = std::move(m_udp_message_queue.front()); + m_udp_message_queue.pop(); + m_udp_dispatch_mutex.unlock(); + OnUDPData(data); + }); +} + +DiscordVoiceClient::~DiscordVoiceClient() { + m_ws.Stop(); + m_udp.Stop(); + m_heartbeat_waiter.kill(); + if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); +} + +void DiscordVoiceClient::Start() { + m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); +} + +void DiscordVoiceClient::SetSessionID(std::string_view session_id) { + m_session_id = session_id; +} + +void DiscordVoiceClient::SetEndpoint(std::string_view endpoint) { + m_endpoint = endpoint; +} + +void DiscordVoiceClient::SetToken(std::string_view token) { + m_token = token; +} + +void DiscordVoiceClient::SetServerID(Snowflake id) { + m_server_id = id; +} + +void DiscordVoiceClient::SetUserID(Snowflake id) { + m_user_id = id; +} + +void DiscordVoiceClient::OnGatewayMessage(const std::string &str) { + VoiceGatewayMessage msg = nlohmann::json::parse(str); + puts(msg.Data.dump(4).c_str()); + switch (msg.Opcode) { + case VoiceGatewayOp::Hello: { + HandleGatewayHello(msg); + } break; + case VoiceGatewayOp::Ready: { + HandleGatewayReady(msg); + } break; + case VoiceGatewayOp::SessionDescription: { + HandleGatewaySessionDescription(msg); + } break; + default: break; + } +} + +void DiscordVoiceClient::HandleGatewayHello(const VoiceGatewayMessage &m) { + VoiceHelloData d = m.Data; + m_heartbeat_msec = d.HeartbeatInterval; + m_heartbeat_thread = std::thread(&DiscordVoiceClient::HeartbeatThread, this); + + Identify(); +} + +void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) { + VoiceReadyData d = m.Data; + m_ip = d.IP; + m_port = d.Port; + m_ssrc = d.SSRC; + if (std::find(d.Modes.begin(), d.Modes.end(), "xsalsa20_poly1305") == d.Modes.end()) { + puts("xsalsa20_poly1305 not in encryption modes"); + } + printf("connect to %s:%u ssrc %u\n", m_ip.c_str(), m_port, m_ssrc); + + m_udp.Connect(m_ip, m_port); + + Discovery(); +} + +void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessage &m) { + VoiceSessionDescriptionData d = m.Data; + printf("receiving with %s secret key: ", d.Mode.c_str()); + for (auto b : d.SecretKey) { + printf("%02X", b); + } + printf("\n"); + m_secret_key = d.SecretKey; + m_udp.SetSSRC(m_ssrc); + m_udp.SetSecretKey(m_secret_key); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); + m_udp.Run(); +} + +void DiscordVoiceClient::Identify() { + VoiceIdentifyMessage msg; + msg.ServerID = m_server_id; + msg.UserID = m_user_id; + msg.SessionID = m_session_id; + msg.Token = m_token; + msg.Video = true; + m_ws.Send(msg); +} + +void DiscordVoiceClient::Discovery() { + std::vector payload; + // 2 bytes = 1, request + payload.push_back(0x00); + payload.push_back(0x01); + // 2 bytes = 70, pl length + payload.push_back(0x00); + payload.push_back(70); + // 4 bytes = ssrc + payload.push_back((m_ssrc >> 24) & 0xFF); + payload.push_back((m_ssrc >> 16) & 0xFF); + payload.push_back((m_ssrc >> 8) & 0xFF); + payload.push_back((m_ssrc >> 0) & 0xFF); + // address and port + for (int i = 0; i < 66; i++) + payload.push_back(0); + m_udp.Send(payload.data(), payload.size()); + auto response = m_udp.Receive(); + if (response.size() >= 74 && response[0] == 0x00 && response[1] == 0x02) { + const char *our_ip = reinterpret_cast(&response[8]); + uint16_t our_port = (response[73] << 8) | response[74]; + printf("we are %s:%u\n", our_ip, our_port); + SelectProtocol(our_ip, our_port); + } else { + puts("received non-discovery packet after discovery"); + } +} + +void DiscordVoiceClient::SelectProtocol(std::string_view ip, uint16_t port) { + VoiceSelectProtocolMessage msg; + msg.Mode = "xsalsa20_poly1305"; + msg.Address = ip; + msg.Port = port; + msg.Protocol = "udp"; + m_ws.Send(msg); +} + +void DiscordVoiceClient::OnUDPData(std::vector data) { + uint8_t *payload = data.data() + 12; + 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())) { + puts("decrypt fail"); + } else { + Abaddon::Get().GetAudio().FeedMeOpus({ payload, payload + data.size() - 12 - crypto_box_MACBYTES }); + } +} + +void DiscordVoiceClient::HeartbeatThread() { + while (true) { + if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec))) + break; + + const auto ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + VoiceHeartbeatMessage msg; + msg.Nonce = static_cast(ms); + m_ws.Send(msg); + } +} + +void from_json(const nlohmann::json &j, VoiceGatewayMessage &m) { + JS_D("op", m.Opcode); + m.Data = j.at("d"); +} + +void from_json(const nlohmann::json &j, VoiceHelloData &m) { + JS_D("heartbeat_interval", m.HeartbeatInterval); +} + +void to_json(nlohmann::json &j, const VoiceHeartbeatMessage &m) { + j["op"] = VoiceGatewayOp::Heartbeat; + j["d"] = m.Nonce; +} + +void to_json(nlohmann::json &j, const VoiceIdentifyMessage &m) { + j["op"] = VoiceGatewayOp::Identify; + j["d"]["server_id"] = m.ServerID; + j["d"]["user_id"] = m.UserID; + j["d"]["session_id"] = m.SessionID; + j["d"]["token"] = m.Token; + j["d"]["video"] = m.Video; + j["d"]["streams"][0]["type"] = "video"; + j["d"]["streams"][0]["rid"] = "100"; + j["d"]["streams"][0]["quality"] = 100; +} + +void from_json(const nlohmann::json &j, VoiceReadyData::VoiceStream &m) { + JS_D("active", m.IsActive); + JS_D("quality", m.Quality); + JS_D("rid", m.RID); + JS_D("rtx_ssrc", m.RTXSSRC); + JS_D("ssrc", m.SSRC); + JS_D("type", m.Type); +} + +void from_json(const nlohmann::json &j, VoiceReadyData &m) { + JS_ON("experiments", m.Experiments); + JS_D("ip", m.IP); + JS_D("modes", m.Modes); + JS_D("port", m.Port); + JS_D("ssrc", m.SSRC); + JS_ON("streams", m.Streams); +} + +void to_json(nlohmann::json &j, const VoiceSelectProtocolMessage &m) { + j["op"] = VoiceGatewayOp::SelectProtocol; + j["d"]["address"] = m.Address; + j["d"]["port"] = m.Port; + j["d"]["protocol"] = m.Protocol; + j["d"]["mode"] = m.Mode; + j["d"]["data"]["address"] = m.Address; + j["d"]["data"]["port"] = m.Port; + j["d"]["data"]["mode"] = m.Mode; +} + +void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m) { + JS_D("mode", m.Mode); + JS_D("secret_key", m.SecretKey); +} diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp new file mode 100644 index 0000000..615bbde --- /dev/null +++ b/src/discord/voiceclient.hpp @@ -0,0 +1,205 @@ +#pragma once +#include "snowflake.hpp" +#include "waiter.hpp" +#include "websocket.hpp" +#include +#include +#include +#include + +enum class VoiceGatewayCloseCode : uint16_t { + UnknownOpcode = 4001, + InvalidPayload = 4002, + NotAuthenticated = 4003, + AuthenticationFailed = 4004, + AlreadyAuthenticated = 4005, + SessionInvalid = 4006, + SessionTimedOut = 4009, + ServerNotFound = 4011, + UnknownProtocol = 4012, + Disconnected = 4014, + ServerCrashed = 4015, + UnknownEncryption = 4016, +}; + +enum class VoiceGatewayOp : int { + Identify = 0, + SelectProtocol = 1, + Ready = 2, + Heartbeat = 3, + SessionDescription = 4, + Speaking = 5, + HeartbeatAck = 6, + Resume = 7, + Hello = 8, + Resumed = 9, + ClientDisconnect = 13, +}; + +struct VoiceGatewayMessage { + VoiceGatewayOp Opcode; + nlohmann::json Data; + + friend void from_json(const nlohmann::json &j, VoiceGatewayMessage &m); +}; + +struct VoiceHelloData { + int HeartbeatInterval; + + friend void from_json(const nlohmann::json &j, VoiceHelloData &m); +}; + +struct VoiceHeartbeatMessage { + uint64_t Nonce; + + friend void to_json(nlohmann::json &j, const VoiceHeartbeatMessage &m); +}; + +struct VoiceIdentifyMessage { + Snowflake ServerID; + Snowflake UserID; + std::string SessionID; + std::string Token; + bool Video; + // todo streams i guess? + + friend void to_json(nlohmann::json &j, const VoiceIdentifyMessage &m); +}; + +struct VoiceReadyData { + struct VoiceStream { + bool IsActive; + int Quality; + std::string RID; + int RTXSSRC; + int SSRC; + std::string Type; + + friend void from_json(const nlohmann::json &j, VoiceStream &m); + }; + + std::vector Experiments; + std::string IP; + std::vector Modes; + uint16_t Port; + uint32_t SSRC; + std::vector Streams; + + friend void from_json(const nlohmann::json &j, VoiceReadyData &m); +}; + +struct VoiceSelectProtocolMessage { + std::string Address; + uint16_t Port; + std::string Mode; + std::string Protocol; + + friend void to_json(nlohmann::json &j, const VoiceSelectProtocolMessage &m); +}; + +struct VoiceSessionDescriptionData { + // std::string AudioCodec; + // std::string VideoCodec; + // std::string MediaSessionID; + std::string Mode; + std::array SecretKey; + + friend void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m); +}; + +class UDPSocket { +public: + UDPSocket(); + ~UDPSocket(); + + void Connect(std::string_view ip, uint16_t port); + void Run(); + void SetSecretKey(std::array key); + void SetSSRC(uint32_t ssrc); + void SendEncrypted(const std::vector &data); + void Send(const uint8_t *data, size_t len); + std::vector Receive(); + void Stop(); + +private: + void ReadThread(); + +#ifdef _WIN32 + SOCKET m_socket; +#else + int m_socket; +#endif + sockaddr_in m_server; + + std::atomic m_running = false; + + std::thread m_thread; + + std::array m_secret_key; + uint32_t m_ssrc; + + uint16_t m_sequence = 0; + uint32_t m_timestamp = 0; + +public: + using type_signal_data = sigc::signal>; + type_signal_data signal_data(); + +private: + type_signal_data m_signal_data; +}; + +class DiscordVoiceClient { +public: + DiscordVoiceClient(); + ~DiscordVoiceClient(); + + void Start(); + + void SetSessionID(std::string_view session_id); + void SetEndpoint(std::string_view endpoint); + void SetToken(std::string_view token); + void SetServerID(Snowflake id); + void SetUserID(Snowflake id); + +private: + void OnGatewayMessage(const std::string &str); + void HandleGatewayHello(const VoiceGatewayMessage &m); + void HandleGatewayReady(const VoiceGatewayMessage &m); + void HandleGatewaySessionDescription(const VoiceGatewayMessage &m); + + void Identify(); + void Discovery(); + void SelectProtocol(std::string_view ip, uint16_t port); + + void OnUDPData(std::vector data); + + void HeartbeatThread(); + + std::string m_session_id; + std::string m_endpoint; + std::string m_token; + Snowflake m_server_id; + Snowflake m_user_id; + + std::string m_ip; + uint16_t m_port; + uint32_t m_ssrc; + + std::array m_secret_key; + + Websocket m_ws; + UDPSocket m_udp; + + Glib::Dispatcher m_dispatcher; + std::queue m_message_queue; + std::mutex m_dispatch_mutex; + + Glib::Dispatcher m_udp_dispatcher; + std::queue> m_udp_message_queue; + std::mutex m_udp_dispatch_mutex; + + int m_heartbeat_msec; + Waiter m_heartbeat_waiter; + std::thread m_heartbeat_thread; +}; diff --git a/src/discord/waiter.hpp b/src/discord/waiter.hpp new file mode 100644 index 0000000..0d5ae92 --- /dev/null +++ b/src/discord/waiter.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include + +class Waiter { +public: + template + bool wait_for(std::chrono::duration const &time) const { + std::unique_lock lock(m); + return !cv.wait_for(lock, time, [&] { return terminate; }); + } + + void kill() { + std::unique_lock lock(m); + terminate = true; + cv.notify_all(); + } + + void revive() { + std::unique_lock lock(m); + terminate = false; + } + +private: + mutable std::condition_variable cv; + mutable std::mutex m; + bool terminate = false; +}; -- cgit v1.2.3 From f2f8afa368218a301b49f3103a6b14f749355532 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:44:30 -0400 Subject: fix compilation maybe --- CMakeLists.txt | 2 ++ src/discord/voiceclient.cpp | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index e779686..3ff2a2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,4 +119,6 @@ if (ENABLE_VOICE) pkg_check_modules(libsodium REQUIRED IMPORTED_TARGET libsodium) target_link_libraries(abaddon PkgConfig::libsodium) + + target_link_libraries(abaddon ${CMAKE_DL_LIBS}) endif () diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 162a5a1..4837d7b 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -4,6 +4,13 @@ #include "abaddon.hpp" #include "audio/manager.hpp" +#ifdef _WIN32 + #define S_ADDR(var) (var).sin_addr.S_un.S_addr + #define socklen_t int +#else + #define S_ADDR(var) (var).sin_addr.s_addr +#endif + UDPSocket::UDPSocket() { m_socket = socket(AF_INET, SOCK_DGRAM, 0); } @@ -15,7 +22,7 @@ UDPSocket::~UDPSocket() { void UDPSocket::Connect(std::string_view ip, uint16_t port) { std::memset(&m_server, 0, sizeof(m_server)); m_server.sin_family = AF_INET; - m_server.sin_addr.S_un.S_addr = inet_addr(ip.data()); + S_ADDR(m_server) = inet_addr(ip.data()); m_server.sin_port = htons(port); bind(m_socket, reinterpret_cast(&m_server), sizeof(m_server)); } @@ -68,12 +75,12 @@ void UDPSocket::Send(const uint8_t *data, size_t len) { std::vector UDPSocket::Receive() { while (true) { sockaddr_in from; - int fromlen = sizeof(from); + socklen_t fromlen = sizeof(from); static std::array buf; int n = recvfrom(m_socket, reinterpret_cast(buf.data()), sizeof(buf), 0, reinterpret_cast(&from), &fromlen); if (n < 0) { return {}; - } else if (from.sin_addr.S_un.S_addr == m_server.sin_addr.S_un.S_addr && from.sin_port == m_server.sin_port) { + } else if (S_ADDR(from) == S_ADDR(m_server) && from.sin_port == m_server.sin_port) { return { buf.begin(), buf.begin() + n }; } } @@ -81,7 +88,11 @@ std::vector UDPSocket::Receive() { void UDPSocket::Stop() { m_running = false; +#ifdef _WIN32 shutdown(m_socket, SD_BOTH); +#else + shutdown(m_socket, SHUT_RDWR); +#endif if (m_thread.joinable()) m_thread.join(); } @@ -89,9 +100,9 @@ void UDPSocket::ReadThread() { while (m_running) { static std::array buf; sockaddr_in from; - int addrlen = sizeof(from); + socklen_t addrlen = sizeof(from); int n = recvfrom(m_socket, reinterpret_cast(buf.data()), sizeof(buf), 0, reinterpret_cast(&from), &addrlen); - if (n > 0 && from.sin_addr.S_un.S_addr == m_server.sin_addr.S_un.S_addr && from.sin_port == m_server.sin_port) { + if (n > 0 && S_ADDR(from) == S_ADDR(m_server) && from.sin_port == m_server.sin_port) { m_signal_data.emit({ buf.begin(), buf.begin() + n }); } } -- cgit v1.2.3 From 12a5fcfcd3bb65ea45a4a11017f30f6ae145c312 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:58:17 -0400 Subject: fix opus include path --- src/audio/manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index af36327..30896ec 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -6,7 +6,7 @@ #include #define MINIAUDIO_IMPLEMENTATION #include -#include +#include #include const uint8_t *StripRTPExtensionHeader(const uint8_t *buf, int num_bytes, size_t &outlen) { -- cgit v1.2.3 From 352c0fd1c151e43b510c4091d4556d15a97a66b8 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 31 Aug 2022 17:07:43 -0400 Subject: fix opus include path (pt 2) --- src/audio/manager.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index ffe7ae9..72d7c2f 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include class AudioManager { public: -- cgit v1.2.3 From 2e9beaaa30bbbbab93c3681ce835ef00b460da3f Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 31 Aug 2022 20:03:24 -0400 Subject: dont send preferred region --- src/discord/objects.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index e4c61c5..5ca8718 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -648,7 +648,7 @@ void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) { j["d"]["self_mute"] = m.SelfMute; j["d"]["self_deaf"] = m.SelfDeaf; j["d"]["self_video"] = m.SelfVideo; - j["d"]["preferred_region"] = m.PreferredRegion; + // j["d"]["preferred_region"] = m.PreferredRegion; } void from_json(const nlohmann::json &j, VoiceStateUpdateData &m) { -- cgit v1.2.3 From 1f4070e52f94470eea3b86a089e3fe6e7fdca2fc Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 31 Aug 2022 20:42:14 -0400 Subject: basic buffering (i think) --- src/audio/manager.cpp | 24 +++++++++++------------- src/audio/manager.hpp | 8 +++++--- 2 files changed, 16 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 30896ec..e686a82 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -9,6 +9,8 @@ #include #include +#define BUFFER_SAMPLES 4800 + const uint8_t *StripRTPExtensionHeader(const uint8_t *buf, int num_bytes, size_t &outlen) { if (buf[0] == 0xbe && buf[1] == 0xde && num_bytes > 4) { uint64_t offset = 4 + 4 * ((buf[2] << 8) | buf[3]); @@ -27,10 +29,8 @@ void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uin const auto buffered_frames = std::min(static_cast(mgr->m_dumb.size() / 2), frameCount); auto *pOutputCast = static_cast(pOutput); - for (ma_uint32 i = 0; i < buffered_frames * 2; i++) { - pOutputCast[i] = mgr->m_dumb.front(); - mgr->m_dumb.pop(); - } + std::copy(mgr->m_dumb.begin(), mgr->m_dumb.begin() + buffered_frames * 2, pOutputCast); + mgr->m_dumb.erase(mgr->m_dumb.begin(), mgr->m_dumb.begin() + buffered_frames * 2); } AudioManager::AudioManager() { @@ -60,7 +60,6 @@ AudioManager::AudioManager() { m_opus_decoder = opus_decoder_create(48000, 2, &err); m_active = true; - // m_thread = std::thread(&AudioManager::testthread, this); } AudioManager::~AudioManager() { @@ -71,22 +70,21 @@ AudioManager::~AudioManager() { void AudioManager::FeedMeOpus(const std::vector &data) { size_t payload_size = 0; const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); - static std::array pcm; + static std::array pcm; int decoded = opus_decode(m_opus_decoder, opus_encoded, static_cast(payload_size), pcm.data(), 120 * 48, 0); if (decoded <= 0) { printf("failed decode: %d\n", decoded); } else { - m_dumb_mutex.lock(); - for (size_t i = 0; i < decoded * 2; i++) { - m_dumb.push(pcm[i]); + m_buffer.insert(m_buffer.end(), pcm.begin(), pcm.begin() + decoded * 2); + if (m_buffer.size() >= BUFFER_SAMPLES * 2) { + m_dumb_mutex.lock(); + m_dumb.insert(m_dumb.end(), m_buffer.begin(), m_buffer.begin() + BUFFER_SAMPLES * 2); + m_dumb_mutex.unlock(); + m_buffer.erase(m_buffer.begin(), m_buffer.begin() + BUFFER_SAMPLES * 2); } - m_dumb_mutex.unlock(); } } -void AudioManager::testthread() { -} - bool AudioManager::OK() const { return m_ok; } diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 72d7c2f..27e973b 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include +#include #include #include -#include +#include #include #include @@ -19,7 +21,6 @@ private: friend void data_callback(ma_device *, void *, const void *, ma_uint32); std::atomic m_active; - void testthread(); std::thread m_thread; bool m_ok; @@ -29,7 +30,8 @@ private: ma_device_config m_device_config; std::mutex m_dumb_mutex; - std::queue m_dumb; + std::deque m_dumb; + std::deque m_buffer; OpusDecoder *m_opus_decoder; }; -- cgit v1.2.3 From 9c8d9e54fe96f97bdda2be26bab571e4cbf0c597 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 2 Sep 2022 00:38:59 -0400 Subject: handle multiple speakers properly --- src/audio/manager.cpp | 49 +++++++++++++++++++++++---------------------- src/audio/manager.hpp | 12 ++++------- src/discord/voiceclient.cpp | 6 +++++- 3 files changed, 34 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index e686a82..9ba8c51 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -9,8 +9,6 @@ #include #include -#define BUFFER_SAMPLES 4800 - const uint8_t *StripRTPExtensionHeader(const uint8_t *buf, int num_bytes, size_t &outlen) { if (buf[0] == 0xbe && buf[1] == 0xde && num_bytes > 4) { uint64_t offset = 4 + 4 * ((buf[2] << 8) | buf[3]); @@ -25,19 +23,24 @@ const uint8_t *StripRTPExtensionHeader(const uint8_t *buf, int num_bytes, size_t 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_dumb_mutex); + std::lock_guard _(mgr->m_mutex); - const auto buffered_frames = std::min(static_cast(mgr->m_dumb.size() / 2), frameCount); - auto *pOutputCast = static_cast(pOutput); - std::copy(mgr->m_dumb.begin(), mgr->m_dumb.begin() + buffered_frames * 2, pOutputCast); - mgr->m_dumb.erase(mgr->m_dumb.begin(), mgr->m_dumb.begin() + buffered_frames * 2); + auto *pOutputF32 = static_cast(pOutput); + for (auto &[ssrc, pair] : mgr->m_sources) { + auto &buf = pair.first; + const size_t n = std::min(buf.size(), frameCount * 2ULL); + for (size_t i = 0; i < n; i++) { + pOutputF32[i] += buf[i] / 32768.F; + } + buf.erase(buf.begin(), buf.begin() + n); + } } AudioManager::AudioManager() { m_ok = true; m_device_config = ma_device_config_init(ma_device_type_playback); - m_device_config.playback.format = ma_format_s16; + m_device_config.playback.format = ma_format_f32; m_device_config.playback.channels = 2; m_device_config.sampleRate = 48000; m_device_config.dataCallback = data_callback; @@ -55,33 +58,31 @@ AudioManager::AudioManager() { m_ok = false; return; } - - int err; - m_opus_decoder = opus_decoder_create(48000, 2, &err); - - m_active = true; } AudioManager::~AudioManager() { - m_active = false; ma_device_uninit(&m_device); + for (auto &[ssrc, pair] : m_sources) { + opus_decoder_destroy(pair.second); + } } -void AudioManager::FeedMeOpus(const std::vector &data) { +void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { size_t payload_size = 0; const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); static std::array pcm; - int decoded = opus_decode(m_opus_decoder, opus_encoded, static_cast(payload_size), pcm.data(), 120 * 48, 0); + if (m_sources.find(ssrc) == m_sources.end()) { + int err; + auto *decoder = opus_decoder_create(48000, 2, &err); + m_sources.insert(std::make_pair(ssrc, std::make_pair(std::deque {}, decoder))); + } + int decoded = opus_decode(m_sources.at(ssrc).second, opus_encoded, static_cast(payload_size), pcm.data(), 120 * 48, 0); if (decoded <= 0) { - printf("failed decode: %d\n", decoded); } else { - m_buffer.insert(m_buffer.end(), pcm.begin(), pcm.begin() + decoded * 2); - if (m_buffer.size() >= BUFFER_SAMPLES * 2) { - m_dumb_mutex.lock(); - m_dumb.insert(m_dumb.end(), m_buffer.begin(), m_buffer.begin() + BUFFER_SAMPLES * 2); - m_dumb_mutex.unlock(); - m_buffer.erase(m_buffer.begin(), m_buffer.begin() + BUFFER_SAMPLES * 2); - } + m_mutex.lock(); + auto &buf = m_sources.at(ssrc).first; + buf.insert(buf.end(), pcm.begin(), pcm.begin() + decoded * 2); + m_mutex.unlock(); } } diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 27e973b..55ecdc3 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,25 +14,20 @@ public: AudioManager(); ~AudioManager(); - void FeedMeOpus(const std::vector &data); + void FeedMeOpus(uint32_t ssrc, const std::vector &data); [[nodiscard]] bool OK() const; private: friend void data_callback(ma_device *, void *, const void *, ma_uint32); - std::atomic m_active; std::thread m_thread; bool m_ok; - ma_engine m_engine; ma_device m_device; ma_device_config m_device_config; - std::mutex m_dumb_mutex; - std::deque m_dumb; - std::deque m_buffer; - - OpusDecoder *m_opus_decoder; + std::mutex m_mutex; + std::unordered_map, OpusDecoder *>> m_sources; }; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 4837d7b..7aaa4a5 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -298,12 +298,16 @@ void DiscordVoiceClient::SelectProtocol(std::string_view ip, uint16_t port) { void DiscordVoiceClient::OnUDPData(std::vector data) { uint8_t *payload = data.data() + 12; + uint32_t ssrc = (data[8] << 24) | + (data[9] << 16) | + (data[10] << 8) | + (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())) { puts("decrypt fail"); } else { - Abaddon::Get().GetAudio().FeedMeOpus({ payload, payload + data.size() - 12 - crypto_box_MACBYTES }); + Abaddon::Get().GetAudio().FeedMeOpus(ssrc, { payload, payload + data.size() - 12 - crypto_box_MACBYTES }); } } -- cgit v1.2.3 From 0a049856782397a7464d03c317ce3788559cdeda Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 2 Sep 2022 01:25:33 -0400 Subject: make compile work if voice support is disabled --- src/abaddon.cpp | 9 +++++++++ src/abaddon.hpp | 9 +++++++++ src/audio/manager.cpp | 4 ++++ src/audio/manager.hpp | 4 ++++ src/components/channels.cpp | 18 ++++++++++++++++++ src/components/channels.hpp | 7 ++++++- src/components/channelscellrenderer.cpp | 12 ++++++++++++ src/components/channelscellrenderer.hpp | 3 +++ src/discord/discord.cpp | 6 ++++++ src/discord/discord.hpp | 12 ++++++++++-- src/discord/objects.cpp | 2 ++ src/discord/objects.hpp | 2 ++ src/discord/voiceclient.cpp | 10 +++++++--- src/discord/voiceclient.hpp | 10 +++++++--- 14 files changed, 99 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index a3a228d..688aff4 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -220,6 +220,7 @@ int Abaddon::StartGTK() { return 1; } +#ifdef WITH_VOICE m_audio = std::make_unique(); if (!m_audio->OK()) { Gtk::MessageDialog dlg(*m_main_window, "The audio engine could not be initialized!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); @@ -227,6 +228,7 @@ int Abaddon::StartGTK() { dlg.run(); return 1; } +#endif // store must be checked before this can be called m_main_window->UpdateComponents(); @@ -247,7 +249,10 @@ int Abaddon::StartGTK() { m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::bind(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened), true)); m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild)); m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings)); + +#ifdef WITH_VOICE m_main_window->GetChannelList()->signal_action_join_voice_channel().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinVoiceChannel)); +#endif m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage)); m_main_window->GetChatWindow()->signal_action_chat_submit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatInputSubmit)); @@ -908,9 +913,11 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) { window->show(); } +#ifdef WITH_VOICE void Abaddon::ActionJoinVoiceChannel(Snowflake channel_id) { m_discord.ConnectToVoice(channel_id); } +#endif std::optional Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) { TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window); @@ -951,9 +958,11 @@ EmojiResource &Abaddon::GetEmojis() { return m_emojis; } +#ifdef WITH_VOICE AudioManager &Abaddon::GetAudio() { return *m_audio.get(); } +#endif int main(int argc, char **argv) { if (std::getenv("ABADDON_NO_FC") == nullptr) diff --git a/src/abaddon.hpp b/src/abaddon.hpp index d67f4ab..9a98c9e 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -53,7 +53,10 @@ public: void ActionAddRecipient(Snowflake channel_id); void ActionViewPins(Snowflake channel_id); void ActionViewThreads(Snowflake channel_id); + +#ifdef WITH_VOICE void ActionJoinVoiceChannel(Snowflake channel_id); +#endif std::optional ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr); bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr); @@ -62,7 +65,10 @@ public: ImageManager &GetImageManager(); EmojiResource &GetEmojis(); + +#ifdef WITH_VOICE AudioManager &GetAudio(); +#endif std::string GetDiscordToken() const; bool IsDiscordActive() const; @@ -141,7 +147,10 @@ private: ImageManager m_img_mgr; EmojiResource m_emojis; + +#ifdef WITH_VOICE std::unique_ptr m_audio; +#endif mutable std::mutex m_mutex; Glib::RefPtr m_gtk_app; diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 9ba8c51..d7d0bb8 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -1,3 +1,5 @@ +#ifdef WITH_VOICE +// clang-format off #ifdef _WIN32 #include #endif @@ -8,6 +10,7 @@ #include #include #include +// clang-format on const uint8_t *StripRTPExtensionHeader(const uint8_t *buf, int num_bytes, size_t &outlen) { if (buf[0] == 0xbe && buf[1] == 0xde && num_bytes > 4) { @@ -89,3 +92,4 @@ void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { bool AudioManager::OK() const { return m_ok; } +#endif diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 55ecdc3..d0f3a21 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -1,4 +1,6 @@ #pragma once +#ifdef WITH_VOICE +// clang-format off #include #include #include @@ -8,6 +10,7 @@ #include #include #include +// clang-format on class AudioManager { public: @@ -31,3 +34,4 @@ private: std::mutex m_mutex; std::unordered_map, OpusDecoder *>> m_sources; }; +#endif diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 566ebd1..201e7c9 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -36,7 +36,11 @@ ChannelList::ChannelList() const auto type = row[m_columns.m_type]; // text channels should not be allowed to be collapsed // maybe they should be but it seems a little difficult to handle expansion to permit this +#ifdef WITH_VOICE if (type != RenderType::TextChannel && type != RenderType::VoiceChannel) { +#else + if (type != RenderType::TextChannel) { +#endif if (row[m_columns.m_expanded]) { m_view.collapse_row(path); row[m_columns.m_expanded] = false; @@ -161,6 +165,7 @@ ChannelList::ChannelList() m_menu_channel.append(m_menu_channel_copy_id); m_menu_channel.show_all(); +#ifdef WITH_VOICE m_menu_voice_channel_join.signal_activate().connect([this]() { const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); printf("join voice: %llu\n", static_cast(id)); @@ -169,6 +174,7 @@ ChannelList::ChannelList() m_menu_voice_channel.append(m_menu_voice_channel_join); m_menu_voice_channel.show_all(); +#endif m_menu_dm_copy_id.signal_activate().connect([this] { Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); @@ -588,7 +594,11 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { for (const auto &channel_ : *guild.Channels) { const auto channel = discord.GetChannel(channel_.ID); if (!channel.has_value()) continue; +#ifdef WITH_VOICE if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) { +#else + if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) { +#endif if (channel->ParentID.has_value()) categories[*channel->ParentID].push_back(*channel); else @@ -618,8 +628,10 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { auto channel_row = *m_model->append(guild_row.children()); if (IsTextChannel(channel.Type)) channel_row[m_columns.m_type] = RenderType::TextChannel; +#ifdef WITH_VOICE else channel_row[m_columns.m_type] = RenderType::VoiceChannel; +#endif channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; @@ -644,8 +656,10 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { auto channel_row = *m_model->append(cat_row.children()); if (IsTextChannel(channel.Type)) channel_row[m_columns.m_type] = RenderType::TextChannel; +#ifdef WITH_VOICE else channel_row[m_columns.m_type] = RenderType::VoiceChannel; +#endif channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_sort] = *channel.Position; @@ -871,10 +885,12 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { OnChannelSubmenuPopup(); m_menu_channel.popup_at_pointer(reinterpret_cast(ev)); break; +#ifdef WITH_VOICE case RenderType::VoiceChannel: OnVoiceChannelSubmenuPopup(); m_menu_voice_channel.popup_at_pointer(reinterpret_cast(ev)); break; +#endif case RenderType::DM: { OnDMSubmenuPopup(); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast(row[m_columns.m_id])); @@ -966,8 +982,10 @@ void ChannelList::OnChannelSubmenuPopup() { m_menu_channel_toggle_mute.set_label("Mute"); } +#ifdef WITH_VOICE void ChannelList::OnVoiceChannelSubmenuPopup() { } +#endif void ChannelList::OnDMSubmenuPopup() { auto iter = m_model->get_iter(m_path_for_menu); diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 53afbdc..8c34735 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -125,8 +125,10 @@ protected: Gtk::MenuItem m_menu_channel_open_tab; #endif +#ifdef WITH_VOICE Gtk::Menu m_menu_voice_channel; Gtk::MenuItem m_menu_voice_channel_join; +#endif Gtk::Menu m_menu_dm; Gtk::MenuItem m_menu_dm_copy_id; @@ -148,10 +150,13 @@ protected: void OnGuildSubmenuPopup(); void OnCategorySubmenuPopup(); void OnChannelSubmenuPopup(); - void OnVoiceChannelSubmenuPopup(); void OnDMSubmenuPopup(); void OnThreadSubmenuPopup(); +#ifdef WITH_VOICE + void OnVoiceChannelSubmenuPopup(); +#endif + bool m_updating_listing = false; Snowflake m_active_channel; diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index e9c43aa..23ee3f0 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -65,8 +65,10 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width); case RenderType::Thread: return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width); +#ifdef WITH_VOICE case RenderType::VoiceChannel: return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width); +#endif case RenderType::DMHeader: return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width); case RenderType::DM: @@ -84,8 +86,10 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width); case RenderType::Thread: return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width); +#ifdef WITH_VOICE case RenderType::VoiceChannel: return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width); +#endif case RenderType::DMHeader: return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width); case RenderType::DM: @@ -103,8 +107,10 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int & return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height); case RenderType::Thread: return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height); +#ifdef WITH_VOICE case RenderType::VoiceChannel: return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height); +#endif case RenderType::DMHeader: return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height); case RenderType::DM: @@ -122,8 +128,10 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height); case RenderType::Thread: return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height); +#ifdef WITH_VOICE case RenderType::VoiceChannel: return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height); +#endif case RenderType::DMHeader: return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height); case RenderType::DM: @@ -141,8 +149,10 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr &cr, return render_vfunc_channel(cr, widget, background_area, cell_area, flags); case RenderType::Thread: return render_vfunc_thread(cr, widget, background_area, cell_area, flags); +#ifdef WITH_VOICE case RenderType::VoiceChannel: return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags); +#endif case RenderType::DMHeader: return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags); case RenderType::DM: @@ -511,6 +521,7 @@ void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtrGuildID.has_value()) return; @@ -1178,6 +1179,7 @@ void DiscordClient::ConnectToVoice(Snowflake channel_id) { m.PreferredRegion = "newark"; m_websocket.Send(m); } +#endif void DiscordClient::SetReferringChannel(Snowflake id) { if (!id.IsValid()) { @@ -1498,12 +1500,14 @@ void DiscordClient::HandleGatewayMessage(std::string str) { case GatewayEvent::GUILD_MEMBERS_CHUNK: { HandleGatewayGuildMembersChunk(m); } break; +#ifdef WITH_VOICE case GatewayEvent::VOICE_STATE_UPDATE: { HandleGatewayVoiceStateUpdate(m); } break; case GatewayEvent::VOICE_SERVER_UPDATE: { HandleGatewayVoiceServerUpdate(m); } break; +#endif } } break; default: @@ -2114,6 +2118,7 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) { m_store.EndTransaction(); } +#ifdef WITH_VOICE void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { VoiceStateUpdateData data = msg.Data; if (data.UserID == m_user_data.ID) { @@ -2132,6 +2137,7 @@ void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) { m_voice.SetUserID(m_user_data.ID); m_voice.Start(); } +#endif void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { ReadySupplementalData data = msg.Data; diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index a6eabd9..907c3e6 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -181,7 +181,9 @@ public: void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot)> &callback); void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot &callback); +#ifdef WITH_VOICE void ConnectToVoice(Snowflake channel_id); +#endif void SetReferringChannel(Snowflake id); @@ -262,11 +264,15 @@ private: void HandleGatewayMessageAck(const GatewayMessage &msg); void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg); void HandleGatewayGuildMembersChunk(const GatewayMessage &msg); - void HandleGatewayVoiceStateUpdate(const GatewayMessage &msg); - void HandleGatewayVoiceServerUpdate(const GatewayMessage &msg); void HandleGatewayReadySupplemental(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg); void HandleGatewayInvalidSession(const GatewayMessage &msg); + +#ifdef WITH_VOICE + void HandleGatewayVoiceStateUpdate(const GatewayMessage &msg); + void HandleGatewayVoiceServerUpdate(const GatewayMessage &msg); +#endif + void HeartbeatThread(); void SendIdentify(); void SendResume(); @@ -326,7 +332,9 @@ private: bool m_wants_resume = false; // reconnecting specifically to resume std::string m_session_id; +#ifdef WITH_VOICE DiscordVoiceClient m_voice; +#endif mutable std::mutex m_msg_mutex; Glib::Dispatcher m_msg_dispatch; diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index 5ca8718..a6a7cc6 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -641,6 +641,7 @@ void from_json(const nlohmann::json &j, GuildMembersChunkData &m) { JS_D("guild_id", m.GuildID); } +#ifdef WITH_VOICE void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) { j["op"] = GatewayOp::VoiceStateUpdate; j["d"]["guild_id"] = m.GuildID; @@ -661,3 +662,4 @@ void from_json(const nlohmann::json &j, VoiceServerUpdateData &m) { JS_D("guild_id", m.GuildID); JS_D("endpoint", m.Endpoint); } +#endif diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp index 240b4c5..c9c8aff 100644 --- a/src/discord/objects.hpp +++ b/src/discord/objects.hpp @@ -867,6 +867,7 @@ struct GuildMembersChunkData { friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m); }; +#ifdef WITH_VOICE struct VoiceStateUpdateMessage { Snowflake GuildID; Snowflake ChannelID; @@ -892,3 +893,4 @@ struct VoiceServerUpdateData { friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m); }; +#endif diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 7aaa4a5..d8855fd 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -1,3 +1,5 @@ +#ifdef WITH_VOICE + // clang-format off #include "voiceclient.hpp" #include "json.hpp" #include @@ -10,6 +12,7 @@ #else #define S_ADDR(var) (var).sin_addr.s_addr #endif +// clang-format on UDPSocket::UDPSocket() { m_socket = socket(AF_INET, SOCK_DGRAM, 0); @@ -88,11 +91,11 @@ std::vector UDPSocket::Receive() { void UDPSocket::Stop() { m_running = false; -#ifdef _WIN32 + #ifdef _WIN32 shutdown(m_socket, SD_BOTH); -#else + #else shutdown(m_socket, SHUT_RDWR); -#endif + #endif if (m_thread.joinable()) m_thread.join(); } @@ -385,3 +388,4 @@ void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m) { JS_D("mode", m.Mode); JS_D("secret_key", m.SecretKey); } +#endif diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 615bbde..4b988d5 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -1,4 +1,6 @@ #pragma once +#ifdef WITH_VOICE +// clang-format off #include "snowflake.hpp" #include "waiter.hpp" #include "websocket.hpp" @@ -6,6 +8,7 @@ #include #include #include +// clang-format on enum class VoiceGatewayCloseCode : uint16_t { UnknownOpcode = 4001, @@ -124,11 +127,11 @@ public: private: void ReadThread(); -#ifdef _WIN32 + #ifdef _WIN32 SOCKET m_socket; -#else + #else int m_socket; -#endif + #endif sockaddr_in m_server; std::atomic m_running = false; @@ -203,3 +206,4 @@ private: Waiter m_heartbeat_waiter; std::thread m_heartbeat_thread; }; +#endif -- cgit v1.2.3 From b7fffb8691c6010864d88b273f346419eb5a5d2b Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 2 Sep 2022 01:47:13 -0400 Subject: fix min call --- src/audio/manager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index d7d0bb8..9d380f9 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -1,5 +1,5 @@ #ifdef WITH_VOICE -// clang-format off + // clang-format off #ifdef _WIN32 #include #endif @@ -31,7 +31,7 @@ void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uin auto *pOutputF32 = static_cast(pOutput); for (auto &[ssrc, pair] : mgr->m_sources) { auto &buf = pair.first; - const size_t n = std::min(buf.size(), frameCount * 2ULL); + const size_t n = std::min(static_cast(buf.size()), static_cast(frameCount * 2ULL)); for (size_t i = 0; i < n; i++) { pOutputF32[i] += buf[i] / 32768.F; } -- cgit v1.2.3 From e93b8715f9e42d25c0930becc22561f23a25a709 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 5 Sep 2022 02:21:37 -0400 Subject: basic voice capture + transmission --- src/audio/manager.cpp | 63 ++++++++++++++++++++++++++++++++++++++++++++- src/audio/manager.hpp | 20 ++++++++++++++ src/discord/voiceclient.cpp | 38 ++++++++++++++++++++++----- src/discord/voiceclient.hpp | 19 ++++++++++++++ 4 files changed, 132 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 9d380f9..b665c81 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -39,9 +39,25 @@ void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uin } } +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); +} + AudioManager::AudioManager() { m_ok = true; + int err; + m_encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_VOIP, &err); + if (err != OPUS_OK) { + printf("failed to initialize opus encoder: %d\n", err); + m_ok = false; + return; + } + opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(64000)); + m_device_config = ma_device_config_init(ma_device_type_playback); m_device_config.playback.format = ma_format_f32; m_device_config.playback.channels = 2; @@ -50,7 +66,7 @@ AudioManager::AudioManager() { m_device_config.pUserData = this; if (ma_device_init(nullptr, &m_device_config, &m_device) != MA_SUCCESS) { - puts("open playabck fail"); + puts("open playback fail"); m_ok = false; return; } @@ -61,15 +77,45 @@ AudioManager::AudioManager() { m_ok = false; return; } + + 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 (ma_device_init(nullptr, &m_capture_config, &m_capture_device) != MA_SUCCESS) { + puts("open capture fail"); + m_ok = false; + return; + } + + if (ma_device_start(&m_capture_device) != MA_SUCCESS) { + puts("failed to start capture"); + ma_device_uninit(&m_capture_device); + m_ok = false; + return; + } + + char device_name[MA_MAX_DEVICE_NAME_LENGTH + 1]; + ma_device_get_name(&m_capture_device, ma_device_type_capture, device_name, sizeof(device_name), nullptr); + printf("using %s for capture\n", device_name); } AudioManager::~AudioManager() { ma_device_uninit(&m_device); + ma_device_uninit(&m_capture_device); for (auto &[ssrc, pair] : m_sources) { opus_decoder_destroy(pair.second); } } +void AudioManager::SetOpusBuffer(uint8_t *ptr) { + m_opus_buffer = ptr; +} + void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { size_t payload_size = 0; const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); @@ -89,7 +135,22 @@ void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { } } +void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { + if (m_opus_buffer == nullptr) return; + + int payload_len = opus_encode(m_encoder, pcm, 480, static_cast(m_opus_buffer), 1275); + if (payload_len < 0) { + printf("encoding error: %d\n", payload_len); + } else { + m_signal_opus_packet.emit(payload_len); + } +} + bool AudioManager::OK() const { return m_ok; } + +AudioManager::type_signal_opus_packet AudioManager::signal_opus_packet() { + return m_signal_opus_packet; +} #endif diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index d0f3a21..700fcc0 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -10,6 +10,7 @@ #include #include #include +#include // clang-format on class AudioManager { @@ -17,21 +18,40 @@ public: AudioManager(); ~AudioManager(); + void SetOpusBuffer(uint8_t *ptr); void FeedMeOpus(uint32_t ssrc, const std::vector &data); [[nodiscard]] bool OK() const; private: + void OnCapturedPCM(const int16_t *pcm, ma_uint32 frames); + 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_device; ma_device_config m_device_config; + // capture + ma_device m_capture_device; + ma_device_config m_capture_config; std::mutex m_mutex; std::unordered_map, OpusDecoder *>> m_sources; + + OpusEncoder *m_encoder; + + uint8_t *m_opus_buffer = nullptr; + +public: + using type_signal_opus_packet = sigc::signal; + type_signal_opus_packet signal_opus_packet(); + +private: + type_signal_opus_packet m_signal_opus_packet; }; #endif diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index d8855fd..3f38eea 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -43,11 +43,11 @@ void UDPSocket::SetSSRC(uint32_t ssrc) { m_ssrc = ssrc; } -void UDPSocket::SendEncrypted(const std::vector &data) { +void UDPSocket::SendEncrypted(const uint8_t *data, size_t len) { m_sequence++; - m_timestamp += (48000 / 100) * 2; + m_timestamp += 480; // this is important - std::vector rtp(12, 0); + std::vector rtp(12 + len + crypto_secretbox_MACBYTES, 0); rtp[0] = 0x80; // ver 2 rtp[1] = 0x78; // payload type 0x78 rtp[2] = (m_sequence >> 8) & 0xFF; @@ -63,14 +63,15 @@ void UDPSocket::SendEncrypted(const std::vector &data) { static std::array nonce = {}; std::memcpy(nonce.data(), rtp.data(), 12); - - std::vector ciphertext(crypto_secretbox_MACBYTES + rtp.size(), 0); - crypto_secretbox_easy(ciphertext.data(), rtp.data(), rtp.size(), nonce.data(), m_secret_key.data()); - rtp.insert(rtp.end(), ciphertext.begin(), ciphertext.end()); + crypto_secretbox_easy(rtp.data() + 12, data, len, nonce.data(), m_secret_key.data()); Send(rtp.data(), rtp.size()); } +void UDPSocket::SendEncrypted(const std::vector &data) { + SendEncrypted(data.data(), data.size()); +} + void UDPSocket::Send(const uint8_t *data, size_t len) { sendto(m_socket, reinterpret_cast(data), static_cast(len), 0, reinterpret_cast(&m_server), sizeof(m_server)); } @@ -172,6 +173,14 @@ DiscordVoiceClient::~DiscordVoiceClient() { void DiscordVoiceClient::Start() { m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); + + // cant put in ctor or deadlock in singleton initialization + auto &aud = Abaddon::Get().GetAudio(); + aud.SetOpusBuffer(m_opus_buffer.data()); + aud.signal_opus_packet().connect([this](int payload_size) { + if (m_connected) + m_udp.SendEncrypted(m_opus_buffer.data(), payload_size); + }); } void DiscordVoiceClient::SetSessionID(std::string_view session_id) { @@ -241,6 +250,13 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa printf("%02X", b); } printf("\n"); + + VoiceSpeakingMessage msg; + msg.Delay = 0; + msg.SSRC = m_ssrc; + msg.Speaking = VoiceSpeakingMessage::Microphone; + m_ws.Send(msg); + m_secret_key = d.SecretKey; m_udp.SetSSRC(m_ssrc); m_udp.SetSecretKey(m_secret_key); @@ -250,6 +266,7 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); m_udp.Run(); + m_connected = true; } void DiscordVoiceClient::Identify() { @@ -388,4 +405,11 @@ void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m) { JS_D("mode", m.Mode); JS_D("secret_key", m.SecretKey); } + +void to_json(nlohmann::json &j, const VoiceSpeakingMessage &m) { + j["op"] = VoiceGatewayOp::Speaking; + j["d"]["speaking"] = m.Speaking; + j["d"]["delay"] = m.Delay; + j["d"]["ssrc"] = m.SSRC; +} #endif diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 4b988d5..67919e5 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -110,6 +110,20 @@ struct VoiceSessionDescriptionData { friend void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m); }; +struct VoiceSpeakingMessage { + enum { + Microphone = 1 << 0, + Soundshare = 1 << 1, + Priority = 1 << 2, + }; + + int Speaking; + int Delay; + uint32_t SSRC; + + friend void to_json(nlohmann::json &j, const VoiceSpeakingMessage &m); +}; + class UDPSocket { public: UDPSocket(); @@ -119,6 +133,7 @@ public: void Run(); void SetSecretKey(std::array key); void SetSSRC(uint32_t ssrc); + void SendEncrypted(const uint8_t *data, size_t len); void SendEncrypted(const std::vector &data); void Send(const uint8_t *data, size_t len); std::vector Receive(); @@ -205,5 +220,9 @@ private: int m_heartbeat_msec; Waiter m_heartbeat_waiter; std::thread m_heartbeat_thread; + + std::array m_opus_buffer; + + std::atomic m_connected = false; }; #endif -- cgit v1.2.3 From 654e225093db165e0c38a658103025dd7233bc71 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 6 Sep 2022 03:25:24 -0400 Subject: try to fix shutdown with select --- src/discord/voiceclient.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 3f38eea..b899638 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -92,22 +92,28 @@ std::vector UDPSocket::Receive() { void UDPSocket::Stop() { m_running = false; - #ifdef _WIN32 - shutdown(m_socket, SD_BOTH); - #else - shutdown(m_socket, SHUT_RDWR); - #endif if (m_thread.joinable()) m_thread.join(); } void UDPSocket::ReadThread() { + timeval tv; while (m_running) { static std::array buf; sockaddr_in from; socklen_t addrlen = sizeof(from); - int n = recvfrom(m_socket, reinterpret_cast(buf.data()), sizeof(buf), 0, reinterpret_cast(&from), &addrlen); - if (n > 0 && S_ADDR(from) == S_ADDR(m_server) && from.sin_port == m_server.sin_port) { - m_signal_data.emit({ buf.begin(), buf.begin() + n }); + + tv.tv_sec = 0; + tv.tv_usec = 1000000; + + fd_set read_fds; + FD_ZERO(&read_fds); + FD_SET(m_socket, &read_fds); + + if (select(m_socket + 1, &read_fds, nullptr, nullptr, &tv) > 0) { + int n = recvfrom(m_socket, reinterpret_cast(buf.data()), sizeof(buf), 0, reinterpret_cast(&from), &addrlen); + if (n > 0 && S_ADDR(from) == S_ADDR(m_server) && from.sin_port == m_server.sin_port) { + m_signal_data.emit({ buf.begin(), buf.begin() + n }); + } } } } -- cgit v1.2.3 From 90437de2c031f6cf0b58603d9cb5582064176374 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 6 Sep 2022 03:29:13 -0400 Subject: make voice disconnect/reconnect work --- src/abaddon.cpp | 5 +++++ src/abaddon.hpp | 1 + src/components/channels.cpp | 25 ++++++++++++++++++++++++- src/components/channels.hpp | 5 +++++ src/discord/discord.cpp | 15 +++++++++++++++ src/discord/discord.hpp | 5 +++++ src/discord/objects.cpp | 10 ++++++++-- src/discord/objects.hpp | 4 ++-- src/discord/voiceclient.cpp | 35 ++++++++++++++++++++++++----------- src/discord/voiceclient.hpp | 4 ++++ 10 files changed, 93 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 688aff4..82741be 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -252,6 +252,7 @@ int Abaddon::StartGTK() { #ifdef WITH_VOICE m_main_window->GetChannelList()->signal_action_join_voice_channel().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinVoiceChannel)); + m_main_window->GetChannelList()->signal_action_disconnect_voice().connect(sigc::mem_fun(*this, &Abaddon::ActionDisconnectVoice)); #endif m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage)); @@ -917,6 +918,10 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) { void Abaddon::ActionJoinVoiceChannel(Snowflake channel_id) { m_discord.ConnectToVoice(channel_id); } + +void Abaddon::ActionDisconnectVoice() { + m_discord.DisconnectFromVoice(); +} #endif std::optional Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) { diff --git a/src/abaddon.hpp b/src/abaddon.hpp index 9a98c9e..9b3199a 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -56,6 +56,7 @@ public: #ifdef WITH_VOICE void ActionJoinVoiceChannel(Snowflake channel_id); + void ActionDisconnectVoice(); #endif std::optional ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr); diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 201e7c9..eb9d688 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -20,6 +20,10 @@ ChannelList::ChannelList() #ifdef WITH_LIBHANDY , m_menu_channel_open_tab("Open in New _Tab", true) , m_menu_dm_open_tab("Open in New _Tab", true) +#endif +#ifdef WITH_VOICE + , m_menu_voice_channel_join("_Join", true) + , m_menu_voice_channel_disconnect("_Disconnect", true) #endif , m_menu_dm_copy_id("_Copy ID", true) , m_menu_dm_close("") // changes depending on if group or not @@ -168,11 +172,15 @@ ChannelList::ChannelList() #ifdef WITH_VOICE m_menu_voice_channel_join.signal_activate().connect([this]() { const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - printf("join voice: %llu\n", static_cast(id)); m_signal_action_join_voice_channel.emit(id); }); + m_menu_voice_channel_disconnect.signal_activate().connect([this]() { + m_signal_action_disconnect_voice.emit(); + }); + m_menu_voice_channel.append(m_menu_voice_channel_join); + m_menu_voice_channel.append(m_menu_voice_channel_disconnect); m_menu_voice_channel.show_all(); #endif @@ -984,6 +992,17 @@ void ChannelList::OnChannelSubmenuPopup() { #ifdef WITH_VOICE void ChannelList::OnVoiceChannelSubmenuPopup() { + const auto iter = m_model->get_iter(m_path_for_menu); + if (!iter) return; + const auto id = static_cast((*iter)[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsConnectedToVoice()) { + m_menu_voice_channel_join.set_sensitive(false); + m_menu_voice_channel_disconnect.set_sensitive(discord.GetVoiceChannelID() == id); + } else { + m_menu_voice_channel_join.set_sensitive(true); + m_menu_voice_channel_disconnect.set_sensitive(false); + } } #endif @@ -1041,6 +1060,10 @@ ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() { return m_signal_action_join_voice_channel; } + +ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() { + return m_signal_action_disconnect_voice; +} #endif ChannelList::ModelColumns::ModelColumns() { diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 8c34735..2d2b257 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -128,6 +128,7 @@ protected: #ifdef WITH_VOICE Gtk::Menu m_menu_voice_channel; Gtk::MenuItem m_menu_voice_channel_join; + Gtk::MenuItem m_menu_voice_channel_disconnect; #endif Gtk::Menu m_menu_dm; @@ -177,7 +178,10 @@ public: #ifdef WITH_VOICE using type_signal_action_join_voice_channel = sigc::signal; + using type_signal_action_disconnect_voice = sigc::signal; + type_signal_action_join_voice_channel signal_action_join_voice_channel(); + type_signal_action_disconnect_voice signal_action_disconnect_voice(); #endif type_signal_action_channel_item_select signal_action_channel_item_select(); @@ -195,5 +199,6 @@ private: #ifdef WITH_VOICE type_signal_action_join_voice_channel m_signal_action_join_voice_channel; + type_signal_action_disconnect_voice m_signal_action_disconnect_voice; #endif }; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 5670d4a..f44c1ed 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1173,12 +1173,27 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI void DiscordClient::ConnectToVoice(Snowflake channel_id) { auto channel = GetChannel(channel_id); if (!channel.has_value() || !channel->GuildID.has_value()) return; + m_voice_channel_id = channel_id; VoiceStateUpdateMessage m; m.GuildID = *channel->GuildID; m.ChannelID = channel_id; m.PreferredRegion = "newark"; m_websocket.Send(m); } + +void DiscordClient::DisconnectFromVoice() { + m_voice.Stop(); + VoiceStateUpdateMessage m; + m_websocket.Send(m); +} + +bool DiscordClient::IsConnectedToVoice() const noexcept { + return m_voice.IsConnected(); +} + +Snowflake DiscordClient::GetVoiceChannelID() const noexcept { + return m_voice_channel_id; +} #endif void DiscordClient::SetReferringChannel(Snowflake id) { diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 907c3e6..0b88519 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -183,6 +183,9 @@ public: #ifdef WITH_VOICE void ConnectToVoice(Snowflake channel_id); + void DisconnectFromVoice(); + [[nodiscard]] bool IsConnectedToVoice() const noexcept; + [[nodiscard]] Snowflake GetVoiceChannelID() const noexcept; #endif void SetReferringChannel(Snowflake id); @@ -334,6 +337,8 @@ private: #ifdef WITH_VOICE DiscordVoiceClient m_voice; + + Snowflake m_voice_channel_id; #endif mutable std::mutex m_msg_mutex; diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index a6a7cc6..3cdc6b5 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -644,8 +644,14 @@ void from_json(const nlohmann::json &j, GuildMembersChunkData &m) { #ifdef WITH_VOICE void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) { j["op"] = GatewayOp::VoiceStateUpdate; - j["d"]["guild_id"] = m.GuildID; - j["d"]["channel_id"] = m.ChannelID; + if (m.GuildID.has_value()) + j["d"]["guild_id"] = *m.GuildID; + else + j["d"]["guild_id"] = nullptr; + if (m.ChannelID.has_value()) + j["d"]["channel_id"] = *m.ChannelID; + else + j["d"]["channel_id"] = nullptr; j["d"]["self_mute"] = m.SelfMute; j["d"]["self_deaf"] = m.SelfDeaf; j["d"]["self_video"] = m.SelfVideo; diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp index c9c8aff..0a947d4 100644 --- a/src/discord/objects.hpp +++ b/src/discord/objects.hpp @@ -869,8 +869,8 @@ struct GuildMembersChunkData { #ifdef WITH_VOICE struct VoiceStateUpdateMessage { - Snowflake GuildID; - Snowflake ChannelID; + std::optional GuildID; + std::optional ChannelID; bool SelfMute = false; bool SelfDeaf = false; bool SelfVideo = false; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index b899638..6d45241 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -168,25 +168,34 @@ DiscordVoiceClient::DiscordVoiceClient() { m_udp_dispatch_mutex.unlock(); OnUDPData(data); }); + + Glib::signal_idle().connect_once([this]() { + // cant put in ctor or deadlock in singleton initialization + auto &aud = Abaddon::Get().GetAudio(); + aud.SetOpusBuffer(m_opus_buffer.data()); + aud.signal_opus_packet().connect([this](int payload_size) { + if (m_connected) + m_udp.SendEncrypted(m_opus_buffer.data(), payload_size); + }); + }); } DiscordVoiceClient::~DiscordVoiceClient() { - m_ws.Stop(); - m_udp.Stop(); - m_heartbeat_waiter.kill(); - if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); + Stop(); } void DiscordVoiceClient::Start() { m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); +} - // cant put in ctor or deadlock in singleton initialization - auto &aud = Abaddon::Get().GetAudio(); - aud.SetOpusBuffer(m_opus_buffer.data()); - aud.signal_opus_packet().connect([this](int payload_size) { - if (m_connected) - m_udp.SendEncrypted(m_opus_buffer.data(), payload_size); - }); +void DiscordVoiceClient::Stop() { + if (m_connected) { + m_ws.Stop(); + m_udp.Stop(); + m_heartbeat_waiter.kill(); + if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); + m_connected = false; + } } void DiscordVoiceClient::SetSessionID(std::string_view session_id) { @@ -209,6 +218,10 @@ void DiscordVoiceClient::SetUserID(Snowflake id) { m_user_id = id; } +bool DiscordVoiceClient::IsConnected() const noexcept { + return m_connected; +} + void DiscordVoiceClient::OnGatewayMessage(const std::string &str) { VoiceGatewayMessage msg = nlohmann::json::parse(str); puts(msg.Data.dump(4).c_str()); diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 67919e5..f81763b 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -173,6 +173,7 @@ public: ~DiscordVoiceClient(); void Start(); + void Stop(); void SetSessionID(std::string_view session_id); void SetEndpoint(std::string_view endpoint); @@ -180,6 +181,8 @@ public: void SetServerID(Snowflake id); void SetUserID(Snowflake id); + [[nodiscard]] bool IsConnected() const noexcept; + private: void OnGatewayMessage(const std::string &str); void HandleGatewayHello(const VoiceGatewayMessage &m); @@ -198,6 +201,7 @@ private: std::string m_endpoint; std::string m_token; Snowflake m_server_id; + Snowflake m_channel_id; Snowflake m_user_id; std::string m_ip; -- cgit v1.2.3 From a79b2d418e1a67b3af034d81fa3df09afc92329e Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 28 Sep 2022 20:44:33 -0400 Subject: synchronize ws close/open to creating thread --- src/discord/websocket.cpp | 15 +++++++++++++-- src/discord/websocket.hpp | 5 +++++ 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/discord/websocket.cpp b/src/discord/websocket.cpp index 46a4b90..04970d3 100644 --- a/src/discord/websocket.cpp +++ b/src/discord/websocket.cpp @@ -1,7 +1,16 @@ #include "websocket.hpp" #include -Websocket::Websocket() = default; +Websocket::Websocket() + : m_close_code(ix::WebSocketCloseConstants::kNormalClosureCode) { + m_open_dispatcher.connect([this]() { + m_signal_open.emit(); + }); + + m_close_dispatcher.connect([this]() { + m_signal_close.emit(m_close_code); + }); +} void Websocket::StartConnection(const std::string &url) { m_websocket.disableAutomaticReconnection(); @@ -44,9 +53,11 @@ void Websocket::Send(const nlohmann::json &j) { void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) { switch (msg->type) { case ix::WebSocketMessageType::Open: { - m_signal_open.emit(); + m_open_dispatcher.emit(); } break; case ix::WebSocketMessageType::Close: { + m_close_code = msg->closeInfo.code; + m_close_dispatcher.emit(); m_signal_close.emit(msg->closeInfo.code); } break; case ix::WebSocketMessageType::Message: { diff --git a/src/discord/websocket.hpp b/src/discord/websocket.hpp index ca7dcc6..ba55254 100644 --- a/src/discord/websocket.hpp +++ b/src/discord/websocket.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -42,4 +43,8 @@ private: type_signal_message m_signal_message; bool m_print_messages = true; + + Glib::Dispatcher m_open_dispatcher; + Glib::Dispatcher m_close_dispatcher; + std::atomic m_close_code; }; -- cgit v1.2.3 From d57d822aa9f85ee023e2a50bd525d530b39a7186 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 28 Sep 2022 20:44:52 -0400 Subject: manage decoders with ssrc updates --- src/abaddon.cpp | 19 +++++++++++++++ src/abaddon.hpp | 5 ++++ src/audio/manager.cpp | 45 +++++++++++++++++++++++----------- src/audio/manager.hpp | 4 +++ src/discord/discord.cpp | 59 ++++++++++++++++++++++++++++++++++++++++++++- src/discord/discord.hpp | 21 ++++++++++++++++ src/discord/objects.cpp | 25 +++++++++++++++---- src/discord/objects.hpp | 34 ++++++++++++++++++++------ src/discord/voiceclient.cpp | 33 +++++++++++++++++++++++-- src/discord/voiceclient.hpp | 38 +++++++++++++++++++++++------ 10 files changed, 246 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index bdedc7c..3245ad4 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -50,6 +50,16 @@ Abaddon::Abaddon() m_discord.signal_thread_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadUpdate)); m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent)); m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect)); + +#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) { + printf("%llu has ssrc %u\n", (uint64_t)m.UserID, m.SSRC); + m_audio->AddSSRC(m.SSRC); + }); +#endif + m_discord.signal_channel_accessibility_changed().connect([this](Snowflake id, bool accessible) { if (!accessible) m_channels_requested.erase(id); @@ -406,6 +416,15 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) { } } +#ifdef WITH_VOICE +void Abaddon::OnVoiceConnected() { +} + +void Abaddon::OnVoiceDisconnected() { + m_audio->RemoveAllSSRCs(); +} +#endif + SettingsManager::Settings &Abaddon::GetSettings() { return m_settings.GetSettings(); } diff --git a/src/abaddon.hpp b/src/abaddon.hpp index ca92370..a15670b 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -89,6 +89,11 @@ public: void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code); void DiscordOnThreadUpdate(const ThreadUpdateData &data); +#ifdef WITH_VOICE + void OnVoiceConnected(); + void OnVoiceDisconnected(); +#endif + SettingsManager::Settings &GetSettings(); Glib::RefPtr GetStyleProvider(); diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index b665c81..797d899 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -107,8 +107,28 @@ AudioManager::AudioManager() { AudioManager::~AudioManager() { ma_device_uninit(&m_device); ma_device_uninit(&m_capture_device); - for (auto &[ssrc, pair] : m_sources) { - opus_decoder_destroy(pair.second); + RemoveAllSSRCs(); +} + +void AudioManager::AddSSRC(uint32_t ssrc) { + int error; + 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) { + if (auto it = m_sources.find(ssrc); it != m_sources.end()) { + opus_decoder_destroy(it->second.second); + m_sources.erase(it); + } +} + +void AudioManager::RemoveAllSSRCs() { + puts("remove all ssrc"); + for (auto it = m_sources.begin(); it != m_sources.end();) { + opus_decoder_destroy(it->second.second); + m_sources.erase(it); + it++; } } @@ -120,18 +140,15 @@ void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { size_t payload_size = 0; const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); static std::array pcm; - if (m_sources.find(ssrc) == m_sources.end()) { - int err; - auto *decoder = opus_decoder_create(48000, 2, &err); - m_sources.insert(std::make_pair(ssrc, std::make_pair(std::deque {}, decoder))); - } - int decoded = opus_decode(m_sources.at(ssrc).second, opus_encoded, static_cast(payload_size), pcm.data(), 120 * 48, 0); - if (decoded <= 0) { - } else { - m_mutex.lock(); - auto &buf = m_sources.at(ssrc).first; - buf.insert(buf.end(), pcm.begin(), pcm.begin() + decoded * 2); - m_mutex.unlock(); + if (auto it = m_sources.find(ssrc); it != m_sources.end()) { + int decoded = opus_decode(it->second.second, opus_encoded, static_cast(payload_size), pcm.data(), 120 * 48, 0); + if (decoded <= 0) { + } else { + m_mutex.lock(); + auto &buf = it->second.first; + buf.insert(buf.end(), pcm.begin(), pcm.begin() + decoded * 2); + m_mutex.unlock(); + } } } diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 700fcc0..3ba6e29 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -18,6 +18,10 @@ public: AudioManager(); ~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); diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 2fff2a1..4202f85 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -23,6 +23,14 @@ DiscordClient::DiscordClient(bool mem_store) m_websocket.signal_open().connect(sigc::mem_fun(*this, &DiscordClient::HandleSocketOpen)); m_websocket.signal_close().connect(sigc::mem_fun(*this, &DiscordClient::HandleSocketClose)); +#ifdef WITH_VOICE + m_voice.signal_connected().connect(sigc::mem_fun(*this, &DiscordClient::OnVoiceConnected)); + m_voice.signal_disconnected().connect(sigc::mem_fun(*this, &DiscordClient::OnVoiceDisconnected)); + m_voice.signal_speaking().connect([this](const VoiceSpeakingData &data) { + m_signal_voice_speaking.emit(data); + }); +#endif + LoadEventMap(); } @@ -2135,11 +2143,16 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) { #ifdef WITH_VOICE void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { - VoiceStateUpdateData data = msg.Data; + VoiceState data = msg.Data; if (data.UserID == m_user_data.ID) { printf("voice session id: %s\n", data.SessionID.c_str()); m_voice.SetSessionID(data.SessionID); } + if (data.ChannelID.has_value()) { + SetVoiceState(data.UserID, *data.ChannelID); + } else { + ClearVoiceState(data.UserID); + } } void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) { @@ -2170,6 +2183,15 @@ void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { m_user_to_status[p.UserID] = PresenceStatus::DND; m_signal_presence_update.emit(*user, m_user_to_status.at(p.UserID)); } +#ifdef WITH_VOICE + for (const auto &g : data.Guilds) { + for (const auto &s : g.VoiceStates) { + if (s.ChannelID.has_value()) { + SetVoiceState(s.UserID, *s.ChannelID); + } + } + } +#endif } void DiscordClient::HandleGatewayReconnect(const GatewayMessage &msg) { @@ -2602,6 +2624,29 @@ void DiscordClient::HandleReadyGuildSettings(const ReadyEventData &data) { } } +#ifdef WITH_VOICE +void DiscordClient::SetVoiceState(Snowflake user_id, Snowflake channel_id) { + m_voice_state_user_channel[user_id] = channel_id; + m_voice_state_channel_users[channel_id].insert(user_id); +} + +void DiscordClient::ClearVoiceState(Snowflake user_id) { + if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) { + m_voice_state_channel_users[it->second].erase(user_id); + // invalidated + m_voice_state_user_channel.erase(user_id); + } +} + +void DiscordClient::OnVoiceConnected() { + m_signal_voice_connected.emit(); +} + +void DiscordClient::OnVoiceDisconnected() { + m_signal_voice_disconnected.emit(); +} +#endif + void DiscordClient::LoadEventMap() { m_event_map["READY"] = GatewayEvent::READY; m_event_map["MESSAGE_CREATE"] = GatewayEvent::MESSAGE_CREATE; @@ -2858,3 +2903,15 @@ DiscordClient::type_signal_channel_accessibility_changed DiscordClient::signal_c DiscordClient::type_signal_message_send_fail DiscordClient::signal_message_send_fail() { return m_signal_message_send_fail; } + +DiscordClient::type_signal_voice_connected DiscordClient::signal_voice_connected() { + return m_signal_voice_connected; +} + +DiscordClient::type_signal_voice_disconnected DiscordClient::signal_voice_disconnected() { + return m_signal_voice_disconnected; +} + +DiscordClient::type_signal_voice_speaking DiscordClient::signal_voice_speaking() { + return m_signal_voice_speaking; +} diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 0b88519..fb0b46d 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -339,6 +339,15 @@ private: DiscordVoiceClient m_voice; Snowflake m_voice_channel_id; + // todo sql i guess + std::unordered_map m_voice_state_user_channel; + std::unordered_map> m_voice_state_channel_users; + + void SetVoiceState(Snowflake user_id, Snowflake channel_id); + void ClearVoiceState(Snowflake user_id); + + void OnVoiceConnected(); + void OnVoiceDisconnected(); #endif mutable std::mutex m_msg_mutex; @@ -413,6 +422,10 @@ public: typedef sigc::signal type_signal_connected; typedef sigc::signal type_signal_message_progress; + using type_signal_voice_connected = sigc::signal; + using type_signal_voice_disconnected = sigc::signal; + using type_signal_voice_speaking = sigc::signal; + type_signal_gateway_ready signal_gateway_ready(); type_signal_message_create signal_message_create(); type_signal_message_delete signal_message_delete(); @@ -467,6 +480,10 @@ public: type_signal_connected signal_connected(); type_signal_message_progress signal_message_progress(); + type_signal_voice_connected signal_voice_connected(); + type_signal_voice_disconnected signal_voice_disconnected(); + type_signal_voice_speaking signal_voice_speaking(); + protected: type_signal_gateway_ready m_signal_gateway_ready; type_signal_message_create m_signal_message_create; @@ -521,4 +538,8 @@ protected: type_signal_disconnected m_signal_disconnected; type_signal_connected m_signal_connected; type_signal_message_progress m_signal_message_progress; + + type_signal_voice_connected m_signal_voice_connected; + type_signal_voice_disconnected m_signal_voice_disconnected; + type_signal_voice_speaking m_signal_voice_speaking; }; diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index 3cdc6b5..21853a7 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -233,6 +233,11 @@ void from_json(const nlohmann::json &j, SupplementalMergedPresencesData &m) { JS_D("friends", m.Friends); } +void from_json(const nlohmann::json &j, SupplementalGuildEntry &m) { + JS_D("id", m.ID); + JS_D("voice_states", m.VoiceStates); +} + void from_json(const nlohmann::json &j, ReadySupplementalData &m) { JS_D("merged_presences", m.MergedPresences); } @@ -658,14 +663,24 @@ void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) { // j["d"]["preferred_region"] = m.PreferredRegion; } -void from_json(const nlohmann::json &j, VoiceStateUpdateData &m) { - JS_ON("user_id", m.UserID); - JS_ON("session_id", m.SessionID); -} - void from_json(const nlohmann::json &j, VoiceServerUpdateData &m) { JS_D("token", m.Token); JS_D("guild_id", m.GuildID); JS_D("endpoint", m.Endpoint); } #endif + +void from_json(const nlohmann::json &j, VoiceState &m) { + JS_O("guild_id", m.GuildID); + JS_N("channel_id", m.ChannelID); + JS_D("deaf", m.IsDeafened); + JS_D("mute", m.IsMuted); + JS_D("self_deaf", m.IsSelfDeafened); + JS_D("self_mute", m.IsSelfMuted); + JS_D("self_video", m.IsSelfVideo); + JS_O("self_stream", m.IsSelfStream); + JS_D("suppress", m.IsSuppressed); + JS_D("user_id", m.UserID); + JS_N("member", m.Member); + JS_D("session_id", m.SessionID); +} diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp index 0a947d4..14dd20c 100644 --- a/src/discord/objects.hpp +++ b/src/discord/objects.hpp @@ -354,8 +354,18 @@ struct SupplementalMergedPresencesData { friend void from_json(const nlohmann::json &j, SupplementalMergedPresencesData &m); }; +struct VoiceState; +struct SupplementalGuildEntry { + // std::vector EmbeddedActivities; + Snowflake ID; + std::vector VoiceStates; + + friend void from_json(const nlohmann::json &j, SupplementalGuildEntry &m); +}; + struct ReadySupplementalData { SupplementalMergedPresencesData MergedPresences; + std::vector Guilds; friend void from_json(const nlohmann::json &j, ReadySupplementalData &m); }; @@ -879,13 +889,6 @@ struct VoiceStateUpdateMessage { friend void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m); }; -struct VoiceStateUpdateData { - Snowflake UserID; - std::string SessionID; - - friend void from_json(const nlohmann::json &j, VoiceStateUpdateData &m); -}; - struct VoiceServerUpdateData { std::string Token; Snowflake GuildID; @@ -894,3 +897,20 @@ struct VoiceServerUpdateData { friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m); }; #endif + +struct VoiceState { + std::optional ChannelID; + bool IsDeafened; + bool IsMuted; + std::optional GuildID; + std::optional Member; + bool IsSelfDeafened; + bool IsSelfMuted; + bool IsSelfVideo; + bool IsSelfStream = false; + std::string SessionID; + bool IsSuppressed; + Snowflake UserID; + + friend void from_json(const nlohmann::json &j, VoiceState &m); +}; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 6d45241..b7fe21b 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -1,5 +1,5 @@ #ifdef WITH_VOICE - // clang-format off +// clang-format off #include "voiceclient.hpp" #include "json.hpp" #include @@ -186,6 +186,7 @@ DiscordVoiceClient::~DiscordVoiceClient() { void DiscordVoiceClient::Start() { m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); + m_heartbeat_waiter.revive(); } void DiscordVoiceClient::Stop() { @@ -195,6 +196,7 @@ void DiscordVoiceClient::Stop() { m_heartbeat_waiter.kill(); if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); m_connected = false; + m_signal_disconnected.emit(); } } @@ -235,6 +237,9 @@ void DiscordVoiceClient::OnGatewayMessage(const std::string &str) { case VoiceGatewayOp::SessionDescription: { HandleGatewaySessionDescription(msg); } break; + case VoiceGatewayOp::Speaking: { + HandleGatewaySpeaking(msg); + } break; default: break; } } @@ -273,7 +278,7 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa VoiceSpeakingMessage msg; msg.Delay = 0; msg.SSRC = m_ssrc; - msg.Speaking = VoiceSpeakingMessage::Microphone; + msg.Speaking = VoiceSpeakingType::Microphone; m_ws.Send(msg); m_secret_key = d.SecretKey; @@ -286,6 +291,12 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); m_udp.Run(); m_connected = true; + m_signal_connected.emit(); +} + +void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) { + VoiceSpeakingData data = m.Data; + m_signal_speaking.emit(data); } void DiscordVoiceClient::Identify() { @@ -365,6 +376,18 @@ void DiscordVoiceClient::HeartbeatThread() { } } +DiscordVoiceClient::type_signal_disconnected DiscordVoiceClient::signal_connected() { + return m_signal_connected; +} + +DiscordVoiceClient::type_signal_disconnected DiscordVoiceClient::signal_disconnected() { + return m_signal_disconnected; +} + +DiscordVoiceClient::type_signal_speaking DiscordVoiceClient::signal_speaking() { + return m_signal_speaking; +} + void from_json(const nlohmann::json &j, VoiceGatewayMessage &m) { JS_D("op", m.Opcode); m.Data = j.at("d"); @@ -431,4 +454,10 @@ void to_json(nlohmann::json &j, const VoiceSpeakingMessage &m) { j["d"]["delay"] = m.Delay; j["d"]["ssrc"] = m.SSRC; } + +void from_json(const nlohmann::json &j, VoiceSpeakingData &m) { + JS_D("user_id", m.UserID); + JS_D("ssrc", m.SSRC); + JS_D("speaking", m.Speaking); +} #endif diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index f81763b..c052f28 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -1,6 +1,6 @@ #pragma once #ifdef WITH_VOICE -// clang-format off + // clang-format off #include "snowflake.hpp" #include "waiter.hpp" #include "websocket.hpp" @@ -8,6 +8,7 @@ #include #include #include +#include // clang-format on enum class VoiceGatewayCloseCode : uint16_t { @@ -110,20 +111,28 @@ struct VoiceSessionDescriptionData { friend void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m); }; -struct VoiceSpeakingMessage { - enum { - Microphone = 1 << 0, - Soundshare = 1 << 1, - Priority = 1 << 2, - }; +enum class VoiceSpeakingType { + Microphone = 1 << 0, + Soundshare = 1 << 1, + Priority = 1 << 2, +}; - int Speaking; +struct VoiceSpeakingMessage { + VoiceSpeakingType Speaking; int Delay; uint32_t SSRC; friend void to_json(nlohmann::json &j, const VoiceSpeakingMessage &m); }; +struct VoiceSpeakingData { + Snowflake UserID; + uint32_t SSRC; + VoiceSpeakingType Speaking; + + friend void from_json(const nlohmann::json &j, VoiceSpeakingData &m); +}; + class UDPSocket { public: UDPSocket(); @@ -188,6 +197,7 @@ private: void HandleGatewayHello(const VoiceGatewayMessage &m); void HandleGatewayReady(const VoiceGatewayMessage &m); void HandleGatewaySessionDescription(const VoiceGatewayMessage &m); + void HandleGatewaySpeaking(const VoiceGatewayMessage &m); void Identify(); void Discovery(); @@ -228,5 +238,17 @@ private: std::array m_opus_buffer; std::atomic m_connected = false; + + using type_signal_connected = sigc::signal; + using type_signal_disconnected = sigc::signal; + using type_signal_speaking = sigc::signal; + type_signal_connected m_signal_connected; + type_signal_disconnected m_signal_disconnected; + type_signal_speaking m_signal_speaking; + +public: + type_signal_connected signal_connected(); + type_signal_disconnected signal_disconnected(); + type_signal_speaking signal_speaking(); }; #endif -- cgit v1.2.3 From a96d96b3aa883c5ee5892e4ff94e3c539989c66a Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 28 Sep 2022 22:10:36 -0400 Subject: basic mute/deafen --- src/abaddon.cpp | 20 ++++++++++++++++++++ src/audio/manager.cpp | 12 +++++++++++- src/audio/manager.hpp | 7 +++++++ src/discord/discord.cpp | 27 +++++++++++++++++++++++++++ src/discord/discord.hpp | 14 ++++++++++++++ src/windows/voicewindow.cpp | 36 ++++++++++++++++++++++++++++++++++++ src/windows/voicewindow.hpp | 30 ++++++++++++++++++++++++++++++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/windows/voicewindow.cpp create mode 100644 src/windows/voicewindow.hpp (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 3245ad4..678353c 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -18,6 +18,7 @@ #include "windows/profilewindow.hpp" #include "windows/pinnedwindow.hpp" #include "windows/threadswindow.hpp" +#include "windows/voicewindow.hpp" #include "startup.hpp" #ifdef WITH_LIBHANDY @@ -418,6 +419,25 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) { #ifdef WITH_VOICE void Abaddon::OnVoiceConnected() { + auto *wnd = new VoiceWindow; + + wnd->signal_mute().connect([this](bool is_mute) { + m_discord.SetVoiceMuted(is_mute); + m_audio->SetCapture(!is_mute); + }); + + wnd->signal_deafen().connect([this](bool is_deaf) { + m_discord.SetVoiceDeafened(is_deaf); + m_audio->SetPlayback(!is_deaf); + }); + + wnd->show(); + wnd->signal_hide().connect([this, wnd]() { + m_discord.DisconnectFromVoice(); + delete wnd; + delete m_user_menu; + SetupUserMenu(); + }); } void Abaddon::OnVoiceDisconnected() { diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 797d899..35a9c76 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -137,6 +137,8 @@ void AudioManager::SetOpusBuffer(uint8_t *ptr) { } void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { + if (!m_should_playback) return; + size_t payload_size = 0; const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); static std::array pcm; @@ -152,8 +154,16 @@ void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { } } +void AudioManager::SetCapture(bool capture) { + m_should_capture = capture; +} + +void AudioManager::SetPlayback(bool playback) { + m_should_playback = playback; +} + void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { - if (m_opus_buffer == nullptr) return; + if (m_opus_buffer == nullptr || !m_should_capture) return; int payload_len = opus_encode(m_encoder, pcm, 480, static_cast(m_opus_buffer), 1275); if (payload_len < 0) { diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 3ba6e29..4986da6 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -1,6 +1,7 @@ #pragma once #ifdef WITH_VOICE // clang-format off + #include #include #include @@ -25,6 +26,9 @@ public: void SetOpusBuffer(uint8_t *ptr); void FeedMeOpus(uint32_t ssrc, const std::vector &data); + void SetCapture(bool capture); + void SetPlayback(bool playback); + [[nodiscard]] bool OK() const; private: @@ -51,6 +55,9 @@ private: uint8_t *m_opus_buffer = nullptr; + std::atomic m_should_capture = true; + std::atomic m_should_playback = true; + public: using type_signal_opus_packet = sigc::signal; type_signal_opus_packet signal_opus_packet(); diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 4202f85..f5a1e1b 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1202,6 +1202,16 @@ bool DiscordClient::IsConnectedToVoice() const noexcept { Snowflake DiscordClient::GetVoiceChannelID() const noexcept { return m_voice_channel_id; } + +void DiscordClient::SetVoiceMuted(bool is_mute) { + m_mute_requested = is_mute; + SendVoiceStateUpdate(); +} + +void DiscordClient::SetVoiceDeafened(bool is_deaf) { + m_deaf_requested = is_deaf; + SendVoiceStateUpdate(); +} #endif void DiscordClient::SetReferringChannel(Snowflake id) { @@ -2625,6 +2635,21 @@ void DiscordClient::HandleReadyGuildSettings(const ReadyEventData &data) { } #ifdef WITH_VOICE +void DiscordClient::SendVoiceStateUpdate() { + VoiceStateUpdateMessage msg; + msg.ChannelID = m_voice_channel_id; + const auto channel = GetChannel(m_voice_channel_id); + if (channel.has_value() && channel->GuildID.has_value()) { + msg.GuildID = *channel->GuildID; + } + + msg.SelfMute = m_mute_requested; + msg.SelfDeaf = m_deaf_requested; + msg.SelfVideo = false; + + m_websocket.Send(msg); +} + void DiscordClient::SetVoiceState(Snowflake user_id, Snowflake channel_id) { m_voice_state_user_channel[user_id] = channel_id; m_voice_state_channel_users[channel_id].insert(user_id); @@ -2904,6 +2929,7 @@ DiscordClient::type_signal_message_send_fail DiscordClient::signal_message_send_ return m_signal_message_send_fail; } +#ifdef WITH_VOICE DiscordClient::type_signal_voice_connected DiscordClient::signal_voice_connected() { return m_signal_voice_connected; } @@ -2915,3 +2941,4 @@ DiscordClient::type_signal_voice_disconnected DiscordClient::signal_voice_discon DiscordClient::type_signal_voice_speaking DiscordClient::signal_voice_speaking() { return m_signal_voice_speaking; } +#endif diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index fb0b46d..b0fecab 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -186,6 +186,9 @@ public: void DisconnectFromVoice(); [[nodiscard]] bool IsConnectedToVoice() const noexcept; [[nodiscard]] Snowflake GetVoiceChannelID() const noexcept; + + void SetVoiceMuted(bool is_mute); + void SetVoiceDeafened(bool is_deaf); #endif void SetReferringChannel(Snowflake id); @@ -338,11 +341,16 @@ private: #ifdef WITH_VOICE DiscordVoiceClient m_voice; + bool m_mute_requested = false; + bool m_deaf_requested = false; + Snowflake m_voice_channel_id; // todo sql i guess std::unordered_map m_voice_state_user_channel; std::unordered_map> m_voice_state_channel_users; + void SendVoiceStateUpdate(); + void SetVoiceState(Snowflake user_id, Snowflake channel_id); void ClearVoiceState(Snowflake user_id); @@ -422,9 +430,11 @@ public: typedef sigc::signal type_signal_connected; typedef sigc::signal type_signal_message_progress; +#ifdef WITH_VOICE using type_signal_voice_connected = sigc::signal; using type_signal_voice_disconnected = sigc::signal; using type_signal_voice_speaking = sigc::signal; +#endif type_signal_gateway_ready signal_gateway_ready(); type_signal_message_create signal_message_create(); @@ -480,9 +490,11 @@ public: type_signal_connected signal_connected(); type_signal_message_progress signal_message_progress(); +#ifdef WITH_VOICE type_signal_voice_connected signal_voice_connected(); type_signal_voice_disconnected signal_voice_disconnected(); type_signal_voice_speaking signal_voice_speaking(); +#endif protected: type_signal_gateway_ready m_signal_gateway_ready; @@ -539,7 +551,9 @@ protected: type_signal_connected m_signal_connected; type_signal_message_progress m_signal_message_progress; +#ifdef WITH_VOICE type_signal_voice_connected m_signal_voice_connected; type_signal_voice_disconnected m_signal_voice_disconnected; type_signal_voice_speaking m_signal_voice_speaking; +#endif }; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp new file mode 100644 index 0000000..b352c86 --- /dev/null +++ b/src/windows/voicewindow.cpp @@ -0,0 +1,36 @@ +#include "voicewindow.hpp" + +VoiceWindow::VoiceWindow() + : m_main(Gtk::ORIENTATION_VERTICAL) + , m_controls(Gtk::ORIENTATION_HORIZONTAL) + , m_mute("Mute") + , m_deafen("Deafen") { + get_style_context()->add_class("app-window"); + + set_default_size(300, 300); + + m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); + m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); + + m_controls.add(m_mute); + m_controls.add(m_deafen); + m_main.add(m_controls); + add(m_main); + show_all_children(); +} + +void VoiceWindow::OnMuteChanged() { + m_signal_mute.emit(m_mute.get_active()); +} + +void VoiceWindow::OnDeafenChanged() { + m_signal_deafen.emit(m_deafen.get_active()); +} + +VoiceWindow::type_signal_mute VoiceWindow::signal_mute() { + return m_signal_mute; +} + +VoiceWindow::type_signal_deafen VoiceWindow::signal_deafen() { + return m_signal_deafen; +} diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp new file mode 100644 index 0000000..bee0a84 --- /dev/null +++ b/src/windows/voicewindow.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include + +class VoiceWindow : public Gtk::Window { +public: + VoiceWindow(); + +private: + void OnMuteChanged(); + void OnDeafenChanged(); + + Gtk::Box m_main; + Gtk::Box m_controls; + + Gtk::CheckButton m_mute; + Gtk::CheckButton m_deafen; + +public: + using type_signal_mute = sigc::signal; + using type_signal_deafen = sigc::signal; + + type_signal_mute signal_mute(); + type_signal_deafen signal_deafen(); + +private: + type_signal_mute m_signal_mute; + type_signal_deafen m_signal_deafen; +}; -- cgit v1.2.3 From dc127d15fb49c108450e691c44ed930a11bb7e59 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:46:15 -0400 Subject: display user list, client side mute --- res/css/application-low-priority.css | 2 +- src/abaddon.cpp | 8 ++++- src/audio/manager.cpp | 16 ++++++++- src/audio/manager.hpp | 7 ++++ src/discord/discord.cpp | 8 +++++ src/discord/discord.hpp | 2 ++ src/discord/objects.cpp | 3 +- src/discord/voiceclient.cpp | 9 +++++ src/discord/voiceclient.hpp | 9 ++++- src/windows/voicewindow.cpp | 70 ++++++++++++++++++++++++++++++++++-- src/windows/voicewindow.hpp | 14 +++++++- 11 files changed, 140 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/res/css/application-low-priority.css b/res/css/application-low-priority.css index cf108f4..5078d22 100644 --- a/res/css/application-low-priority.css +++ b/res/css/application-low-priority.css @@ -44,7 +44,7 @@ has to be separate to allow main.css to override certain things background: @secondary_color; } -.app-popup list { +.app-window list, .app-popup list { background: @secondary_color; } diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 678353c..4c879ca 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -419,7 +419,7 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) { #ifdef WITH_VOICE void Abaddon::OnVoiceConnected() { - auto *wnd = new VoiceWindow; + auto *wnd = new VoiceWindow(m_discord.GetVoiceChannelID()); wnd->signal_mute().connect([this](bool is_mute) { m_discord.SetVoiceMuted(is_mute); @@ -431,6 +431,12 @@ void Abaddon::OnVoiceConnected() { m_audio->SetPlayback(!is_deaf); }); + 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); + } + }); + wnd->show(); wnd->signal_hide().connect([this, wnd]() { m_discord.DisconnectFromVoice(); diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 35a9c76..e025390 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -1,5 +1,6 @@ #ifdef WITH_VOICE - // clang-format off +// clang-format off + #ifdef _WIN32 #include #endif @@ -138,6 +139,10 @@ void AudioManager::SetOpusBuffer(uint8_t *ptr) { void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { if (!m_should_playback) return; + { + std::lock_guard _(m_muted_ssrc_mutex); + if (m_muted_ssrcs.find(ssrc) != m_muted_ssrcs.end()) return; + } size_t payload_size = 0; const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); @@ -162,6 +167,15 @@ void AudioManager::SetPlayback(bool playback) { m_should_playback = playback; } +void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { + std::lock_guard _(m_muted_ssrc_mutex); + if (mute) { + m_muted_ssrcs.insert(ssrc); + } else { + m_muted_ssrcs.erase(ssrc); + } +} + void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { if (m_opus_buffer == nullptr || !m_should_capture) return; diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 4986da6..277f2f3 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,8 @@ public: void SetCapture(bool capture); void SetPlayback(bool playback); + void SetMuteSSRC(uint32_t ssrc, bool mute); + [[nodiscard]] bool OK() const; private: @@ -58,6 +61,10 @@ private: std::atomic m_should_capture = true; std::atomic m_should_playback = true; + std::unordered_set m_muted_ssrcs; + + mutable std::mutex m_muted_ssrc_mutex; + public: using type_signal_opus_packet = sigc::signal; type_signal_opus_packet signal_opus_packet(); diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index f5a1e1b..b6bcc07 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1203,6 +1203,14 @@ Snowflake DiscordClient::GetVoiceChannelID() const noexcept { return m_voice_channel_id; } +std::unordered_set DiscordClient::GetUsersInVoiceChannel(Snowflake channel_id) { + return m_voice_state_channel_users[channel_id]; +} + +std::optional DiscordClient::GetSSRCOfUser(Snowflake id) const { + return m_voice.GetSSRCOfUser(id); +} + void DiscordClient::SetVoiceMuted(bool is_mute) { m_mute_requested = is_mute; SendVoiceStateUpdate(); diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index b0fecab..9c6c748 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -186,6 +186,8 @@ public: void DisconnectFromVoice(); [[nodiscard]] bool IsConnectedToVoice() const noexcept; [[nodiscard]] Snowflake GetVoiceChannelID() const noexcept; + [[nodiscard]] std::unordered_set GetUsersInVoiceChannel(Snowflake channel_id); + [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; void SetVoiceMuted(bool is_mute); void SetVoiceDeafened(bool is_deaf); diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index 21853a7..3e87e43 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -240,6 +240,7 @@ void from_json(const nlohmann::json &j, SupplementalGuildEntry &m) { void from_json(const nlohmann::json &j, ReadySupplementalData &m) { JS_D("merged_presences", m.MergedPresences); + JS_D("guilds", m.Guilds); } void to_json(nlohmann::json &j, const IdentifyProperties &m) { @@ -681,6 +682,6 @@ void from_json(const nlohmann::json &j, VoiceState &m) { JS_O("self_stream", m.IsSelfStream); JS_D("suppress", m.IsSuppressed); JS_D("user_id", m.UserID); - JS_N("member", m.Member); + JS_ON("member", m.Member); JS_D("session_id", m.SessionID); } diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index b7fe21b..5f6ab31 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -1,5 +1,6 @@ #ifdef WITH_VOICE // clang-format off + #include "voiceclient.hpp" #include "json.hpp" #include @@ -220,6 +221,13 @@ void DiscordVoiceClient::SetUserID(Snowflake id) { m_user_id = id; } +std::optional DiscordVoiceClient::GetSSRCOfUser(Snowflake id) const { + if (const auto it = m_ssrc_map.find(id); it != m_ssrc_map.end()) { + return it->second; + } + return {}; +} + bool DiscordVoiceClient::IsConnected() const noexcept { return m_connected; } @@ -296,6 +304,7 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) { VoiceSpeakingData data = m.Data; + m_ssrc_map[data.UserID] = data.SSRC; m_signal_speaking.emit(data); } diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index c052f28..1dc6e1a 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -1,14 +1,17 @@ #pragma once #ifdef WITH_VOICE - // clang-format off +// clang-format off + #include "snowflake.hpp" #include "waiter.hpp" #include "websocket.hpp" +#include #include #include #include #include #include +#include // clang-format on enum class VoiceGatewayCloseCode : uint16_t { @@ -190,6 +193,8 @@ public: void SetServerID(Snowflake id); void SetUserID(Snowflake id); + [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; + [[nodiscard]] bool IsConnected() const noexcept; private: @@ -218,6 +223,8 @@ private: uint16_t m_port; uint32_t m_ssrc; + std::unordered_map m_ssrc_map; + std::array m_secret_key; Websocket m_ws; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index b352c86..17f8ec2 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -1,24 +1,86 @@ #include "voicewindow.hpp" +#include "components/lazyimage.hpp" +#include "abaddon.hpp" -VoiceWindow::VoiceWindow() +class VoiceWindowUserListEntry : public Gtk::ListBoxRow { +public: + VoiceWindowUserListEntry(Snowflake id) + : m_main(Gtk::ORIENTATION_HORIZONTAL) + , m_avatar(32, 32) + , m_mute("Mute") { + m_name.set_halign(Gtk::ALIGN_START); + m_name.set_hexpand(true); + m_mute.set_halign(Gtk::ALIGN_END); + + m_main.add(m_avatar); + m_main.add(m_name); + m_main.add(m_mute); + add(m_main); + show_all_children(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto user = discord.GetUser(id); + if (user.has_value()) { + m_name.set_text(user->Username); + } else { + m_name.set_text("Unknown user"); + } + + m_mute.signal_toggled().connect([this]() { + m_signal_mute_cs.emit(m_mute.get_active()); + }); + } + +private: + Gtk::Box m_main; + LazyImage m_avatar; + Gtk::Label m_name; + Gtk::CheckButton m_mute; + +public: + using type_signal_mute_cs = sigc::signal; + type_signal_mute_cs signal_mute_cs() { + return m_signal_mute_cs; + } + +private: + type_signal_mute_cs m_signal_mute_cs; +}; + +VoiceWindow::VoiceWindow(Snowflake channel_id) : m_main(Gtk::ORIENTATION_VERTICAL) , m_controls(Gtk::ORIENTATION_HORIZONTAL) , m_mute("Mute") - , m_deafen("Deafen") { + , m_deafen("Deafen") + , m_channel_id(channel_id) { get_style_context()->add_class("app-window"); set_default_size(300, 300); + auto &discord = Abaddon::Get().GetDiscordClient(); + SetUsers(discord.GetUsersInVoiceChannel(m_channel_id)); + m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); m_controls.add(m_mute); m_controls.add(m_deafen); m_main.add(m_controls); + m_main.add(m_user_list); add(m_main); show_all_children(); } +void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { + for (auto id : user_ids) { + auto *row = Gtk::make_managed(id); + row->signal_mute_cs().connect([this, id](bool is_muted) { + m_signal_mute_user_cs.emit(id, is_muted); + }); + m_user_list.add(*row); + } +} + void VoiceWindow::OnMuteChanged() { m_signal_mute.emit(m_mute.get_active()); } @@ -34,3 +96,7 @@ VoiceWindow::type_signal_mute VoiceWindow::signal_mute() { VoiceWindow::type_signal_deafen VoiceWindow::signal_deafen() { return m_signal_deafen; } + +VoiceWindow::type_signal_mute_user_cs VoiceWindow::signal_mute_user_cs() { + return m_signal_mute_user_cs; +} diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index bee0a84..a2162b4 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -1,13 +1,18 @@ #pragma once +#include "discord/snowflake.hpp" #include #include +#include #include +#include class VoiceWindow : public Gtk::Window { public: - VoiceWindow(); + VoiceWindow(Snowflake channel_id); private: + void SetUsers(const std::unordered_set &user_ids); + void OnMuteChanged(); void OnDeafenChanged(); @@ -17,14 +22,21 @@ private: Gtk::CheckButton m_mute; Gtk::CheckButton m_deafen; + Gtk::ListBox m_user_list; + + Snowflake m_channel_id; + public: using type_signal_mute = sigc::signal; using type_signal_deafen = sigc::signal; + using type_signal_mute_user_cs = sigc::signal; type_signal_mute signal_mute(); type_signal_deafen signal_deafen(); + type_signal_mute_user_cs signal_mute_user_cs(); private: type_signal_mute m_signal_mute; type_signal_deafen m_signal_deafen; + type_signal_mute_user_cs m_signal_mute_user_cs; }; -- cgit v1.2.3 From b735feb901387d26984ceba66bb0c555b16c1838 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 29 Sep 2022 22:47:00 -0400 Subject: add udp keepalive --- src/discord/voiceclient.cpp | 16 ++++++++++++++++ src/discord/voiceclient.hpp | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 5f6ab31..7b4989e 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -188,6 +188,7 @@ DiscordVoiceClient::~DiscordVoiceClient() { void DiscordVoiceClient::Start() { m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); m_heartbeat_waiter.revive(); + m_keepalive_waiter.revive(); } void DiscordVoiceClient::Stop() { @@ -196,6 +197,8 @@ void DiscordVoiceClient::Stop() { m_udp.Stop(); m_heartbeat_waiter.kill(); if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); + m_keepalive_waiter.kill(); + if (m_keepalive_thread.joinable()) m_keepalive_thread.join(); m_connected = false; m_signal_disconnected.emit(); } @@ -271,6 +274,7 @@ void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) { printf("connect to %s:%u ssrc %u\n", m_ip.c_str(), m_port, m_ssrc); m_udp.Connect(m_ip, m_port); + m_keepalive_thread = std::thread(&DiscordVoiceClient::KeepaliveThread, this); Discovery(); } @@ -385,6 +389,18 @@ void DiscordVoiceClient::HeartbeatThread() { } } +void DiscordVoiceClient::KeepaliveThread() { + while (true) { + if (!m_keepalive_waiter.wait_for(std::chrono::seconds(10))) + break; + + if (IsConnected()) { + const static uint8_t KEEPALIVE[] = { 0x13, 0x37 }; + m_udp.Send(KEEPALIVE, sizeof(KEEPALIVE)); + } + } +} + DiscordVoiceClient::type_signal_disconnected DiscordVoiceClient::signal_connected() { return m_signal_connected; } diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 1dc6e1a..634c333 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -5,8 +5,8 @@ #include "snowflake.hpp" #include "waiter.hpp" #include "websocket.hpp" -#include #include +#include #include #include #include @@ -211,6 +211,7 @@ private: void OnUDPData(std::vector data); void HeartbeatThread(); + void KeepaliveThread(); std::string m_session_id; std::string m_endpoint; @@ -242,6 +243,9 @@ private: Waiter m_heartbeat_waiter; std::thread m_heartbeat_thread; + Waiter m_keepalive_waiter; + std::thread m_keepalive_thread; + std::array m_opus_buffer; std::atomic m_connected = false; -- cgit v1.2.3 From f8ae99ee7bdc9c65938f083d3c98ba86cce94706 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 29 Sep 2022 22:47:10 -0400 Subject: fix crash on disconnect --- src/audio/manager.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index e025390..de04866 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -126,11 +126,10 @@ void AudioManager::RemoveSSRC(uint32_t ssrc) { void AudioManager::RemoveAllSSRCs() { puts("remove all ssrc"); - for (auto it = m_sources.begin(); it != m_sources.end();) { - opus_decoder_destroy(it->second.second); - m_sources.erase(it); - it++; + for (auto &[ssrc, pair] : m_sources) { + opus_decoder_destroy(pair.second); } + m_sources.clear(); } void AudioManager::SetOpusBuffer(uint8_t *ptr) { -- cgit v1.2.3 From 0438b11c910e1bfea2d82d61ed24ec9f47a20598 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 30 Sep 2022 01:09:51 -0400 Subject: dont dispatch udp to main --- src/audio/manager.cpp | 13 +++++++++---- src/discord/voiceclient.cpp | 16 +--------------- src/discord/voiceclient.hpp | 4 ---- 3 files changed, 10 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index de04866..8321c3e 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -112,12 +112,16 @@ AudioManager::~AudioManager() { } void AudioManager::AddSSRC(uint32_t ssrc) { + std::lock_guard _(m_mutex); int error; - auto *decoder = opus_decoder_create(48000, 2, &error); - m_sources.insert(std::make_pair(ssrc, std::make_pair(std::deque {}, decoder))); + 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); @@ -126,6 +130,7 @@ void AudioManager::RemoveSSRC(uint32_t ssrc) { void AudioManager::RemoveAllSSRCs() { puts("remove all ssrc"); + std::lock_guard _(m_mutex); for (auto &[ssrc, pair] : m_sources) { opus_decoder_destroy(pair.second); } @@ -146,14 +151,14 @@ void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { size_t payload_size = 0; const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); static std::array pcm; + + std::lock_guard _(m_mutex); if (auto it = m_sources.find(ssrc); it != m_sources.end()) { int decoded = opus_decode(it->second.second, opus_encoded, static_cast(payload_size), pcm.data(), 120 * 48, 0); if (decoded <= 0) { } else { - m_mutex.lock(); auto &buf = it->second.first; buf.insert(buf.end(), pcm.begin(), pcm.begin() + decoded * 2); - m_mutex.unlock(); } } } diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 7b4989e..0b87032 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -141,9 +141,7 @@ DiscordVoiceClient::DiscordVoiceClient() { }); m_udp.signal_data().connect([this](const std::vector &data) { - std::lock_guard _(m_udp_dispatch_mutex); - m_udp_message_queue.push(data); - m_udp_dispatcher.emit(); + OnUDPData(data); }); m_dispatcher.connect([this]() { @@ -158,18 +156,6 @@ DiscordVoiceClient::DiscordVoiceClient() { OnGatewayMessage(msg); }); - m_udp_dispatcher.connect([this]() { - m_udp_dispatch_mutex.lock(); - if (m_udp_message_queue.empty()) { - m_udp_dispatch_mutex.unlock(); - return; - } - auto data = std::move(m_udp_message_queue.front()); - m_udp_message_queue.pop(); - m_udp_dispatch_mutex.unlock(); - OnUDPData(data); - }); - Glib::signal_idle().connect_once([this]() { // cant put in ctor or deadlock in singleton initialization auto &aud = Abaddon::Get().GetAudio(); diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 634c333..1d8b952 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -235,10 +235,6 @@ private: std::queue m_message_queue; std::mutex m_dispatch_mutex; - Glib::Dispatcher m_udp_dispatcher; - std::queue> m_udp_message_queue; - std::mutex m_udp_dispatch_mutex; - int m_heartbeat_msec; Waiter m_heartbeat_waiter; std::thread m_heartbeat_thread; -- cgit v1.2.3 From 3e3afde2239c70404830375a0bc15878ea4c6f5d Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 30 Sep 2022 01:23:58 -0400 Subject: try to fix mindeps build --- src/windows/voicewindow.cpp | 6 ++++++ src/windows/voicewindow.hpp | 5 +++++ 2 files changed, 11 insertions(+) (limited to 'src') diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 17f8ec2..847ba81 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -1,6 +1,11 @@ +#ifdef WITH_VOICE + +// clang-format off + #include "voicewindow.hpp" #include "components/lazyimage.hpp" #include "abaddon.hpp" +// clang-format on class VoiceWindowUserListEntry : public Gtk::ListBoxRow { public: @@ -100,3 +105,4 @@ VoiceWindow::type_signal_deafen VoiceWindow::signal_deafen() { VoiceWindow::type_signal_mute_user_cs VoiceWindow::signal_mute_user_cs() { return m_signal_mute_user_cs; } +#endif diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index a2162b4..50943f4 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -1,10 +1,14 @@ #pragma once +#ifdef WITH_VOICE +// clang-format off + #include "discord/snowflake.hpp" #include #include #include #include #include +// clang-format on class VoiceWindow : public Gtk::Window { public: @@ -40,3 +44,4 @@ private: type_signal_deafen m_signal_deafen; type_signal_mute_user_cs m_signal_mute_user_cs; }; +#endif -- cgit v1.2.3 From e08e3106d68da08c58f3a5bde968d4367bef78df Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sat, 1 Oct 2022 17:46:10 -0400 Subject: rudimentary dm voice call support --- src/components/channels.cpp | 28 +++++++++++++++++++++++++++- src/components/channels.hpp | 4 ++++ src/discord/discord.cpp | 13 ++++++++++--- src/discord/objects.cpp | 5 +++-- src/discord/objects.hpp | 3 ++- 5 files changed, 46 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/components/channels.cpp b/src/components/channels.cpp index eb9d688..dbd0aed 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -27,6 +27,10 @@ ChannelList::ChannelList() #endif , m_menu_dm_copy_id("_Copy ID", true) , m_menu_dm_close("") // changes depending on if group or not +#ifdef WITH_VOICE + , m_menu_dm_join_voice("Join _Voice", true) + , m_menu_dm_disconnect_voice("_Disconnect Voice", true) +#endif , m_menu_thread_copy_id("_Copy ID", true) , m_menu_thread_leave("_Leave", true) , m_menu_thread_archive("_Archive", true) @@ -215,6 +219,17 @@ ChannelList::ChannelList() #endif m_menu_dm.append(m_menu_dm_toggle_mute); m_menu_dm.append(m_menu_dm_close); +#ifdef WITH_VOICE + m_menu_dm_join_voice.signal_activate().connect([this]() { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + m_signal_action_join_voice_channel.emit(id); + }); + m_menu_dm_disconnect_voice.signal_activate().connect([this]() { + m_signal_action_disconnect_voice.emit(); + }); + m_menu_dm.append(m_menu_dm_join_voice); + m_menu_dm.append(m_menu_dm_disconnect_voice); +#endif m_menu_dm.append(m_menu_dm_copy_id); m_menu_dm.show_all(); @@ -1010,10 +1025,21 @@ void ChannelList::OnDMSubmenuPopup() { auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast((*iter)[m_columns.m_id]); - if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id)) + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) m_menu_dm_toggle_mute.set_label("Unmute"); else m_menu_dm_toggle_mute.set_label("Mute"); + +#ifdef WITH_VOICE + if (discord.IsConnectedToVoice()) { + m_menu_dm_join_voice.set_sensitive(false); + m_menu_dm_disconnect_voice.set_sensitive(discord.GetVoiceChannelID() == id); + } else { + m_menu_dm_join_voice.set_sensitive(true); + m_menu_dm_disconnect_voice.set_sensitive(false); + } +#endif } void ChannelList::OnThreadSubmenuPopup() { diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 2d2b257..d1986a0 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -135,6 +135,10 @@ protected: Gtk::MenuItem m_menu_dm_copy_id; Gtk::MenuItem m_menu_dm_close; Gtk::MenuItem m_menu_dm_toggle_mute; +#ifdef WITH_VOICE + Gtk::MenuItem m_menu_dm_join_voice; + Gtk::MenuItem m_menu_dm_disconnect_voice; +#endif #ifdef WITH_LIBHANDY Gtk::MenuItem m_menu_dm_open_tab; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index b6bcc07..e0748fb 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1180,10 +1180,11 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI #ifdef WITH_VOICE void DiscordClient::ConnectToVoice(Snowflake channel_id) { auto channel = GetChannel(channel_id); - if (!channel.has_value() || !channel->GuildID.has_value()) return; + if (!channel.has_value()) return; m_voice_channel_id = channel_id; VoiceStateUpdateMessage m; - m.GuildID = *channel->GuildID; + if (channel->GuildID.has_value()) + m.GuildID = channel->GuildID; m.ChannelID = channel_id; m.PreferredRegion = "newark"; m_websocket.Send(m); @@ -2179,7 +2180,13 @@ void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) { printf("token: %s\n", data.Token.c_str()); m_voice.SetEndpoint(data.Endpoint); m_voice.SetToken(data.Token); - m_voice.SetServerID(data.GuildID); + if (data.GuildID.has_value()) { + m_voice.SetServerID(*data.GuildID); + } else if (data.ChannelID.has_value()) { + m_voice.SetServerID(*data.ChannelID); + } else { + puts("no guild or channel id in voice server?"); + } m_voice.SetUserID(m_user_data.ID); m_voice.Start(); } diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index 3e87e43..37a9241 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -666,13 +666,14 @@ void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) { void from_json(const nlohmann::json &j, VoiceServerUpdateData &m) { JS_D("token", m.Token); - JS_D("guild_id", m.GuildID); JS_D("endpoint", m.Endpoint); + JS_ON("guild_id", m.GuildID); + JS_ON("channel_id", m.ChannelID); } #endif void from_json(const nlohmann::json &j, VoiceState &m) { - JS_O("guild_id", m.GuildID); + JS_ON("guild_id", m.GuildID); JS_N("channel_id", m.ChannelID); JS_D("deaf", m.IsDeafened); JS_D("mute", m.IsMuted); diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp index 14dd20c..120ffa2 100644 --- a/src/discord/objects.hpp +++ b/src/discord/objects.hpp @@ -891,8 +891,9 @@ struct VoiceStateUpdateMessage { struct VoiceServerUpdateData { std::string Token; - Snowflake GuildID; std::string Endpoint; + std::optional GuildID; + std::optional ChannelID; friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m); }; -- cgit v1.2.3 From d8d9f1b857da6e7beaf8b998f50b20ea295ab92a Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sun, 2 Oct 2022 02:50:48 -0400 Subject: close voice window on context menu disconnect --- src/abaddon.cpp | 5 +++++ src/abaddon.hpp | 1 + 2 files changed, 6 insertions(+) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 4c879ca..6dc5fd1 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -420,6 +420,7 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) { #ifdef WITH_VOICE void Abaddon::OnVoiceConnected() { auto *wnd = new VoiceWindow(m_discord.GetVoiceChannelID()); + m_voice_window = wnd; wnd->signal_mute().connect([this](bool is_mute) { m_discord.SetVoiceMuted(is_mute); @@ -440,6 +441,7 @@ void Abaddon::OnVoiceConnected() { wnd->show(); wnd->signal_hide().connect([this, wnd]() { m_discord.DisconnectFromVoice(); + m_voice_window = nullptr; delete wnd; delete m_user_menu; SetupUserMenu(); @@ -448,6 +450,9 @@ void Abaddon::OnVoiceConnected() { void Abaddon::OnVoiceDisconnected() { m_audio->RemoveAllSSRCs(); + if (m_voice_window != nullptr) { + m_voice_window->close(); + } } #endif diff --git a/src/abaddon.hpp b/src/abaddon.hpp index a15670b..f45f435 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -162,6 +162,7 @@ private: #ifdef WITH_VOICE std::unique_ptr m_audio; + Gtk::Window *m_voice_window = nullptr; #endif mutable std::mutex m_mutex; -- cgit v1.2.3 From 05acb8c857c923391a651f38018204b5dc35e39d Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 3 Oct 2022 00:16:56 -0400 Subject: try and handle voice socket closure properly maybe --- src/discord/voiceclient.cpp | 17 +++++++++-------- src/discord/websocket.cpp | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 0b87032..c0679ce 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -132,6 +132,7 @@ DiscordVoiceClient::DiscordVoiceClient() { m_ws.signal_close().connect([this](uint16_t code) { printf("vws close %u\n", code); + Stop(); }); m_ws.signal_message().connect([this](const std::string &str) { @@ -175,16 +176,18 @@ void DiscordVoiceClient::Start() { m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); m_heartbeat_waiter.revive(); m_keepalive_waiter.revive(); + m_connected = true; + m_signal_connected.emit(); } void DiscordVoiceClient::Stop() { + m_ws.Stop(); + m_udp.Stop(); + m_heartbeat_waiter.kill(); + if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); + m_keepalive_waiter.kill(); + if (m_keepalive_thread.joinable()) m_keepalive_thread.join(); if (m_connected) { - m_ws.Stop(); - m_udp.Stop(); - m_heartbeat_waiter.kill(); - if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); - m_keepalive_waiter.kill(); - if (m_keepalive_thread.joinable()) m_keepalive_thread.join(); m_connected = false; m_signal_disconnected.emit(); } @@ -288,8 +291,6 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); m_udp.Run(); - m_connected = true; - m_signal_connected.emit(); } void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) { diff --git a/src/discord/websocket.cpp b/src/discord/websocket.cpp index 04970d3..cf0111c 100644 --- a/src/discord/websocket.cpp +++ b/src/discord/websocket.cpp @@ -8,6 +8,7 @@ Websocket::Websocket() }); m_close_dispatcher.connect([this]() { + Stop(); m_signal_close.emit(m_close_code); }); } @@ -58,7 +59,6 @@ void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) { case ix::WebSocketMessageType::Close: { m_close_code = msg->closeInfo.code; m_close_dispatcher.emit(); - m_signal_close.emit(msg->closeInfo.code); } break; case ix::WebSocketMessageType::Message: { m_signal_message.emit(msg->str); -- cgit v1.2.3 From 9394ac9b93b718bf93c5678c2af484799980dc49 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 3 Oct 2022 16:05:31 -0400 Subject: support voice text channels --- src/components/channels.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/components/channels.cpp b/src/components/channels.cpp index dbd0aed..a5747dc 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -58,7 +58,7 @@ ChannelList::ChannelList() } } - if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) { + if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel) { const auto id = static_cast(row[m_columns.m_id]); m_signal_action_channel_item_select.emit(id); Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {}); @@ -784,7 +784,7 @@ bool ChannelList::SelectionFunc(const Glib::RefPtr &model, const m_last_selected = m_model->get_path(row); auto type = (*m_model->get_iter(path))[m_columns.m_type]; - return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread; + return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel; } void ChannelList::AddPrivateChannels() { -- cgit v1.2.3 From 9dc2e863e847c03cec1cdb53ba7c58e0740ad481 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 4 Oct 2022 02:08:48 -0400 Subject: temp mindeps build fix --- src/components/channels.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src') diff --git a/src/components/channels.cpp b/src/components/channels.cpp index a5747dc..a36b215 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -58,7 +58,11 @@ ChannelList::ChannelList() } } +#ifdef WITH_VOICE if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel) { +#else + if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) { +#endif const auto id = static_cast(row[m_columns.m_id]); m_signal_action_channel_item_select.emit(id); Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {}); @@ -784,7 +788,11 @@ bool ChannelList::SelectionFunc(const Glib::RefPtr &model, const m_last_selected = m_model->get_path(row); auto type = (*m_model->get_iter(path))[m_columns.m_type]; +#ifdef WITH_VOICE return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel; +#else + return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread; +#endif } void ChannelList::AddPrivateChannels() { -- cgit v1.2.3 From 92c70bda08472d880e23df30661376d526406230 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 5 Oct 2022 18:43:44 -0400 Subject: add per user volume slider --- res/css/application-low-priority.css | 8 ++++++++ src/abaddon.cpp | 6 ++++++ src/audio/manager.cpp | 14 +++++++++++++- src/audio/manager.hpp | 3 +++ src/windows/voicewindow.cpp | 33 +++++++++++++++++++++++++++++---- src/windows/voicewindow.hpp | 3 +++ 6 files changed, 62 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/res/css/application-low-priority.css b/res/css/application-low-priority.css index 5078d22..130033f 100644 --- a/res/css/application-low-priority.css +++ b/res/css/application-low-priority.css @@ -87,3 +87,11 @@ has to be separate to allow main.css to override certain things .app-window colorswatch { box-shadow: 0 1px rgba(0, 0, 0, 0); } + +.app-window scale { + padding-top: 0px; + padding-bottom: 0px; + margin-top: 0px; + margin-bottom: 0px; + color: @text_color; +} diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 6dc5fd1..8a92de7 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -438,6 +438,12 @@ void Abaddon::OnVoiceConnected() { } }); + wnd->signal_user_volume_changed().connect([this](Snowflake id, double volume) { + if (const auto ssrc = m_discord.GetSSRCOfUser(id); ssrc.has_value()) { + m_audio->SetVolumeSSRC(*ssrc, volume); + } + }); + wnd->show(); wnd->signal_hide().connect([this, wnd]() { m_discord.DisconnectFromVoice(); diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 8321c3e..37f696e 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -28,13 +28,18 @@ void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uin AudioManager *mgr = reinterpret_cast(pDevice->pUserData); if (mgr == nullptr) return; std::lock_guard _(mgr->m_mutex); + std::lock_guard _2(mgr->m_volume_ssrc_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] += buf[i] / 32768.F; + pOutputF32[i] += volume * buf[i] / 32768.F; } buf.erase(buf.begin(), buf.begin() + n); } @@ -180,6 +185,13 @@ void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { } } +void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { + std::lock_guard _(m_volume_ssrc_mutex); + volume *= 0.01; + constexpr const double E = 2.71828182845904523536; + m_volume_ssrc[ssrc] = (std::exp(volume) - 1) / (E - 1); +} + void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { if (m_opus_buffer == nullptr || !m_should_capture) return; diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 277f2f3..4d2299e 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -31,6 +31,7 @@ public: void SetPlayback(bool playback); void SetMuteSSRC(uint32_t ssrc, bool mute); + void SetVolumeSSRC(uint32_t ssrc, double volume); [[nodiscard]] bool OK() const; @@ -62,8 +63,10 @@ private: std::atomic m_should_playback = true; std::unordered_set m_muted_ssrcs; + std::unordered_map m_volume_ssrc; mutable std::mutex m_muted_ssrc_mutex; + mutable std::mutex m_volume_ssrc_mutex; public: using type_signal_opus_packet = sigc::signal; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 847ba81..ed9971e 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -10,16 +10,26 @@ class VoiceWindowUserListEntry : public Gtk::ListBoxRow { public: VoiceWindowUserListEntry(Snowflake id) - : m_main(Gtk::ORIENTATION_HORIZONTAL) + : m_main(Gtk::ORIENTATION_VERTICAL) + , m_horz(Gtk::ORIENTATION_HORIZONTAL) , m_avatar(32, 32) , m_mute("Mute") { m_name.set_halign(Gtk::ALIGN_START); m_name.set_hexpand(true); m_mute.set_halign(Gtk::ALIGN_END); - m_main.add(m_avatar); - m_main.add(m_name); - m_main.add(m_mute); + m_volume.set_range(0.0, 200.0); + m_volume.set_value_pos(Gtk::POS_LEFT); + m_volume.set_value(100.0); + m_volume.signal_value_changed().connect([this]() { + m_signal_volume.emit(m_volume.get_value()); + }); + + m_horz.add(m_avatar); + m_horz.add(m_name); + m_horz.add(m_mute); + m_main.add(m_horz); + m_main.add(m_volume); add(m_main); show_all_children(); @@ -38,18 +48,26 @@ public: private: Gtk::Box m_main; + Gtk::Box m_horz; LazyImage m_avatar; Gtk::Label m_name; Gtk::CheckButton m_mute; + Gtk::Scale m_volume; public: using type_signal_mute_cs = sigc::signal; + using type_signal_volume = sigc::signal; type_signal_mute_cs signal_mute_cs() { return m_signal_mute_cs; } + type_signal_volume signal_volume() { + return m_signal_volume; + } + private: type_signal_mute_cs m_signal_mute_cs; + type_signal_volume m_signal_volume; }; VoiceWindow::VoiceWindow(Snowflake channel_id) @@ -82,6 +100,9 @@ void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { row->signal_mute_cs().connect([this, id](bool is_muted) { m_signal_mute_user_cs.emit(id, is_muted); }); + row->signal_volume().connect([this, id](double volume) { + m_signal_user_volume_changed.emit(id, volume); + }); m_user_list.add(*row); } } @@ -105,4 +126,8 @@ VoiceWindow::type_signal_deafen VoiceWindow::signal_deafen() { VoiceWindow::type_signal_mute_user_cs VoiceWindow::signal_mute_user_cs() { return m_signal_mute_user_cs; } + +VoiceWindow::type_signal_user_volume_changed VoiceWindow::signal_user_volume_changed() { + return m_signal_user_volume_changed; +} #endif diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index 50943f4..da4987e 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -34,14 +34,17 @@ public: using type_signal_mute = sigc::signal; using type_signal_deafen = sigc::signal; using type_signal_mute_user_cs = sigc::signal; + using type_signal_user_volume_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(); 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; }; #endif -- cgit v1.2.3 From d2c9985c5793c8a6f20b01db87380d8bf241f077 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 5 Oct 2022 21:39:18 -0400 Subject: one mutex is enough --- src/audio/manager.cpp | 14 +++++--------- src/audio/manager.hpp | 3 --- 2 files changed, 5 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 37f696e..cf6b545 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -28,7 +28,6 @@ void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uin AudioManager *mgr = reinterpret_cast(pDevice->pUserData); if (mgr == nullptr) return; std::lock_guard _(mgr->m_mutex); - std::lock_guard _2(mgr->m_volume_ssrc_mutex); auto *pOutputF32 = static_cast(pOutput); for (auto &[ssrc, pair] : mgr->m_sources) { @@ -148,16 +147,13 @@ void AudioManager::SetOpusBuffer(uint8_t *ptr) { void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { if (!m_should_playback) return; - { - std::lock_guard _(m_muted_ssrc_mutex); - if (m_muted_ssrcs.find(ssrc) != m_muted_ssrcs.end()) return; - } + + std::lock_guard _(m_mutex); + if (m_muted_ssrcs.find(ssrc) != m_muted_ssrcs.end()) return; size_t payload_size = 0; const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast(data.size()), payload_size); static std::array pcm; - - std::lock_guard _(m_mutex); if (auto it = m_sources.find(ssrc); it != m_sources.end()) { int decoded = opus_decode(it->second.second, opus_encoded, static_cast(payload_size), pcm.data(), 120 * 48, 0); if (decoded <= 0) { @@ -177,7 +173,7 @@ void AudioManager::SetPlayback(bool playback) { } void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { - std::lock_guard _(m_muted_ssrc_mutex); + std::lock_guard _(m_mutex); if (mute) { m_muted_ssrcs.insert(ssrc); } else { @@ -186,7 +182,7 @@ void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { } void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { - std::lock_guard _(m_volume_ssrc_mutex); + std::lock_guard _(m_mutex); volume *= 0.01; constexpr const double E = 2.71828182845904523536; m_volume_ssrc[ssrc] = (std::exp(volume) - 1) / (E - 1); diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 4d2299e..9d270fa 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -65,9 +65,6 @@ private: std::unordered_set m_muted_ssrcs; std::unordered_map m_volume_ssrc; - mutable std::mutex m_muted_ssrc_mutex; - mutable std::mutex m_volume_ssrc_mutex; - public: using type_signal_opus_packet = sigc::signal; type_signal_opus_packet signal_opus_packet(); -- cgit v1.2.3 From 9edac7838048f177d1634b80157294f077bae703 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 5 Oct 2022 21:54:07 -0400 Subject: put voice member list in a scrolled window --- src/windows/voicewindow.cpp | 7 ++++++- src/windows/voicewindow.hpp | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index ed9971e..2e68239 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -86,10 +86,15 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); + m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + m_scroll.set_hexpand(true); + m_scroll.set_vexpand(true); + + m_scroll.add(m_user_list); m_controls.add(m_mute); m_controls.add(m_deafen); m_main.add(m_controls); - m_main.add(m_user_list); + m_main.add(m_scroll); add(m_main); show_all_children(); } diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index da4987e..b3edf46 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include // clang-format on @@ -26,6 +27,7 @@ private: Gtk::CheckButton m_mute; Gtk::CheckButton m_deafen; + Gtk::ScrolledWindow m_scroll; Gtk::ListBox m_user_list; Snowflake m_channel_id; -- cgit v1.2.3 From dfcfe4353ae40e11bd4bc779bc1f04f74c9f925f Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 5 Oct 2022 22:01:23 -0400 Subject: center voice window --- src/abaddon.cpp | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 8a92de7..be62f69 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -444,6 +444,8 @@ void Abaddon::OnVoiceConnected() { } }); + wnd->set_position(Gtk::WIN_POS_CENTER); + wnd->show(); wnd->signal_hide().connect([this, wnd]() { m_discord.DisconnectFromVoice(); -- cgit v1.2.3 From c30d17ebb2cb5010f40769c45ca76954f927fe99 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 7 Oct 2022 20:43:41 -0400 Subject: show avatar in voice window --- src/windows/voicewindow.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 2e68239..b8ade8f 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -37,6 +37,7 @@ public: const auto user = discord.GetUser(id); if (user.has_value()) { m_name.set_text(user->Username); + m_avatar.SetURL(user->GetAvatarURL("png", "32")); } else { m_name.set_text("Unknown user"); } -- cgit v1.2.3 From 78a5b9599c900c5da56da0a15835f425e455b510 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 10 Oct 2022 00:27:47 -0400 Subject: remove user from list on disconnect --- src/discord/discord.cpp | 19 +++++++++++++++++++ src/discord/discord.hpp | 4 ++++ src/windows/voicewindow.cpp | 12 ++++++++++++ src/windows/voicewindow.hpp | 4 ++++ 4 files changed, 39 insertions(+) (limited to 'src') diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index c62cd2a..91ba1e3 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1208,6 +1208,13 @@ std::optional DiscordClient::GetSSRCOfUser(Snowflake id) const { return m_voice.GetSSRCOfUser(id); } +std::optional DiscordClient::GetVoiceState(Snowflake user_id) const { + if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) { + return it->second; + } + return std::nullopt; +} + void DiscordClient::SetVoiceMuted(bool is_mute) { m_mute_requested = is_mute; SendVoiceStateUpdate(); @@ -2164,9 +2171,17 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { m_voice.SetSessionID(data.SessionID); } if (data.ChannelID.has_value()) { + const auto old_state = GetVoiceState(data.UserID); SetVoiceState(data.UserID, *data.ChannelID); + if (old_state.has_value() && *old_state != *data.ChannelID) { + m_signal_voice_user_disconnect.emit(data.UserID, *old_state); + } } else { + const auto old_state = GetVoiceState(data.UserID); ClearVoiceState(data.UserID); + if (old_state.has_value()) { + m_signal_voice_user_disconnect.emit(data.UserID, *old_state); + } } } @@ -2952,4 +2967,8 @@ DiscordClient::type_signal_voice_disconnected DiscordClient::signal_voice_discon DiscordClient::type_signal_voice_speaking DiscordClient::signal_voice_speaking() { return m_signal_voice_speaking; } + +DiscordClient::type_signal_voice_user_disconnect DiscordClient::signal_voice_user_disconnect() { + return m_signal_voice_user_disconnect; +} #endif diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index a3a2ba7..d0f5be0 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -187,6 +187,7 @@ public: [[nodiscard]] Snowflake GetVoiceChannelID() const noexcept; [[nodiscard]] std::unordered_set GetUsersInVoiceChannel(Snowflake channel_id); [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; + [[nodiscard]] std::optional GetVoiceState(Snowflake user_id) const; void SetVoiceMuted(bool is_mute); void SetVoiceDeafened(bool is_deaf); @@ -435,6 +436,7 @@ public: using type_signal_voice_connected = sigc::signal; using type_signal_voice_disconnected = sigc::signal; using type_signal_voice_speaking = sigc::signal; + using type_signal_voice_user_disconnect = sigc::signal; #endif type_signal_gateway_ready signal_gateway_ready(); @@ -495,6 +497,7 @@ public: type_signal_voice_connected signal_voice_connected(); type_signal_voice_disconnected signal_voice_disconnected(); type_signal_voice_speaking signal_voice_speaking(); + type_signal_voice_user_disconnect signal_voice_user_disconnect(); #endif protected: @@ -556,5 +559,6 @@ protected: type_signal_voice_connected m_signal_voice_connected; type_signal_voice_disconnected m_signal_voice_disconnected; type_signal_voice_speaking m_signal_voice_speaking; + type_signal_voice_user_disconnect m_signal_voice_user_disconnect; #endif }; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index b8ade8f..7019140 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -84,6 +84,8 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) auto &discord = Abaddon::Get().GetDiscordClient(); SetUsers(discord.GetUsersInVoiceChannel(m_channel_id)); + discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserDisconnect)); + m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); @@ -103,6 +105,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { for (auto id : user_ids) { auto *row = Gtk::make_managed(id); + m_rows[id] = row; row->signal_mute_cs().connect([this, id](bool is_muted) { m_signal_mute_user_cs.emit(id, is_muted); }); @@ -121,6 +124,15 @@ void VoiceWindow::OnDeafenChanged() { m_signal_deafen.emit(m_deafen.get_active()); } +void VoiceWindow::OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id) { + if (m_channel_id == from_channel_id) { + if (auto it = m_rows.find(user_id); it != m_rows.end()) { + delete it->second; + m_rows.erase(it); + } + } +} + VoiceWindow::type_signal_mute VoiceWindow::signal_mute() { return m_signal_mute; } diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index b3edf46..c968acb 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -18,6 +18,8 @@ public: private: void SetUsers(const std::unordered_set &user_ids); + void OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id); + void OnMuteChanged(); void OnDeafenChanged(); @@ -32,6 +34,8 @@ private: Snowflake m_channel_id; + std::unordered_map m_rows; + public: using type_signal_mute = sigc::signal; using type_signal_deafen = sigc::signal; -- cgit v1.2.3 From 17e7478bb4827e094a06faca6c5f2d5f4e5a45cc Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 12 Oct 2022 01:51:32 -0400 Subject: add user row on voice connect --- src/discord/discord.cpp | 5 +++++ src/discord/discord.hpp | 3 +++ src/windows/voicewindow.cpp | 32 +++++++++++++++++++++++--------- src/windows/voicewindow.hpp | 3 +++ 4 files changed, 34 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 91ba1e3..7b1c7b9 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -2176,6 +2176,7 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { if (old_state.has_value() && *old_state != *data.ChannelID) { m_signal_voice_user_disconnect.emit(data.UserID, *old_state); } + m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } else { const auto old_state = GetVoiceState(data.UserID); ClearVoiceState(data.UserID); @@ -2971,4 +2972,8 @@ DiscordClient::type_signal_voice_speaking DiscordClient::signal_voice_speaking() DiscordClient::type_signal_voice_user_disconnect DiscordClient::signal_voice_user_disconnect() { return m_signal_voice_user_disconnect; } + +DiscordClient::type_signal_voice_user_connect DiscordClient::signal_voice_user_connect() { + return m_signal_voice_user_connect; +} #endif diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index d0f5be0..fc714e6 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -437,6 +437,7 @@ public: using type_signal_voice_disconnected = sigc::signal; using type_signal_voice_speaking = sigc::signal; using type_signal_voice_user_disconnect = sigc::signal; + using type_signal_voice_user_connect = sigc::signal; #endif type_signal_gateway_ready signal_gateway_ready(); @@ -498,6 +499,7 @@ public: type_signal_voice_disconnected signal_voice_disconnected(); type_signal_voice_speaking signal_voice_speaking(); type_signal_voice_user_disconnect signal_voice_user_disconnect(); + type_signal_voice_user_connect signal_voice_user_connect(); #endif protected: @@ -560,5 +562,6 @@ protected: type_signal_voice_disconnected m_signal_voice_disconnected; type_signal_voice_speaking m_signal_voice_speaking; type_signal_voice_user_disconnect m_signal_voice_user_disconnect; + type_signal_voice_user_connect m_signal_voice_user_connect; #endif }; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 7019140..0af8176 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -85,6 +85,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) SetUsers(discord.GetUsersInVoiceChannel(m_channel_id)); discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserDisconnect)); + discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserConnect)); m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); @@ -104,18 +105,23 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { for (auto id : user_ids) { - auto *row = Gtk::make_managed(id); - m_rows[id] = row; - row->signal_mute_cs().connect([this, id](bool is_muted) { - m_signal_mute_user_cs.emit(id, is_muted); - }); - row->signal_volume().connect([this, id](double volume) { - m_signal_user_volume_changed.emit(id, volume); - }); - m_user_list.add(*row); + m_user_list.add(*CreateRow(id)); } } +Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) { + auto *row = Gtk::make_managed(id); + m_rows[id] = row; + row->signal_mute_cs().connect([this, id](bool is_muted) { + m_signal_mute_user_cs.emit(id, is_muted); + }); + row->signal_volume().connect([this, id](double volume) { + m_signal_user_volume_changed.emit(id, volume); + }); + row->show_all(); + return row; +} + void VoiceWindow::OnMuteChanged() { m_signal_mute.emit(m_mute.get_active()); } @@ -124,6 +130,14 @@ void VoiceWindow::OnDeafenChanged() { m_signal_deafen.emit(m_deafen.get_active()); } +void VoiceWindow::OnUserConnect(Snowflake user_id, Snowflake to_channel_id) { + if (m_channel_id == to_channel_id) { + if (auto it = m_rows.find(user_id); it == m_rows.end()) { + m_user_list.add(*CreateRow(user_id)); + } + } +} + void VoiceWindow::OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id) { if (m_channel_id == from_channel_id) { if (auto it = m_rows.find(user_id); it != m_rows.end()) { diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index c968acb..aaf77d7 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -18,6 +18,9 @@ public: private: void SetUsers(const std::unordered_set &user_ids); + Gtk::ListBoxRow *CreateRow(Snowflake id); + + void OnUserConnect(Snowflake user_id, Snowflake to_channel_id); void OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id); void OnMuteChanged(); -- cgit v1.2.3 From 621beb13444eb3ca16a756bfb8fdc6b4f79723f5 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sun, 16 Oct 2022 23:12:26 -0400 Subject: basic volume meters --- src/audio/manager.cpp | 49 +++++++++++++++++++++++++++++++++++++++++++++ src/audio/manager.hpp | 14 ++++++++++++- src/windows/voicewindow.cpp | 21 +++++++++++++++++++ src/windows/voicewindow.hpp | 8 +++++++- 4 files changed, 90 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index cf6b545..5bcc3bb 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -7,6 +7,7 @@ #include "manager.hpp" #include +#include #define MINIAUDIO_IMPLEMENTATION #include #include @@ -107,6 +108,8 @@ AudioManager::AudioManager() { char device_name[MA_MAX_DEVICE_NAME_LENGTH + 1]; ma_device_get_name(&m_capture_device, ma_device_type_capture, device_name, sizeof(device_name), nullptr); printf("using %s for capture\n", device_name); + + Glib::signal_timeout().connect(sigc::mem_fun(*this, &AudioManager::DecayVolumeMeters), 40); } AudioManager::~AudioManager() { @@ -158,6 +161,7 @@ void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { int decoded = opus_decode(it->second.second, opus_encoded, static_cast(payload_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); } @@ -191,6 +195,8 @@ void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { if (m_opus_buffer == nullptr || !m_should_capture) return; + UpdateCaptureVolume(pcm, frames); + int payload_len = opus_encode(m_encoder, pcm, 480, static_cast(m_opus_buffer), 1275); if (payload_len < 0) { printf("encoding error: %d\n", payload_len); @@ -199,10 +205,53 @@ void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { } } +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 -= 300; + if (m_capture_peak_meter < 0) m_capture_peak_meter = 0; + + std::lock_guard _(m_vol_mtx); + + for (auto &[ssrc, meter] : m_volumes) { + meter -= 0.01; + if (meter < 0.0) meter = 0.0; + } + + return true; +} + 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; +} + AudioManager::type_signal_opus_packet AudioManager::signal_opus_packet() { return m_signal_opus_packet; } diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 9d270fa..43d8a1e 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -35,9 +35,18 @@ public: [[nodiscard]] bool OK() const; + [[nodiscard]] double GetCaptureVolumeLevel() const noexcept; + [[nodiscard]] double GetSSRCVolumeLevel(uint32_t ssrc) const noexcept; + 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(); + friend void data_callback(ma_device *, void *, const void *, ma_uint32); friend void capture_data_callback(ma_device *, void *, const void *, ma_uint32); @@ -52,7 +61,7 @@ private: ma_device m_capture_device; ma_device_config m_capture_config; - std::mutex m_mutex; + mutable std::mutex m_mutex; std::unordered_map, OpusDecoder *>> m_sources; OpusEncoder *m_encoder; @@ -65,6 +74,9 @@ private: std::unordered_set m_muted_ssrcs; std::unordered_map m_volume_ssrc; + mutable std::mutex m_vol_mtx; + std::unordered_map m_volumes; + public: using type_signal_opus_packet = sigc::signal; type_signal_opus_packet signal_opus_packet(); diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 0af8176..0e42f32 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -5,6 +5,7 @@ #include "voicewindow.hpp" #include "components/lazyimage.hpp" #include "abaddon.hpp" +#include "audio/manager.hpp" // clang-format on class VoiceWindowUserListEntry : public Gtk::ListBoxRow { @@ -30,6 +31,7 @@ public: m_horz.add(m_mute); m_main.add(m_horz); m_main.add(m_volume); + m_main.add(m_meter); add(m_main); show_all_children(); @@ -47,6 +49,10 @@ public: }); } + void SetVolumeMeter(double frac) { + m_meter.set_fraction(frac); + } + private: Gtk::Box m_main; Gtk::Box m_horz; @@ -54,6 +60,7 @@ private: Gtk::Label m_name; Gtk::CheckButton m_mute; Gtk::Scale m_volume; + Gtk::ProgressBar m_meter; public: using type_signal_mute_cs = sigc::signal; @@ -98,9 +105,12 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_controls.add(m_mute); m_controls.add(m_deafen); m_main.add(m_controls); + m_main.add(m_capture_volume); m_main.add(m_scroll); add(m_main); show_all_children(); + + Glib::signal_timeout().connect(sigc::mem_fun(*this, &VoiceWindow::UpdateVoiceMeters), 40); } void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { @@ -130,6 +140,17 @@ void VoiceWindow::OnDeafenChanged() { m_signal_deafen.emit(m_deafen.get_active()); } +bool VoiceWindow::UpdateVoiceMeters() { + m_capture_volume.set_fraction(Abaddon::Get().GetAudio().GetCaptureVolumeLevel()); + for (auto [id, row] : m_rows) { + const auto ssrc = Abaddon::Get().GetDiscordClient().GetSSRCOfUser(id); + if (ssrc.has_value()) { + row->SetVolumeMeter(Abaddon::Get().GetAudio().GetSSRCVolumeLevel(*ssrc)); + } + } + return true; +} + void VoiceWindow::OnUserConnect(Snowflake user_id, Snowflake to_channel_id) { if (m_channel_id == to_channel_id) { if (auto it = m_rows.find(user_id); it == m_rows.end()) { diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index aaf77d7..d28a0cd 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -6,11 +6,13 @@ #include #include #include +#include #include #include #include // clang-format on +class VoiceWindowUserListEntry; class VoiceWindow : public Gtk::Window { public: VoiceWindow(Snowflake channel_id); @@ -26,6 +28,8 @@ private: void OnMuteChanged(); void OnDeafenChanged(); + bool UpdateVoiceMeters(); + Gtk::Box m_main; Gtk::Box m_controls; @@ -35,9 +39,11 @@ private: Gtk::ScrolledWindow m_scroll; Gtk::ListBox m_user_list; + Gtk::ProgressBar m_capture_volume; + Snowflake m_channel_id; - std::unordered_map m_rows; + std::unordered_map m_rows; public: using type_signal_mute = sigc::signal; -- cgit v1.2.3 From 5a3bce7498aea568cb6eb83d04313f8d777e8c94 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 18 Oct 2022 01:47:43 -0400 Subject: basic voice gate --- src/abaddon.cpp | 4 ++++ src/audio/manager.cpp | 6 ++++++ src/audio/manager.hpp | 4 ++++ src/windows/voicewindow.cpp | 12 ++++++++++++ src/windows/voicewindow.hpp | 5 +++++ 5 files changed, 31 insertions(+) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 701a3f1..45fca80 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -430,6 +430,10 @@ void Abaddon::OnVoiceConnected() { m_audio->SetPlayback(!is_deaf); }); + wnd->signal_gate().connect([this](double gate) { + m_audio->SetCaptureGate(gate); + }); + 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); diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 5bcc3bb..7416ff7 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -176,6 +176,10 @@ void AudioManager::SetPlayback(bool playback) { m_should_playback = playback; } +void AudioManager::SetCaptureGate(double gate) { + m_capture_gate = gate * 0.01; +} + void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { std::lock_guard _(m_mutex); if (mute) { @@ -197,6 +201,8 @@ void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { UpdateCaptureVolume(pcm, frames); + if (m_capture_peak_meter / 32768.0 < m_capture_gate) return; + int payload_len = opus_encode(m_encoder, pcm, 480, static_cast(m_opus_buffer), 1275); if (payload_len < 0) { printf("encoding error: %d\n", payload_len); diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 43d8a1e..67241fb 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -30,6 +30,8 @@ public: void SetCapture(bool capture); void SetPlayback(bool playback); + void SetCaptureGate(double gate); + void SetMuteSSRC(uint32_t ssrc, bool mute); void SetVolumeSSRC(uint32_t ssrc, double volume); @@ -71,6 +73,8 @@ private: std::atomic m_should_capture = true; std::atomic m_should_playback = true; + std::atomic m_capture_gate = 0.0; + std::unordered_set m_muted_ssrcs; std::unordered_map m_volume_ssrc; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 0e42f32..596f502 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -101,11 +101,19 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_scroll.set_hexpand(true); m_scroll.set_vexpand(true); + m_capture_gate.set_range(0.0, 100.0); + m_capture_gate.set_value_pos(Gtk::POS_LEFT); + m_capture_gate.set_value(0.0); + m_capture_gate.signal_value_changed().connect([this]() { + m_signal_gate.emit(m_capture_gate.get_value()); + }); + m_scroll.add(m_user_list); m_controls.add(m_mute); m_controls.add(m_deafen); m_main.add(m_controls); m_main.add(m_capture_volume); + m_main.add(m_capture_gate); m_main.add(m_scroll); add(m_main); show_all_children(); @@ -176,6 +184,10 @@ VoiceWindow::type_signal_deafen VoiceWindow::signal_deafen() { return m_signal_deafen; } +VoiceWindow::type_signal_gate VoiceWindow::signal_gate() { + return m_signal_gate; +} + VoiceWindow::type_signal_mute_user_cs VoiceWindow::signal_mute_user_cs() { return m_signal_mute_user_cs; } diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index d28a0cd..5f5436b 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ private: Gtk::ListBox m_user_list; Gtk::ProgressBar m_capture_volume; + Gtk::Scale m_capture_gate; Snowflake m_channel_id; @@ -48,17 +50,20 @@ private: public: using type_signal_mute = sigc::signal; using type_signal_deafen = sigc::signal; + using type_signal_gate = sigc::signal; using type_signal_mute_user_cs = sigc::signal; using type_signal_user_volume_changed = sigc::signal; type_signal_mute signal_mute(); type_signal_deafen signal_deafen(); + type_signal_gate signal_gate(); type_signal_mute_user_cs signal_mute_user_cs(); type_signal_user_volume_changed signal_user_volume_changed(); private: type_signal_mute m_signal_mute; type_signal_deafen m_signal_deafen; + type_signal_gate m_signal_gate; type_signal_mute_user_cs m_signal_mute_user_cs; type_signal_user_volume_changed m_signal_user_volume_changed; }; -- cgit v1.2.3 From 88f2e63eeb0b0b9912133ab14ad1f24a452b1c2b Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 18 Oct 2022 02:53:11 -0400 Subject: custom draw capture volume with gate indicator --- src/components/statusindicator.hpp | 4 +- src/components/volumemeter.cpp | 119 +++++++++++++++++++++++++++++++++++++ src/components/volumemeter.hpp | 29 +++++++++ src/windows/voicewindow.cpp | 7 ++- src/windows/voicewindow.hpp | 3 +- 5 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 src/components/volumemeter.cpp create mode 100644 src/components/volumemeter.hpp (limited to 'src') diff --git a/src/components/statusindicator.hpp b/src/components/statusindicator.hpp index ad9122b..ce86d93 100644 --- a/src/components/statusindicator.hpp +++ b/src/components/statusindicator.hpp @@ -11,9 +11,9 @@ public: protected: Gtk::SizeRequestMode get_request_mode_vfunc() const override; void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; - void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; - void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; void on_size_allocate(Gtk::Allocation &allocation) override; void on_map() override; void on_unmap() override; diff --git a/src/components/volumemeter.cpp b/src/components/volumemeter.cpp new file mode 100644 index 0000000..82f47a5 --- /dev/null +++ b/src/components/volumemeter.cpp @@ -0,0 +1,119 @@ +#include "volumemeter.hpp" +#include + +VolumeMeter::VolumeMeter() + : Glib::ObjectBase("volumemeter") + , Gtk::Widget() { + set_has_window(true); +} + +void VolumeMeter::SetVolume(double fraction) { + m_fraction = fraction; + queue_draw(); +} + +void VolumeMeter::SetTick(double fraction) { + m_tick = fraction; + queue_draw(); +} + +Gtk::SizeRequestMode VolumeMeter::get_request_mode_vfunc() const { + return Gtk::Widget::get_request_mode_vfunc(); +} + +void VolumeMeter::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const { + const int width = get_allocated_width(); + minimum_width = natural_width = width; +} + +void VolumeMeter::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const { + get_preferred_width_vfunc(minimum_width, natural_width); +} + +void VolumeMeter::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const { + // blehhh :PPP + const int height = get_allocated_height(); + minimum_height = natural_height = 4; +} + +void VolumeMeter::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const { + get_preferred_height_vfunc(minimum_height, natural_height); +} + +void VolumeMeter::on_size_allocate(Gtk::Allocation &allocation) { + set_allocation(allocation); + + if (m_window) + m_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(), allocation.get_height()); +} + +void VolumeMeter::on_map() { + Gtk::Widget::on_map(); +} + +void VolumeMeter::on_unmap() { + Gtk::Widget::on_unmap(); +} + +void VolumeMeter::on_realize() { + set_realized(true); + + if (!m_window) { + GdkWindowAttr attributes; + std::memset(&attributes, 0, sizeof(attributes)); + + auto allocation = get_allocation(); + + attributes.x = allocation.get_x(); + attributes.y = allocation.get_y(); + attributes.width = allocation.get_width(); + attributes.height = allocation.get_height(); + + attributes.event_mask = get_events() | Gdk::EXPOSURE_MASK; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + + m_window = Gdk::Window::create(get_parent_window(), &attributes, GDK_WA_X | GDK_WA_Y); + set_window(m_window); + + m_window->set_user_data(gobj()); + } +} + +void VolumeMeter::on_unrealize() { + m_window.reset(); + + Gtk::Widget::on_unrealize(); +} + +bool VolumeMeter::on_draw(const Cairo::RefPtr &cr) { + const auto allocation = get_allocation(); + const auto width = allocation.get_width(); + const auto height = allocation.get_height(); + + const double LOW_MAX = 0.7 * width; + const double MID_MAX = 0.85 * width; + const double desired_width = width * m_fraction; + + const double draw_low = std::min(desired_width, LOW_MAX); + const double draw_mid = std::min(desired_width, MID_MAX); + const double draw_hi = desired_width; + + cr->set_source_rgb(1.0, 0.0, 0.0); + cr->rectangle(0.0, 0.0, draw_hi, height); + cr->fill(); + cr->set_source_rgb(1.0, 0.5, 0.0); + cr->rectangle(0.0, 0.0, draw_mid, height); + cr->fill(); + cr->set_source_rgb(.0, 1.0, 0.0); + cr->rectangle(0.0, 0.0, draw_low, height); + cr->fill(); + + const double tick_base = width * m_tick; + + cr->set_source_rgb(0.8, 0.8, 0.8); + cr->rectangle(tick_base, 0, 4, height); + cr->fill(); + + return true; +} diff --git a/src/components/volumemeter.hpp b/src/components/volumemeter.hpp new file mode 100644 index 0000000..e9656b6 --- /dev/null +++ b/src/components/volumemeter.hpp @@ -0,0 +1,29 @@ +#pragma once +#include + +class VolumeMeter : public Gtk::Widget { +public: + VolumeMeter(); + + void SetVolume(double fraction); + void SetTick(double fraction); + +protected: + Gtk::SizeRequestMode get_request_mode_vfunc() const override; + void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; + void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; + void on_size_allocate(Gtk::Allocation &allocation) override; + void on_map() override; + void on_unmap() override; + void on_realize() override; + void on_unrealize() override; + bool on_draw(const Cairo::RefPtr &cr) override; + +private: + Glib::RefPtr m_window; + + double m_fraction = 0.0; + double m_tick = 0.0; +}; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 596f502..69eb969 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -105,7 +105,10 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_capture_gate.set_value_pos(Gtk::POS_LEFT); m_capture_gate.set_value(0.0); m_capture_gate.signal_value_changed().connect([this]() { - m_signal_gate.emit(m_capture_gate.get_value()); + // todo this should probably emit 0-1 i dont think the mgr should be responsible for scaling down + const double val = m_capture_gate.get_value(); + m_signal_gate.emit(val); + m_capture_volume.SetTick(val / 100.0); }); m_scroll.add(m_user_list); @@ -149,7 +152,7 @@ void VoiceWindow::OnDeafenChanged() { } bool VoiceWindow::UpdateVoiceMeters() { - m_capture_volume.set_fraction(Abaddon::Get().GetAudio().GetCaptureVolumeLevel()); + m_capture_volume.SetVolume(Abaddon::Get().GetAudio().GetCaptureVolumeLevel()); for (auto [id, row] : m_rows) { const auto ssrc = Abaddon::Get().GetDiscordClient().GetSSRCOfUser(id); if (ssrc.has_value()) { diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index 5f5436b..86cedb8 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -2,6 +2,7 @@ #ifdef WITH_VOICE // clang-format off +#include "components/volumemeter.hpp" #include "discord/snowflake.hpp" #include #include @@ -40,7 +41,7 @@ private: Gtk::ScrolledWindow m_scroll; Gtk::ListBox m_user_list; - Gtk::ProgressBar m_capture_volume; + VolumeMeter m_capture_volume; Gtk::Scale m_capture_gate; Snowflake m_channel_id; -- cgit v1.2.3 From cf53831b2a368c5413f10565e2ead88d8db8d1b4 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 18 Oct 2022 18:34:02 -0400 Subject: decay capture meter faster --- src/audio/manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 7416ff7..60969cc 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -229,7 +229,7 @@ void AudioManager::UpdateCaptureVolume(const int16_t *pcm, ma_uint32 frames) { } bool AudioManager::DecayVolumeMeters() { - m_capture_peak_meter -= 300; + m_capture_peak_meter -= 600; if (m_capture_peak_meter < 0) m_capture_peak_meter = 0; std::lock_guard _(m_vol_mtx); -- cgit v1.2.3 From e2110c22eefa899bf5d588eb34c477a3cc9c365e Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 18 Oct 2022 18:34:14 -0400 Subject: store user data from voice state updates --- src/discord/discord.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src') diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 7b1c7b9..cc066cc 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -2166,10 +2166,19 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) { #ifdef WITH_VOICE void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { VoiceState data = msg.Data; + if (data.UserID == m_user_data.ID) { printf("voice session id: %s\n", data.SessionID.c_str()); m_voice.SetSessionID(data.SessionID); + } else { + if (data.GuildID.has_value() && data.Member.has_value()) { + if (data.Member->User.has_value()) { + m_store.SetUser(data.UserID, *data.Member->User); + } + m_store.SetGuildMember(*data.GuildID, data.UserID, *data.Member); + } } + if (data.ChannelID.has_value()) { const auto old_state = GetVoiceState(data.UserID); SetVoiceState(data.UserID, *data.ChannelID); -- cgit v1.2.3 From 848e75f5774d56ebb1a67ba4326a96c8ea5ac3f1 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 20 Oct 2022 02:17:50 -0400 Subject: use new volume meter for other users --- src/components/volumemeter.cpp | 14 ++++++++++---- src/components/volumemeter.hpp | 2 ++ src/windows/voicewindow.cpp | 6 ++++-- 3 files changed, 16 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/components/volumemeter.cpp b/src/components/volumemeter.cpp index 82f47a5..380af55 100644 --- a/src/components/volumemeter.cpp +++ b/src/components/volumemeter.cpp @@ -17,6 +17,10 @@ void VolumeMeter::SetTick(double fraction) { queue_draw(); } +void VolumeMeter::SetShowTick(bool show) { + m_show_tick = show; +} + Gtk::SizeRequestMode VolumeMeter::get_request_mode_vfunc() const { return Gtk::Widget::get_request_mode_vfunc(); } @@ -109,11 +113,13 @@ bool VolumeMeter::on_draw(const Cairo::RefPtr &cr) { cr->rectangle(0.0, 0.0, draw_low, height); cr->fill(); - const double tick_base = width * m_tick; + if (m_show_tick) { + const double tick_base = width * m_tick; - cr->set_source_rgb(0.8, 0.8, 0.8); - cr->rectangle(tick_base, 0, 4, height); - cr->fill(); + cr->set_source_rgb(0.8, 0.8, 0.8); + cr->rectangle(tick_base, 0, 4, height); + cr->fill(); + } return true; } diff --git a/src/components/volumemeter.hpp b/src/components/volumemeter.hpp index e9656b6..83e0861 100644 --- a/src/components/volumemeter.hpp +++ b/src/components/volumemeter.hpp @@ -7,6 +7,7 @@ public: void SetVolume(double fraction); void SetTick(double fraction); + void SetShowTick(bool show); protected: Gtk::SizeRequestMode get_request_mode_vfunc() const override; @@ -26,4 +27,5 @@ private: double m_fraction = 0.0; double m_tick = 0.0; + bool m_show_tick = false; }; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 69eb969..80a388e 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -50,7 +50,7 @@ public: } void SetVolumeMeter(double frac) { - m_meter.set_fraction(frac); + m_meter.SetVolume(frac); } private: @@ -60,7 +60,7 @@ private: Gtk::Label m_name; Gtk::CheckButton m_mute; Gtk::Scale m_volume; - Gtk::ProgressBar m_meter; + VolumeMeter m_meter; public: using type_signal_mute_cs = sigc::signal; @@ -101,6 +101,8 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_scroll.set_hexpand(true); m_scroll.set_vexpand(true); + m_capture_volume.SetShowTick(true); + m_capture_gate.set_range(0.0, 100.0); m_capture_gate.set_value_pos(Gtk::POS_LEFT); m_capture_gate.set_value(0.0); -- cgit v1.2.3 From e888306272304224ac8edd37b3b2bd24ad8c0765 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 21 Oct 2022 01:23:37 -0400 Subject: add gain slider (how 2 loudmic? 🤓) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/abaddon.cpp | 4 ++++ src/audio/manager.cpp | 16 ++++++++++++++-- src/audio/manager.hpp | 2 ++ src/windows/voicewindow.cpp | 13 +++++++++++++ src/windows/voicewindow.hpp | 4 ++++ 5 files changed, 37 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 45fca80..664c0bd 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -434,6 +434,10 @@ void Abaddon::OnVoiceConnected() { m_audio->SetCaptureGate(gate); }); + wnd->signal_gain().connect([this](double gain) { + m_audio->SetCaptureGain(gain); + }); + 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); diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 60969cc..0461ee8 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -180,6 +180,10 @@ void AudioManager::SetCaptureGate(double gate) { m_capture_gate = gate * 0.01; } +void AudioManager::SetCaptureGain(double gain) { + m_capture_gain = gain; +} + void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { std::lock_guard _(m_mutex); if (mute) { @@ -199,11 +203,19 @@ void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { if (m_opus_buffer == nullptr || !m_should_capture) return; - UpdateCaptureVolume(pcm, frames); + const double gain = m_capture_gain; + // i have a suspicion i can cast the const away... but i wont + 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); + } + + UpdateCaptureVolume(new_pcm.data(), frames); if (m_capture_peak_meter / 32768.0 < m_capture_gate) return; - int payload_len = opus_encode(m_encoder, pcm, 480, static_cast(m_opus_buffer), 1275); + int payload_len = opus_encode(m_encoder, new_pcm.data(), 480, static_cast(m_opus_buffer), 1275); if (payload_len < 0) { printf("encoding error: %d\n", payload_len); } else { diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 67241fb..13dbfa4 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -31,6 +31,7 @@ public: void SetPlayback(bool playback); void SetCaptureGate(double gate); + void SetCaptureGain(double gain); void SetMuteSSRC(uint32_t ssrc, bool mute); void SetVolumeSSRC(uint32_t ssrc, double volume); @@ -74,6 +75,7 @@ private: std::atomic m_should_playback = true; std::atomic m_capture_gate = 0.0; + std::atomic m_capture_gain = 1.0; std::unordered_set m_muted_ssrcs; std::unordered_map m_volume_ssrc; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 80a388e..9f53638 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -113,12 +113,21 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_capture_volume.SetTick(val / 100.0); }); + m_capture_gain.set_range(0.0, 200.0); + m_capture_gain.set_value_pos(Gtk::POS_LEFT); + m_capture_gain.set_value(100.0); + m_capture_gain.signal_value_changed().connect([this]() { + const double val = m_capture_gain.get_value(); + m_signal_gain.emit(val / 100.0); + }); + m_scroll.add(m_user_list); m_controls.add(m_mute); m_controls.add(m_deafen); m_main.add(m_controls); m_main.add(m_capture_volume); m_main.add(m_capture_gate); + m_main.add(m_capture_gain); m_main.add(m_scroll); add(m_main); show_all_children(); @@ -193,6 +202,10 @@ VoiceWindow::type_signal_gate VoiceWindow::signal_gate() { return m_signal_gate; } +VoiceWindow::type_signal_gate VoiceWindow::signal_gain() { + return m_signal_gain; +} + VoiceWindow::type_signal_mute_user_cs VoiceWindow::signal_mute_user_cs() { return m_signal_mute_user_cs; } diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index 86cedb8..8eb02f3 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -43,6 +43,7 @@ private: VolumeMeter m_capture_volume; Gtk::Scale m_capture_gate; + Gtk::Scale m_capture_gain; Snowflake m_channel_id; @@ -52,12 +53,14 @@ public: using type_signal_mute = sigc::signal; using type_signal_deafen = sigc::signal; using type_signal_gate = sigc::signal; + using type_signal_gain = sigc::signal; using type_signal_mute_user_cs = sigc::signal; using type_signal_user_volume_changed = sigc::signal; type_signal_mute signal_mute(); type_signal_deafen signal_deafen(); type_signal_gate signal_gate(); + type_signal_gain signal_gain(); type_signal_mute_user_cs signal_mute_user_cs(); type_signal_user_volume_changed signal_user_volume_changed(); @@ -65,6 +68,7 @@ private: type_signal_mute m_signal_mute; type_signal_deafen m_signal_deafen; type_signal_gate m_signal_gate; + type_signal_gain m_signal_gain; type_signal_mute_user_cs m_signal_mute_user_cs; type_signal_user_volume_changed m_signal_user_volume_changed; }; -- cgit v1.2.3 From cb690b6defde4851889d04a68efa4f06d7e38847 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 24 Oct 2022 22:10:50 -0400 Subject: only enable microphone when in a voice channel --- src/abaddon.cpp | 3 +++ src/audio/manager.cpp | 19 ++++++++++++------- src/audio/manager.hpp | 3 +++ 3 files changed, 18 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 692526c..b39b854 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -427,6 +427,8 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) { #ifdef WITH_VOICE void Abaddon::OnVoiceConnected() { + m_audio->StartCaptureDevice(); + auto *wnd = new VoiceWindow(m_discord.GetVoiceChannelID()); m_voice_window = wnd; @@ -473,6 +475,7 @@ void Abaddon::OnVoiceConnected() { } void Abaddon::OnVoiceDisconnected() { + m_audio->StopCaptureDevice(); m_audio->RemoveAllSSRCs(); if (m_voice_window != nullptr) { m_voice_window->close(); diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 0461ee8..21c522e 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -98,13 +98,6 @@ AudioManager::AudioManager() { return; } - if (ma_device_start(&m_capture_device) != MA_SUCCESS) { - puts("failed to start capture"); - ma_device_uninit(&m_capture_device); - m_ok = false; - return; - } - char device_name[MA_MAX_DEVICE_NAME_LENGTH + 1]; ma_device_get_name(&m_capture_device, ma_device_type_capture, device_name, sizeof(device_name), nullptr); printf("using %s for capture\n", device_name); @@ -168,6 +161,18 @@ void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { } } +void AudioManager::StartCaptureDevice() { + if (ma_device_start(&m_capture_device) != MA_SUCCESS) { + puts("failed to start capture"); + } +} + +void AudioManager::StopCaptureDevice() { + if (ma_device_stop(&m_capture_device) != MA_SUCCESS) { + puts("failed to stop capture"); + } +} + void AudioManager::SetCapture(bool capture) { m_should_capture = capture; } diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 13dbfa4..7d8f479 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -27,6 +27,9 @@ public: void SetOpusBuffer(uint8_t *ptr); void FeedMeOpus(uint32_t ssrc, const std::vector &data); + void StartCaptureDevice(); + void StopCaptureDevice(); + void SetCapture(bool capture); void SetPlayback(bool playback); -- cgit v1.2.3 From f8f9a907c931623b8ef43d7af45b10c49d41afaa Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sat, 5 Nov 2022 02:32:43 -0400 Subject: add basic combobox to choose output device, start using spdlog --- CMakeLists.txt | 3 ++ src/abaddon.cpp | 8 +++++ src/audio/devices.cpp | 33 +++++++++++++++++ src/audio/devices.hpp | 26 ++++++++++++++ src/audio/manager.cpp | 87 ++++++++++++++++++++++++++++++++++++++++----- src/audio/manager.hpp | 13 +++++++ src/discord/voiceclient.cpp | 22 ++++++------ src/windows/voicewindow.cpp | 12 +++++++ src/windows/voicewindow.hpp | 3 ++ 9 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 src/audio/devices.cpp create mode 100644 src/audio/devices.hpp (limited to 'src') diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ff2a2b..d4c499e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,9 @@ if (Fontconfig_FOUND) target_link_libraries(abaddon Fontconfig::Fontconfig) endif () +find_package(spdlog REQUIRED) +target_link_libraries(abaddon spdlog::spdlog) + target_link_libraries(abaddon ${SQLite3_LIBRARIES}) target_link_libraries(abaddon ${GTKMM_LIBRARIES}) target_link_libraries(abaddon ${CURL_LIBRARIES}) diff --git a/src/abaddon.cpp b/src/abaddon.cpp index b39b854..a0e73f2 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -1,5 +1,8 @@ #include #include +#include +#include +#include #include #include #include "platform.hpp" @@ -1098,6 +1101,11 @@ int main(int argc, char **argv) { if (buf[0] != '1') SetEnvironmentVariableA("GTK_CSD", "0"); #endif + + spdlog::cfg::load_env_levels(); + auto log_audio = spdlog::stdout_color_mt("audio"); + auto log_voice = spdlog::stdout_color_mt("voice"); + Gtk::Main::init_gtkmm_internals(); // why??? return Abaddon::Get().StartGTK(); } diff --git a/src/audio/devices.cpp b/src/audio/devices.cpp new file mode 100644 index 0000000..f315a17 --- /dev/null +++ b/src/audio/devices.cpp @@ -0,0 +1,33 @@ +#include "devices.hpp" + +AudioDevices::AudioDevices() + : m_playback(Gtk::ListStore::create(m_playback_columns)) { +} + +Glib::RefPtr AudioDevices::GetPlaybackDeviceModel() { + return m_playback; +} + +void AudioDevices::SetDevices(ma_device_info *pPlayback, ma_uint32 playback_count, ma_device_info *pCapture, ma_uint32 capture_count) { + m_playback->clear(); + + for (ma_uint32 i = 0; i < playback_count; i++) { + auto &d = pPlayback[i]; + auto row = *m_playback->append(); + row[m_playback_columns.Name] = d.name; + row[m_playback_columns.DeviceID] = d.id; + } +} + +std::optional AudioDevices::GetDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) { + if (iter) { + return static_cast((*iter)[m_playback_columns.DeviceID]); + } else { + return std::nullopt; + } +} + +AudioDevices::PlaybackColumns::PlaybackColumns() { + add(Name); + add(DeviceID); +} diff --git a/src/audio/devices.hpp b/src/audio/devices.hpp new file mode 100644 index 0000000..0a88873 --- /dev/null +++ b/src/audio/devices.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +class AudioDevices { +public: + AudioDevices(); + + Glib::RefPtr GetPlaybackDeviceModel(); + + void SetDevices(ma_device_info *pPlayback, ma_uint32 playback_count, ma_device_info *pCapture, ma_uint32 capture_count); + std::optional GetDeviceIDFromModel(const Gtk::TreeModel::iterator &iter); + +private: + class PlaybackColumns : public Gtk::TreeModel::ColumnRecord { + public: + PlaybackColumns(); + + Gtk::TreeModelColumn Name; + Gtk::TreeModelColumn DeviceID; + }; + PlaybackColumns m_playback_columns; + Glib::RefPtr m_playback; +}; diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 21c522e..c965658 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -8,6 +8,7 @@ #include "manager.hpp" #include #include +#include #define MINIAUDIO_IMPLEMENTATION #include #include @@ -58,12 +59,20 @@ AudioManager::AudioManager() { int err; m_encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_VOIP, &err); if (err != OPUS_OK) { - printf("failed to initialize opus encoder: %d\n", err); + spdlog::get("audio")->error("failed to initialize opus encoder: {}", err); m_ok = false; return; } opus_encoder_ctl(m_encoder, OPUS_SET_BITRATE(64000)); + if (ma_context_init(nullptr, 0, nullptr, &m_context) != MA_SUCCESS) { + spdlog::get("audio")->error("failed to initialize context"); + m_ok = false; + return; + } + + Enumerate(); + m_device_config = ma_device_config_init(ma_device_type_playback); m_device_config.playback.format = ma_format_f32; m_device_config.playback.channels = 2; @@ -72,13 +81,13 @@ AudioManager::AudioManager() { m_device_config.pUserData = this; if (ma_device_init(nullptr, &m_device_config, &m_device) != MA_SUCCESS) { - puts("open playback fail"); + spdlog::get("audio")->error("failed to initialize playback device"); m_ok = false; return; } if (ma_device_start(&m_device) != MA_SUCCESS) { - puts("failed to start playback"); + spdlog::get("audio")->error("failed to start playback"); ma_device_uninit(&m_device); m_ok = false; return; @@ -93,14 +102,14 @@ AudioManager::AudioManager() { m_capture_config.pUserData = this; if (ma_device_init(nullptr, &m_capture_config, &m_capture_device) != MA_SUCCESS) { - puts("open capture fail"); + spdlog::get("audio")->error("failed to initialize capture device"); m_ok = false; return; } char device_name[MA_MAX_DEVICE_NAME_LENGTH + 1]; ma_device_get_name(&m_capture_device, ma_device_type_capture, device_name, sizeof(device_name), nullptr); - printf("using %s for capture\n", device_name); + spdlog::get("audio")->info("using {} as capture device", device_name); Glib::signal_timeout().connect(sigc::mem_fun(*this, &AudioManager::DecayVolumeMeters), 40); } @@ -108,6 +117,7 @@ AudioManager::AudioManager() { AudioManager::~AudioManager() { ma_device_uninit(&m_device); ma_device_uninit(&m_capture_device); + ma_context_uninit(&m_context); RemoveAllSSRCs(); } @@ -129,7 +139,7 @@ void AudioManager::RemoveSSRC(uint32_t ssrc) { } void AudioManager::RemoveAllSSRCs() { - puts("remove all ssrc"); + spdlog::get("audio")->info("removing all ssrc"); std::lock_guard _(m_mutex); for (auto &[ssrc, pair] : m_sources) { opus_decoder_destroy(pair.second); @@ -163,13 +173,45 @@ void AudioManager::FeedMeOpus(uint32_t ssrc, const std::vector &data) { void AudioManager::StartCaptureDevice() { if (ma_device_start(&m_capture_device) != MA_SUCCESS) { - puts("failed to start capture"); + spdlog::get("audio")->error("Failed to start capture device"); } } void AudioManager::StopCaptureDevice() { if (ma_device_stop(&m_capture_device) != MA_SUCCESS) { - puts("failed to stop capture"); + 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.GetDeviceIDFromModel(iter); + if (!device_id) { + spdlog::get("audio")->error("Requested ID from iterator is invalid"); + return; + } + + m_playback_id = *device_id; + + ma_device_uninit(&m_device); + + m_device_config = ma_device_config_init(ma_device_type_playback); + m_device_config.playback.format = ma_format_f32; + m_device_config.playback.channels = 2; + m_device_config.playback.pDeviceID = &m_playback_id; + m_device_config.sampleRate = 48000; + m_device_config.dataCallback = data_callback; + m_device_config.pUserData = this; + + if (ma_device_init(&m_context, &m_device_config, &m_device) != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to initialize new device"); + return; + } + + if (ma_device_start(&m_device) != MA_SUCCESS) { + spdlog::get("audio")->error("Failed to start new device"); + return; } } @@ -205,6 +247,29 @@ void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { m_volume_ssrc[ssrc] = (std::exp(volume) - 1) / (E - 1); } +void AudioManager::Enumerate() { + ma_device_info *pPlaybackDeviceInfo; + ma_uint32 playbackDeviceCount; + ma_device_info *pCaptureDeviceInfo; + ma_uint32 captureDeviceCount; + + 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; @@ -222,7 +287,7 @@ void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { int payload_len = opus_encode(m_encoder, new_pcm.data(), 480, static_cast(m_opus_buffer), 1275); if (payload_len < 0) { - printf("encoding error: %d\n", payload_len); + spdlog::get("audio")->error("encoding error: {}", payload_len); } else { m_signal_opus_packet.emit(payload_len); } @@ -275,6 +340,10 @@ double AudioManager::GetSSRCVolumeLevel(uint32_t ssrc) const noexcept { return 0.0; } +AudioDevices &AudioManager::GetDevices() { + return m_devices; +} + AudioManager::type_signal_opus_packet AudioManager::signal_opus_packet() { return m_signal_opus_packet; } diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 7d8f479..ff50390 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include "devices.hpp" // clang-format on class AudioManager { @@ -30,6 +32,8 @@ public: void StartCaptureDevice(); void StopCaptureDevice(); + void SetPlaybackDevice(const Gtk::TreeModel::iterator &iter); + void SetCapture(bool capture); void SetPlayback(bool playback); @@ -39,11 +43,15 @@ public: void SetMuteSSRC(uint32_t ssrc, bool mute); void SetVolumeSSRC(uint32_t ssrc, double volume); + void Enumerate(); + [[nodiscard]] bool OK() const; [[nodiscard]] double GetCaptureVolumeLevel() const noexcept; [[nodiscard]] double GetSSRCVolumeLevel(uint32_t ssrc) const noexcept; + [[nodiscard]] AudioDevices &GetDevices(); + private: void OnCapturedPCM(const int16_t *pcm, ma_uint32 frames); @@ -63,10 +71,13 @@ private: // playback ma_device m_device; ma_device_config m_device_config; + ma_device_id m_playback_id; // capture ma_device m_capture_device; ma_device_config m_capture_config; + ma_context m_context; + mutable std::mutex m_mutex; std::unordered_map, OpusDecoder *>> m_sources; @@ -86,6 +97,8 @@ private: mutable std::mutex m_vol_mtx; std::unordered_map m_volumes; + AudioDevices m_devices; + public: using type_signal_opus_packet = sigc::signal; type_signal_opus_packet signal_opus_packet(); diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index c0679ce..291b975 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -4,6 +4,8 @@ #include "voiceclient.hpp" #include "json.hpp" #include +#include +#include #include "abaddon.hpp" #include "audio/manager.hpp" @@ -127,11 +129,11 @@ DiscordVoiceClient::DiscordVoiceClient() { sodium_init(); m_ws.signal_open().connect([this]() { - puts("vws open"); + spdlog::get("voice")->info("Websocket open"); }); m_ws.signal_close().connect([this](uint16_t code) { - printf("vws close %u\n", code); + spdlog::get("voice")->info("Websocket closed with code {}", code); Stop(); }); @@ -258,9 +260,9 @@ void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) { m_port = d.Port; m_ssrc = d.SSRC; if (std::find(d.Modes.begin(), d.Modes.end(), "xsalsa20_poly1305") == d.Modes.end()) { - puts("xsalsa20_poly1305 not in encryption modes"); + spdlog::get("voice")->error("xsalsa20_poly1305 not in encryption modes"); } - printf("connect to %s:%u ssrc %u\n", m_ip.c_str(), m_port, m_ssrc); + spdlog::get("voice")->info("connect to {}:{} ssrc {}", m_ip, m_port, m_ssrc); m_udp.Connect(m_ip, m_port); m_keepalive_thread = std::thread(&DiscordVoiceClient::KeepaliveThread, this); @@ -270,11 +272,7 @@ void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) { void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessage &m) { VoiceSessionDescriptionData d = m.Data; - printf("receiving with %s secret key: ", d.Mode.c_str()); - for (auto b : d.SecretKey) { - printf("%02X", b); - } - printf("\n"); + spdlog::get("voice")->debug("receiving with {}, secret key: {:ns}", d.Mode, spdlog::to_hex(std::begin(d.SecretKey), std::end(d.SecretKey))); VoiceSpeakingMessage msg; msg.Delay = 0; @@ -330,10 +328,10 @@ void DiscordVoiceClient::Discovery() { if (response.size() >= 74 && response[0] == 0x00 && response[1] == 0x02) { const char *our_ip = reinterpret_cast(&response[8]); uint16_t our_port = (response[73] << 8) | response[74]; - printf("we are %s:%u\n", our_ip, our_port); + spdlog::get("voice")->debug("IP address discovered: {}:{}\n", our_ip, our_port); SelectProtocol(our_ip, our_port); } else { - puts("received non-discovery packet after discovery"); + spdlog::get("voice")->error("Received non-discovery packet after discovery"); } } @@ -355,7 +353,7 @@ void DiscordVoiceClient::OnUDPData(std::vector data) { 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())) { - puts("decrypt fail"); + // spdlog::get("voice")->trace("UDP payload decryption failure"); } else { Abaddon::Get().GetAudio().FeedMeOpus(ssrc, { payload, payload + data.size() - 12 - crypto_box_MACBYTES }); } diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 9f53638..95d1036 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -121,6 +121,17 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_signal_gain.emit(val / 100.0); }); + auto *renderer = Gtk::make_managed(); + m_playback_combo.set_valign(Gtk::ALIGN_END); + m_playback_combo.set_hexpand(true); + m_playback_combo.set_halign(Gtk::ALIGN_FILL); + m_playback_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetPlaybackDeviceModel()); + m_playback_combo.pack_start(*renderer); + m_playback_combo.add_attribute(*renderer, "text", 0); + m_playback_combo.signal_changed().connect([this]() { + Abaddon::Get().GetAudio().SetPlaybackDevice(m_playback_combo.get_active()); + }); + m_scroll.add(m_user_list); m_controls.add(m_mute); m_controls.add(m_deafen); @@ -129,6 +140,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_main.add(m_capture_gate); m_main.add(m_capture_gain); m_main.add(m_scroll); + m_main.add(m_playback_combo); add(m_main); show_all_children(); diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index 8eb02f3..e36846c 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -6,6 +6,7 @@ #include "discord/snowflake.hpp" #include #include +#include #include #include #include @@ -45,6 +46,8 @@ private: Gtk::Scale m_capture_gate; Gtk::Scale m_capture_gain; + Gtk::ComboBox m_playback_combo; + Snowflake m_channel_id; std::unordered_map m_rows; -- cgit v1.2.3 From f97a6ff266881936c2885c3265ae20524d66c874 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 8 Nov 2022 02:32:45 -0500 Subject: fix up CI: add libhandy as dependency change ubuntu actions environment update nlohmann/json to latest release add preprocessor checks --- .github/workflows/ci.yml | 16 +++++++++------- src/audio/devices.cpp | 7 +++++++ src/audio/devices.hpp | 6 ++++++ 3 files changed, 22 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a16289..be2c5a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: Abaddon CI -on: [push, pull_request] +on: [ push, pull_request ] jobs: msys2: @@ -8,8 +8,8 @@ jobs: runs-on: windows-latest strategy: matrix: - buildtype: [Debug, RelWithDebInfo, MinSizeRel] - mindeps: [false] + buildtype: [ Debug, RelWithDebInfo, MinSizeRel ] + mindeps: [ false ] include: - buildtype: RelWithDebInfo mindeps: true @@ -119,7 +119,7 @@ jobs: runs-on: macos-latest strategy: matrix: - buildtype: [Debug, RelWithDebInfo] + buildtype: [ Debug, RelWithDebInfo ] steps: - uses: actions/checkout@v1 with: @@ -136,6 +136,7 @@ jobs: brew install opus brew install libsodium brew install spdlog + brew install libhandy - name: Build uses: lukka/run-cmake@v3 @@ -158,10 +159,10 @@ jobs: linux: name: linux-${{ matrix.buildtype }} - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: - buildtype: [Debug, RelWithDebInfo, MinSizeRel] + buildtype: [ Debug, RelWithDebInfo, MinSizeRel ] steps: - uses: actions/checkout@v1 with: @@ -177,7 +178,7 @@ jobs: cd deps git clone https://github.com/nlohmann/json cd json - git checkout db78ac1d7716f56fc9f1b030b715f872f93964e4 + git checkout bc889afb4c5bf1c0d8ee29ef35eaaf4c8bef8a5d mkdir build cd build cmake .. @@ -188,6 +189,7 @@ jobs: sudo apt-get install libopus-dev sudo apt-get install libsodium-dev sudo apt-get install libspdlog-dev + sudo apt-get install libhandy-1-dev - name: Build uses: lukka/run-cmake@v3 diff --git a/src/audio/devices.cpp b/src/audio/devices.cpp index f315a17..c9636b7 100644 --- a/src/audio/devices.cpp +++ b/src/audio/devices.cpp @@ -1,5 +1,11 @@ +#ifdef WITH_VOICE + +// clang-format off + #include "devices.hpp" +// clang-format on + AudioDevices::AudioDevices() : m_playback(Gtk::ListStore::create(m_playback_columns)) { } @@ -31,3 +37,4 @@ AudioDevices::PlaybackColumns::PlaybackColumns() { add(Name); add(DeviceID); } +#endif diff --git a/src/audio/devices.hpp b/src/audio/devices.hpp index 0a88873..c11744b 100644 --- a/src/audio/devices.hpp +++ b/src/audio/devices.hpp @@ -1,9 +1,14 @@ #pragma once +#ifdef WITH_VOICE + +// clang-format off #include #include #include +// clang-format on + class AudioDevices { public: AudioDevices(); @@ -24,3 +29,4 @@ private: PlaybackColumns m_playback_columns; Glib::RefPtr m_playback; }; +#endif -- cgit v1.2.3 From 047168873235c3d835aaf9836f549de08bd96a47 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 8 Nov 2022 04:01:54 -0500 Subject: add ability to set capture device --- src/abaddon.cpp | 1 + src/audio/devices.cpp | 31 +++++++++++++++++++++++++++++-- src/audio/devices.hpp | 14 +++++++++++++- src/audio/manager.cpp | 36 +++++++++++++++++++++++++++++++++++- src/audio/manager.hpp | 2 ++ src/discord/discord.cpp | 9 +++++---- src/windows/voicewindow.cpp | 18 +++++++++++++++--- src/windows/voicewindow.hpp | 1 + 8 files changed, 101 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index a0e73f2..67fd2bd 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -1105,6 +1105,7 @@ int main(int argc, char **argv) { spdlog::cfg::load_env_levels(); auto log_audio = spdlog::stdout_color_mt("audio"); auto log_voice = spdlog::stdout_color_mt("voice"); + auto log_discord = spdlog::stdout_color_mt("discord"); Gtk::Main::init_gtkmm_internals(); // why??? return Abaddon::Get().StartGTK(); diff --git a/src/audio/devices.cpp b/src/audio/devices.cpp index c9636b7..5e7720b 100644 --- a/src/audio/devices.cpp +++ b/src/audio/devices.cpp @@ -7,13 +7,18 @@ // clang-format on AudioDevices::AudioDevices() - : m_playback(Gtk::ListStore::create(m_playback_columns)) { + : m_playback(Gtk::ListStore::create(m_playback_columns)) + , m_capture(Gtk::ListStore::create(m_capture_columns)) { } Glib::RefPtr AudioDevices::GetPlaybackDeviceModel() { return m_playback; } +Glib::RefPtr AudioDevices::GetCaptureDeviceModel() { + return m_capture; +} + void AudioDevices::SetDevices(ma_device_info *pPlayback, ma_uint32 playback_count, ma_device_info *pCapture, ma_uint32 capture_count) { m_playback->clear(); @@ -23,9 +28,18 @@ void AudioDevices::SetDevices(ma_device_info *pPlayback, ma_uint32 playback_coun row[m_playback_columns.Name] = d.name; row[m_playback_columns.DeviceID] = d.id; } + + m_capture->clear(); + + for (ma_uint32 i = 0; i < capture_count; i++) { + auto &d = pCapture[i]; + auto row = *m_capture->append(); + row[m_capture_columns.Name] = d.name; + row[m_capture_columns.DeviceID] = d.id; + } } -std::optional AudioDevices::GetDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) { +std::optional AudioDevices::GetPlaybackDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const { if (iter) { return static_cast((*iter)[m_playback_columns.DeviceID]); } else { @@ -33,8 +47,21 @@ std::optional AudioDevices::GetDeviceIDFromModel(const Gtk::TreeMo } } +std::optional AudioDevices::GetCaptureDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const { + if (iter) { + return static_cast((*iter)[m_capture_columns.DeviceID]); + } else { + return std::nullopt; + } +} + AudioDevices::PlaybackColumns::PlaybackColumns() { add(Name); add(DeviceID); } + +AudioDevices::CaptureColumns::CaptureColumns() { + add(Name); + add(DeviceID); +} #endif diff --git a/src/audio/devices.hpp b/src/audio/devices.hpp index c11744b..9d3de7d 100644 --- a/src/audio/devices.hpp +++ b/src/audio/devices.hpp @@ -14,9 +14,11 @@ 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); - std::optional GetDeviceIDFromModel(const Gtk::TreeModel::iterator &iter); + std::optional GetPlaybackDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const; + std::optional GetCaptureDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const; private: class PlaybackColumns : public Gtk::TreeModel::ColumnRecord { @@ -28,5 +30,15 @@ private: }; PlaybackColumns m_playback_columns; Glib::RefPtr m_playback; + + class CaptureColumns : public Gtk::TreeModel::ColumnRecord { + public: + CaptureColumns(); + + Gtk::TreeModelColumn Name; + Gtk::TreeModelColumn DeviceID; + }; + CaptureColumns m_capture_columns; + Glib::RefPtr m_capture; }; #endif diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index c965658..839bfc8 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -186,7 +186,7 @@ void AudioManager::StopCaptureDevice() { void AudioManager::SetPlaybackDevice(const Gtk::TreeModel::iterator &iter) { spdlog::get("audio")->debug("Setting new playback device"); - const auto device_id = m_devices.GetDeviceIDFromModel(iter); + const auto device_id = m_devices.GetPlaybackDeviceIDFromModel(iter); if (!device_id) { spdlog::get("audio")->error("Requested ID from iterator is invalid"); return; @@ -215,6 +215,40 @@ void AudioManager::SetPlaybackDevice(const Gtk::TreeModel::iterator &iter) { } } +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_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; } diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index ff50390..7805b30 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -33,6 +33,7 @@ public: void StopCaptureDevice(); void SetPlaybackDevice(const Gtk::TreeModel::iterator &iter); + void SetCaptureDevice(const Gtk::TreeModel::iterator &iter); void SetCapture(bool capture); void SetPlayback(bool playback); @@ -75,6 +76,7 @@ private: // capture ma_device m_capture_device; ma_device_config m_capture_config; + ma_device_id m_capture_id; ma_context m_context; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index cc066cc..2a08574 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1,6 +1,7 @@ #include "abaddon.hpp" #include "discord.hpp" #include "util.hpp" +#include #include #include @@ -2168,7 +2169,7 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { VoiceState data = msg.Data; if (data.UserID == m_user_data.ID) { - printf("voice session id: %s\n", data.SessionID.c_str()); + spdlog::get("discord")->debug("Voice session ID: {}", data.SessionID); m_voice.SetSessionID(data.SessionID); } else { if (data.GuildID.has_value() && data.Member.has_value()) { @@ -2197,8 +2198,8 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) { VoiceServerUpdateData data = msg.Data; - printf("endpoint: %s\n", data.Endpoint.c_str()); - printf("token: %s\n", data.Token.c_str()); + spdlog::get("discord")->debug("Voice server endpoint: {}", data.Endpoint); + spdlog::get("discord")->debug("Voice token: {}", data.Token); m_voice.SetEndpoint(data.Endpoint); m_voice.SetToken(data.Token); if (data.GuildID.has_value()) { @@ -2206,7 +2207,7 @@ void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) { } else if (data.ChannelID.has_value()) { m_voice.SetServerID(*data.ChannelID); } else { - puts("no guild or channel id in voice server?"); + spdlog::get("discord")->error("No guild or channel ID in voice server?"); } m_voice.SetUserID(m_user_data.ID); m_voice.Start(); diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 95d1036..19d9863 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -121,17 +121,28 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_signal_gain.emit(val / 100.0); }); - auto *renderer = Gtk::make_managed(); + auto *playback_renderer = Gtk::make_managed(); m_playback_combo.set_valign(Gtk::ALIGN_END); m_playback_combo.set_hexpand(true); m_playback_combo.set_halign(Gtk::ALIGN_FILL); m_playback_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetPlaybackDeviceModel()); - m_playback_combo.pack_start(*renderer); - m_playback_combo.add_attribute(*renderer, "text", 0); + 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()); }); + auto *capture_renderer = Gtk::make_managed(); + m_capture_combo.set_valign(Gtk::ALIGN_END); + m_capture_combo.set_hexpand(true); + m_capture_combo.set_halign(Gtk::ALIGN_FILL); + m_capture_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetCaptureDeviceModel()); + 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_scroll.add(m_user_list); m_controls.add(m_mute); m_controls.add(m_deafen); @@ -141,6 +152,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_main.add(m_capture_gain); m_main.add(m_scroll); m_main.add(m_playback_combo); + m_main.add(m_capture_combo); add(m_main); show_all_children(); diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index e36846c..f9ea23f 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -47,6 +47,7 @@ private: Gtk::Scale m_capture_gain; Gtk::ComboBox m_playback_combo; + Gtk::ComboBox m_capture_combo; Snowflake m_channel_id; -- cgit v1.2.3 From e2784cd97bd9a4b5995c556d35bd4f08a2f4bad7 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Wed, 9 Nov 2022 19:03:53 -0500 Subject: model stuff to track active device also minor refactor --- src/audio/devices.cpp | 62 ++++++++++++++++++++++++++++++++++++++--- src/audio/devices.hpp | 18 ++++++++++-- src/audio/manager.cpp | 68 +++++++++++++++++++++++++++++---------------- src/audio/manager.hpp | 4 +-- src/windows/voicewindow.cpp | 2 ++ 5 files changed, 122 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/audio/devices.cpp b/src/audio/devices.cpp index 5e7720b..dfb7164 100644 --- a/src/audio/devices.cpp +++ b/src/audio/devices.cpp @@ -3,6 +3,8 @@ // clang-format off #include "devices.hpp" +#include +#include // clang-format on @@ -24,35 +26,87 @@ void AudioDevices::SetDevices(ma_device_info *pPlayback, ma_uint32 playback_coun for (ma_uint32 i = 0; i < playback_count; i++) { auto &d = pPlayback[i]; + auto row = *m_playback->append(); row[m_playback_columns.Name] = d.name; row[m_playback_columns.DeviceID] = d.id; + + if (d.isDefault) { + m_default_playback_iter = row; + SetActivePlaybackDevice(row); + } } m_capture->clear(); for (ma_uint32 i = 0; i < capture_count; i++) { auto &d = pCapture[i]; + auto row = *m_capture->append(); row[m_capture_columns.Name] = d.name; row[m_capture_columns.DeviceID] = d.id; + + if (d.isDefault) { + m_default_capture_iter = row; + SetActiveCaptureDevice(row); + } + } + + if (!m_default_playback_iter) { + spdlog::get("audio")->warn("No default playback device found"); + } + + if (!m_default_capture_iter) { + spdlog::get("audio")->warn("No default capture device found"); } } std::optional AudioDevices::GetPlaybackDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const { if (iter) { return static_cast((*iter)[m_playback_columns.DeviceID]); - } else { - return std::nullopt; } + + return std::nullopt; } std::optional AudioDevices::GetCaptureDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const { if (iter) { return static_cast((*iter)[m_capture_columns.DeviceID]); - } else { - return std::nullopt; } + + return std::nullopt; +} + +std::optional AudioDevices::GetDefaultPlayback() const { + if (m_default_playback_iter) { + return static_cast((*m_default_playback_iter)[m_playback_columns.DeviceID]); + } + + return std::nullopt; +} + +std::optional AudioDevices::GetDefaultCapture() const { + if (m_default_capture_iter) { + return static_cast((*m_default_capture_iter)[m_capture_columns.DeviceID]); + } + + return std::nullopt; +} + +void AudioDevices::SetActivePlaybackDevice(const Gtk::TreeModel::iterator &iter) { + m_active_playback_iter = iter; +} + +void AudioDevices::SetActiveCaptureDevice(const Gtk::TreeModel::iterator &iter) { + m_active_capture_iter = iter; +} + +Gtk::TreeModel::iterator AudioDevices::GetActivePlaybackDevice() { + return m_active_playback_iter; +} + +Gtk::TreeModel::iterator AudioDevices::GetActiveCaptureDevice() { + return m_active_capture_iter; } AudioDevices::PlaybackColumns::PlaybackColumns() { diff --git a/src/audio/devices.hpp b/src/audio/devices.hpp index 9d3de7d..c83bdb4 100644 --- a/src/audio/devices.hpp +++ b/src/audio/devices.hpp @@ -17,8 +17,18 @@ public: Glib::RefPtr GetCaptureDeviceModel(); void SetDevices(ma_device_info *pPlayback, ma_uint32 playback_count, ma_device_info *pCapture, ma_uint32 capture_count); - std::optional GetPlaybackDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const; - std::optional GetCaptureDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const; + + [[nodiscard]] std::optional GetPlaybackDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const; + [[nodiscard]] std::optional GetCaptureDeviceIDFromModel(const Gtk::TreeModel::iterator &iter) const; + + [[nodiscard]] std::optional GetDefaultPlayback() const; + [[nodiscard]] std::optional GetDefaultCapture() const; + + void SetActivePlaybackDevice(const Gtk::TreeModel::iterator &iter); + void SetActiveCaptureDevice(const Gtk::TreeModel::iterator &iter); + + Gtk::TreeModel::iterator GetActivePlaybackDevice(); + Gtk::TreeModel::iterator GetActiveCaptureDevice(); private: class PlaybackColumns : public Gtk::TreeModel::ColumnRecord { @@ -30,6 +40,8 @@ private: }; PlaybackColumns m_playback_columns; Glib::RefPtr m_playback; + Gtk::TreeModel::iterator m_active_playback_iter; + Gtk::TreeModel::iterator m_default_playback_iter; class CaptureColumns : public Gtk::TreeModel::ColumnRecord { public: @@ -40,5 +52,7 @@ private: }; CaptureColumns m_capture_columns; Glib::RefPtr m_capture; + 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 839bfc8..fcfa9f2 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -71,24 +71,31 @@ AudioManager::AudioManager() { return; } + spdlog::get("audio")->info("Audio backend: {}", ma_get_backend_name(m_context.backend)); + Enumerate(); - m_device_config = ma_device_config_init(ma_device_type_playback); - m_device_config.playback.format = ma_format_f32; - m_device_config.playback.channels = 2; - m_device_config.sampleRate = 48000; - m_device_config.dataCallback = data_callback; - m_device_config.pUserData = this; + 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 (ma_device_init(nullptr, &m_device_config, &m_device) != MA_SUCCESS) { + if (ma_device_init(&m_context, &m_playback_config, &m_playback_device) != MA_SUCCESS) { spdlog::get("audio")->error("failed to initialize playback device"); m_ok = false; return; } - if (ma_device_start(&m_device) != MA_SUCCESS) { + if (ma_device_start(&m_playback_device) != MA_SUCCESS) { spdlog::get("audio")->error("failed to start playback"); - ma_device_uninit(&m_device); + ma_device_uninit(&m_playback_device); m_ok = false; return; } @@ -101,21 +108,30 @@ AudioManager::AudioManager() { m_capture_config.dataCallback = capture_data_callback; m_capture_config.pUserData = this; - if (ma_device_init(nullptr, &m_capture_config, &m_capture_device) != MA_SUCCESS) { + 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 (ma_device_init(&m_context, &m_capture_config, &m_capture_device) != MA_SUCCESS) { spdlog::get("audio")->error("failed to initialize capture device"); m_ok = false; return; } - char device_name[MA_MAX_DEVICE_NAME_LENGTH + 1]; - ma_device_get_name(&m_capture_device, ma_device_type_capture, device_name, sizeof(device_name), nullptr); - spdlog::get("audio")->info("using {} as capture device", device_name); + 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); + + 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_device); + ma_device_uninit(&m_playback_device); ma_device_uninit(&m_capture_device); ma_context_uninit(&m_context); RemoveAllSSRCs(); @@ -192,24 +208,26 @@ void AudioManager::SetPlaybackDevice(const Gtk::TreeModel::iterator &iter) { return; } + m_devices.SetActivePlaybackDevice(iter); + m_playback_id = *device_id; - ma_device_uninit(&m_device); + ma_device_uninit(&m_playback_device); - m_device_config = ma_device_config_init(ma_device_type_playback); - m_device_config.playback.format = ma_format_f32; - m_device_config.playback.channels = 2; - m_device_config.playback.pDeviceID = &m_playback_id; - m_device_config.sampleRate = 48000; - m_device_config.dataCallback = data_callback; - m_device_config.pUserData = this; + 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_device_config, &m_device) != MA_SUCCESS) { + 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_device) != MA_SUCCESS) { + if (ma_device_start(&m_playback_device) != MA_SUCCESS) { spdlog::get("audio")->error("Failed to start new device"); return; } @@ -224,6 +242,8 @@ void AudioManager::SetCaptureDevice(const Gtk::TreeModel::iterator &iter) { return; } + m_devices.SetActiveCaptureDevice(iter); + m_capture_id = *device_id; ma_device_uninit(&m_capture_device); diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 7805b30..7516918 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -70,8 +70,8 @@ private: bool m_ok; // playback - ma_device m_device; - ma_device_config m_device_config; + ma_device m_playback_device; + ma_device_config m_playback_config; ma_device_id m_playback_id; // capture ma_device m_capture_device; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 19d9863..7b574e4 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -126,6 +126,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_playback_combo.set_hexpand(true); m_playback_combo.set_halign(Gtk::ALIGN_FILL); m_playback_combo.set_model(Abaddon::Get().GetAudio().GetDevices().GetPlaybackDeviceModel()); + m_playback_combo.set_active(Abaddon::Get().GetAudio().GetDevices().GetActivePlaybackDevice()); m_playback_combo.pack_start(*playback_renderer); m_playback_combo.add_attribute(*playback_renderer, "text", 0); m_playback_combo.signal_changed().connect([this]() { @@ -137,6 +138,7 @@ 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()); + m_capture_combo.set_active(Abaddon::Get().GetAudio().GetDevices().GetActiveCaptureDevice()); m_capture_combo.pack_start(*capture_renderer); m_capture_combo.add_attribute(*capture_renderer, "text", 0); m_capture_combo.signal_changed().connect([this]() { -- cgit v1.2.3 From 38c5230a1d194cb2fc4cf36fb6b417fdefb32788 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 14 Nov 2022 01:28:07 -0500 Subject: add window to change more stuff with opus --- src/audio/manager.cpp | 62 ++++++++++++++++++ src/audio/manager.hpp | 9 +++ src/windows/voicesettingswindow.cpp | 125 ++++++++++++++++++++++++++++++++++++ src/windows/voicesettingswindow.hpp | 25 ++++++++ src/windows/voicewindow.cpp | 14 +++- src/windows/voicewindow.hpp | 6 ++ 6 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/windows/voicesettingswindow.cpp create mode 100644 src/windows/voicesettingswindow.hpp (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index fcfa9f2..1a7bda5 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -301,6 +301,66 @@ void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { m_volume_ssrc[ssrc] = (std::exp(volume) - 1) / (E - 1); } +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; +} + void AudioManager::Enumerate() { ma_device_info *pPlaybackDeviceInfo; ma_uint32 playbackDeviceCount; @@ -339,7 +399,9 @@ void AudioManager::OnCapturedPCM(const int16_t *pcm, ma_uint32 frames) { if (m_capture_peak_meter / 32768.0 < m_capture_gate) return; + m_enc_mutex.lock(); int 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 { diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 7516918..dbb0b6e 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -44,6 +44,13 @@ public: void SetMuteSSRC(uint32_t ssrc, bool mute); void SetVolumeSSRC(uint32_t ssrc, double volume); + void SetEncodingApplication(int application); + [[nodiscard]] int GetEncodingApplication(); + void SetSignalHint(int signal); + [[nodiscard]] int GetSignalHint(); + void SetBitrate(int bitrate); + [[nodiscard]] int GetBitrate(); + void Enumerate(); [[nodiscard]] bool OK() const; @@ -81,6 +88,8 @@ private: ma_context m_context; mutable std::mutex m_mutex; + mutable std::mutex m_enc_mutex; + std::unordered_map, OpusDecoder *>> m_sources; OpusEncoder *m_encoder; diff --git a/src/windows/voicesettingswindow.cpp b/src/windows/voicesettingswindow.cpp new file mode 100644 index 0000000..c009cbf --- /dev/null +++ b/src/windows/voicesettingswindow.cpp @@ -0,0 +1,125 @@ +#ifdef WITH_VOICE + +// clang-format off + +#include "voicesettingswindow.hpp" +#include "abaddon.hpp" +#include "audio/manager.hpp" +#include + +// clang-format on + +VoiceSettingsWindow::VoiceSettingsWindow() + : m_main(Gtk::ORIENTATION_VERTICAL) { + get_style_context()->add_class("app-window"); + set_default_size(300, 300); + + m_encoding_mode.append("Voice"); + m_encoding_mode.append("Music"); + m_encoding_mode.append("Restricted"); + m_encoding_mode.set_tooltip_text( + "Sets the coding mode for the Opus encoder\n" + "Voice - Optimize for voice quality\n" + "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) { + m_encoding_mode.set_active(0); + } else if (mode == OPUS_APPLICATION_AUDIO) { + m_encoding_mode.set_active(1); + } else if (mode == OPUS_APPLICATION_RESTRICTED_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(); + spdlog::get("audio")->debug("Chose encoding mode: {}", mode.c_str()); + if (mode == "Voice") { + audio.SetEncodingApplication(OPUS_APPLICATION_VOIP); + } else if (mode == "Music") { + spdlog::get("audio")->debug("music/audio"); + audio.SetEncodingApplication(OPUS_APPLICATION_AUDIO); + } else if (mode == "Restricted") { + audio.SetEncodingApplication(OPUS_APPLICATION_RESTRICTED_LOWDELAY); + } + }); + + m_signal.append("Auto"); + m_signal.append("Voice"); + m_signal.append("Music"); + m_signal.set_tooltip_text( + "Signal hint. Tells Opus what the current signal is\n" + "Auto - Let Opus figure it out\n" + "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) { + m_signal.set_active(0); + } else if (signal == OPUS_SIGNAL_VOICE) { + m_signal.set_active(1); + } else if (signal == OPUS_SIGNAL_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(); + spdlog::get("audio")->debug("Chose signal hint: {}", signal.c_str()); + if (signal == "Auto") { + audio.SetSignalHint(OPUS_AUTO); + } else if (signal == "Voice") { + audio.SetSignalHint(OPUS_SIGNAL_VOICE); + } else if (signal == "Music") { + audio.SetSignalHint(OPUS_SIGNAL_MUSIC); + } + }); + + // exponential scale for bitrate because higher bitrates dont sound much different + constexpr static auto MAX_BITRATE = 128000.0; + constexpr static auto MIN_BITRATE = 2400.0; + const auto bitrate_scale = [this](double value) -> double { + value /= 100.0; + return (MAX_BITRATE - MIN_BITRATE) * value * value * value + MIN_BITRATE; + }; + + const auto bitrate_scale_r = [this](double value) -> double { + return 100.0 * std::cbrt((value - MIN_BITRATE) / (MAX_BITRATE - MIN_BITRATE)); + }; + + 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.signal_format_value().connect([this, bitrate_scale](double value) { + const auto scaled = bitrate_scale(value); + if (value <= 99.9) { + return Glib::ustring(std::to_string(static_cast(scaled))); + } else { + return Glib::ustring("MAX"); + } + }); + m_bitrate.signal_value_changed().connect([this, bitrate_scale]() { + const auto value = m_bitrate.get_value(); + const auto scaled = bitrate_scale(value); + if (value <= 99.9) { + Abaddon::Get().GetAudio().SetBitrate(static_cast(scaled)); + } else { + Abaddon::Get().GetAudio().SetBitrate(OPUS_BITRATE_MAX); + } + }); + + m_main.add(m_encoding_mode); + m_main.add(m_signal); + m_main.add(m_bitrate); + add(m_main); + show_all_children(); + + // no need to bring in ManageHeapWindow, no user menu + signal_hide().connect([this]() { + delete this; + }); +} + +#endif diff --git a/src/windows/voicesettingswindow.hpp b/src/windows/voicesettingswindow.hpp new file mode 100644 index 0000000..cf6b477 --- /dev/null +++ b/src/windows/voicesettingswindow.hpp @@ -0,0 +1,25 @@ +#pragma once +#ifdef WITH_VOICE + +// clang-format off + +#include +#include +#include +#include + +// clang-format on + +class VoiceSettingsWindow : public Gtk::Window { +public: + VoiceSettingsWindow(); + + Gtk::Box m_main; + Gtk::ComboBoxText m_encoding_mode; + Gtk::ComboBoxText m_signal; + Gtk::Scale m_bitrate; + +private: +}; + +#endif diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 7b574e4..7c90e96 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -6,6 +6,7 @@ #include "components/lazyimage.hpp" #include "abaddon.hpp" #include "audio/manager.hpp" +#include "voicesettingswindow.hpp" // clang-format on class VoiceWindowUserListEntry : public Gtk::ListBoxRow { @@ -83,7 +84,9 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) , m_controls(Gtk::ORIENTATION_HORIZONTAL) , m_mute("Mute") , m_deafen("Deafen") - , m_channel_id(channel_id) { + , m_channel_id(channel_id) + , m_menu_view("View") + , m_menu_view_settings("More _Settings", true) { get_style_context()->add_class("app-window"); set_default_size(300, 300); @@ -145,9 +148,18 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) Abaddon::Get().GetAudio().SetCaptureDevice(m_capture_combo.get_active()); }); + m_menu_bar.append(m_menu_view); + m_menu_view.set_submenu(m_menu_view_sub); + m_menu_view_sub.append(m_menu_view_settings); + m_menu_view_settings.signal_activate().connect([this]() { + auto *window = new VoiceSettingsWindow; + window->show(); + }); + m_scroll.add(m_user_list); m_controls.add(m_mute); m_controls.add(m_deafen); + m_main.add(m_menu_bar); m_main.add(m_controls); m_main.add(m_capture_volume); m_main.add(m_capture_gate); diff --git a/src/windows/voicewindow.hpp b/src/windows/voicewindow.hpp index f9ea23f..805d94a 100644 --- a/src/windows/voicewindow.hpp +++ b/src/windows/voicewindow.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,11 @@ private: std::unordered_map m_rows; + Gtk::MenuBar m_menu_bar; + Gtk::MenuItem m_menu_view; + Gtk::Menu m_menu_view_sub; + Gtk::MenuItem m_menu_view_settings; + public: using type_signal_mute = sigc::signal; using type_signal_deafen = sigc::signal; -- cgit v1.2.3 From 929ebf1a6008d861473e3ceffd11cd4eca90d620 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 15 Nov 2022 02:14:49 -0500 Subject: mess with some websocket stuff to try and fix things to be honest, im not sure what ive done here. whatever memory i have of the issue i was trying to fix has long disappeared by the time im committing this. theres still some issues with being force disconnected and i really dont understand it ill figure it out eventually... maybe :/ --- src/discord/discord.cpp | 13 ++++-- src/discord/discord.hpp | 2 +- src/discord/voiceclient.cpp | 103 +++++++++++++++++++++++++++++++++++--------- src/discord/voiceclient.hpp | 24 ++++++++++- src/discord/websocket.cpp | 20 ++++++--- src/discord/websocket.hpp | 9 ++-- 6 files changed, 137 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 2a08574..cd16aa8 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -9,7 +9,8 @@ using namespace std::string_literals; DiscordClient::DiscordClient(bool mem_store) : m_decompress_buf(InflateChunkSize) - , m_store(mem_store) { + , m_store(mem_store) + , m_websocket("gateway-ws") { m_msg_dispatch.connect(sigc::mem_fun(*this, &DiscordClient::MessageDispatch)); auto dispatch_cb = [this]() { m_generic_mutex.lock(); @@ -2171,6 +2172,11 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { if (data.UserID == m_user_data.ID) { spdlog::get("discord")->debug("Voice session ID: {}", data.SessionID); m_voice.SetSessionID(data.SessionID); + + // channel_id = null means disconnect. stop cuz out of order maybe + if (!data.ChannelID.has_value()) { + m_voice.Stop(); + } } else { if (data.GuildID.has_value() && data.Member.has_value()) { if (data.Member->User.has_value()) { @@ -2504,9 +2510,8 @@ void DiscordClient::SetSuperPropertiesFromIdentity(const IdentifyMessage &identi void DiscordClient::HandleSocketOpen() { } -void DiscordClient::HandleSocketClose(uint16_t code) { - printf("got socket close code: %d\n", code); - auto close_code = static_cast(code); +void DiscordClient::HandleSocketClose(const ix::WebSocketCloseInfo &info) { + auto close_code = static_cast(info.code); auto cb = [this, close_code]() { m_heartbeat_waiter.kill(); if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index fc714e6..2d8db91 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -289,7 +289,7 @@ private: void SetSuperPropertiesFromIdentity(const IdentifyMessage &identity); void HandleSocketOpen(); - void HandleSocketClose(uint16_t code); + void HandleSocketClose(const ix::WebSocketCloseInfo &info); static bool CheckCode(const http::response_type &r); static bool CheckCode(const http::response_type &r, int expected); diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 291b975..3118cec 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -126,18 +126,26 @@ UDPSocket::type_signal_data UDPSocket::signal_data() { } DiscordVoiceClient::DiscordVoiceClient() { - sodium_init(); + if (sodium_init() == -1) { + spdlog::get("voice")->critical("sodium_init() failed"); + } + + m_ws = std::make_unique("voice-ws"); - m_ws.signal_open().connect([this]() { + m_ws->signal_open().connect([this]() { spdlog::get("voice")->info("Websocket open"); + SetState(State::Opened); }); - m_ws.signal_close().connect([this](uint16_t code) { - spdlog::get("voice")->info("Websocket closed with code {}", code); + m_ws->signal_close().connect([this](const ix::WebSocketCloseInfo &info) { + if (info.remote) { + SetState(State::ClosingByServer); + } Stop(); }); - m_ws.signal_message().connect([this](const std::string &str) { + m_ws->signal_message().connect([this](const std::string &str) { + spdlog::get("voice-ws")->trace("Recv: {}", str); std::lock_guard _(m_dispatch_mutex); m_message_queue.push(str); m_dispatcher.emit(); @@ -175,7 +183,8 @@ DiscordVoiceClient::~DiscordVoiceClient() { } void DiscordVoiceClient::Start() { - m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); + SetState(State::Opening); + m_ws->StartConnection("wss://" + m_endpoint + "/?v=7"); m_heartbeat_waiter.revive(); m_keepalive_waiter.revive(); m_connected = true; @@ -183,15 +192,29 @@ void DiscordVoiceClient::Start() { } void DiscordVoiceClient::Stop() { - m_ws.Stop(); - m_udp.Stop(); - m_heartbeat_waiter.kill(); - if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); - m_keepalive_waiter.kill(); - if (m_keepalive_thread.joinable()) m_keepalive_thread.join(); - if (m_connected) { - m_connected = false; - m_signal_disconnected.emit(); + if (IsOpening() || IsOpened()) { + spdlog::get("voice")->debug("Requested voice client stop"); + SetState(State::ClosingByClient); + m_ws->Stop(); + } else if (IsClosing()) { + spdlog::get("voice")->debug("Completing stop in closing"); + if (m_state == State::ClosingByClient) { + SetState(State::ClosedByClient); + } else if (m_state == State::ClosingByServer) { + SetState(State::ClosedByServer); + } + m_ws->Stop(); + m_udp.Stop(); + m_heartbeat_waiter.kill(); + if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); + m_keepalive_waiter.kill(); + if (m_keepalive_thread.joinable()) m_keepalive_thread.join(); + if (m_connected) { + m_connected = false; + m_signal_disconnected.emit(); + } + } else { + spdlog::get("voice")->debug("Stop called, but already stopped"); } } @@ -228,7 +251,6 @@ bool DiscordVoiceClient::IsConnected() const noexcept { void DiscordVoiceClient::OnGatewayMessage(const std::string &str) { VoiceGatewayMessage msg = nlohmann::json::parse(str); - puts(msg.Data.dump(4).c_str()); switch (msg.Opcode) { case VoiceGatewayOp::Hello: { HandleGatewayHello(msg); @@ -278,7 +300,7 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa msg.Delay = 0; msg.SSRC = m_ssrc; msg.Speaking = VoiceSpeakingType::Microphone; - m_ws.Send(msg); + m_ws->Send(msg); m_secret_key = d.SecretKey; m_udp.SetSSRC(m_ssrc); @@ -304,7 +326,7 @@ void DiscordVoiceClient::Identify() { msg.SessionID = m_session_id; msg.Token = m_token; msg.Video = true; - m_ws.Send(msg); + m_ws->Send(msg); } void DiscordVoiceClient::Discovery() { @@ -341,7 +363,7 @@ void DiscordVoiceClient::SelectProtocol(std::string_view ip, uint16_t port) { msg.Address = ip; msg.Port = port; msg.Protocol = "udp"; - m_ws.Send(msg); + m_ws->Send(msg); } void DiscordVoiceClient::OnUDPData(std::vector data) { @@ -370,7 +392,7 @@ void DiscordVoiceClient::HeartbeatThread() { VoiceHeartbeatMessage msg; msg.Nonce = static_cast(ms); - m_ws.Send(msg); + m_ws->Send(msg); } } @@ -386,6 +408,47 @@ void DiscordVoiceClient::KeepaliveThread() { } } +void DiscordVoiceClient::SetState(State state) { + m_state = state; + + switch (state) { + case State::Opening: + spdlog::get("voice")->debug("WS state: Opening"); + break; + case State::Opened: + spdlog::get("voice")->debug("WS state: Opened"); + break; + case State::ClosingByClient: + spdlog::get("voice")->debug("WS state: Closing (Client)"); + break; + case State::ClosingByServer: + spdlog::get("voice")->debug("WS state: Closing (Server)"); + break; + case State::ClosedByClient: + spdlog::get("voice")->debug("WS state: Closed (Client)"); + break; + case State::ClosedByServer: + spdlog::get("voice")->debug("WS state: Closed (Server)"); + break; + } +} + +bool DiscordVoiceClient::IsOpening() const noexcept { + return m_state == State::Opening; +} + +bool DiscordVoiceClient::IsOpened() const noexcept { + return m_state == State::Opened; +} + +bool DiscordVoiceClient::IsClosing() const noexcept { + return m_state == State::ClosingByClient || m_state == State::ClosingByServer; +} + +bool DiscordVoiceClient::IsClosed() const noexcept { + return m_state == State::ClosedByClient || m_state == State::ClosedByServer; +} + DiscordVoiceClient::type_signal_disconnected DiscordVoiceClient::signal_connected() { return m_signal_connected; } diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 1d8b952..61b329c 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -15,6 +15,7 @@ // clang-format on enum class VoiceGatewayCloseCode : uint16_t { + Normal = 4000, UnknownOpcode = 4001, InvalidPayload = 4002, NotAuthenticated = 4003, @@ -228,7 +229,10 @@ private: std::array m_secret_key; - Websocket m_ws; + // this is a unique_ptr because Websocket/ixwebsocket seems to have some strange behavior + // and quite frankly i do not feel like figuring out what is wrong + // so using a unique_ptr will just let me nuke the whole thing and make a new one + std::unique_ptr m_ws; UDPSocket m_udp; Glib::Dispatcher m_dispatcher; @@ -246,6 +250,24 @@ private: std::atomic m_connected = false; + enum class State { + Opening, + Opened, + ClosingByClient, + ClosedByClient, + ClosingByServer, + ClosedByServer, + }; + + void SetState(State state); + + [[nodiscard]] bool IsOpening() const noexcept; + [[nodiscard]] bool IsOpened() const noexcept; + [[nodiscard]] bool IsClosing() const noexcept; + [[nodiscard]] bool IsClosed() const noexcept; + + std::atomic m_state; + using type_signal_connected = sigc::signal; using type_signal_disconnected = sigc::signal; using type_signal_speaking = sigc::signal; diff --git a/src/discord/websocket.cpp b/src/discord/websocket.cpp index cf0111c..349913a 100644 --- a/src/discord/websocket.cpp +++ b/src/discord/websocket.cpp @@ -1,19 +1,25 @@ #include "websocket.hpp" +#include #include -Websocket::Websocket() - : m_close_code(ix::WebSocketCloseConstants::kNormalClosureCode) { +Websocket::Websocket(const std::string &id) + : m_close_info { 1000, "Normal", false } { + if (m_log = spdlog::get(id); !m_log) { + m_log = spdlog::stdout_color_mt(id); + } + m_open_dispatcher.connect([this]() { m_signal_open.emit(); }); m_close_dispatcher.connect([this]() { Stop(); - m_signal_close.emit(m_close_code); + m_signal_close.emit(m_close_info); }); } void Websocket::StartConnection(const std::string &url) { + m_log->debug("Starting connection to {}", url); m_websocket.disableAutomaticReconnection(); m_websocket.setUrl(url); m_websocket.setOnMessageCallback([this](auto &&msg) { OnMessage(std::forward(msg)); }); @@ -34,16 +40,18 @@ void Websocket::SetPrintMessages(bool show) noexcept { } void Websocket::Stop() { + m_log->debug("Stopping with default close code"); Stop(ix::WebSocketCloseConstants::kNormalClosureCode); } void Websocket::Stop(uint16_t code) { + m_log->debug("Stopping with close code {}", code); m_websocket.stop(code); } void Websocket::Send(const std::string &str) { if (m_print_messages) - printf("sending %s\n", str.c_str()); + m_log->trace("Send: {}", str); m_websocket.sendText(str); } @@ -54,10 +62,12 @@ void Websocket::Send(const nlohmann::json &j) { void Websocket::OnMessage(const ix::WebSocketMessagePtr &msg) { switch (msg->type) { case ix::WebSocketMessageType::Open: { + m_log->debug("Received open frame, dispatching"); m_open_dispatcher.emit(); } break; case ix::WebSocketMessageType::Close: { - m_close_code = msg->closeInfo.code; + m_log->debug("Received close frame, dispatching. {} ({}){}", msg->closeInfo.code, msg->closeInfo.reason, msg->closeInfo.remote ? " Remote" : ""); + m_close_info = msg->closeInfo; m_close_dispatcher.emit(); } break; case ix::WebSocketMessageType::Message: { diff --git a/src/discord/websocket.hpp b/src/discord/websocket.hpp index ba55254..768121e 100644 --- a/src/discord/websocket.hpp +++ b/src/discord/websocket.hpp @@ -6,10 +6,11 @@ #include #include #include +#include class Websocket { public: - Websocket(); + Websocket(const std::string &id); void StartConnection(const std::string &url); void SetUserAgent(std::string agent); @@ -30,7 +31,7 @@ private: public: using type_signal_open = sigc::signal; - using type_signal_close = sigc::signal; + using type_signal_close = sigc::signal; using type_signal_message = sigc::signal; type_signal_open signal_open(); @@ -46,5 +47,7 @@ private: Glib::Dispatcher m_open_dispatcher; Glib::Dispatcher m_close_dispatcher; - std::atomic m_close_code; + ix::WebSocketCloseInfo m_close_info; + + std::shared_ptr m_log; }; -- cgit v1.2.3 From afaba05293fa723734449777e487ac49e676e741 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 15 Nov 2022 15:38:39 -0500 Subject: actually reconstruct websocket on voice connect --- src/discord/voiceclient.cpp | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 3118cec..765c2af 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -130,27 +130,6 @@ DiscordVoiceClient::DiscordVoiceClient() { spdlog::get("voice")->critical("sodium_init() failed"); } - m_ws = std::make_unique("voice-ws"); - - m_ws->signal_open().connect([this]() { - spdlog::get("voice")->info("Websocket open"); - SetState(State::Opened); - }); - - m_ws->signal_close().connect([this](const ix::WebSocketCloseInfo &info) { - if (info.remote) { - SetState(State::ClosingByServer); - } - Stop(); - }); - - m_ws->signal_message().connect([this](const std::string &str) { - spdlog::get("voice-ws")->trace("Recv: {}", str); - std::lock_guard _(m_dispatch_mutex); - m_message_queue.push(str); - m_dispatcher.emit(); - }); - m_udp.signal_data().connect([this](const std::vector &data) { OnUDPData(data); }); @@ -183,6 +162,27 @@ DiscordVoiceClient::~DiscordVoiceClient() { } void DiscordVoiceClient::Start() { + m_ws = std::make_unique("voice-ws"); + + m_ws->signal_open().connect([this]() { + spdlog::get("voice")->info("Websocket open"); + SetState(State::Opened); + }); + + m_ws->signal_close().connect([this](const ix::WebSocketCloseInfo &info) { + if (info.remote) { + SetState(State::ClosingByServer); + } + Stop(); + }); + + m_ws->signal_message().connect([this](const std::string &str) { + spdlog::get("voice-ws")->trace("Recv: {}", str); + std::lock_guard _(m_dispatch_mutex); + m_message_queue.push(str); + m_dispatcher.emit(); + }); + SetState(State::Opening); m_ws->StartConnection("wss://" + m_endpoint + "/?v=7"); m_heartbeat_waiter.revive(); -- cgit v1.2.3 From 4740965f4c1ee879a733b4068f4026a0dfa626fd Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 3 Jan 2023 19:01:33 -0500 Subject: rewrite DiscordVoiceClient and stuff --- src/components/channels.cpp | 4 +- src/discord/discord.cpp | 8 +- src/discord/discord.hpp | 4 +- src/discord/voiceclient.cpp | 326 ++++++++++++++++++++++---------------------- src/discord/voiceclient.hpp | 63 +++++---- src/discord/websocket.cpp | 17 ++- src/discord/websocket.hpp | 2 +- 7 files changed, 214 insertions(+), 210 deletions(-) (limited to 'src') diff --git a/src/components/channels.cpp b/src/components/channels.cpp index a36b215..09e40c3 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -1019,7 +1019,7 @@ void ChannelList::OnVoiceChannelSubmenuPopup() { if (!iter) return; const auto id = static_cast((*iter)[m_columns.m_id]); auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsConnectedToVoice()) { + if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) { m_menu_voice_channel_join.set_sensitive(false); m_menu_voice_channel_disconnect.set_sensitive(discord.GetVoiceChannelID() == id); } else { @@ -1040,7 +1040,7 @@ void ChannelList::OnDMSubmenuPopup() { m_menu_dm_toggle_mute.set_label("Mute"); #ifdef WITH_VOICE - if (discord.IsConnectedToVoice()) { + if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) { m_menu_dm_join_voice.set_sensitive(false); m_menu_dm_disconnect_voice.set_sensitive(discord.GetVoiceChannelID() == id); } else { diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index cd16aa8..c068243 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1194,10 +1194,14 @@ void DiscordClient::DisconnectFromVoice() { m_websocket.Send(m); } -bool DiscordClient::IsConnectedToVoice() const noexcept { +bool DiscordClient::IsVoiceConnected() const noexcept { return m_voice.IsConnected(); } +bool DiscordClient::IsVoiceConnecting() const noexcept { + return m_voice.IsConnecting(); +} + Snowflake DiscordClient::GetVoiceChannelID() const noexcept { return m_voice_channel_id; } @@ -2174,7 +2178,7 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { m_voice.SetSessionID(data.SessionID); // channel_id = null means disconnect. stop cuz out of order maybe - if (!data.ChannelID.has_value()) { + if (!data.ChannelID.has_value() && (m_voice.IsConnected() || m_voice.IsConnecting())) { m_voice.Stop(); } } else { diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 2d8db91..1e500e9 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -183,7 +183,9 @@ public: #ifdef WITH_VOICE void ConnectToVoice(Snowflake channel_id); void DisconnectFromVoice(); - [[nodiscard]] bool IsConnectedToVoice() const noexcept; + // Is fully connected + [[nodiscard]] bool IsVoiceConnected() const noexcept; + [[nodiscard]] bool IsVoiceConnecting() const noexcept; [[nodiscard]] Snowflake GetVoiceChannelID() const noexcept; [[nodiscard]] std::unordered_set GetUsersInVoiceChannel(Snowflake channel_id); [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 765c2af..fe72f87 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -17,8 +17,8 @@ #endif // clang-format on -UDPSocket::UDPSocket() { - m_socket = socket(AF_INET, SOCK_DGRAM, 0); +UDPSocket::UDPSocket() + : m_socket(INVALID_SOCKET) { } UDPSocket::~UDPSocket() { @@ -30,6 +30,7 @@ void UDPSocket::Connect(std::string_view ip, uint16_t port) { m_server.sin_family = AF_INET; S_ADDR(m_server) = inet_addr(ip.data()); m_server.sin_port = htons(port); + m_socket = socket(AF_INET, SOCK_DGRAM, 0); bind(m_socket, reinterpret_cast(&m_server), sizeof(m_server)); } @@ -94,6 +95,11 @@ std::vector UDPSocket::Receive() { } void UDPSocket::Stop() { + #ifdef _WIN32 + closesocket(m_socket); + #else + close(m_socket); + #endif m_running = false; if (m_thread.joinable()) m_thread.join(); } @@ -125,97 +131,62 @@ UDPSocket::type_signal_data UDPSocket::signal_data() { return m_signal_data; } -DiscordVoiceClient::DiscordVoiceClient() { +DiscordVoiceClient::DiscordVoiceClient() + : m_state(State::DisconnectedByClient) + , m_ws("voice-ws") + , m_log(spdlog::get("voice")) { if (sodium_init() == -1) { - spdlog::get("voice")->critical("sodium_init() failed"); + m_log->critical("sodium_init() failed"); } m_udp.signal_data().connect([this](const std::vector &data) { OnUDPData(data); }); - m_dispatcher.connect([this]() { - m_dispatch_mutex.lock(); - if (m_message_queue.empty()) { - m_dispatch_mutex.unlock(); - return; - } - auto msg = std::move(m_message_queue.front()); - m_message_queue.pop(); - m_dispatch_mutex.unlock(); - OnGatewayMessage(msg); - }); + m_ws.signal_open().connect(sigc::mem_fun(*this, &DiscordVoiceClient::OnWebsocketOpen)); + m_ws.signal_close().connect(sigc::mem_fun(*this, &DiscordVoiceClient::OnWebsocketClose)); + m_ws.signal_message().connect(sigc::mem_fun(*this, &DiscordVoiceClient::OnWebsocketMessage)); + + m_dispatcher.connect(sigc::mem_fun(*this, &DiscordVoiceClient::OnDispatch)); + // idle or else singleton deadlock Glib::signal_idle().connect_once([this]() { - // cant put in ctor or deadlock in singleton initialization - auto &aud = Abaddon::Get().GetAudio(); - aud.SetOpusBuffer(m_opus_buffer.data()); - aud.signal_opus_packet().connect([this](int payload_size) { - if (m_connected) - m_udp.SendEncrypted(m_opus_buffer.data(), payload_size); + auto &audio = Abaddon::Get().GetAudio(); + audio.SetOpusBuffer(m_opus_buffer.data()); + audio.signal_opus_packet().connect([this](int payload_size) { + // SendEncrypted if udp is connected }); }); } DiscordVoiceClient::~DiscordVoiceClient() { - Stop(); + if (IsConnected() || IsConnecting()) Stop(); } void DiscordVoiceClient::Start() { - m_ws = std::make_unique("voice-ws"); - - m_ws->signal_open().connect([this]() { - spdlog::get("voice")->info("Websocket open"); - SetState(State::Opened); - }); - - m_ws->signal_close().connect([this](const ix::WebSocketCloseInfo &info) { - if (info.remote) { - SetState(State::ClosingByServer); - } - Stop(); - }); - - m_ws->signal_message().connect([this](const std::string &str) { - spdlog::get("voice-ws")->trace("Recv: {}", str); - std::lock_guard _(m_dispatch_mutex); - m_message_queue.push(str); - m_dispatcher.emit(); - }); - - SetState(State::Opening); - m_ws->StartConnection("wss://" + m_endpoint + "/?v=7"); + SetState(State::ConnectingToWebsocket); m_heartbeat_waiter.revive(); m_keepalive_waiter.revive(); - m_connected = true; + m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); + m_signal_connected.emit(); } void DiscordVoiceClient::Stop() { - if (IsOpening() || IsOpened()) { - spdlog::get("voice")->debug("Requested voice client stop"); - SetState(State::ClosingByClient); - m_ws->Stop(); - } else if (IsClosing()) { - spdlog::get("voice")->debug("Completing stop in closing"); - if (m_state == State::ClosingByClient) { - SetState(State::ClosedByClient); - } else if (m_state == State::ClosingByServer) { - SetState(State::ClosedByServer); - } - m_ws->Stop(); - m_udp.Stop(); - m_heartbeat_waiter.kill(); - if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); - m_keepalive_waiter.kill(); - if (m_keepalive_thread.joinable()) m_keepalive_thread.join(); - if (m_connected) { - m_connected = false; - m_signal_disconnected.emit(); - } - } else { - spdlog::get("voice")->debug("Stop called, but already stopped"); + if (!IsConnected() && !IsConnecting()) { + m_log->warn("Requested stop while not connected (from {})", GetStateName(m_state)); + return; } + + SetState(State::DisconnectedByClient); + m_ws.Stop(4014); + m_udp.Stop(); + m_heartbeat_waiter.kill(); + if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join(); + m_keepalive_waiter.kill(); + if (m_keepalive_thread.joinable()) m_keepalive_thread.join(); + + m_signal_disconnected.emit(); } void DiscordVoiceClient::SetSessionID(std::string_view session_id) { @@ -239,37 +210,59 @@ void DiscordVoiceClient::SetUserID(Snowflake id) { } std::optional DiscordVoiceClient::GetSSRCOfUser(Snowflake id) const { - if (const auto it = m_ssrc_map.find(id); it != m_ssrc_map.end()) { - return it->second; - } - return {}; + return std::nullopt; } bool DiscordVoiceClient::IsConnected() const noexcept { - return m_connected; + return m_state == State::Connected; +} + +bool DiscordVoiceClient::IsConnecting() const noexcept { + return m_state == State::ConnectingToWebsocket || m_state == State::EstablishingConnection; } void DiscordVoiceClient::OnGatewayMessage(const std::string &str) { VoiceGatewayMessage msg = nlohmann::json::parse(str); switch (msg.Opcode) { - case VoiceGatewayOp::Hello: { + case VoiceGatewayOp::Hello: HandleGatewayHello(msg); - } break; - case VoiceGatewayOp::Ready: { + break; + case VoiceGatewayOp::Ready: HandleGatewayReady(msg); - } break; - case VoiceGatewayOp::SessionDescription: { + break; + case VoiceGatewayOp::SessionDescription: HandleGatewaySessionDescription(msg); - } break; - case VoiceGatewayOp::Speaking: { + break; + case VoiceGatewayOp::Speaking: HandleGatewaySpeaking(msg); - } break; - default: break; + break; + default: + m_log->warn("Unhandled opcode: {}", static_cast(msg.Opcode)); + } +} + +const char *DiscordVoiceClient::GetStateName(State state) { + switch (state) { + case State::DisconnectedByClient: + return "DisconnectedByClient"; + case State::DisconnectedByServer: + return "DisconnectedByServer"; + case State::ConnectingToWebsocket: + return "ConnectingToWebsocket"; + case State::EstablishingConnection: + return "EstablishingConnection"; + case State::Connected: + return "Connected"; + default: + return "Unknown"; } } void DiscordVoiceClient::HandleGatewayHello(const VoiceGatewayMessage &m) { VoiceHelloData d = m.Data; + + m_log->debug("Received hello: {}ms", d.HeartbeatInterval); + m_heartbeat_msec = d.HeartbeatInterval; m_heartbeat_thread = std::thread(&DiscordVoiceClient::HeartbeatThread, this); @@ -278,13 +271,16 @@ void DiscordVoiceClient::HandleGatewayHello(const VoiceGatewayMessage &m) { void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) { VoiceReadyData d = m.Data; + + m_log->debug("Received ready: {}:{} (ssrc: {})", d.IP, d.Port, d.SSRC); + m_ip = d.IP; m_port = d.Port; m_ssrc = d.SSRC; + if (std::find(d.Modes.begin(), d.Modes.end(), "xsalsa20_poly1305") == d.Modes.end()) { - spdlog::get("voice")->error("xsalsa20_poly1305 not in encryption modes"); + m_log->warn("xsalsa20_poly1305 not in modes"); } - spdlog::get("voice")->info("connect to {}:{} ssrc {}", m_ip, m_port, m_ssrc); m_udp.Connect(m_ip, m_port); m_keepalive_thread = std::thread(&DiscordVoiceClient::KeepaliveThread, this); @@ -294,29 +290,26 @@ void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) { void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessage &m) { VoiceSessionDescriptionData d = m.Data; - spdlog::get("voice")->debug("receiving with {}, secret key: {:ns}", d.Mode, spdlog::to_hex(std::begin(d.SecretKey), std::end(d.SecretKey))); + + m_log->debug("Received session description (mode: {}) (key: {:ns}) ", d.Mode, spdlog::to_hex(d.SecretKey.begin(), d.SecretKey.end())); VoiceSpeakingMessage msg; msg.Delay = 0; msg.SSRC = m_ssrc; msg.Speaking = VoiceSpeakingType::Microphone; - m_ws->Send(msg); + m_ws.Send(msg); m_secret_key = d.SecretKey; m_udp.SetSSRC(m_ssrc); m_udp.SetSecretKey(m_secret_key); m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); - m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); - m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); - m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); - m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); m_udp.Run(); } void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) { - VoiceSpeakingData data = m.Data; - m_ssrc_map[data.UserID] = data.SSRC; - m_signal_speaking.emit(data); + VoiceSpeakingData d = m.Data; + // ssrc map + m_signal_speaking.emit(d); } void DiscordVoiceClient::Identify() { @@ -326,80 +319,92 @@ void DiscordVoiceClient::Identify() { msg.SessionID = m_session_id; msg.Token = m_token; msg.Video = true; - m_ws->Send(msg); + + m_ws.Send(msg); } void DiscordVoiceClient::Discovery() { std::vector payload; - // 2 bytes = 1, request + // request payload.push_back(0x00); payload.push_back(0x01); - // 2 bytes = 70, pl length + // payload length (70) payload.push_back(0x00); - payload.push_back(70); - // 4 bytes = ssrc + payload.push_back(0x46); + // ssrc payload.push_back((m_ssrc >> 24) & 0xFF); payload.push_back((m_ssrc >> 16) & 0xFF); payload.push_back((m_ssrc >> 8) & 0xFF); payload.push_back((m_ssrc >> 0) & 0xFF); - // address and port - for (int i = 0; i < 66; i++) - payload.push_back(0); + // space for address and port + for (int i = 0; i < 66; i++) payload.push_back(0x00); + m_udp.Send(payload.data(), payload.size()); - auto response = m_udp.Receive(); - if (response.size() >= 74 && response[0] == 0x00 && response[1] == 0x02) { - const char *our_ip = reinterpret_cast(&response[8]); - uint16_t our_port = (response[73] << 8) | response[74]; - spdlog::get("voice")->debug("IP address discovered: {}:{}\n", our_ip, our_port); - SelectProtocol(our_ip, our_port); - } else { - spdlog::get("voice")->error("Received non-discovery packet after discovery"); + + constexpr int MAX_TRIES = 100; + for (int i = 0; i < MAX_TRIES; i++) { + const auto response = m_udp.Receive(); + if (response.size() >= 74 && response[0] == 0x00 && response[1] == 0x02) { + const char *ip = reinterpret_cast(response.data() + 8); + uint16_t port = (response[73] << 8) | response[74]; + m_log->info("Discovered IP and port: {}:{}", ip, port); + SelectProtocol(ip, port); + break; + } else { + m_log->error("Received non-discovery packet after sending request (try {}/{})", i + 1, MAX_TRIES); + } } } -void DiscordVoiceClient::SelectProtocol(std::string_view ip, uint16_t port) { +void DiscordVoiceClient::SelectProtocol(const char *ip, uint16_t port) { VoiceSelectProtocolMessage msg; msg.Mode = "xsalsa20_poly1305"; msg.Address = ip; msg.Port = port; msg.Protocol = "udp"; - m_ws->Send(msg); + + m_ws.Send(msg); } -void DiscordVoiceClient::OnUDPData(std::vector data) { - uint8_t *payload = data.data() + 12; - uint32_t ssrc = (data[8] << 24) | - (data[9] << 16) | - (data[10] << 8) | - (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"); +void DiscordVoiceClient::OnWebsocketOpen() { + m_log->info("Websocket opened"); + SetState(State::EstablishingConnection); +} + +void DiscordVoiceClient::OnWebsocketClose(const ix::WebSocketCloseInfo &info) { + if (info.remote) { + m_log->debug("Websocket closed (remote): {} ({})", info.code, info.reason); } else { - Abaddon::Get().GetAudio().FeedMeOpus(ssrc, { payload, payload + data.size() - 12 - crypto_box_MACBYTES }); + m_log->debug("Websocket closed (local): {} ({})", info.code, info.reason); } } +void DiscordVoiceClient::OnWebsocketMessage(const std::string &data) { + m_dispatch_mutex.lock(); + m_dispatch_queue.push(data); + m_dispatcher.emit(); + m_dispatch_mutex.unlock(); +} + void DiscordVoiceClient::HeartbeatThread() { while (true) { - if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec))) - break; + if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec))) break; + + const auto ms = static_cast(std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count()); - const auto ms = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); + m_log->trace("Heartbeat: {}", ms); VoiceHeartbeatMessage msg; - msg.Nonce = static_cast(ms); - m_ws->Send(msg); + msg.Nonce = ms; + m_ws.Send(msg); } } void DiscordVoiceClient::KeepaliveThread() { while (true) { - if (!m_keepalive_waiter.wait_for(std::chrono::seconds(10))) - break; + if (!m_heartbeat_waiter.wait_for(std::chrono::seconds(10))) break; if (IsConnected()) { const static uint8_t KEEPALIVE[] = { 0x13, 0x37 }; @@ -409,44 +414,35 @@ void DiscordVoiceClient::KeepaliveThread() { } void DiscordVoiceClient::SetState(State state) { + m_log->debug("Changing state to {}", GetStateName(state)); m_state = state; - - switch (state) { - case State::Opening: - spdlog::get("voice")->debug("WS state: Opening"); - break; - case State::Opened: - spdlog::get("voice")->debug("WS state: Opened"); - break; - case State::ClosingByClient: - spdlog::get("voice")->debug("WS state: Closing (Client)"); - break; - case State::ClosingByServer: - spdlog::get("voice")->debug("WS state: Closing (Server)"); - break; - case State::ClosedByClient: - spdlog::get("voice")->debug("WS state: Closed (Client)"); - break; - case State::ClosedByServer: - spdlog::get("voice")->debug("WS state: Closed (Server)"); - break; - } -} - -bool DiscordVoiceClient::IsOpening() const noexcept { - return m_state == State::Opening; } -bool DiscordVoiceClient::IsOpened() const noexcept { - return m_state == State::Opened; -} - -bool DiscordVoiceClient::IsClosing() const noexcept { - return m_state == State::ClosingByClient || m_state == State::ClosingByServer; +void DiscordVoiceClient::OnUDPData(std::vector data) { + uint8_t *payload = data.data() + 12; + uint32_t ssrc = (data[8] << 24) | + (data[9] << 16) | + (data[10] << 8) | + (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 { + Abaddon::Get().GetAudio().FeedMeOpus(ssrc, { payload, payload + data.size() - 12 - crypto_box_MACBYTES }); + } } -bool DiscordVoiceClient::IsClosed() const noexcept { - return m_state == State::ClosedByClient || m_state == State::ClosedByServer; +void DiscordVoiceClient::OnDispatch() { + m_dispatch_mutex.lock(); + if (m_dispatch_queue.empty()) { + m_dispatch_mutex.unlock(); + return; + } + auto msg = std::move(m_dispatch_queue.front()); + m_dispatch_queue.pop(); + m_dispatch_mutex.unlock(); + OnGatewayMessage(msg); } DiscordVoiceClient::type_signal_disconnected DiscordVoiceClient::signal_connected() { diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 61b329c..47caec6 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include // clang-format on @@ -196,10 +197,21 @@ public: [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; + // Is a websocket and udp connection fully established [[nodiscard]] bool IsConnected() const noexcept; + [[nodiscard]] bool IsConnecting() const noexcept; private: - void OnGatewayMessage(const std::string &str); + enum class State { + ConnectingToWebsocket, + EstablishingConnection, + Connected, + DisconnectedByClient, + DisconnectedByServer, + }; + static const char *GetStateName(State state); + + void OnGatewayMessage(const std::string &msg); void HandleGatewayHello(const VoiceGatewayMessage &m); void HandleGatewayReady(const VoiceGatewayMessage &m); void HandleGatewaySessionDescription(const VoiceGatewayMessage &m); @@ -207,13 +219,19 @@ private: void Identify(); void Discovery(); - void SelectProtocol(std::string_view ip, uint16_t port); + void SelectProtocol(const char *ip, uint16_t port); - void OnUDPData(std::vector data); + void OnWebsocketOpen(); + void OnWebsocketClose(const ix::WebSocketCloseInfo &info); + void OnWebsocketMessage(const std::string &str); void HeartbeatThread(); void KeepaliveThread(); + void SetState(State state); + + void OnUDPData(std::vector data); + std::string m_session_id; std::string m_endpoint; std::string m_token; @@ -221,24 +239,12 @@ private: Snowflake m_channel_id; Snowflake m_user_id; + std::array m_secret_key; + std::string m_ip; uint16_t m_port; uint32_t m_ssrc; - std::unordered_map m_ssrc_map; - - std::array m_secret_key; - - // this is a unique_ptr because Websocket/ixwebsocket seems to have some strange behavior - // and quite frankly i do not feel like figuring out what is wrong - // so using a unique_ptr will just let me nuke the whole thing and make a new one - std::unique_ptr m_ws; - UDPSocket m_udp; - - Glib::Dispatcher m_dispatcher; - std::queue m_message_queue; - std::mutex m_dispatch_mutex; - int m_heartbeat_msec; Waiter m_heartbeat_waiter; std::thread m_heartbeat_thread; @@ -246,25 +252,18 @@ private: Waiter m_keepalive_waiter; std::thread m_keepalive_thread; - std::array m_opus_buffer; + Websocket m_ws; + UDPSocket m_udp; - std::atomic m_connected = false; + Glib::Dispatcher m_dispatcher; + std::queue m_dispatch_queue; + std::mutex m_dispatch_mutex; - enum class State { - Opening, - Opened, - ClosingByClient, - ClosedByClient, - ClosingByServer, - ClosedByServer, - }; + void OnDispatch(); - void SetState(State state); + std::array m_opus_buffer; - [[nodiscard]] bool IsOpening() const noexcept; - [[nodiscard]] bool IsOpened() const noexcept; - [[nodiscard]] bool IsClosing() const noexcept; - [[nodiscard]] bool IsClosed() const noexcept; + std::shared_ptr m_log; std::atomic m_state; diff --git a/src/discord/websocket.cpp b/src/discord/websocket.cpp index 349913a..d40d057 100644 --- a/src/discord/websocket.cpp +++ b/src/discord/websocket.cpp @@ -20,11 +20,14 @@ Websocket::Websocket(const std::string &id) void Websocket::StartConnection(const std::string &url) { m_log->debug("Starting connection to {}", url); - m_websocket.disableAutomaticReconnection(); - m_websocket.setUrl(url); - m_websocket.setOnMessageCallback([this](auto &&msg) { OnMessage(std::forward(msg)); }); - m_websocket.setExtraHeaders(ix::WebSocketHttpHeaders { { "User-Agent", m_agent } }); // idk if this actually works - m_websocket.start(); + + m_websocket = std::make_unique(); + + m_websocket->disableAutomaticReconnection(); + m_websocket->setUrl(url); + m_websocket->setOnMessageCallback([this](auto &&msg) { OnMessage(std::forward(msg)); }); + m_websocket->setExtraHeaders(ix::WebSocketHttpHeaders { { "User-Agent", m_agent } }); // idk if this actually works + m_websocket->start(); } void Websocket::SetUserAgent(std::string agent) { @@ -46,13 +49,13 @@ void Websocket::Stop() { void Websocket::Stop(uint16_t code) { m_log->debug("Stopping with close code {}", code); - m_websocket.stop(code); + m_websocket-> stop(code); } void Websocket::Send(const std::string &str) { if (m_print_messages) m_log->trace("Send: {}", str); - m_websocket.sendText(str); + m_websocket->sendText(str); } void Websocket::Send(const nlohmann::json &j) { diff --git a/src/discord/websocket.hpp b/src/discord/websocket.hpp index 768121e..a77bf55 100644 --- a/src/discord/websocket.hpp +++ b/src/discord/websocket.hpp @@ -26,7 +26,7 @@ public: private: void OnMessage(const ix::WebSocketMessagePtr &msg); - ix::WebSocket m_websocket; + std::unique_ptr m_websocket; std::string m_agent; public: -- cgit v1.2.3 From 02583b851253538d9da2efe6cc9b407545bb7ecc Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 3 Jan 2023 19:47:56 -0500 Subject: re-add ssrc map --- src/discord/voiceclient.cpp | 5 ++++- src/discord/voiceclient.hpp | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index fe72f87..7e801f7 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -210,6 +210,9 @@ void DiscordVoiceClient::SetUserID(Snowflake id) { } std::optional DiscordVoiceClient::GetSSRCOfUser(Snowflake id) const { + if (const auto it = m_ssrc_map.find(id); it != m_ssrc_map.end()) { + return it->second; + } return std::nullopt; } @@ -308,7 +311,7 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) { VoiceSpeakingData d = m.Data; - // ssrc map + m_ssrc_map[d.UserID] = d.SSRC; m_signal_speaking.emit(d); } diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 47caec6..7e4dee3 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -239,6 +239,8 @@ private: Snowflake m_channel_id; Snowflake m_user_id; + std::unordered_map m_ssrc_map; + std::array m_secret_key; std::string m_ip; -- cgit v1.2.3 From dff93e103adf03b25e85ab753693479878c4efbf Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 3 Jan 2023 19:57:38 -0500 Subject: actually go to connected state and transmit data --- src/discord/voiceclient.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 7e801f7..379a65c 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -154,7 +154,9 @@ DiscordVoiceClient::DiscordVoiceClient() auto &audio = Abaddon::Get().GetAudio(); audio.SetOpusBuffer(m_opus_buffer.data()); audio.signal_opus_packet().connect([this](int payload_size) { - // SendEncrypted if udp is connected + if (IsConnected()) { + m_udp.SendEncrypted(m_opus_buffer.data(), payload_size); + } }); }); } @@ -307,6 +309,8 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa m_udp.SetSecretKey(m_secret_key); m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE }); m_udp.Run(); + + SetState(State::Connected); } void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) { -- cgit v1.2.3 From c4590f8b2382fb23a733d2cf2f6c645f02e2fc48 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 3 Jan 2023 22:52:41 -0500 Subject: start voice info box --- res/css/main.css | 18 +++++++++ src/components/voiceinfobox.cpp | 89 +++++++++++++++++++++++++++++++++++++++++ src/components/voiceinfobox.hpp | 19 +++++++++ src/discord/discord.cpp | 17 ++++++++ src/discord/discord.hpp | 9 +++++ src/discord/voiceclient.cpp | 5 +++ src/discord/voiceclient.hpp | 6 ++- src/windows/mainwindow.cpp | 13 ++++-- src/windows/mainwindow.hpp | 4 ++ 9 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/components/voiceinfobox.cpp create mode 100644 src/components/voiceinfobox.hpp (limited to 'src') diff --git a/res/css/main.css b/res/css/main.css index ace3b0b..4b76bb4 100644 --- a/res/css/main.css +++ b/res/css/main.css @@ -360,3 +360,21 @@ background-color: #dd3300; margin-left: 1px; } + +.voice-info { + background-color: #0B0B0B; + padding: 5px; + border: 1px solid #202020; +} + +.voice-info-disconnect-image { + color: #DDDDDD; +} + +.voice-info-status { + font-weight: bold; +} + +.voice-info-location { + +} diff --git a/src/components/voiceinfobox.cpp b/src/components/voiceinfobox.cpp new file mode 100644 index 0000000..f4dc6ed --- /dev/null +++ b/src/components/voiceinfobox.cpp @@ -0,0 +1,89 @@ +#include "voiceinfobox.hpp" +#include "abaddon.hpp" +#include "util.hpp" + +VoiceInfoBox::VoiceInfoBox() + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , m_left(Gtk::ORIENTATION_VERTICAL) { + m_disconnect_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { + if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) { + spdlog::get("discord")->debug("Request disconnect from info box"); + Abaddon::Get().GetDiscordClient().DisconnectFromVoice(); + return true; + } + + return false; + }); + + AddPointerCursor(m_disconnect_ev); + + get_style_context()->add_class("voice-info"); + m_status.get_style_context()->add_class("voice-info-status"); + m_location.get_style_context()->add_class("voice-info-location"); + m_disconnect_img.get_style_context()->add_class("voice-info-disconnect-image"); + + m_status.set_label("You shouldn't see me"); + m_location.set_label("You shouldn't see me"); + + Abaddon::Get().GetDiscordClient().signal_voice_requested_connect().connect([this](Snowflake channel_id) { + show(); + + if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(channel_id); channel.has_value() && channel->Name.has_value()) { + if (channel->GuildID.has_value()) { + if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*channel->GuildID); guild.has_value()) { + m_location.set_label(*channel->Name + " / " + guild->Name); + return; + } + } + + m_location.set_label(*channel->Name); + return; + } + + m_location.set_label("Unknown"); + }); + + Abaddon::Get().GetDiscordClient().signal_voice_requested_disconnect().connect([this]() { + hide(); + }); + + Abaddon::Get().GetDiscordClient().signal_voice_client_state_update().connect([this](DiscordVoiceClient::State state) { + Glib::ustring label; + switch (state) { + case DiscordVoiceClient::State::ConnectingToWebsocket: + label = "Connecting"; + break; + case DiscordVoiceClient::State::EstablishingConnection: + label = "Establishing connection"; + break; + case DiscordVoiceClient::State::Connected: + label = "Connected"; + break; + case DiscordVoiceClient::State::DisconnectedByServer: + case DiscordVoiceClient::State::DisconnectedByClient: + label = "Disconnected"; + break; + default: + label = "Unknown"; + break; + } + m_status.set_label(label); + }); + + m_status.set_ellipsize(Pango::ELLIPSIZE_END); + m_location.set_ellipsize(Pango::ELLIPSIZE_END); + + m_disconnect_ev.add(m_disconnect_img); + m_disconnect_img.property_icon_name() = "call-stop-symbolic"; + m_disconnect_img.property_icon_size() = 5; + m_disconnect_img.set_hexpand(true); + m_disconnect_img.set_halign(Gtk::ALIGN_END); + + m_left.add(m_status); + m_left.add(m_location); + + add(m_left); + add(m_disconnect_ev); + + show_all_children(); +} diff --git a/src/components/voiceinfobox.hpp b/src/components/voiceinfobox.hpp new file mode 100644 index 0000000..e0bc4e8 --- /dev/null +++ b/src/components/voiceinfobox.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include + +class VoiceInfoBox : public Gtk::Box { +public: + VoiceInfoBox(); + +private: + Gtk::Box m_left; + Gtk::Label m_status; + Gtk::Label m_location; + + Gtk::EventBox m_disconnect_ev; + Gtk::Image m_disconnect_img; +}; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index c068243..44f64a9 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -31,6 +31,9 @@ DiscordClient::DiscordClient(bool mem_store) m_voice.signal_speaking().connect([this](const VoiceSpeakingData &data) { m_signal_voice_speaking.emit(data); }); + m_voice.signal_state_update().connect([this](DiscordVoiceClient::State state) { + m_signal_voice_client_state_update.emit(state); + }); #endif LoadEventMap(); @@ -1186,12 +1189,14 @@ void DiscordClient::ConnectToVoice(Snowflake channel_id) { m.ChannelID = channel_id; m.PreferredRegion = "newark"; m_websocket.Send(m); + m_signal_voice_requested_connect.emit(channel_id); } void DiscordClient::DisconnectFromVoice() { m_voice.Stop(); VoiceStateUpdateMessage m; m_websocket.Send(m); + m_signal_voice_requested_disconnect.emit(); } bool DiscordClient::IsVoiceConnected() const noexcept { @@ -2995,4 +3000,16 @@ DiscordClient::type_signal_voice_user_disconnect DiscordClient::signal_voice_use DiscordClient::type_signal_voice_user_connect DiscordClient::signal_voice_user_connect() { return m_signal_voice_user_connect; } + +DiscordClient::type_signal_voice_requested_connect DiscordClient::signal_voice_requested_connect() { + return m_signal_voice_requested_connect; +} + +DiscordClient::type_signal_voice_requested_disconnect DiscordClient::signal_voice_requested_disconnect() { + return m_signal_voice_requested_disconnect; +} + +DiscordClient::type_signal_voice_client_state_update DiscordClient::signal_voice_client_state_update() { + return m_signal_voice_client_state_update; +} #endif diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 1e500e9..a7e3f79 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -440,6 +440,9 @@ public: using type_signal_voice_speaking = sigc::signal; using type_signal_voice_user_disconnect = sigc::signal; using type_signal_voice_user_connect = sigc::signal; + using type_signal_voice_requested_connect = sigc::signal; + using type_signal_voice_requested_disconnect = sigc::signal; + using type_signal_voice_client_state_update = sigc::signal; #endif type_signal_gateway_ready signal_gateway_ready(); @@ -502,6 +505,9 @@ public: type_signal_voice_speaking signal_voice_speaking(); type_signal_voice_user_disconnect signal_voice_user_disconnect(); type_signal_voice_user_connect signal_voice_user_connect(); + type_signal_voice_requested_connect signal_voice_requested_connect(); + type_signal_voice_requested_disconnect signal_voice_requested_disconnect(); + type_signal_voice_client_state_update signal_voice_client_state_update(); #endif protected: @@ -565,5 +571,8 @@ protected: type_signal_voice_speaking m_signal_voice_speaking; type_signal_voice_user_disconnect m_signal_voice_user_disconnect; type_signal_voice_user_connect m_signal_voice_user_connect; + type_signal_voice_requested_connect m_signal_voice_requested_connect; + type_signal_voice_requested_disconnect m_signal_voice_requested_disconnect; + type_signal_voice_client_state_update m_signal_voice_client_state_update; #endif }; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 379a65c..ed83e3c 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -423,6 +423,7 @@ void DiscordVoiceClient::KeepaliveThread() { void DiscordVoiceClient::SetState(State state) { m_log->debug("Changing state to {}", GetStateName(state)); m_state = state; + m_signal_state_update.emit(state); } void DiscordVoiceClient::OnUDPData(std::vector data) { @@ -464,6 +465,10 @@ DiscordVoiceClient::type_signal_speaking DiscordVoiceClient::signal_speaking() { return m_signal_speaking; } +DiscordVoiceClient::type_signal_state_update DiscordVoiceClient::signal_state_update() { + return m_signal_state_update; +} + void from_json(const nlohmann::json &j, VoiceGatewayMessage &m) { JS_D("op", m.Opcode); m.Data = j.at("d"); diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 7e4dee3..916d070 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -201,7 +201,6 @@ public: [[nodiscard]] bool IsConnected() const noexcept; [[nodiscard]] bool IsConnecting() const noexcept; -private: enum class State { ConnectingToWebsocket, EstablishingConnection, @@ -209,6 +208,8 @@ private: DisconnectedByClient, DisconnectedByServer, }; + +private: static const char *GetStateName(State state); void OnGatewayMessage(const std::string &msg); @@ -272,13 +273,16 @@ private: using type_signal_connected = sigc::signal; using type_signal_disconnected = sigc::signal; using type_signal_speaking = sigc::signal; + using type_signal_state_update = sigc::signal; type_signal_connected m_signal_connected; type_signal_disconnected m_signal_disconnected; type_signal_speaking m_signal_speaking; + type_signal_state_update m_signal_state_update; public: type_signal_connected signal_connected(); type_signal_disconnected signal_disconnected(); type_signal_speaking signal_speaking(); + type_signal_state_update signal_state_update(); }; #endif diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp index 20da46b..c80dfda 100644 --- a/src/windows/mainwindow.cpp +++ b/src/windows/mainwindow.cpp @@ -6,6 +6,7 @@ MainWindow::MainWindow() , m_content_box(Gtk::ORIENTATION_HORIZONTAL) , m_chan_content_paned(Gtk::ORIENTATION_HORIZONTAL) , m_content_members_paned(Gtk::ORIENTATION_HORIZONTAL) + , m_left_pane(Gtk::ORIENTATION_VERTICAL) , m_accels(Gtk::AccelGroup::create()) { set_default_size(1200, 800); get_style_context()->add_class("app-window"); @@ -51,12 +52,18 @@ MainWindow::MainWindow() m_content_stack.set_visible_child("chat"); m_content_stack.show(); - m_chan_content_paned.pack1(m_channel_list); + m_voice_info.show(); + + m_left_pane.add(m_channel_list); + m_left_pane.add(m_voice_info); + m_left_pane.show(); + + m_chan_content_paned.pack1(m_left_pane); m_chan_content_paned.pack2(m_content_members_paned); m_chan_content_paned.child_property_shrink(m_content_members_paned) = true; m_chan_content_paned.child_property_resize(m_content_members_paned) = true; - m_chan_content_paned.child_property_shrink(m_channel_list) = true; - m_chan_content_paned.child_property_resize(m_channel_list) = true; + m_chan_content_paned.child_property_shrink(m_left_pane) = true; + m_chan_content_paned.child_property_resize(m_left_pane) = true; m_chan_content_paned.set_position(200); m_chan_content_paned.show(); m_content_box.add(m_chan_content_paned); diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp index 6e95b72..d4013dc 100644 --- a/src/windows/mainwindow.hpp +++ b/src/windows/mainwindow.hpp @@ -3,6 +3,7 @@ #include "components/chatwindow.hpp" #include "components/memberlist.hpp" #include "components/friendslist.hpp" +#include "components/voiceinfobox.hpp" #include class MainWindow : public Gtk::Window { @@ -53,6 +54,9 @@ private: ChatWindow m_chat; MemberList m_members; FriendsList m_friends; + VoiceInfoBox m_voice_info; + + Gtk::Box m_left_pane; Gtk::Stack m_content_stack; -- cgit v1.2.3 From 67e924e53879409733f1f462d865ac44762c21dc Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 6 Jan 2023 18:40:11 -0500 Subject: display users in voice in channel list --- src/abaddon.cpp | 3 ++- src/components/channels.cpp | 23 ++++++++++++++-- src/components/channelscellrenderer.cpp | 47 ++++++++++++++++++++++++++++++++- src/components/channelscellrenderer.hpp | 13 +++++++++ src/discord/discord.cpp | 6 +++++ src/discord/discord.hpp | 3 +++ 6 files changed, 91 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 67fd2bd..8b760fd 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -41,7 +41,8 @@ Abaddon::Abaddon() std::string ua = GetSettings().UserAgent; m_discord.SetUserAgent(ua); - m_discord.signal_gateway_ready().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReady)); + // todo rename funcs + m_discord.signal_gateway_ready_supplemental().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReady)); m_discord.signal_message_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageCreate)); m_discord.signal_message_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageDelete)); m_discord.signal_message_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageUpdate)); diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 09e40c3..dc14688 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -651,13 +651,30 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { m_tmp_channel_map[thread.ID] = CreateThreadRow(row.children(), thread); }; + auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) { + for (auto user_id : discord.GetUsersInVoiceChannel(channel.ID)) { + const auto user = discord.GetUser(user_id); + + auto user_row = *m_model->append(root); + user_row[m_columns.m_type] = RenderType::VoiceParticipant; + user_row[m_columns.m_id] = user_id; + if (user.has_value()) { + user_row[m_columns.m_name] = user->GetEscapedName(); + } else { + user_row[m_columns.m_name] = "Unknown"; + } + } + }; + for (const auto &channel : orphan_channels) { auto channel_row = *m_model->append(guild_row.children()); if (IsTextChannel(channel.Type)) channel_row[m_columns.m_type] = RenderType::TextChannel; #ifdef WITH_VOICE - else + else { channel_row[m_columns.m_type] = RenderType::VoiceChannel; + add_voice_participants(channel, channel_row->children()); + } #endif channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); @@ -684,8 +701,10 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { if (IsTextChannel(channel.Type)) channel_row[m_columns.m_type] = RenderType::TextChannel; #ifdef WITH_VOICE - else + else { channel_row[m_columns.m_type] = RenderType::VoiceChannel; + add_voice_participants(channel, channel_row->children()); + } #endif channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index 23ee3f0..47fca4e 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -68,6 +68,8 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m #ifdef WITH_VOICE case RenderType::VoiceChannel: return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width); + case RenderType::VoiceParticipant: + return get_preferred_width_vfunc_voice_participant(widget, minimum_width, natural_width); #endif case RenderType::DMHeader: return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width); @@ -89,6 +91,8 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid #ifdef WITH_VOICE case RenderType::VoiceChannel: return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width); + case RenderType::VoiceParticipant: + return get_preferred_width_for_height_vfunc_voice_participant(widget, height, minimum_width, natural_width); #endif case RenderType::DMHeader: return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width); @@ -110,6 +114,8 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int & #ifdef WITH_VOICE case RenderType::VoiceChannel: return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height); + case RenderType::VoiceParticipant: + return get_preferred_height_vfunc_voice_participant(widget, minimum_height, natural_height); #endif case RenderType::DMHeader: return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height); @@ -131,6 +137,8 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid #ifdef WITH_VOICE case RenderType::VoiceChannel: return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height); + case RenderType::VoiceParticipant: + return get_preferred_height_for_width_vfunc_voice_participant(widget, width, minimum_height, natural_height); #endif case RenderType::DMHeader: return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height); @@ -152,6 +160,8 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr &cr, #ifdef WITH_VOICE case RenderType::VoiceChannel: return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags); + case RenderType::VoiceParticipant: + return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags); #endif case RenderType::DMHeader: return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags); @@ -519,9 +529,10 @@ void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + Gtk::Requisition minimum_size, natural_size; + m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); + + const int text_x = background_area.get_x() + 27; + const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2; + const int text_w = natural_size.width; + const int text_h = natural_size.height; + + Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); + m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#f00"); + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; +} + #endif // dm header diff --git a/src/components/channelscellrenderer.hpp b/src/components/channelscellrenderer.hpp index 8e4025a..e8ba5f5 100644 --- a/src/components/channelscellrenderer.hpp +++ b/src/components/channelscellrenderer.hpp @@ -11,8 +11,10 @@ enum class RenderType : uint8_t { TextChannel, Thread, +// TODO: maybe enable anyways but without ability to join if no voice support #ifdef WITH_VOICE VoiceChannel, + VoiceParticipant, #endif DMHeader, @@ -98,6 +100,17 @@ protected: const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags); + + // voice channel + void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_voice_participant(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_voice_participant(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); #endif // dm header diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 44f64a9..12ce4e8 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -2254,6 +2254,8 @@ void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { } } #endif + + m_signal_gateway_ready_supplemental.emit(); } void DiscordClient::HandleGatewayReconnect(const GatewayMessage &msg) { @@ -2776,6 +2778,10 @@ DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { return m_signal_gateway_ready; } +DiscordClient::type_signal_gateway_ready_supplemental DiscordClient::signal_gateway_ready_supplemental() { + return m_signal_gateway_ready_supplemental; +} + DiscordClient::type_signal_message_create DiscordClient::signal_message_create() { return m_signal_message_create; } diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index a7e3f79..fa399cc 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -379,6 +379,7 @@ private: // signals public: typedef sigc::signal type_signal_gateway_ready; + typedef sigc::signal type_signal_gateway_ready_supplemental; typedef sigc::signal type_signal_message_create; typedef sigc::signal type_signal_message_delete; typedef sigc::signal type_signal_message_update; @@ -446,6 +447,7 @@ public: #endif type_signal_gateway_ready signal_gateway_ready(); + type_signal_gateway_ready_supplemental signal_gateway_ready_supplemental(); type_signal_message_create signal_message_create(); type_signal_message_delete signal_message_delete(); type_signal_message_update signal_message_update(); @@ -512,6 +514,7 @@ public: protected: type_signal_gateway_ready m_signal_gateway_ready; + type_signal_gateway_ready_supplemental m_signal_gateway_ready_supplemental; type_signal_message_create m_signal_message_create; type_signal_message_delete m_signal_message_delete; type_signal_message_update m_signal_message_update; -- cgit v1.2.3 From 182602a71faa33037a7a07dd6e77ed8f7b61b5c6 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 6 Jan 2023 23:11:07 -0500 Subject: remove voice participant row on disconnect --- src/components/channels.cpp | 69 +++++++++++++++++++++++++++++++++------------ src/components/channels.hpp | 6 +++- 2 files changed, 56 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/components/channels.cpp b/src/components/channels.cpp index dc14688..7aae480 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -287,6 +287,11 @@ ChannelList::ChannelList() discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelUnmute)); discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildMute)); discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildUnmute)); + +#if WITH_VOICE + discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserConnect)); + discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserDisconnect)); +#endif } void ChannelList::UsePanedHack(Gtk::Paned &paned) { @@ -337,13 +342,13 @@ void ChannelList::UpdateRemoveGuild(Snowflake id) { } void ChannelList::UpdateRemoveChannel(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); + auto iter = GetIteratorForRowFromID(id); if (!iter) return; m_model->erase(iter); } void ChannelList::UpdateChannel(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); + auto iter = GetIteratorForRowFromID(id); auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!iter || !channel.has_value()) return; if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel); @@ -358,7 +363,7 @@ void ChannelList::UpdateChannel(Snowflake id) { // check if the parent has changed Gtk::TreeModel::iterator new_parent; if (channel->ParentID.has_value()) - new_parent = GetIteratorForChannelFromID(*channel->ParentID); + new_parent = GetIteratorForRowFromID(*channel->ParentID); else if (channel->GuildID.has_value()) new_parent = GetIteratorForGuildFromID(*channel->GuildID); @@ -375,7 +380,7 @@ void ChannelList::UpdateCreateChannel(const ChannelData &channel) { bool orphan; if (channel.ParentID.has_value()) { orphan = false; - auto iter = GetIteratorForChannelFromID(*channel.ParentID); + auto iter = GetIteratorForRowFromID(*channel.ParentID); channel_row = *m_model->append(iter->children()); } else { orphan = true; @@ -417,10 +422,10 @@ void ChannelList::UpdateGuild(Snowflake id) { } void ChannelList::OnThreadJoined(Snowflake id) { - if (GetIteratorForChannelFromID(id)) return; + if (GetIteratorForRowFromID(id)) return; const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!channel.has_value()) return; - const auto parent = GetIteratorForChannelFromID(*channel->ParentID); + const auto parent = GetIteratorForRowFromID(*channel->ParentID); if (parent) CreateThreadRow(parent->children(), *channel); } @@ -435,7 +440,7 @@ void ChannelList::OnThreadDelete(const ThreadDeleteData &data) { // todo probably make the row stick around if its selected until the selection changes void ChannelList::OnThreadUpdate(const ThreadUpdateData &data) { - auto iter = GetIteratorForChannelFromID(data.Thread.ID); + auto iter = GetIteratorForRowFromID(data.Thread.ID); if (iter) (*iter)[m_columns.m_name] = "- " + Glib::Markup::escape_text(*data.Thread.Name); @@ -462,7 +467,7 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { // delete all threads not present in the synced data for (auto thread_id : threads) { if (std::find_if(data.Threads.begin(), data.Threads.end(), [thread_id](const auto &x) { return x.ID == thread_id; }) == data.Threads.end()) { - auto iter = GetIteratorForChannelFromID(thread_id); + auto iter = GetIteratorForRowFromID(thread_id); m_model->erase(iter); } } @@ -470,25 +475,36 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { // delete all archived threads for (auto thread : data.Threads) { if (thread.ThreadMetadata->IsArchived) { - if (auto iter = GetIteratorForChannelFromID(thread.ID)) + if (auto iter = GetIteratorForRowFromID(thread.ID)) m_model->erase(iter); } } } +#ifdef WITH_VOICE +void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { +} + +void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { + if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { + m_model->erase(iter); + } +} +#endif + void ChannelList::DeleteThreadRow(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); + auto iter = GetIteratorForRowFromID(id); if (iter) m_model->erase(iter); } void ChannelList::OnChannelMute(Snowflake id) { - if (auto iter = GetIteratorForChannelFromID(id)) + if (auto iter = GetIteratorForRowFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } void ChannelList::OnChannelUnmute(Snowflake id) { - if (auto iter = GetIteratorForChannelFromID(id)) + if (auto iter = GetIteratorForRowFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } @@ -519,7 +535,7 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { m_temporary_thread_row = {}; } - const auto channel_iter = GetIteratorForChannelFromID(id); + const auto channel_iter = GetIteratorForRowFromID(id); if (channel_iter) { if (expand_to) { m_view.expand_to_path(m_model->get_path(channel_iter)); @@ -529,7 +545,7 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { m_view.get_selection()->unselect_all(); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!channel.has_value() || !channel->IsThread()) return; - auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID); + auto parent_iter = GetIteratorForRowFromID(*channel->ParentID); if (!parent_iter) return; m_temporary_thread_row = CreateThreadRow(parent_iter->children(), *channel); m_view.get_selection()->select(m_temporary_thread_row); @@ -745,7 +761,7 @@ Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildre } void ChannelList::UpdateChannelCategory(const ChannelData &channel) { - auto iter = GetIteratorForChannelFromID(channel.ID); + auto iter = GetIteratorForRowFromID(channel.ID); if (!iter) return; (*iter)[m_columns.m_sort] = *channel.Position; @@ -760,7 +776,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { return {}; } -Gtk::TreeModel::iterator ChannelList::GetIteratorForChannelFromID(Snowflake id) { +Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) { std::queue queue; for (const auto &child : m_model->children()) for (const auto &child2 : child.children()) @@ -777,6 +793,23 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForChannelFromID(Snowflake id) return {}; } +Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) { + std::queue queue; + for (const auto &child : m_model->children()) + for (const auto &child2 : child.children()) + queue.push(child2); + + while (!queue.empty()) { + auto item = queue.front(); + if ((*item)[m_columns.m_type] == type && (*item)[m_columns.m_id] == id) return item; + for (const auto &child : item->children()) + queue.push(child); + queue.pop(); + } + + return {}; +} + bool ChannelList::IsTextChannel(ChannelType type) { return type == ChannelType::GUILD_TEXT || type == ChannelType::GUILD_NEWS; } @@ -895,7 +928,7 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { void ChannelList::OnMessageAck(const MessageAckData &data) { // trick renderer into redrawing m_model->row_changed(Gtk::TreeModel::Path("0"), m_model->get_iter("0")); // 0 is always path for dm header - auto iter = GetIteratorForChannelFromID(data.ChannelID); + auto iter = GetIteratorForRowFromID(data.ChannelID); if (iter) m_model->row_changed(m_model->get_path(iter), iter); auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID); if (channel.has_value() && channel->GuildID.has_value()) { @@ -905,7 +938,7 @@ void ChannelList::OnMessageAck(const MessageAckData &data) { } void ChannelList::OnMessageCreate(const Message &msg) { - auto iter = GetIteratorForChannelFromID(msg.ChannelID); + auto iter = GetIteratorForRowFromID(msg.ChannelID); if (iter) m_model->row_changed(m_model->get_path(iter), iter); // redraw const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID); if (!channel.has_value()) return; diff --git a/src/components/channels.hpp b/src/components/channels.hpp index d1986a0..8d076a3 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -48,6 +48,9 @@ protected: void OnThreadUpdate(const ThreadUpdateData &data); void OnThreadListSync(const ThreadListSyncData &data); + void OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id); + void OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id); + Gtk::TreeView m_view; class ModelColumns : public Gtk::TreeModel::ColumnRecord { @@ -80,7 +83,8 @@ protected: // separation necessary because a channel and guild can share the same id Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id); - Gtk::TreeModel::iterator GetIteratorForChannelFromID(Snowflake id); + Gtk::TreeModel::iterator GetIteratorForRowFromID(Snowflake id); + Gtk::TreeModel::iterator GetIteratorForRowFromIDOfType(Snowflake id, RenderType type); bool IsTextChannel(ChannelType type); -- cgit v1.2.3 From ef37d5a4860329814d53233290438383928215bb Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 6 Jan 2023 23:18:03 -0500 Subject: add voice participant row on connect --- src/components/channels.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'src') diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 7aae480..d177495 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -483,6 +483,19 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { #ifdef WITH_VOICE void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { + auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel); + if (!parent_iter) return; + const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id); + if (!user.has_value()) return; + + auto user_row = *m_model->append(parent_iter->children()); + user_row[m_columns.m_type] = RenderType::VoiceParticipant; + user_row[m_columns.m_id] = user_id; + if (user.has_value()) { + user_row[m_columns.m_name] = user->GetEscapedName(); + } else { + user_row[m_columns.m_name] = "Unknown"; + } } void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { -- cgit v1.2.3 From 78adcf2f5c9858d241dcf6f6e48f2f64bbc63f4e Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sat, 7 Jan 2023 20:13:57 -0500 Subject: fix voice_user_connect signal --- src/discord/discord.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 12ce4e8..2c7358b 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -2200,8 +2200,9 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { SetVoiceState(data.UserID, *data.ChannelID); if (old_state.has_value() && *old_state != *data.ChannelID) { m_signal_voice_user_disconnect.emit(data.UserID, *old_state); + } else if (!old_state.has_value()) { + m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } - m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } else { const auto old_state = GetVoiceState(data.UserID); ClearVoiceState(data.UserID); -- cgit v1.2.3 From 5e85d16cd61a9f28bd046d4c7921be8f6cc2b2fc Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sat, 7 Jan 2023 20:14:16 -0500 Subject: dont disconnect on voice window close, add ability to reopen --- src/abaddon.cpp | 22 +++++++++++++--------- src/abaddon.hpp | 2 ++ src/components/voiceinfobox.cpp | 12 +++++++++++- src/components/voiceinfobox.hpp | 1 + 4 files changed, 27 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 8b760fd..5ed17be 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -432,6 +432,19 @@ 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() { + if (m_voice_window != nullptr) return; auto *wnd = new VoiceWindow(m_discord.GetVoiceChannelID()); m_voice_window = wnd; @@ -470,21 +483,12 @@ void Abaddon::OnVoiceConnected() { wnd->show(); wnd->signal_hide().connect([this, wnd]() { - m_discord.DisconnectFromVoice(); m_voice_window = nullptr; delete wnd; delete m_user_menu; SetupUserMenu(); }); } - -void Abaddon::OnVoiceDisconnected() { - m_audio->StopCaptureDevice(); - m_audio->RemoveAllSSRCs(); - if (m_voice_window != nullptr) { - m_voice_window->close(); - } -} #endif SettingsManager::Settings &Abaddon::GetSettings() { diff --git a/src/abaddon.hpp b/src/abaddon.hpp index f45f435..5540c5a 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -92,6 +92,8 @@ public: #ifdef WITH_VOICE void OnVoiceConnected(); void OnVoiceDisconnected(); + + void ShowVoiceWindow(); #endif SettingsManager::Settings &GetSettings(); diff --git a/src/components/voiceinfobox.cpp b/src/components/voiceinfobox.cpp index f4dc6ed..456c5f0 100644 --- a/src/components/voiceinfobox.cpp +++ b/src/components/voiceinfobox.cpp @@ -70,6 +70,15 @@ VoiceInfoBox::VoiceInfoBox() m_status.set_label(label); }); + AddPointerCursor(m_status_ev); + m_status_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { + if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) { + Abaddon::Get().ShowVoiceWindow(); + return true; + } + return false; + }); + m_status.set_ellipsize(Pango::ELLIPSIZE_END); m_location.set_ellipsize(Pango::ELLIPSIZE_END); @@ -79,7 +88,8 @@ VoiceInfoBox::VoiceInfoBox() m_disconnect_img.set_hexpand(true); m_disconnect_img.set_halign(Gtk::ALIGN_END); - m_left.add(m_status); + m_status_ev.add(m_status); + m_left.add(m_status_ev); m_left.add(m_location); add(m_left); diff --git a/src/components/voiceinfobox.hpp b/src/components/voiceinfobox.hpp index e0bc4e8..0117c0d 100644 --- a/src/components/voiceinfobox.hpp +++ b/src/components/voiceinfobox.hpp @@ -11,6 +11,7 @@ public: private: Gtk::Box m_left; + Gtk::EventBox m_status_ev; Gtk::Label m_status; Gtk::Label m_location; -- cgit v1.2.3 From ea04035f0db8fa990dd7ca8dd1a64f56bceb82e2 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 16 Jan 2023 22:52:39 -0500 Subject: persist voice settings, handle volume w/ no ssrc --- src/abaddon.cpp | 5 ++--- src/audio/manager.cpp | 23 +++++++++++++++++++---- src/audio/manager.hpp | 3 +++ src/discord/discord.cpp | 4 ++++ src/discord/discord.hpp | 2 ++ src/discord/voiceclient.cpp | 25 +++++++++++++++++++++++++ src/discord/voiceclient.hpp | 4 ++++ src/windows/voicewindow.cpp | 21 +++++++++++++++------ 8 files changed, 74 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 5ed17be..417d7af 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -474,9 +474,8 @@ void Abaddon::ShowVoiceWindow() { }); wnd->signal_user_volume_changed().connect([this](Snowflake id, double volume) { - if (const auto ssrc = m_discord.GetSSRCOfUser(id); ssrc.has_value()) { - m_audio->SetVolumeSSRC(*ssrc, volume); - } + auto &vc = m_discord.GetVoiceClient(); + vc.SetUserVolume(id, volume); }); wnd->set_position(Gtk::WIN_POS_CENTER); diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 1a7bda5..618773d 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -278,13 +278,21 @@ void AudioManager::SetPlayback(bool playback) { } void AudioManager::SetCaptureGate(double gate) { - m_capture_gate = gate * 0.01; + 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) { @@ -296,9 +304,15 @@ void AudioManager::SetMuteSSRC(uint32_t ssrc, bool mute) { void AudioManager::SetVolumeSSRC(uint32_t ssrc, double volume) { std::lock_guard _(m_mutex); - volume *= 0.01; - constexpr const double E = 2.71828182845904523536; - m_volume_ssrc[ssrc] = (std::exp(volume) - 1) / (E - 1); + 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) { @@ -463,4 +477,5 @@ AudioDevices &AudioManager::GetDevices() { AudioManager::type_signal_opus_packet AudioManager::signal_opus_packet() { return m_signal_opus_packet; } + #endif diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index dbb0b6e..9cd7f42 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -40,9 +40,12 @@ public: void SetCaptureGate(double gate); void SetCaptureGain(double gain); + [[nodiscard]] double GetCaptureGate() const noexcept; + [[nodiscard]] double GetCaptureGain() const noexcept; void SetMuteSSRC(uint32_t ssrc, bool mute); void SetVolumeSSRC(uint32_t ssrc, double volume); + [[nodiscard]] double GetVolumeSSRC(uint32_t ssrc) const; void SetEncodingApplication(int application); [[nodiscard]] int GetEncodingApplication(); diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 2c7358b..45e8919 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1226,6 +1226,10 @@ std::optional DiscordClient::GetVoiceState(Snowflake user_id) const { return std::nullopt; } +DiscordVoiceClient &DiscordClient::GetVoiceClient() { + return m_voice; +} + void DiscordClient::SetVoiceMuted(bool is_mute) { m_mute_requested = is_mute; SendVoiceStateUpdate(); diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index fa399cc..10b1d30 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -191,6 +191,8 @@ public: [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; [[nodiscard]] std::optional GetVoiceState(Snowflake user_id) const; + DiscordVoiceClient &GetVoiceClient(); + void SetVoiceMuted(bool is_mute); void SetVoiceDeafened(bool is_deaf); #endif diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index ed83e3c..3d246b1 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -167,6 +167,7 @@ DiscordVoiceClient::~DiscordVoiceClient() { void DiscordVoiceClient::Start() { SetState(State::ConnectingToWebsocket); + m_ssrc_map.clear(); m_heartbeat_waiter.revive(); m_keepalive_waiter.revive(); m_ws.StartConnection("wss://" + m_endpoint + "/?v=7"); @@ -188,6 +189,8 @@ void DiscordVoiceClient::Stop() { m_keepalive_waiter.kill(); if (m_keepalive_thread.joinable()) m_keepalive_thread.join(); + m_ssrc_map.clear(); + m_signal_disconnected.emit(); } @@ -211,6 +214,20 @@ void DiscordVoiceClient::SetUserID(Snowflake id) { m_user_id = 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 { + if (const auto it = m_user_volumes.find(id); it != m_user_volumes.end()) { + return it->second; + } + return 1.0f; +} + std::optional DiscordVoiceClient::GetSSRCOfUser(Snowflake id) const { if (const auto it = m_ssrc_map.find(id); it != m_ssrc_map.end()) { return it->second; @@ -315,6 +332,14 @@ void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessa void DiscordVoiceClient::HandleGatewaySpeaking(const VoiceGatewayMessage &m) { VoiceSpeakingData d = m.Data; + + // 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); + } + } + m_ssrc_map[d.UserID] = d.SSRC; m_signal_speaking.emit(d); } diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 916d070..7bf4295 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -195,6 +195,9 @@ public: void SetServerID(Snowflake id); void SetUserID(Snowflake id); + // todo serialize + void SetUserVolume(Snowflake id, float volume); + [[nodiscard]] float GetUserVolume(Snowflake id) const; [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; // Is a websocket and udp connection fully established @@ -241,6 +244,7 @@ private: Snowflake m_user_id; std::unordered_map m_ssrc_map; + std::unordered_map m_user_volumes; std::array m_secret_key; diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index 7c90e96..c82a0aa 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -24,7 +24,7 @@ public: m_volume.set_value_pos(Gtk::POS_LEFT); m_volume.set_value(100.0); m_volume.signal_value_changed().connect([this]() { - m_signal_volume.emit(m_volume.get_value()); + m_signal_volume.emit(m_volume.get_value() * 0.01); }); m_horz.add(m_avatar); @@ -54,6 +54,10 @@ public: m_meter.SetVolume(frac); } + void RestoreGain(double frac) { + m_volume.set_value(frac * 100.0); + } + private: Gtk::Box m_main; Gtk::Box m_horz; @@ -92,6 +96,8 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) set_default_size(300, 300); auto &discord = Abaddon::Get().GetDiscordClient(); + auto &audio = Abaddon::Get().GetAudio(); + SetUsers(discord.GetUsersInVoiceChannel(m_channel_id)); discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserDisconnect)); @@ -108,17 +114,16 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_capture_gate.set_range(0.0, 100.0); m_capture_gate.set_value_pos(Gtk::POS_LEFT); - m_capture_gate.set_value(0.0); + m_capture_gate.set_value(audio.GetCaptureGate() * 100.0); m_capture_gate.signal_value_changed().connect([this]() { - // todo this should probably emit 0-1 i dont think the mgr should be responsible for scaling down - const double val = m_capture_gate.get_value(); + const double val = m_capture_gate.get_value() * 0.01; m_signal_gate.emit(val); - m_capture_volume.SetTick(val / 100.0); + m_capture_volume.SetTick(val); }); m_capture_gain.set_range(0.0, 200.0); m_capture_gain.set_value_pos(Gtk::POS_LEFT); - m_capture_gain.set_value(100.0); + m_capture_gain.set_value(audio.GetCaptureGain() * 100.0); m_capture_gain.signal_value_changed().connect([this]() { const double val = m_capture_gain.get_value(); m_signal_gain.emit(val / 100.0); @@ -174,7 +179,9 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) } void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { + const auto me = Abaddon::Get().GetDiscordClient().GetUserData().ID; for (auto id : user_ids) { + if (id == me) continue; m_user_list.add(*CreateRow(id)); } } @@ -182,6 +189,8 @@ void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) { auto *row = Gtk::make_managed(id); m_rows[id] = row; + auto &vc = Abaddon::Get().GetDiscordClient().GetVoiceClient(); + row->RestoreGain(vc.GetUserVolume(id)); row->signal_mute_cs().connect([this, id](bool is_muted) { m_signal_mute_user_cs.emit(id, is_muted); }); -- cgit v1.2.3 From 345a84e654fa539b3d33811e068c6273cda81ffe Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 14 Feb 2023 19:54:07 -0500 Subject: fix build --- src/discord/voiceclient.cpp | 2 +- src/windows/mainwindow.hpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index 3d246b1..c58d4b8 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -18,7 +18,7 @@ // clang-format on UDPSocket::UDPSocket() - : m_socket(INVALID_SOCKET) { + : m_socket(-1) { } UDPSocket::~UDPSocket() { diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp index 90e49b4..5cad82f 100644 --- a/src/windows/mainwindow.hpp +++ b/src/windows/mainwindow.hpp @@ -3,11 +3,11 @@ #include "components/chatwindow.hpp" #include "components/memberlist.hpp" #include "components/friendslist.hpp" -<<<<<<< HEAD #include "components/voiceinfobox.hpp" -#include -======= ->>>>>>> master +#include +#include +#include +#include class MainWindow : public Gtk::Window { public: -- cgit v1.2.3 From c075f16c17d8048446a994a41b0ebf46a0c00bdd Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 14 Mar 2023 13:24:02 -0400 Subject: try to fix build --- .github/workflows/ci.yml | 2 +- src/components/channels.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae78868..953f2c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: cmake -GNinja -Bbuild -DCMAKE_BUILD_TYPE=${{ matrix.buildtype }} cmake --build build - - name: Build (2) }} + - name: Build (2) run: ${{ steps.build.outputs.value }} - name: Setup Artifact diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 0da3de9..e45b5c0 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -763,6 +763,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk m_tmp_row_map[thread.ID] = CreateThreadRow(row.children(), thread); }; +#ifdef WITH_VOICE auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) { for (auto user_id : discord.GetUsersInVoiceChannel(channel.ID)) { const auto user = discord.GetUser(user_id); @@ -777,6 +778,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk } } }; +#endif for (const auto &channel : orphan_channels) { auto channel_row = *m_model->append(guild_row.children()); -- cgit v1.2.3 From e51d5d1980b1ab0c1738836762900f7bd9bc3c3f Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 14 Mar 2023 14:18:54 -0400 Subject: more WITH_VOICE guards --- src/components/voiceinfobox.cpp | 10 +++++++--- src/components/voiceinfobox.hpp | 12 ++++++++---- src/windows/mainwindow.cpp | 4 ++++ src/windows/mainwindow.hpp | 3 +++ 4 files changed, 22 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/components/voiceinfobox.cpp b/src/components/voiceinfobox.cpp index 456c5f0..b870794 100644 --- a/src/components/voiceinfobox.cpp +++ b/src/components/voiceinfobox.cpp @@ -1,6 +1,8 @@ -#include "voiceinfobox.hpp" -#include "abaddon.hpp" -#include "util.hpp" +#ifdef WITH_VOICE + + #include "voiceinfobox.hpp" + #include "abaddon.hpp" + #include "util.hpp" VoiceInfoBox::VoiceInfoBox() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) @@ -97,3 +99,5 @@ VoiceInfoBox::VoiceInfoBox() show_all_children(); } + +#endif diff --git a/src/components/voiceinfobox.hpp b/src/components/voiceinfobox.hpp index 0117c0d..74aad27 100644 --- a/src/components/voiceinfobox.hpp +++ b/src/components/voiceinfobox.hpp @@ -1,9 +1,11 @@ #pragma once -#include -#include -#include -#include +#ifdef WITH_VOICE + + #include + #include + #include + #include class VoiceInfoBox : public Gtk::Box { public: @@ -18,3 +20,5 @@ private: Gtk::EventBox m_disconnect_ev; Gtk::Image m_disconnect_img; }; + +#endif diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp index 73b78de..f867db9 100644 --- a/src/windows/mainwindow.cpp +++ b/src/windows/mainwindow.cpp @@ -51,10 +51,14 @@ MainWindow::MainWindow() m_content_stack.set_visible_child("chat"); m_content_stack.show(); +#ifdef WITH_VOICE m_voice_info.show(); +#endif m_left_pane.add(m_channel_list); +#ifdef WITH_VOICE m_left_pane.add(m_voice_info); +#endif m_left_pane.show(); m_chan_content_paned.pack1(m_left_pane); diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp index 5cad82f..e075c10 100644 --- a/src/windows/mainwindow.hpp +++ b/src/windows/mainwindow.hpp @@ -57,7 +57,10 @@ private: ChatWindow m_chat; MemberList m_members; FriendsList m_friends; + +#ifdef WITH_VOICE VoiceInfoBox m_voice_info; +#endif Gtk::Box m_left_pane; -- cgit v1.2.3 From d30be3326d43aa57760fdcbc6b4021b3570dc4a3 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sat, 29 Apr 2023 21:27:16 -0400 Subject: make standalone file for miniaudio implementation --- src/audio/ma_impl.cpp | 2 ++ src/audio/manager.cpp | 1 - src/notifications/notifier_gio.cpp | 3 --- 3 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 src/audio/ma_impl.cpp (limited to 'src') diff --git a/src/audio/ma_impl.cpp b/src/audio/ma_impl.cpp new file mode 100644 index 0000000..662e2b2 --- /dev/null +++ b/src/audio/ma_impl.cpp @@ -0,0 +1,2 @@ +#define MINIAUDIO_IMPLEMENTATION +#include diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 618773d..6e85ed8 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -9,7 +9,6 @@ #include #include #include -#define MINIAUDIO_IMPLEMENTATION #include #include #include diff --git a/src/notifications/notifier_gio.cpp b/src/notifications/notifier_gio.cpp index 22d4c87..128167f 100644 --- a/src/notifications/notifier_gio.cpp +++ b/src/notifications/notifier_gio.cpp @@ -1,9 +1,6 @@ #include "notifier.hpp" #include -#define MINIAUDIO_IMPLEMENTATION -#include - Notifier::Notifier() { #ifdef ENABLE_NOTIFICATION_SOUNDS if (ma_engine_init(nullptr, &m_engine) != MA_SUCCESS) { -- cgit v1.2.3 From 9fb5935d081e0dcac8b483d95344207e216cc998 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sat, 29 Apr 2023 21:41:26 -0400 Subject: include guard miniaudio --- src/audio/ma_impl.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/audio/ma_impl.cpp b/src/audio/ma_impl.cpp index 662e2b2..a83ddaf 100644 --- a/src/audio/ma_impl.cpp +++ b/src/audio/ma_impl.cpp @@ -1,2 +1,4 @@ -#define MINIAUDIO_IMPLEMENTATION -#include +#ifdef WITH_MINIAUDIO + #define MINIAUDIO_IMPLEMENTATION + #include +#endif -- cgit v1.2.3 From 75158f2c295bdc52ab4743b3529ce9092ccfd502 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 8 May 2023 00:28:59 -0400 Subject: handle channel moves + region change --- src/components/voiceinfobox.cpp | 39 +++++++++++++++++++++++------------- src/components/voiceinfobox.hpp | 2 ++ src/discord/discord.cpp | 44 +++++++++++++++++++++++++++++++++++++++++ src/discord/discord.hpp | 3 +++ src/discord/voiceclient.cpp | 4 ++++ src/discord/websocket.cpp | 7 ++++++- 6 files changed, 84 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/components/voiceinfobox.cpp b/src/components/voiceinfobox.cpp index b870794..4c5a127 100644 --- a/src/components/voiceinfobox.cpp +++ b/src/components/voiceinfobox.cpp @@ -29,20 +29,7 @@ VoiceInfoBox::VoiceInfoBox() Abaddon::Get().GetDiscordClient().signal_voice_requested_connect().connect([this](Snowflake channel_id) { show(); - - if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(channel_id); channel.has_value() && channel->Name.has_value()) { - if (channel->GuildID.has_value()) { - if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*channel->GuildID); guild.has_value()) { - m_location.set_label(*channel->Name + " / " + guild->Name); - return; - } - } - - m_location.set_label(*channel->Name); - return; - } - - m_location.set_label("Unknown"); + UpdateLocation(); }); Abaddon::Get().GetDiscordClient().signal_voice_requested_disconnect().connect([this]() { @@ -72,6 +59,10 @@ VoiceInfoBox::VoiceInfoBox() m_status.set_label(label); }); + Abaddon::Get().GetDiscordClient().signal_voice_channel_changed().connect([this](Snowflake channel_id) { + UpdateLocation(); + }); + AddPointerCursor(m_status_ev); m_status_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) { @@ -100,4 +91,24 @@ VoiceInfoBox::VoiceInfoBox() show_all_children(); } +void VoiceInfoBox::UpdateLocation() { + auto &discord = Abaddon::Get().GetDiscordClient(); + + const auto channel_id = discord.GetVoiceChannelID(); + + if (const auto channel = discord.GetChannel(channel_id); channel.has_value() && channel->Name.has_value()) { + if (channel->GuildID.has_value()) { + if (const auto guild = discord.GetGuild(*channel->GuildID); guild.has_value()) { + m_location.set_label(*channel->Name + " / " + guild->Name); + return; + } + } + + m_location.set_label(*channel->Name); + return; + } + + m_location.set_label("Unknown"); +} + #endif diff --git a/src/components/voiceinfobox.hpp b/src/components/voiceinfobox.hpp index 74aad27..9988c63 100644 --- a/src/components/voiceinfobox.hpp +++ b/src/components/voiceinfobox.hpp @@ -12,6 +12,8 @@ public: VoiceInfoBox(); private: + void UpdateLocation(); + Gtk::Box m_left; Gtk::EventBox m_status_ev; Gtk::Label m_status; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 64988d2..675b640 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -2207,7 +2207,42 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) { } #ifdef WITH_VOICE + +/* + * When you connect to a voice channel: + * C->S: VOICE_STATE_UPDATE + * S->C: VOICE_STATE_UPDATE + * S->C: VOICE_SERVER_UPDATE + * + * A new websocket is opened to the ws specified by VOICE_SERVER_UPDATE then: + * S->C: HELLO + * C->S: IDENTIFY + * S->C: READY + * C->U: discover + * U->C: discover result + * C->S: SELECT_PROTOCOL + * S->C: SESSION_DESCRIPTION + * Done!!! + * + * When you get disconnected (no particular order): + * S->C: 4014 Disconnected (Server to voice gateway) + * S->C: VOICE_STATE_UPDATE (Server to main gateway) + * + * When you get moved: + * S->C: VOICE_STATE_UPDATE + * S->C: VOICE_SERVER_UPDATE (usually) + * S->C: 4014 Disconnected (Server to voice gateway) + * + * Key thing: 4014 Disconnected can come before or after or in between main gateway messages + * + * Region change: + * Same thing but close code 4000 + * + */ + void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { + spdlog::get("discord")->trace("VOICE_STATE_UPDATE"); + VoiceState data = msg.Data; if (data.UserID == m_user_data.ID) { @@ -2217,6 +2252,9 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { // channel_id = null means disconnect. stop cuz out of order maybe if (!data.ChannelID.has_value() && (m_voice.IsConnected() || m_voice.IsConnecting())) { m_voice.Stop(); + } else if (data.ChannelID.has_value()) { + m_voice_channel_id = *data.ChannelID; + m_signal_voice_channel_changed.emit(m_voice_channel_id); } } else { if (data.GuildID.has_value() && data.Member.has_value()) { @@ -2245,6 +2283,8 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { } void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) { + spdlog::get("discord")->trace("VOICE_SERVER_UPDATE"); + VoiceServerUpdateData data = msg.Data; spdlog::get("discord")->debug("Voice server endpoint: {}", data.Endpoint); spdlog::get("discord")->debug("Voice token: {}", data.Token); @@ -3055,4 +3095,8 @@ DiscordClient::type_signal_voice_requested_disconnect DiscordClient::signal_voic DiscordClient::type_signal_voice_client_state_update DiscordClient::signal_voice_client_state_update() { return m_signal_voice_client_state_update; } + +DiscordClient::type_signal_voice_channel_changed DiscordClient::signal_voice_channel_changed() { + return m_signal_voice_channel_changed; +} #endif diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index aadb72d..cb76ef6 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -454,6 +454,7 @@ public: using type_signal_voice_requested_connect = sigc::signal; using type_signal_voice_requested_disconnect = sigc::signal; using type_signal_voice_client_state_update = sigc::signal; + using type_signal_voice_channel_changed = sigc::signal; #endif type_signal_gateway_ready signal_gateway_ready(); @@ -520,6 +521,7 @@ public: type_signal_voice_requested_connect signal_voice_requested_connect(); type_signal_voice_requested_disconnect signal_voice_requested_disconnect(); type_signal_voice_client_state_update signal_voice_client_state_update(); + type_signal_voice_channel_changed signal_voice_channel_changed(); #endif protected: @@ -587,5 +589,6 @@ protected: type_signal_voice_requested_connect m_signal_voice_requested_connect; type_signal_voice_requested_disconnect m_signal_voice_requested_disconnect; type_signal_voice_client_state_update m_signal_voice_client_state_update; + type_signal_voice_channel_changed m_signal_voice_channel_changed; #endif }; diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index c58d4b8..c37ba7b 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -166,6 +166,10 @@ DiscordVoiceClient::~DiscordVoiceClient() { } void DiscordVoiceClient::Start() { + if (IsConnected() || IsConnecting()) { + Stop(); + } + SetState(State::ConnectingToWebsocket); m_ssrc_map.clear(); m_heartbeat_waiter.revive(); diff --git a/src/discord/websocket.cpp b/src/discord/websocket.cpp index d40d057..f886e69 100644 --- a/src/discord/websocket.cpp +++ b/src/discord/websocket.cpp @@ -49,7 +49,12 @@ void Websocket::Stop() { void Websocket::Stop(uint16_t code) { m_log->debug("Stopping with close code {}", code); - m_websocket-> stop(code); + m_websocket->stop(code); + m_log->trace("Socket::stop complete"); + while (Gtk::Main::events_pending()) { + Gtk::Main::iteration(); + } + m_log->trace("No events pending"); } void Websocket::Send(const std::string &str) { -- cgit v1.2.3 From f6dd890775f29f3031747b9d076359f219d492b4 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 11 May 2023 20:31:34 -0400 Subject: emit user voice connect more consistently (?) --- src/discord/discord.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 675b640..bcbf048 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -2270,9 +2270,8 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { SetVoiceState(data.UserID, *data.ChannelID); if (old_state.has_value() && *old_state != *data.ChannelID) { m_signal_voice_user_disconnect.emit(data.UserID, *old_state); - } else if (!old_state.has_value()) { - m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } + m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } else { const auto old_state = GetVoiceState(data.UserID); ClearVoiceState(data.UserID); @@ -2781,11 +2780,13 @@ void DiscordClient::SendVoiceStateUpdate() { } void DiscordClient::SetVoiceState(Snowflake user_id, Snowflake channel_id) { + spdlog::get("discord")->debug("SetVoiceState: {} -> {}", user_id, channel_id); m_voice_state_user_channel[user_id] = channel_id; m_voice_state_channel_users[channel_id].insert(user_id); } void DiscordClient::ClearVoiceState(Snowflake user_id) { + spdlog::get("discord")->debug("ClearVoiceState: {}", user_id); if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) { m_voice_state_channel_users[it->second].erase(user_id); // invalidated -- cgit v1.2.3 From 8890f5a07111283f9eb0407890cd367a4127fa7b Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 11 May 2023 20:39:18 -0400 Subject: fix voice channel display name in infobox --- src/components/voiceinfobox.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/components/voiceinfobox.cpp b/src/components/voiceinfobox.cpp index 4c5a127..26e3720 100644 --- a/src/components/voiceinfobox.cpp +++ b/src/components/voiceinfobox.cpp @@ -96,15 +96,15 @@ void VoiceInfoBox::UpdateLocation() { const auto channel_id = discord.GetVoiceChannelID(); - if (const auto channel = discord.GetChannel(channel_id); channel.has_value() && channel->Name.has_value()) { - if (channel->GuildID.has_value()) { + if (const auto channel = discord.GetChannel(channel_id); channel.has_value()) { + if (channel->Name.has_value() && channel->GuildID.has_value()) { if (const auto guild = discord.GetGuild(*channel->GuildID); guild.has_value()) { m_location.set_label(*channel->Name + " / " + guild->Name); return; } } - m_location.set_label(*channel->Name); + m_location.set_label(channel->GetDisplayName()); return; } -- cgit v1.2.3 From cf121917ec8e9c7c5ddb89817b6f530ae426594b Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 11 May 2023 20:46:17 -0400 Subject: center align status in voice infobox --- src/components/voiceinfobox.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/components/voiceinfobox.cpp b/src/components/voiceinfobox.cpp index 26e3720..cecaa7d 100644 --- a/src/components/voiceinfobox.cpp +++ b/src/components/voiceinfobox.cpp @@ -78,9 +78,10 @@ VoiceInfoBox::VoiceInfoBox() m_disconnect_ev.add(m_disconnect_img); m_disconnect_img.property_icon_name() = "call-stop-symbolic"; m_disconnect_img.property_icon_size() = 5; - m_disconnect_img.set_hexpand(true); m_disconnect_img.set_halign(Gtk::ALIGN_END); + m_status_ev.set_hexpand(true); + m_status_ev.add(m_status); m_left.add(m_status_ev); m_left.add(m_location); -- cgit v1.2.3 From 5d3e5eec5e32c49247a0a1fbae1817a4b183f6cd Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 12 May 2023 20:28:44 -0400 Subject: try to fix voice connect signal again --- src/discord/discord.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index bcbf048..3c311ed 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -2270,8 +2270,10 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { SetVoiceState(data.UserID, *data.ChannelID); if (old_state.has_value() && *old_state != *data.ChannelID) { m_signal_voice_user_disconnect.emit(data.UserID, *old_state); + m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); + } else if (!old_state.has_value()) { + m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } - m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } else { const auto old_state = GetVoiceState(data.UserID); ClearVoiceState(data.UserID); -- cgit v1.2.3 From 49f6cd021beb70a2f9e8c359275af69922bffb83 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sun, 14 May 2023 21:12:59 -0400 Subject: fix duplicate guild appearances --- src/discord/discord.cpp | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src') diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 3c311ed..40c900b 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -2457,6 +2457,13 @@ void DiscordClient::HandleGatewayGuildMemberListUpdate(const GatewayMessage &msg void DiscordClient::HandleGatewayGuildCreate(const GatewayMessage &msg) { GuildData data = msg.Data; + + // TODO: figure out why this is even happening... maybe? + // ignore guild if already stored + if (m_store.GetGuild(data.ID).has_value()) { + return; + } + ProcessNewGuild(data); m_signal_guild_create.emit(data); -- cgit v1.2.3 From d0fa9cc289d8398a27ddf2a66783b5d460e2210a Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sun, 14 May 2023 23:55:38 -0400 Subject: make voice channel list UI look better --- src/components/channels.cpp | 61 +++++++++++------------- src/components/channels.hpp | 2 + src/components/channelscellrenderer.cpp | 84 ++++++++++++++++++++++++++++----- 3 files changed, 103 insertions(+), 44 deletions(-) (limited to 'src') diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 9cb8b09..3327b02 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -42,11 +42,7 @@ ChannelList::ChannelList() const auto type = row[m_columns.m_type]; // text channels should not be allowed to be collapsed // maybe they should be but it seems a little difficult to handle expansion to permit this -#ifdef WITH_VOICE - if (type != RenderType::TextChannel && type != RenderType::VoiceChannel) { -#else if (type != RenderType::TextChannel) { -#endif if (row[m_columns.m_expanded]) { m_view.collapse_row(path); row[m_columns.m_expanded] = false; @@ -56,11 +52,7 @@ ChannelList::ChannelList() } } -#ifdef WITH_VOICE - if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel) { -#else if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) { -#endif const auto id = static_cast(row[m_columns.m_id]); m_signal_action_channel_item_select.emit(id); Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {}); @@ -537,14 +529,7 @@ void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id); if (!user.has_value()) return; - auto user_row = *m_model->append(parent_iter->children()); - user_row[m_columns.m_type] = RenderType::VoiceParticipant; - user_row[m_columns.m_id] = user_id; - if (user.has_value()) { - user_row[m_columns.m_name] = user->GetEscapedName(); - } else { - user_row[m_columns.m_name] = "Unknown"; - } + CreateVoiceParticipantRow(*user, parent_iter->children()); } void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { @@ -776,15 +761,8 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk #ifdef WITH_VOICE auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) { for (auto user_id : discord.GetUsersInVoiceChannel(channel.ID)) { - const auto user = discord.GetUser(user_id); - - auto user_row = *m_model->append(root); - user_row[m_columns.m_type] = RenderType::VoiceParticipant; - user_row[m_columns.m_id] = user_id; - if (user.has_value()) { - user_row[m_columns.m_name] = user->GetEscapedName(); - } else { - user_row[m_columns.m_name] = "Unknown"; + if (const auto user = discord.GetUser(user_id); user.has_value()) { + CreateVoiceParticipantRow(*user, root); } } }; @@ -792,16 +770,18 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk for (const auto &channel : orphan_channels) { auto channel_row = *m_model->append(guild_row.children()); - if (IsTextChannel(channel.Type)) + if (IsTextChannel(channel.Type)) { channel_row[m_columns.m_type] = RenderType::TextChannel; + channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); + } #ifdef WITH_VOICE else { channel_row[m_columns.m_type] = RenderType::VoiceChannel; + channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); add_voice_participants(channel, channel_row->children()); } #endif channel_row[m_columns.m_id] = channel.ID; - channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; channel_row[m_columns.m_nsfw] = channel.NSFW(); add_threads(channel, channel_row); @@ -822,16 +802,18 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk for (const auto &channel : channels) { auto channel_row = *m_model->append(cat_row.children()); - if (IsTextChannel(channel.Type)) + if (IsTextChannel(channel.Type)) { channel_row[m_columns.m_type] = RenderType::TextChannel; + channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); + } #ifdef WITH_VOICE else { channel_row[m_columns.m_type] = RenderType::VoiceChannel; + channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); add_voice_participants(channel, channel_row->children()); } #endif channel_row[m_columns.m_id] = channel.ID; - channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_sort] = *channel.Position; channel_row[m_columns.m_nsfw] = channel.NSFW(); add_threads(channel, channel_row); @@ -868,6 +850,23 @@ Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildre return thread_iter; } +Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent) { + auto row = *m_model->append(parent); + row[m_columns.m_type] = RenderType::VoiceParticipant; + row[m_columns.m_id] = user.ID; + row[m_columns.m_name] = user.GetEscapedName(); + + auto &img = Abaddon::Get().GetImageManager(); + row[m_columns.m_icon] = img.GetPlaceholder(VoiceParticipantIconSize); + const auto cb = [this, user_id = user.ID](const Glib::RefPtr &pb) { + auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant); + if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(VoiceParticipantIconSize, VoiceParticipantIconSize, Gdk::INTERP_BILINEAR); + }; + img.LoadFromURL(user.GetAvatarURL("png", "32"), sigc::track_obj(cb, *this)); + + return row; +} + void ChannelList::UpdateChannelCategory(const ChannelData &channel) { auto iter = GetIteratorForRowFromID(channel.ID); if (!iter) return; @@ -971,11 +970,7 @@ bool ChannelList::SelectionFunc(const Glib::RefPtr &model, const m_last_selected = m_model->get_path(row); auto type = (*m_model->get_iter(path))[m_columns.m_type]; -#ifdef WITH_VOICE - return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel; -#else return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread; -#endif } void ChannelList::AddPrivateChannels() { diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 36c7766..8aee733 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -16,6 +16,7 @@ constexpr static int GuildIconSize = 24; constexpr static int DMIconSize = 20; +constexpr static int VoiceParticipantIconSize = 18; constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list class ChannelList : public Gtk::ScrolledWindow { @@ -84,6 +85,7 @@ protected: Gtk::TreeModel::iterator AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root); Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel); Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel); + Gtk::TreeModel::iterator CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent); void UpdateChannelCategory(const ChannelData &channel); diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index fb82682..ab3113c 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -626,18 +626,58 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc_voice_channel(Gt } void CellRendererChannels::render_vfunc_voice_channel(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + // channel name text Gtk::Requisition minimum_size, natural_size; m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); - const int text_x = background_area.get_x() + 21; + const int text_x = background_area.get_x() + 35; const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2; const int text_w = natural_size.width; const int text_h = natural_size.height; Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); - m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#0f0"); m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - m_renderer_text.property_foreground_set() = false; + + // speaker character + Pango::FontDescription font; + font.set_family("sans 14"); + + auto layout = widget.create_pango_layout("\U0001F50A"); + layout->set_font_description(font); + layout->set_alignment(Pango::ALIGN_LEFT); + cr->set_source_rgba(1.0, 1.0, 1.0, 1.0); + int width, height; + layout->get_pixel_size(width, height); + cr->move_to( + background_area.get_x() + 1, + cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0); + layout->show_in_cairo_context(cr); + + // expander + constexpr static int len = 5; + constexpr static int offset = 24; + int x1, y1, x2, y2, x3, y3; + if (property_expanded()) { + x1 = background_area.get_x() + offset; + y1 = background_area.get_y() + background_area.get_height() / 2 - len; + x2 = background_area.get_x() + offset + len; + y2 = background_area.get_y() + background_area.get_height() / 2 + len; + x3 = background_area.get_x() + offset + len * 2; + y3 = background_area.get_y() + background_area.get_height() / 2 - len; + } else { + x1 = background_area.get_x() + offset; + y1 = background_area.get_y() + background_area.get_height() / 2 - len; + x2 = background_area.get_x() + offset + len * 2; + y2 = background_area.get_y() + background_area.get_height() / 2; + x3 = background_area.get_x() + offset; + y3 = background_area.get_y() + background_area.get_height() / 2 + len; + } + cr->move_to(x1, y1); + cr->line_to(x2, y2); + cr->line_to(x3, y3); + const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor); + cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue()); + cr->stroke(); } // voice participant @@ -659,18 +699,40 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc_voice_participan } void CellRendererChannels::render_vfunc_voice_participant(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - Gtk::Requisition minimum_size, natural_size; - m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); + Gtk::Requisition text_minimum, text_natural; + m_renderer_text.get_preferred_size(widget, text_minimum, text_natural); - const int text_x = background_area.get_x() + 27; - const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2; - const int text_w = natural_size.width; - const int text_h = natural_size.height; + Gtk::Requisition minimum, natural; + get_preferred_size(widget, minimum, natural); + + int pixbuf_w = 0; + int pixbuf_h = 0; + + if (auto pixbuf = m_property_pixbuf.get_value()) { + pixbuf_w = pixbuf->get_width(); + pixbuf_h = pixbuf->get_height(); + } + + const double icon_w = pixbuf_w; + const double icon_h = pixbuf_h; + const double icon_x = background_area.get_x() + 28; + const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0; + + const double text_x = icon_x + icon_w + 5.0; + const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0; + const double text_w = text_natural.width; + const double text_h = text_natural.height; Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); - m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#f00"); + m_renderer_text.property_scale() = Pango::SCALE_SMALL; m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - m_renderer_text.property_foreground_set() = false; + m_renderer_text.property_scale_set() = false; + + if (auto pixbuf = m_property_pixbuf.get_value()) { + Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y); + cr->rectangle(icon_x, icon_y, icon_w, icon_h); + cr->fill(); + } } #endif -- cgit v1.2.3 From 0a1138fbd025da2c3f28cec73f823d9274ebc4db Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 18 May 2023 20:31:58 -0400 Subject: try to fix build --- src/components/channels.cpp | 2 ++ src/components/channels.hpp | 3 +++ 2 files changed, 5 insertions(+) (limited to 'src') diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 3327b02..5e3da86 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -850,6 +850,7 @@ Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildre return thread_iter; } +#ifdef WITH_VOICE Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent) { auto row = *m_model->append(parent); row[m_columns.m_type] = RenderType::VoiceParticipant; @@ -866,6 +867,7 @@ Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData & return row; } +#endif void ChannelList::UpdateChannelCategory(const ChannelData &channel) { auto iter = GetIteratorForRowFromID(channel.ID); diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 8aee733..2ad1a7c 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -85,7 +85,10 @@ protected: Gtk::TreeModel::iterator AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root); Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel); Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel); + +#ifdef WITH_VOICE Gtk::TreeModel::iterator CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent); +#endif void UpdateChannelCategory(const ChannelData &channel); -- cgit v1.2.3 From 3e67e8a7aeb265914d538314b17f36a988017081 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Fri, 19 May 2023 20:20:33 -0400 Subject: add voice state icons to participant rows --- .github/workflows/ci.yml | 14 ++++----- ci/used-icons.txt | 7 +++-- res/css/main.css | 4 +++ src/components/channels.cpp | 14 +++++++++ src/components/channels.hpp | 2 ++ src/components/channelscellrenderer.cpp | 52 ++++++++++++++++++++++++++++++++- src/components/channelscellrenderer.hpp | 7 ++++- src/discord/discord.cpp | 47 ++++++++++++++++++++--------- src/discord/discord.hpp | 10 +++++-- src/discord/voicestateflags.hpp | 17 +++++++++++ 10 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 src/discord/voicestateflags.hpp (limited to 'src') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63511cc..2a1bd25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,13 +93,13 @@ jobs: cd ${artifact_dir}/share/icons/Adwaita mkdir -p 16x16/actions 24x24/actions 32x32/actions 48x48/actions 64x64/actions 96x96/actions scalable/actions cd ${GITHUB_WORKSPACE} - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/16x16/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/24x24/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/32x32/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/32x32/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/48x48/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/48x48/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/64x64/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/actions/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/96x96/actions || : - cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/actions/%.svg ${artifact_dir}/share/icons/Adwaita/scalable/actions || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/16x16/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/24x24/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/32x32/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/32x32/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/48x48/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/48x48/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/64x64/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/%.symbolic.png ${artifact_dir}/share/icons/Adwaita/96x96/%.symbolic.png || : + cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/%.svg ${artifact_dir}/share/icons/Adwaita/scalable/%.svg || : - name: Upload build (1) uses: haya14busa/action-cond@v1 diff --git a/ci/used-icons.txt b/ci/used-icons.txt index 7e00c12..0ea18ae 100644 --- a/ci/used-icons.txt +++ b/ci/used-icons.txt @@ -1,2 +1,5 @@ -document-send-symbolic -call-stop-symbolic +actions/document-send-symbolic +actions/call-stop-symbolic +status/microphone-disabled-symbolic +status/audio-volume-muted-symbolic +devices/camera-web-symbolic diff --git a/res/css/main.css b/res/css/main.css index 4b76bb4..dcbdf4f 100644 --- a/res/css/main.css +++ b/res/css/main.css @@ -378,3 +378,7 @@ .voice-info-location { } + +.voice-state-server { + color: red; +} diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 5e3da86..0e97837 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -96,6 +96,7 @@ ChannelList::ChannelList() column->add_attribute(renderer->property_expanded(), m_columns.m_expanded); column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw); column->add_attribute(renderer->property_color(), m_columns.m_color); + column->add_attribute(renderer->property_voice_state(), m_columns.m_voice_flags); m_view.append_column(*column); m_menu_guild_copy_id.signal_activate().connect([this] { @@ -282,6 +283,7 @@ ChannelList::ChannelList() #if WITH_VOICE discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserConnect)); discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserDisconnect)); + discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceStateSet)); #endif } @@ -537,6 +539,12 @@ void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) m_model->erase(iter); } } + +void ChannelList::OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) { + if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { + (*iter)[m_columns.m_voice_flags] = flags; + } +} #endif void ChannelList::DeleteThreadRow(Snowflake id) { @@ -857,6 +865,11 @@ Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData & row[m_columns.m_id] = user.ID; row[m_columns.m_name] = user.GetEscapedName(); + const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID); + if (voice_state.has_value()) { + row[m_columns.m_voice_flags] = voice_state->second; + } + auto &img = Abaddon::Get().GetImageManager(); row[m_columns.m_icon] = img.GetPlaceholder(VoiceParticipantIconSize); const auto cb = [this, user_id = user.ID](const Glib::RefPtr &pb) { @@ -1283,4 +1296,5 @@ ChannelList::ModelColumns::ModelColumns() { add(m_nsfw); add(m_expanded); add(m_color); + add(m_voice_flags); } diff --git a/src/components/channels.hpp b/src/components/channels.hpp index 2ad1a7c..7a23b3d 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -55,6 +55,7 @@ protected: void OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id); void OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id); + void OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags); Gtk::TreeView m_view; @@ -70,6 +71,7 @@ protected: Gtk::TreeModelColumn m_sort; Gtk::TreeModelColumn m_nsfw; Gtk::TreeModelColumn> m_color; // for folders right now + Gtk::TreeModelColumn m_voice_flags; // Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children // because otherwise it doesnt count as an "expander" (property_is_expander) // so this solution will have to do which i hate but the alternative is adding invisible children diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index ab3113c..6de7a00 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -17,7 +17,8 @@ CellRendererChannels::CellRendererChannels() , m_property_pixbuf_animation(*this, "pixbuf-animation") , m_property_expanded(*this, "expanded") , m_property_nsfw(*this, "nsfw") - , m_property_color(*this, "color") { + , m_property_color(*this, "color") + , m_property_voice_state(*this, "voice-state") { property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; property_xpad() = 2; property_ypad() = 2; @@ -58,6 +59,10 @@ Glib::PropertyProxy> CellRendererChannels::property_col return m_property_color.get_proxy(); } +Glib::PropertyProxy CellRendererChannels::property_voice_state() { + return m_property_voice_state.get_proxy(); +} + void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { switch (m_property_type.get_value()) { case RenderType::Folder: @@ -733,6 +738,51 @@ void CellRendererChannels::render_vfunc_voice_participant(const Cairo::RefPtrrectangle(icon_x, icon_y, icon_w, icon_h); cr->fill(); } + + auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); + if (paned != nullptr) { + const auto edge = std::min(paned->get_position(), background_area.get_width()); + + const static std::array, 3> icon_order = { { + { VoiceStateFlags::SelfMute | VoiceStateFlags::Mute, "microphone-disabled-symbolic" }, + { VoiceStateFlags::SelfDeaf | VoiceStateFlags::Deaf, "audio-volume-muted-symbolic" }, + { VoiceStateFlags::SelfVideo, "camera-web-symbolic" }, + } }; + + constexpr static int IconSize = 18; + constexpr static int IconPad = 2; + + const VoiceStateFlags voice_flags = m_property_voice_state.get_value(); + + int offset = 0; + for (auto iter = icon_order.rbegin(); iter != icon_order.rend(); iter++) { + const auto &[flag, icon] = *iter; + if ((voice_flags & flag) == VoiceStateFlags::Clear) continue; + + const double icon_w = 18; + const double icon_h = 18; + const double icon_x = background_area.get_x() + edge - icon_w + offset; + const double icon_y = background_area.get_y() + background_area.get_height() / 2 - icon_h / 2; + Gdk::Rectangle icon_cell_area(icon_x, icon_y, icon_w, icon_h); + + offset -= (IconSize + IconPad); + + const bool is_server_mute = (voice_flags & VoiceStateFlags::Mute) == VoiceStateFlags::Mute; + const bool is_server_deaf = (voice_flags & VoiceStateFlags::Deaf) == VoiceStateFlags::Deaf; + auto context = widget.get_style_context(); + if (is_server_mute || is_server_deaf) { + context->context_save(); + context->add_class("voice-state-server"); + } + + m_renderer_pixbuf.property_icon_name() = icon; + m_renderer_pixbuf.render(cr, widget, background_area, icon_cell_area, flags); + + if (is_server_mute || is_server_deaf) { + context->context_restore(); + } + } + } } #endif diff --git a/src/components/channelscellrenderer.hpp b/src/components/channelscellrenderer.hpp index f6859dc..934ce5b 100644 --- a/src/components/channelscellrenderer.hpp +++ b/src/components/channelscellrenderer.hpp @@ -3,6 +3,8 @@ #include #include #include "discord/snowflake.hpp" +#include "discord/voicestateflags.hpp" +#include "misc/bitwise.hpp" enum class RenderType : uint8_t { Folder, @@ -34,6 +36,7 @@ public: Glib::PropertyProxy property_expanded(); Glib::PropertyProxy property_nsfw(); Glib::PropertyProxy> property_color(); + Glib::PropertyProxy property_voice_state(); protected: void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; @@ -113,7 +116,7 @@ protected: const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags); - // voice channel + // voice participant void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; void get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; void get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; @@ -152,6 +155,7 @@ protected: private: Gtk::CellRendererText m_renderer_text; + Gtk::CellRendererPixbuf m_renderer_pixbuf; Glib::Property m_property_type; // all Glib::Property m_property_name; // all @@ -161,6 +165,7 @@ private: Glib::Property m_property_expanded; // category Glib::Property m_property_nsfw; // channel Glib::Property> m_property_color; // folder + Glib::Property m_property_voice_state; // same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39 // this will manifest though since guild icons can change diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index be939be..37d4f28 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -1227,8 +1227,8 @@ std::optional DiscordClient::GetSSRCOfUser(Snowflake id) const { return m_voice.GetSSRCOfUser(id); } -std::optional DiscordClient::GetVoiceState(Snowflake user_id) const { - if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) { +std::optional> DiscordClient::GetVoiceState(Snowflake user_id) const { + if (const auto it = m_voice_states.find(user_id); it != m_voice_states.end()) { return it->second; } return std::nullopt; @@ -2267,9 +2267,9 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { if (data.ChannelID.has_value()) { const auto old_state = GetVoiceState(data.UserID); - SetVoiceState(data.UserID, *data.ChannelID); - if (old_state.has_value() && *old_state != *data.ChannelID) { - m_signal_voice_user_disconnect.emit(data.UserID, *old_state); + SetVoiceState(data.UserID, data); + if (old_state.has_value() && old_state->first != *data.ChannelID) { + m_signal_voice_user_disconnect.emit(data.UserID, old_state->first); m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); } else if (!old_state.has_value()) { m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); @@ -2278,7 +2278,7 @@ void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) { const auto old_state = GetVoiceState(data.UserID); ClearVoiceState(data.UserID); if (old_state.has_value()) { - m_signal_voice_user_disconnect.emit(data.UserID, *old_state); + m_signal_voice_user_disconnect.emit(data.UserID, old_state->first); } } } @@ -2333,7 +2333,7 @@ void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { for (const auto &g : data.Guilds) { for (const auto &s : g.VoiceStates) { if (s.ChannelID.has_value()) { - SetVoiceState(s.UserID, *s.ChannelID); + SetVoiceState(s.UserID, s); } } } @@ -2798,18 +2798,33 @@ void DiscordClient::SendVoiceStateUpdate() { m_websocket.Send(msg); } -void DiscordClient::SetVoiceState(Snowflake user_id, Snowflake channel_id) { - spdlog::get("discord")->debug("SetVoiceState: {} -> {}", user_id, channel_id); - m_voice_state_user_channel[user_id] = channel_id; - m_voice_state_channel_users[channel_id].insert(user_id); +void DiscordClient::SetVoiceState(Snowflake user_id, const VoiceState &state) { + if (!state.ChannelID.has_value()) { + spdlog::get("discord")->error("SetVoiceState called with missing channel ID"); + return; + } + spdlog::get("discord")->debug("SetVoiceState: {} -> {}", user_id, *state.ChannelID); + + auto flags = VoiceStateFlags::Clear; + if (state.IsSelfMuted) flags |= VoiceStateFlags::SelfMute; + if (state.IsSelfDeafened) flags |= VoiceStateFlags::SelfDeaf; + if (state.IsMuted) flags |= VoiceStateFlags::Mute; + if (state.IsDeafened) flags |= VoiceStateFlags::Deaf; + if (state.IsSelfStream) flags |= VoiceStateFlags::SelfStream; + if (state.IsSelfVideo) flags |= VoiceStateFlags::SelfVideo; + + m_voice_states[user_id] = std::make_pair(*state.ChannelID, flags); + m_voice_state_channel_users[*state.ChannelID].insert(user_id); + + m_signal_voice_state_set.emit(user_id, *state.ChannelID, flags); } void DiscordClient::ClearVoiceState(Snowflake user_id) { spdlog::get("discord")->debug("ClearVoiceState: {}", user_id); - if (const auto it = m_voice_state_user_channel.find(user_id); it != m_voice_state_user_channel.end()) { - m_voice_state_channel_users[it->second].erase(user_id); + if (const auto it = m_voice_states.find(user_id); it != m_voice_states.end()) { + m_voice_state_channel_users[it->second.first].erase(user_id); // invalidated - m_voice_state_user_channel.erase(user_id); + m_voice_states.erase(user_id); } } @@ -3119,4 +3134,8 @@ DiscordClient::type_signal_voice_client_state_update DiscordClient::signal_voice DiscordClient::type_signal_voice_channel_changed DiscordClient::signal_voice_channel_changed() { return m_signal_voice_channel_changed; } + +DiscordClient::type_signal_voice_state_set DiscordClient::signal_voice_state_set() { + return m_signal_voice_state_set; +} #endif diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index cb76ef6..7f7518c 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -5,6 +5,7 @@ #include "objects.hpp" #include "store.hpp" #include "voiceclient.hpp" +#include "voicestateflags.hpp" #include "websocket.hpp" #include #include @@ -192,7 +193,7 @@ public: [[nodiscard]] Snowflake GetVoiceChannelID() const noexcept; [[nodiscard]] std::unordered_set GetUsersInVoiceChannel(Snowflake channel_id); [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; - [[nodiscard]] std::optional GetVoiceState(Snowflake user_id) const; + [[nodiscard]] std::optional> GetVoiceState(Snowflake user_id) const; DiscordVoiceClient &GetVoiceClient(); @@ -360,12 +361,12 @@ private: Snowflake m_voice_channel_id; // todo sql i guess - std::unordered_map m_voice_state_user_channel; + std::unordered_map> m_voice_states; std::unordered_map> m_voice_state_channel_users; void SendVoiceStateUpdate(); - void SetVoiceState(Snowflake user_id, Snowflake channel_id); + void SetVoiceState(Snowflake user_id, const VoiceState &state); void ClearVoiceState(Snowflake user_id); void OnVoiceConnected(); @@ -455,6 +456,7 @@ public: using type_signal_voice_requested_disconnect = sigc::signal; using type_signal_voice_client_state_update = sigc::signal; using type_signal_voice_channel_changed = sigc::signal; + using type_signal_voice_state_set = sigc::signal; #endif type_signal_gateway_ready signal_gateway_ready(); @@ -522,6 +524,7 @@ public: type_signal_voice_requested_disconnect signal_voice_requested_disconnect(); type_signal_voice_client_state_update signal_voice_client_state_update(); type_signal_voice_channel_changed signal_voice_channel_changed(); + type_signal_voice_state_set signal_voice_state_set(); #endif protected: @@ -590,5 +593,6 @@ protected: type_signal_voice_requested_disconnect m_signal_voice_requested_disconnect; type_signal_voice_client_state_update m_signal_voice_client_state_update; type_signal_voice_channel_changed m_signal_voice_channel_changed; + type_signal_voice_state_set m_signal_voice_state_set; #endif }; diff --git a/src/discord/voicestateflags.hpp b/src/discord/voicestateflags.hpp new file mode 100644 index 0000000..e369001 --- /dev/null +++ b/src/discord/voicestateflags.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +enum class VoiceStateFlags : uint8_t { + Clear = 0, + Deaf = 1 << 0, + Mute = 1 << 1, + SelfDeaf = 1 << 2, + SelfMute = 1 << 3, + SelfStream = 1 << 4, + SelfVideo = 1 << 5, +}; + +template<> +struct Bitwise { + static const bool enable = true; +}; -- cgit v1.2.3 From 2c459728620eee4b15323320995f5f6fc0ae924a Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sat, 20 May 2023 21:16:41 -0400 Subject: restore mute + deaf buttons on window popup --- src/windows/voicewindow.cpp | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/windows/voicewindow.cpp b/src/windows/voicewindow.cpp index c82a0aa..9e2efee 100644 --- a/src/windows/voicewindow.cpp +++ b/src/windows/voicewindow.cpp @@ -103,6 +103,11 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserDisconnect)); discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserConnect)); + if (const auto self_state = discord.GetVoiceState(discord.GetUserData().ID); self_state.has_value()) { + m_mute.set_active((self_state->second & VoiceStateFlags::SelfMute) == VoiceStateFlags::SelfMute); + m_deafen.set_active((self_state->second & VoiceStateFlags::SelfDeaf) == VoiceStateFlags::SelfDeaf); + } + m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); -- cgit v1.2.3 From a204388c5c078d19a950b1a8886188567e30ea94 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Tue, 23 May 2023 01:09:05 -0400 Subject: hide voice info box on startup --- src/windows/mainwindow.cpp | 4 ---- 1 file changed, 4 deletions(-) (limited to 'src') diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp index f867db9..bb09735 100644 --- a/src/windows/mainwindow.cpp +++ b/src/windows/mainwindow.cpp @@ -51,10 +51,6 @@ MainWindow::MainWindow() m_content_stack.set_visible_child("chat"); m_content_stack.show(); -#ifdef WITH_VOICE - m_voice_info.show(); -#endif - m_left_pane.add(m_channel_list); #ifdef WITH_VOICE m_left_pane.add(m_voice_info); -- cgit v1.2.3 From b5f2b7171f7c0a19d4c4a2974cd8977fd4955464 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sun, 28 May 2023 16:53:07 -0400 Subject: make RTP timestamp strictly linear this should fix some artificial delay --- src/audio/manager.cpp | 13 +++++++++++++ src/audio/manager.hpp | 4 ++++ src/discord/voiceclient.cpp | 11 ++++++----- src/discord/voiceclient.hpp | 1 - 4 files changed, 23 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp index 6e85ed8..ce20940 100644 --- a/src/audio/manager.cpp +++ b/src/audio/manager.cpp @@ -50,6 +50,15 @@ void capture_data_callback(ma_device *pDevice, void *pOutput, const void *pInput 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; } AudioManager::AudioManager() { @@ -473,6 +482,10 @@ AudioDevices &AudioManager::GetDevices() { return m_devices; } +uint32_t AudioManager::GetRTPTimestamp() const noexcept { + return m_rtp_timestamp; +} + AudioManager::type_signal_opus_packet AudioManager::signal_opus_packet() { return m_signal_opus_packet; } diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp index 9cd7f42..ed40f35 100644 --- a/src/audio/manager.hpp +++ b/src/audio/manager.hpp @@ -63,6 +63,8 @@ public: [[nodiscard]] AudioDevices &GetDevices(); + [[nodiscard]] uint32_t GetRTPTimestamp() const noexcept; + private: void OnCapturedPCM(const int16_t *pcm, ma_uint32 frames); @@ -113,6 +115,8 @@ private: AudioDevices m_devices; + std::atomic m_rtp_timestamp = 0; + public: using type_signal_opus_packet = sigc::signal; type_signal_opus_packet signal_opus_packet(); diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp index c37ba7b..741b07f 100644 --- a/src/discord/voiceclient.cpp +++ b/src/discord/voiceclient.cpp @@ -49,17 +49,18 @@ void UDPSocket::SetSSRC(uint32_t ssrc) { void UDPSocket::SendEncrypted(const uint8_t *data, size_t len) { m_sequence++; - m_timestamp += 480; // this is important + + const uint32_t timestamp = Abaddon::Get().GetAudio().GetRTPTimestamp(); std::vector rtp(12 + len + crypto_secretbox_MACBYTES, 0); rtp[0] = 0x80; // ver 2 rtp[1] = 0x78; // payload type 0x78 rtp[2] = (m_sequence >> 8) & 0xFF; rtp[3] = (m_sequence >> 0) & 0xFF; - rtp[4] = (m_timestamp >> 24) & 0xFF; - rtp[5] = (m_timestamp >> 16) & 0xFF; - rtp[6] = (m_timestamp >> 8) & 0xFF; - rtp[7] = (m_timestamp >> 0) & 0xFF; + rtp[4] = (timestamp >> 24) & 0xFF; + rtp[5] = (timestamp >> 16) & 0xFF; + rtp[6] = (timestamp >> 8) & 0xFF; + rtp[7] = (timestamp >> 0) & 0xFF; rtp[8] = (m_ssrc >> 24) & 0xFF; rtp[9] = (m_ssrc >> 16) & 0xFF; rtp[10] = (m_ssrc >> 8) & 0xFF; diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp index 7bf4295..0112749 100644 --- a/src/discord/voiceclient.hpp +++ b/src/discord/voiceclient.hpp @@ -171,7 +171,6 @@ private: uint32_t m_ssrc; uint16_t m_sequence = 0; - uint32_t m_timestamp = 0; public: using type_signal_data = sigc::signal>; -- cgit v1.2.3 From 1f765e1ddf0991084843a90446b6b4aec59ffb48 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Sun, 28 May 2023 19:48:23 -0400 Subject: fix state saving --- src/abaddon.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 5c6f136..bbca8df 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -341,8 +341,8 @@ void Abaddon::StartDiscord() { } void Abaddon::StopDiscord() { - if (m_discord.Stop()) - SaveState(); + if (m_discord.IsStarted()) SaveState(); + m_discord.Stop(); m_main_window->UpdateMenus(); } -- cgit v1.2.3 From 7da37a2fa90cebf43631c74aa1abfb9842502291 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 29 May 2023 01:20:41 -0400 Subject: printf to log --- src/abaddon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/abaddon.cpp b/src/abaddon.cpp index bbca8df..e8911f9 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -58,7 +58,7 @@ Abaddon::Abaddon() 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) { - printf("%llu has ssrc %u\n", (uint64_t)m.UserID, m.SSRC); + spdlog::get("voice")->debug("{} SSRC: {}", m.UserID, m.SSRC); m_audio->AddSSRC(m.SSRC); }); #endif -- cgit v1.2.3