1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-14 07:57:35 +00:00
serenity/Userland/Applications/SoundPlayer/Playlist.cpp
Leandro Pereira 314b8a374b 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).
2021-10-25 23:37:18 +02:00

110 lines
3.3 KiB
C++

/*
* Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
* Copyright (c) 2021, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Playlist.h"
#include <AK/LexicalPath.h>
#include <AK/Random.h>
#include <LibAudio/Loader.h>
#include <LibGUI/MessageBox.h>
bool Playlist::load(StringView path)
{
auto parser = M3UParser::from_file(path);
auto items = parser->parse(true);
if (items->size() <= 0)
return false;
try_fill_missing_info(*items, path);
for (auto& item : *items)
m_model->items().append(item);
m_model->invalidate();
return true;
}
void Playlist::try_fill_missing_info(Vector<M3UEntry>& entries, StringView path)
{
LexicalPath playlist_path(path);
Vector<M3UEntry*> to_delete;
for (auto& entry : entries) {
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()) {
auto size = Core::File::size(entry.path);
if (size.is_error())
continue;
entry.extended_info->file_size_in_bytes = size.value();
} else if (!Core::File::exists(entry.path)) {
to_delete.append(&entry);
continue;
}
if (!entry.extended_info->track_display_title.has_value())
entry.extended_info->track_display_title = LexicalPath::title(entry.path);
if (!entry.extended_info->track_length_in_seconds.has_value()) {
//TODO: Implement embedded metadata extractor for other audio formats
if (auto reader = Audio::Loader::create(entry.path); !reader->has_error())
entry.extended_info->track_length_in_seconds = reader->total_samples() / reader->sample_rate();
}
//TODO: Implement a metadata parser for the uncomfortably numerous popular embedded metadata formats
}
for (auto& entry : to_delete)
entries.remove_first_matching([&](M3UEntry& e) { return &e == entry; });
}
StringView Playlist::next()
{
if (m_next_index_to_play >= size()) {
if (!looping())
return {};
m_next_index_to_play = 0;
}
auto next = m_model->items().at(m_next_index_to_play).path;
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;
}
StringView Playlist::previous()
{
m_next_index_to_play--;
if (m_next_index_to_play < 0) {
m_next_index_to_play = 0;
return {};
}
return m_model->items().at(m_next_index_to_play).path;
}