mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 05:47:35 +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
7 changed files with 106 additions and 11 deletions
|
@ -92,6 +92,15 @@ void Player::set_volume(double volume)
|
||||||
volume_changed(m_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()
|
void Player::play()
|
||||||
{
|
{
|
||||||
m_playback_manager.play();
|
m_playback_manager.play();
|
||||||
|
|
|
@ -24,6 +24,10 @@ public:
|
||||||
File,
|
File,
|
||||||
Playlist,
|
Playlist,
|
||||||
};
|
};
|
||||||
|
enum class ShuffleMode {
|
||||||
|
None,
|
||||||
|
Shuffling,
|
||||||
|
};
|
||||||
|
|
||||||
explicit Player(Audio::ClientConnection& audio_client_connection);
|
explicit Player(Audio::ClientConnection& audio_client_connection);
|
||||||
virtual ~Player() { }
|
virtual ~Player() { }
|
||||||
|
@ -34,10 +38,13 @@ public:
|
||||||
StringView loaded_filename() const { return m_loaded_filename; }
|
StringView loaded_filename() const { return m_loaded_filename; }
|
||||||
|
|
||||||
PlayState play_state() const { return m_play_state; }
|
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; }
|
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; }
|
double volume() const { return m_volume; }
|
||||||
void set_volume(double value);
|
void set_volume(double value);
|
||||||
|
@ -52,11 +59,12 @@ public:
|
||||||
virtual void loop_mode_changed(LoopMode) = 0;
|
virtual void loop_mode_changed(LoopMode) = 0;
|
||||||
virtual void time_elapsed(int) = 0;
|
virtual void time_elapsed(int) = 0;
|
||||||
virtual void file_name_changed(StringView) = 0;
|
virtual void file_name_changed(StringView) = 0;
|
||||||
virtual void playlist_loaded(StringView, bool) { }
|
virtual void playlist_loaded(StringView, bool) = 0;
|
||||||
virtual void audio_load_error(StringView, StringView) { }
|
virtual void audio_load_error(StringView, StringView) = 0;
|
||||||
virtual void volume_changed(double) { }
|
virtual void shuffle_mode_changed(ShuffleMode) = 0;
|
||||||
virtual void total_samples_changed(int) { }
|
virtual void volume_changed(double) = 0;
|
||||||
virtual void sound_buffer_played(RefPtr<Audio::Buffer>, [[maybe_unused]] int sample_rate, [[maybe_unused]] int samples_played) { }
|
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:
|
protected:
|
||||||
void done_initializing()
|
void done_initializing()
|
||||||
|
@ -71,6 +79,7 @@ private:
|
||||||
Playlist m_playlist;
|
Playlist m_playlist;
|
||||||
PlayState m_play_state;
|
PlayState m_play_state;
|
||||||
LoopMode m_loop_mode;
|
LoopMode m_loop_mode;
|
||||||
|
ShuffleMode m_shuffle_mode;
|
||||||
|
|
||||||
Audio::ClientConnection& m_audio_client_connection;
|
Audio::ClientConnection& m_audio_client_connection;
|
||||||
PlaybackManager m_playback_manager;
|
PlaybackManager m_playback_manager;
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include "Playlist.h"
|
#include "Playlist.h"
|
||||||
|
|
||||||
#include <AK/LexicalPath.h>
|
#include <AK/LexicalPath.h>
|
||||||
|
#include <AK/Random.h>
|
||||||
#include <LibAudio/Loader.h>
|
#include <LibAudio/Loader.h>
|
||||||
#include <LibGUI/MessageBox.h>
|
#include <LibGUI/MessageBox.h>
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ void Playlist::try_fill_missing_info(Vector<M3UEntry>& entries, StringView path)
|
||||||
Vector<M3UEntry*> to_delete;
|
Vector<M3UEntry*> to_delete;
|
||||||
|
|
||||||
for (auto& entry : entries) {
|
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);
|
entry.path = String::formatted("{}/{}", playlist_path.dirname(), entry.path);
|
||||||
|
|
||||||
if (!entry.extended_info->file_size_in_bytes.has_value()) {
|
if (!entry.extended_info->file_size_in_bytes.has_value()) {
|
||||||
|
@ -69,11 +70,35 @@ StringView Playlist::next()
|
||||||
return {};
|
return {};
|
||||||
m_next_index_to_play = 0;
|
m_next_index_to_play = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto next = m_model->items().at(m_next_index_to_play).path;
|
auto next = m_model->items().at(m_next_index_to_play).path;
|
||||||
|
if (!shuffling()) {
|
||||||
m_next_index_to_play++;
|
m_next_index_to_play++;
|
||||||
return next;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
StringView Playlist::previous()
|
StringView Playlist::previous()
|
||||||
{
|
{
|
||||||
m_next_index_to_play--;
|
m_next_index_to_play--;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021, the SerenityOS developers.
|
* Copyright (c) 2021, the SerenityOS developers.
|
||||||
|
* Copyright (c) 2021, Leandro A. F. Pereira <leandro@tia.mat.br>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -8,6 +9,7 @@
|
||||||
|
|
||||||
#include "M3UParser.h"
|
#include "M3UParser.h"
|
||||||
#include "PlaylistWidget.h"
|
#include "PlaylistWidget.h"
|
||||||
|
#include <AK/StringHash.h>
|
||||||
#include <AK/StringView.h>
|
#include <AK/StringView.h>
|
||||||
#include <AK/Vector.h>
|
#include <AK/Vector.h>
|
||||||
|
|
||||||
|
@ -29,11 +31,40 @@ public:
|
||||||
void set_looping(bool looping) { m_looping = looping; }
|
void set_looping(bool looping) { m_looping = looping; }
|
||||||
bool looping() const { return m_looping; }
|
bool looping() const { return m_looping; }
|
||||||
|
|
||||||
|
void set_shuffling(bool shuffling) { m_shuffling = shuffling; }
|
||||||
|
bool shuffling() const { return m_shuffling; }
|
||||||
|
|
||||||
private:
|
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);
|
void try_fill_missing_info(Vector<M3UEntry>&, StringView);
|
||||||
|
|
||||||
RefPtr<PlaylistModel> m_model;
|
RefPtr<PlaylistModel> m_model;
|
||||||
bool m_looping { false };
|
bool m_looping { false };
|
||||||
|
bool m_shuffling { false };
|
||||||
|
|
||||||
|
BloomFilter m_previously_played_paths;
|
||||||
int m_next_index_to_play { 0 };
|
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)
|
void SoundPlayerWidgetAdvancedView::play_state_changed(Player::PlayState state)
|
||||||
{
|
{
|
||||||
m_back_button->set_enabled(playlist().size() > 1);
|
sync_previous_next_buttons();
|
||||||
m_next_button->set_enabled(playlist().size() > 1);
|
|
||||||
|
|
||||||
m_play_button->set_enabled(state != PlayState::NoFileLoaded);
|
m_play_button->set_enabled(state != PlayState::NoFileLoaded);
|
||||||
m_play_button->set_icon(state == PlayState::Playing ? *m_pause_icon : *m_play_icon);
|
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)
|
void SoundPlayerWidgetAdvancedView::time_elapsed(int seconds)
|
||||||
{
|
{
|
||||||
m_timestamp_label->set_text(String::formatted("Elapsed: {:02}:{:02}:{:02}", seconds / 3600, seconds / 60, seconds % 60));
|
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 play_state_changed(PlayState) override;
|
||||||
virtual void loop_mode_changed(LoopMode) override;
|
virtual void loop_mode_changed(LoopMode) override;
|
||||||
|
virtual void shuffle_mode_changed(ShuffleMode) override;
|
||||||
virtual void time_elapsed(int) override;
|
virtual void time_elapsed(int) override;
|
||||||
virtual void file_name_changed(StringView) override;
|
virtual void file_name_changed(StringView) override;
|
||||||
virtual void playlist_loaded(StringView, bool) override;
|
virtual void playlist_loaded(StringView, bool) override;
|
||||||
|
@ -50,6 +51,8 @@ protected:
|
||||||
void keydown_event(GUI::KeyEvent&) override;
|
void keydown_event(GUI::KeyEvent&) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void sync_previous_next_buttons();
|
||||||
|
|
||||||
void drop_event(GUI::DropEvent& event) override;
|
void drop_event(GUI::DropEvent& event) override;
|
||||||
GUI::Window& m_window;
|
GUI::Window& m_window;
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,14 @@ int main(int argc, char** argv)
|
||||||
playlist_toggle->set_checked(true);
|
playlist_toggle->set_checked(true);
|
||||||
playback_menu.add_action(playlist_toggle);
|
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");
|
auto& visualization_menu = window->add_menu("&Visualization");
|
||||||
GUI::ActionGroup visualization_actions;
|
GUI::ActionGroup visualization_actions;
|
||||||
visualization_actions.set_exclusive(true);
|
visualization_actions.set_exclusive(true);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue