From 314b8a374ba82152bb092d9c6ff0c2b5d5c21005 Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Thu, 30 Sep 2021 07:41:00 -0700 Subject: [PATCH] SoundPlayer: Implement playlist shuffle mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shuffling algorithm uses a naïve bloom filter to provide random uniformity, avoiding items that were recently played. With 32 bits, double hashing, and an error rate of ~10%, this bloom filter should be able to hold around ~16 keys, which should be sufficient to give the illusion of fairness to the shuffling algorithm. This avoids having to shuffle the playlist itself (user might have spent quite a bit of time to sort them, so it's not a good idea to mess with it), or having to create a proxy model that shuffles (that could potentially use quite a bit of memory). --- Userland/Applications/SoundPlayer/Player.cpp | 9 ++++++ Userland/Applications/SoundPlayer/Player.h | 23 +++++++++----- .../Applications/SoundPlayer/Playlist.cpp | 29 +++++++++++++++-- Userland/Applications/SoundPlayer/Playlist.h | 31 +++++++++++++++++++ .../SoundPlayerWidgetAdvancedView.cpp | 14 +++++++-- .../SoundPlayerWidgetAdvancedView.h | 3 ++ Userland/Applications/SoundPlayer/main.cpp | 8 +++++ 7 files changed, 106 insertions(+), 11 deletions(-) diff --git a/Userland/Applications/SoundPlayer/Player.cpp b/Userland/Applications/SoundPlayer/Player.cpp index e0cb980bac..273e4c668d 100644 --- a/Userland/Applications/SoundPlayer/Player.cpp +++ b/Userland/Applications/SoundPlayer/Player.cpp @@ -92,6 +92,15 @@ void Player::set_volume(double volume) volume_changed(m_volume); } +void Player::set_shuffle_mode(ShuffleMode mode) +{ + if (m_shuffle_mode != mode) { + m_shuffle_mode = mode; + m_playlist.set_shuffling(mode == ShuffleMode::Shuffling); + shuffle_mode_changed(mode); + } +} + void Player::play() { m_playback_manager.play(); diff --git a/Userland/Applications/SoundPlayer/Player.h b/Userland/Applications/SoundPlayer/Player.h index 9846fc2177..74806231b3 100644 --- a/Userland/Applications/SoundPlayer/Player.h +++ b/Userland/Applications/SoundPlayer/Player.h @@ -24,6 +24,10 @@ public: File, Playlist, }; + enum class ShuffleMode { + None, + Shuffling, + }; explicit Player(Audio::ClientConnection& audio_client_connection); virtual ~Player() { } @@ -34,10 +38,13 @@ public: StringView loaded_filename() const { return m_loaded_filename; } PlayState play_state() const { return m_play_state; } - void set_play_state(PlayState state); + void set_play_state(PlayState); LoopMode loop_mode() const { return m_loop_mode; } - void set_loop_mode(LoopMode mode); + void set_loop_mode(LoopMode); + + ShuffleMode shuffle_mode() const { return m_shuffle_mode; } + void set_shuffle_mode(ShuffleMode); double volume() const { return m_volume; } void set_volume(double value); @@ -52,11 +59,12 @@ public: virtual void loop_mode_changed(LoopMode) = 0; virtual void time_elapsed(int) = 0; virtual void file_name_changed(StringView) = 0; - virtual void playlist_loaded(StringView, bool) { } - virtual void audio_load_error(StringView, StringView) { } - virtual void volume_changed(double) { } - virtual void total_samples_changed(int) { } - virtual void sound_buffer_played(RefPtr, [[maybe_unused]] int sample_rate, [[maybe_unused]] int samples_played) { } + virtual void playlist_loaded(StringView, bool) = 0; + virtual void audio_load_error(StringView, StringView) = 0; + virtual void shuffle_mode_changed(ShuffleMode) = 0; + virtual void volume_changed(double) = 0; + virtual void total_samples_changed(int) = 0; + virtual void sound_buffer_played(RefPtr, [[maybe_unused]] int sample_rate, [[maybe_unused]] int samples_played) = 0; protected: void done_initializing() @@ -71,6 +79,7 @@ private: Playlist m_playlist; PlayState m_play_state; LoopMode m_loop_mode; + ShuffleMode m_shuffle_mode; Audio::ClientConnection& m_audio_client_connection; PlaybackManager m_playback_manager; diff --git a/Userland/Applications/SoundPlayer/Playlist.cpp b/Userland/Applications/SoundPlayer/Playlist.cpp index 5faf3a10cd..b20490b2dc 100644 --- a/Userland/Applications/SoundPlayer/Playlist.cpp +++ b/Userland/Applications/SoundPlayer/Playlist.cpp @@ -8,6 +8,7 @@ #include "Playlist.h" #include +#include #include #include @@ -33,7 +34,7 @@ void Playlist::try_fill_missing_info(Vector& entries, StringView path) Vector to_delete; for (auto& entry : entries) { - if (!LexicalPath(entry.path).is_absolute()) + if (!LexicalPath { entry.path }.is_absolute()) entry.path = String::formatted("{}/{}", playlist_path.dirname(), entry.path); if (!entry.extended_info->file_size_in_bytes.has_value()) { @@ -69,8 +70,32 @@ StringView Playlist::next() return {}; m_next_index_to_play = 0; } + auto next = m_model->items().at(m_next_index_to_play).path; - m_next_index_to_play++; + if (!shuffling()) { + m_next_index_to_play++; + return next; + } + + // Try a few times getting an item to play that has not been + // recently played. But do not try too hard, as we don't want + // to wait forever. + int shuffle_try; + int const max_times_to_try = min(4, size()); + for (shuffle_try = 0; shuffle_try < max_times_to_try; shuffle_try++) { + if (!m_previously_played_paths.maybe_contains(next)) + break; + + m_next_index_to_play = get_random_uniform(size()); + next = m_model->items().at(m_next_index_to_play).path; + } + if (shuffle_try == max_times_to_try) { + // If we tried too much, maybe it's time to try resetting + // the bloom filter and start over. + m_previously_played_paths.reset(); + } + + m_previously_played_paths.add(next); return next; } diff --git a/Userland/Applications/SoundPlayer/Playlist.h b/Userland/Applications/SoundPlayer/Playlist.h index d41d38e208..370fc6b341 100644 --- a/Userland/Applications/SoundPlayer/Playlist.h +++ b/Userland/Applications/SoundPlayer/Playlist.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, the SerenityOS developers. + * Copyright (c) 2021, Leandro A. F. Pereira * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,6 +9,7 @@ #include "M3UParser.h" #include "PlaylistWidget.h" +#include #include #include @@ -29,11 +31,40 @@ public: void set_looping(bool looping) { m_looping = looping; } bool looping() const { return m_looping; } + void set_shuffling(bool shuffling) { m_shuffling = shuffling; } + bool shuffling() const { return m_shuffling; } + private: + // This naïve bloom filter is used in the shuffling algorithm to + // provide random uniformity, avoiding playing items that were recently + // played. + class BloomFilter { + public: + void reset() { m_bitmap = 0; } + void add(const StringView key) { m_bitmap |= mask_for_key(key); } + bool maybe_contains(const StringView key) const + { + auto mask = mask_for_key(key); + return (m_bitmap & mask) == mask; + } + + private: + u64 mask_for_key(StringView key) const + { + auto key_chars = key.characters_without_null_termination(); + auto hash1 = string_hash(key_chars, key.length(), 0xdeadbeef); + auto hash2 = string_hash(key_chars, key.length(), 0xbebacafe); + return 1ULL << (hash1 & 63) | 1ULL << (hash2 & 63); + } + u64 m_bitmap { 0 }; + }; + void try_fill_missing_info(Vector&, StringView); RefPtr m_model; bool m_looping { false }; + bool m_shuffling { false }; + BloomFilter m_previously_played_paths; int m_next_index_to_play { 0 }; }; diff --git a/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp b/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp index 8259a4041e..5ac1c80ab2 100644 --- a/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp +++ b/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp @@ -157,8 +157,7 @@ void SoundPlayerWidgetAdvancedView::set_playlist_visible(bool visible) void SoundPlayerWidgetAdvancedView::play_state_changed(Player::PlayState state) { - m_back_button->set_enabled(playlist().size() > 1); - m_next_button->set_enabled(playlist().size() > 1); + sync_previous_next_buttons(); m_play_button->set_enabled(state != PlayState::NoFileLoaded); m_play_button->set_icon(state == PlayState::Playing ? *m_pause_icon : *m_play_icon); @@ -172,6 +171,17 @@ void SoundPlayerWidgetAdvancedView::loop_mode_changed(Player::LoopMode) { } +void SoundPlayerWidgetAdvancedView::sync_previous_next_buttons() +{ + m_back_button->set_enabled(playlist().size() > 1 && !playlist().shuffling()); + m_next_button->set_enabled(playlist().size() > 1); +} + +void SoundPlayerWidgetAdvancedView::shuffle_mode_changed(Player::ShuffleMode) +{ + sync_previous_next_buttons(); +} + void SoundPlayerWidgetAdvancedView::time_elapsed(int seconds) { m_timestamp_label->set_text(String::formatted("Elapsed: {:02}:{:02}:{:02}", seconds / 3600, seconds / 60, seconds % 60)); diff --git a/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h b/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h index 44437e1aa5..f63ad7658c 100644 --- a/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h +++ b/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h @@ -38,6 +38,7 @@ public: virtual void play_state_changed(PlayState) override; virtual void loop_mode_changed(LoopMode) override; + virtual void shuffle_mode_changed(ShuffleMode) override; virtual void time_elapsed(int) override; virtual void file_name_changed(StringView) override; virtual void playlist_loaded(StringView, bool) override; @@ -50,6 +51,8 @@ protected: void keydown_event(GUI::KeyEvent&) override; private: + void sync_previous_next_buttons(); + void drop_event(GUI::DropEvent& event) override; GUI::Window& m_window; diff --git a/Userland/Applications/SoundPlayer/main.cpp b/Userland/Applications/SoundPlayer/main.cpp index 889208a983..9d9e68e1a9 100644 --- a/Userland/Applications/SoundPlayer/main.cpp +++ b/Userland/Applications/SoundPlayer/main.cpp @@ -98,6 +98,14 @@ int main(int argc, char** argv) playlist_toggle->set_checked(true); playback_menu.add_action(playlist_toggle); + auto shuffle_mode = GUI::Action::create_checkable("S&huffle Playlist", [&](auto& action) { + if (action.is_checked()) + player->set_shuffle_mode(Player::ShuffleMode::Shuffling); + else + player->set_shuffle_mode(Player::ShuffleMode::None); + }); + playback_menu.add_action(shuffle_mode); + auto& visualization_menu = window->add_menu("&Visualization"); GUI::ActionGroup visualization_actions; visualization_actions.set_exclusive(true);