From 0018788780d756dbf0d3a77f6b40b384183348f7 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 14 Apr 2025 11:38:52 +0200 Subject: Redesign audio to support multiple audio tracks explicitly --- TODO | 4 +- images/trash.png | Bin 0 -> 691 bytes include/Config.hpp | 17 +- include/SafeVector.hpp | 137 +++++++++---- include/Theme.hpp | 1 + include/Utils.hpp | 1 + include/gui/List.hpp | 5 +- include/gui/Page.hpp | 4 +- include/gui/ScrollablePage.hpp | 1 + include/gui/SettingsPage.hpp | 35 ++-- include/gui/Subsection.hpp | 5 + include/gui/Widget.hpp | 6 +- src/Config.cpp | 82 +++++++- src/GlobalHotkeysLinux.cpp | 2 + src/GsrInfo.cpp | 5 - src/Overlay.cpp | 117 ++++++------ src/Theme.cpp | 9 +- src/Utils.cpp | 5 + src/gui/ComboBox.cpp | 2 +- src/gui/GlobalSettingsPage.cpp | 2 +- src/gui/GsrPage.cpp | 5 +- src/gui/List.cpp | 30 +-- src/gui/Page.cpp | 15 +- src/gui/ScreenshotSettingsPage.cpp | 3 +- src/gui/ScrollablePage.cpp | 13 +- src/gui/SettingsPage.cpp | 382 ++++++++++++++++++++++--------------- src/gui/StaticPage.cpp | 5 +- src/gui/Subsection.cpp | 17 +- src/gui/Widget.cpp | 16 +- src/main.cpp | 5 +- 30 files changed, 596 insertions(+), 335 deletions(-) create mode 100644 images/trash.png diff --git a/TODO b/TODO index 1a91e87..50aae53 100644 --- a/TODO +++ b/TODO @@ -123,8 +123,6 @@ Maybe change gsr-ui startup retry time in the systemd service, from 5 seconds to Add support for window capture. This should not prompt for window selection directly but instead prompt for window selection when recording starts and hide the ui first. For screenshots window capture should exist but "follow focused" option should not exist. -Improve audio design. It should have a button to add/remove audio tracks and button to add audio into each audio track separately and "record audio from all applications except the selected ones" for each audio track. Then also remove the "merge audio tracks" option. - Make it possible to take a screenshot through a button in the ui instead of having to use hotkey. Handle failing to save a replay. gsr should output "failed to save replay, or something like that" to make it possible to detect that. @@ -161,4 +159,4 @@ If CursorTrackerWayland fails then fallback to getting focused monitor by window Maybe automatically switch to recording with the device that controls the monitor. In that case also add all monitors available to capture in the capture list and automatically choose the gpu that controls the monitor. -Support cjk font. Use fc-match to find the location of the font. This also works in flatpak, in which case the fonts are in /run/host/..., where it lists system fonts. +Support cjk font. Use fc-match to find the location of the font. This also works in flatpak, in which case the fonts are in /run/host/..., where it lists system fonts. \ No newline at end of file diff --git a/images/trash.png b/images/trash.png new file mode 100644 index 0000000..cf1d9de Binary files /dev/null and b/images/trash.png differ diff --git a/include/Config.hpp b/include/Config.hpp index 0e8e4eb..bbb5381 100644 --- a/include/Config.hpp +++ b/include/Config.hpp @@ -6,7 +6,7 @@ #include #include -#define GSR_CONFIG_FILE_VERSION 1 +#define GSR_CONFIG_FILE_VERSION 2 namespace gsr { struct SupportedCaptureOptions; @@ -30,6 +30,14 @@ namespace gsr { std::string to_string(bool spaces = true, bool modifier_side = true) const; }; + struct AudioTrack { + std::vector audio_inputs; // ids + bool application_audio_invert = false; + + bool operator==(const AudioTrack &other) const; + bool operator!=(const AudioTrack &other) const; + }; + struct RecordOptions { std::string record_area_option = "screen"; int32_t record_area_width = 0; @@ -38,10 +46,11 @@ namespace gsr { int32_t video_height = 0; int32_t fps = 60; int32_t video_bitrate = 15000; - bool merge_audio_tracks = true; // Currently unused for streaming because all known streaming sites only support 1 audio track - bool application_audio_invert = false; + bool merge_audio_tracks = true; // TODO: Remove in the future + bool application_audio_invert = false; // TODO: Remove in the future bool change_video_resolution = false; - std::vector audio_tracks; + std::vector audio_tracks; // ids, TODO: Remove in the future + std::vector audio_tracks_list; std::string color_range = "limited"; std::string video_quality = "very_high"; std::string video_codec = "auto"; diff --git a/include/SafeVector.hpp b/include/SafeVector.hpp index 6cb4725..63125ad 100644 --- a/include/SafeVector.hpp +++ b/include/SafeVector.hpp @@ -1,8 +1,15 @@ #pragma once #include -#include #include +#include +#include "gui/Widget.hpp" + +template +struct SafeVectorItem { + T item; + bool alive = false; +}; // A vector that can be modified while iterating template @@ -10,64 +17,84 @@ class SafeVector { public: using PointerType = typename std::pointer_traits::element_type*; + SafeVector() = default; + SafeVector(const SafeVector&) = delete; + SafeVector& operator=(const SafeVector&) = delete; + ~SafeVector() { + clear(); + } + void push_back(T item) { - data.push_back(std::move(item)); + data.push_back({std::move(item), true}); + ++num_items_alive; } // Safe to call when vector is empty // TODO: Make this iterator safe void pop_back() { - if(!data.empty()) + if(!data.empty()) { + gsr::add_widget_to_remove(std::move(data.back().item)); data.pop_back(); + --num_items_alive; + } } // Might not remove the data immediately if inside for_each loop. // In that case the item is removed at the end of the loop. void remove(PointerType item_to_remove) { - if(for_each_depth == 0) + if(for_each_depth == 0) { remove_item(item_to_remove); - else - remove_queue.push_back(item_to_remove); + return; + } + + SafeVectorItem *item = get_item(item_to_remove); + if(item && item->alive) { + item->alive = false; + --num_items_alive; + has_items_to_remove = true; + } } // Safe to call when vector is empty, in which case it returns nullptr T* back() { - if(data.empty()) - return nullptr; - else - return &data.back(); + for(auto it = data.rbegin(), end = data.rend(); it != end; ++it) { + if(it->alive) + return &it->item; + } + return nullptr; } // TODO: Make this iterator safe void clear() { + for(auto &item : data) { + gsr::add_widget_to_remove(std::move(item.item)); + } data.clear(); - remove_queue.clear(); + num_items_alive = 0; } // Return true from |callback| to continue. This function returns false if |callback| returned false - bool for_each(std::function callback) { + bool for_each(std::function callback, bool include_dead = false) { bool result = true; ++for_each_depth; for(size_t i = 0; i < data.size(); ++i) { - result = callback(data[i]); - if(!result) - break; + if(data[i].alive || include_dead) { + result = callback(data[i].item); + if(!result) + break; + } } --for_each_depth; - if(for_each_depth == 0) { - for(PointerType item_to_remove : remove_queue) { - remove_item(item_to_remove); - } - remove_queue.clear(); - } + if(for_each_depth == 0) + remove_dead_items(); return result; } // Return true from |callback| to continue. This function returns false if |callback| returned false - bool for_each_reverse(std::function callback) { + bool for_each_reverse(std::function callback, bool include_dead = false) { bool result = true; ++for_each_depth; @@ -80,50 +107,84 @@ public: if(i < 0) break; - result = callback(data[i]); - if(!result) - break; + if(data[i].alive || include_dead) { + result = callback(data[i].item); + if(!result) + break; + } --i; } --for_each_depth; - if(for_each_depth == 0) { - for(PointerType item_to_remove : remove_queue) { - remove_item(item_to_remove); - } - remove_queue.clear(); - } + if(for_each_depth == 0) + remove_dead_items(); return result; } T& operator[](size_t index) { - return data[index]; + assert(index < data.size()); + return data[index].item; } const T& operator[](size_t index) const { - return data[index]; + assert(index < data.size()); + return data[index].item; } size_t size() const { - return data.size(); + return (size_t)num_items_alive; } bool empty() const { - return data.empty(); + return num_items_alive == 0; + } + + void replace_item(PointerType item_to_replace, T new_item) { + SafeVectorItem *item = get_item(item_to_replace); + if(item->alive) { + gsr::add_widget_to_remove(std::move(item->item)); + item->item = std::move(new_item); + } } private: void remove_item(PointerType item_to_remove) { for(auto it = data.begin(), end = data.end(); it != end; ++it) { - if(&*(*it) == item_to_remove) { + if(&*(it->item) == item_to_remove) { + gsr::add_widget_to_remove(std::move(it->item)); data.erase(it); + --num_items_alive; return; } } } + + SafeVectorItem* get_item(PointerType item_to_remove) { + for(auto &item : data) { + if(&*(item.item) == item_to_remove) + return &item; + } + return nullptr; + } + + void remove_dead_items() { + if(!has_items_to_remove) + return; + + for(auto it = data.begin(); it != data.end();) { + if(it->alive) { + ++it; + } else { + gsr::add_widget_to_remove(std::move(it->item)); + it = data.erase(it); + } + } + has_items_to_remove = false; + } private: - std::vector data; - std::vector remove_queue; + std::vector> data; int for_each_depth = 0; + int num_items_alive = 0; + bool has_items_to_remove = false; }; \ No newline at end of file diff --git a/include/Theme.hpp b/include/Theme.hpp index 4305072..670980f 100644 --- a/include/Theme.hpp +++ b/include/Theme.hpp @@ -42,6 +42,7 @@ namespace gsr { mgl::Texture pause_texture; mgl::Texture save_texture; mgl::Texture screenshot_texture; + mgl::Texture trash_texture; mgl::Texture ps4_home_texture; mgl::Texture ps4_options_texture; diff --git a/include/Utils.hpp b/include/Utils.hpp index 19700df..1415209 100644 --- a/include/Utils.hpp +++ b/include/Utils.hpp @@ -14,6 +14,7 @@ namespace gsr { using StringSplitCallback = std::function; void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func); + bool starts_with(std::string_view str, const char *substr); std::string get_home_dir(); std::string get_config_dir(); diff --git a/include/gui/List.hpp b/include/gui/List.hpp index 72c5353..f79d165 100644 --- a/include/gui/List.hpp +++ b/include/gui/List.hpp @@ -21,19 +21,20 @@ namespace gsr { List(Orientation orientation, Alignment content_alignment = Alignment::START); List(const List&) = delete; List& operator=(const List&) = delete; + virtual ~List() override; bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override; void draw(mgl::Window &window, mgl::vec2f offset) override; - //void remove_child_widget(Widget *widget) override; - void add_widget(std::unique_ptr widget); void remove_widget(Widget *widget); + void replace_widget(Widget *widget, std::unique_ptr new_widget); void clear(); // Return true from |callback| to continue void for_each_child_widget(std::function &widget)> callback); // Returns nullptr if index is invalid Widget* get_child_widget_by_index(size_t index) const; + size_t get_num_children() const; void set_spacing(float spacing); diff --git a/include/gui/Page.hpp b/include/gui/Page.hpp index 0d8536a..00a53c6 100644 --- a/include/gui/Page.hpp +++ b/include/gui/Page.hpp @@ -10,13 +10,11 @@ namespace gsr { Page() = default; Page(const Page&) = delete; Page& operator=(const Page&) = delete; - virtual ~Page() = default; + virtual ~Page() override; virtual void on_navigate_to_page() {} virtual void on_navigate_away_from_page() {} - //void remove_child_widget(Widget *widget) override; - virtual void add_widget(std::unique_ptr widget); protected: SafeVector> widgets; diff --git a/include/gui/ScrollablePage.hpp b/include/gui/ScrollablePage.hpp index 452d0e9..54ec2cb 100644 --- a/include/gui/ScrollablePage.hpp +++ b/include/gui/ScrollablePage.hpp @@ -12,6 +12,7 @@ namespace gsr { ScrollablePage(mgl::vec2f size); ScrollablePage(const ScrollablePage&) = delete; ScrollablePage& operator=(const ScrollablePage&) = delete; + virtual ~ScrollablePage() override; bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override; void draw(mgl::Window &window, mgl::vec2f offset) override; diff --git a/include/gui/SettingsPage.hpp b/include/gui/SettingsPage.hpp index b9f5cde..fb705cc 100644 --- a/include/gui/SettingsPage.hpp +++ b/include/gui/SettingsPage.hpp @@ -18,6 +18,7 @@ namespace gsr { class ScrollablePage; class Label; class LineSeparator; + class Subsection; class SettingsPage : public StaticPage { public: @@ -54,19 +55,20 @@ namespace gsr { std::unique_ptr create_change_video_resolution_section(); std::unique_ptr create_capture_target_section(); std::unique_ptr create_audio_device_selection_combobox(); - std::unique_ptr