mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-12-04 05:20:30 +00:00
SoundPlayer: Implement playlist shuffle mode
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).
This commit is contained in:
parent
0812965f50
commit
314b8a374b
Notes:
sideshowbarker
2024-07-18 01:52:41 +09:00
Author: https://github.com/lpereira Commit: https://github.com/SerenityOS/serenity/commit/314b8a374ba Pull-request: https://github.com/SerenityOS/serenity/pull/10278 Reviewed-by: https://github.com/kleinesfilmroellchen ✅
7 changed files with 106 additions and 11 deletions
|
@ -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();
|
||||
|
|
|
@ -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<Audio::Buffer>, [[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<Audio::Buffer>, [[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;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "Playlist.h"
|
||||
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/Random.h>
|
||||
#include <LibAudio/Loader.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
|
||||
|
@ -33,7 +34,7 @@ void Playlist::try_fill_missing_info(Vector<M3UEntry>& entries, StringView path)
|
|||
Vector<M3UEntry*> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2021, the SerenityOS developers.
|
||||
* Copyright (c) 2021, Leandro A. F. Pereira <leandro@tia.mat.br>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
@ -8,6 +9,7 @@
|
|||
|
||||
#include "M3UParser.h"
|
||||
#include "PlaylistWidget.h"
|
||||
#include <AK/StringHash.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
|
@ -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<M3UEntry>&, StringView);
|
||||
|
||||
RefPtr<PlaylistModel> m_model;
|
||||
bool m_looping { false };
|
||||
bool m_shuffling { false };
|
||||
|
||||
BloomFilter m_previously_played_paths;
|
||||
int m_next_index_to_play { 0 };
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue