SoundPlayer: Fix inconsistencies and code duplication

This is a first pass at refactoring SoundPlayer so that the View widget
is decoupled from the player itself.

In doing so, this fixed a couple of issues, including possibly
inconsistent states (e.g. player could be paused and stopped at the
same time).

With the change, Player actually controls the show, and calls methods
overriden by its subclasses to perform actions, such as update the Seek
bar; the hard work of massaging the raw data is done by the Player
class, so subclasses don't need to reimplement any of these things.

This also removes some copies of playlist management code that happened
to be copied+pasted inside callbacks of buttons -- it now lives inside
a neatly packaged Playlist class, and the Player only asks for the next
song to play.

In addition, the menu bar has been slightly rearranged.
This commit is contained in:
Leandro Pereira 2021-09-29 21:55:42 -07:00 committed by Andreas Kling
parent 73924f9416
commit 3126b78903
Notes: sideshowbarker 2024-07-18 01:52:49 +09:00
14 changed files with 447 additions and 304 deletions

View File

@ -6,13 +6,12 @@
#pragma once
#include "VisualizationBase.h"
#include "VisualizationWidget.h"
#include <AK/Complex.h>
#include <LibAudio/Buffer.h>
#include <LibGUI/Frame.h>
class BarsVisualizationWidget final : public GUI::Frame
, public Visualization {
class BarsVisualizationWidget final : public VisualizationWidget {
C_OBJECT(BarsVisualizationWidget)
public:

View File

@ -7,6 +7,8 @@ serenity_component(
set(SOURCES
main.cpp
Player.cpp
Playlist.cpp
PlaybackManager.cpp
SampleWidget.cpp
SoundPlayerWidgetAdvancedView.cpp

View File

@ -6,12 +6,11 @@
#pragma once
#include "VisualizationBase.h"
#include "VisualizationWidget.h"
#include <LibAudio/Buffer.h>
#include <LibGUI/Frame.h>
class NoVisualizationWidget final : public GUI::Frame
, public Visualization {
class NoVisualizationWidget final : public VisualizationWidget {
C_OBJECT(NoVisualizationWidget)
public:

View File

@ -24,6 +24,7 @@ public:
void loop(bool);
bool toggle_pause();
void set_loader(NonnullRefPtr<Audio::Loader>&&);
RefPtr<Audio::Loader> loader() const { return m_loader; }
size_t device_sample_rate() const { return m_device_sample_rate; }
int last_seek() const { return m_last_seek; }

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
* Copyright (c) 2021, Leandro A. F. Pereira <leandro@tia.mat.br>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Player.h"
Player::Player(Audio::ClientConnection& audio_client_connection)
: m_audio_client_connection(audio_client_connection)
, m_playback_manager(audio_client_connection)
{
m_playback_manager.on_update = [&]() {
auto samples_played = m_audio_client_connection.get_played_samples();
auto sample_rate = m_playback_manager.loader()->sample_rate();
float source_to_dest_ratio = static_cast<float>(sample_rate) / m_playback_manager.device_sample_rate();
samples_played *= source_to_dest_ratio;
samples_played += m_playback_manager.last_seek();
auto played_seconds = samples_played / sample_rate;
time_elapsed(played_seconds);
sound_buffer_played(m_playback_manager.current_buffer(), m_playback_manager.device_sample_rate(), samples_played);
};
m_playback_manager.on_finished_playing = [&]() {
set_play_state(PlayState::Stopped);
switch (loop_mode()) {
case LoopMode::File:
play_file_path(loaded_filename());
return;
case LoopMode::Playlist:
play_file_path(m_playlist.next());
return;
case LoopMode::None:
return;
}
};
}
void Player::play_file_path(StringView path)
{
if (path.is_null())
return;
if (!Core::File::exists(path)) {
audio_load_error(path, "File does not exist");
return;
}
if (path.ends_with(".m3u", AK::CaseSensitivity::CaseInsensitive) || path.ends_with(".m3u8", AK::CaseSensitivity::CaseInsensitive)) {
playlist_loaded(path, m_playlist.load(path));
return;
}
NonnullRefPtr<Audio::Loader> loader = Audio::Loader::create(path);
if (loader->has_error()) {
audio_load_error(path, loader->error_string());
return;
}
m_loaded_filename = path;
file_name_changed(path);
total_samples_changed(loader->total_samples());
m_playback_manager.set_loader(move(loader));
play();
}
void Player::set_play_state(PlayState state)
{
if (m_play_state != state) {
m_play_state = state;
play_state_changed(state);
}
}
void Player::set_loop_mode(LoopMode mode)
{
if (m_loop_mode != mode) {
m_loop_mode = mode;
m_playlist.set_looping(mode == LoopMode::Playlist);
loop_mode_changed(mode);
}
}
void Player::set_volume(double volume)
{
m_volume = clamp(volume, 0, 1.0);
m_audio_client_connection.set_self_volume(m_volume);
volume_changed(m_volume);
}
void Player::play()
{
m_playback_manager.play();
set_play_state(PlayState::Playing);
}
void Player::pause()
{
m_playback_manager.pause();
set_play_state(PlayState::Paused);
}
void Player::toggle_pause()
{
bool paused = m_playback_manager.toggle_pause();
set_play_state(paused ? PlayState::Paused : PlayState::Playing);
}
void Player::stop()
{
m_playback_manager.stop();
set_play_state(PlayState::Stopped);
}
void Player::seek(int sample)
{
m_playback_manager.seek(sample);
}

View File

@ -7,65 +7,74 @@
#pragma once
#include "PlaybackManager.h"
#include "Playlist.h"
#include "PlaylistWidget.h"
#include "VisualizationBase.h"
#include <AK/RefPtr.h>
struct PlayerState {
bool is_paused;
bool is_stopped;
bool has_loaded_file;
bool is_looping_file;
bool is_looping_playlist;
int loaded_file_samplerate;
double volume;
Audio::ClientConnection& connection;
PlaybackManager& manager;
String loaded_filename;
};
class Player {
public:
explicit Player(PlayerState& state)
: m_player_state(state) {};
virtual void open_file(StringView path) = 0;
virtual void play() = 0;
enum class PlayState {
NoFileLoaded,
Paused,
Stopped,
Playing,
};
enum class LoopMode {
None,
File,
Playlist,
};
PlayerState& get_player_state() { return m_player_state; }
bool is_stopped() const { return m_player_state.is_stopped; }
bool is_paused() const { return m_player_state.is_paused; }
bool has_loaded_file() const { return m_player_state.has_loaded_file; }
double volume() const { return m_player_state.volume; }
bool looping() const { return m_player_state.is_looping_file; }
bool looping_playlist() const { return m_player_state.is_looping_playlist; }
const String& loaded_filename() { return m_player_state.loaded_filename; }
int loaded_file_samplerate() { return m_player_state.loaded_file_samplerate; }
explicit Player(Audio::ClientConnection& audio_client_connection);
virtual ~Player() { }
virtual void set_stopped(bool stopped) { m_player_state.is_stopped = stopped; }
virtual void set_paused(bool paused) { m_player_state.is_paused = paused; }
virtual void set_has_loaded_file(bool loaded) { m_player_state.has_loaded_file = loaded; }
virtual void set_volume(double volume)
{
m_player_state.volume = volume;
client_connection().set_self_volume(volume);
}
virtual void set_loaded_file_samplerate(int samplerate) { m_player_state.loaded_file_samplerate = samplerate; }
virtual void set_looping_file(bool loop)
{
m_player_state.is_looping_file = loop;
}
virtual void set_looping_playlist(bool loop)
{
m_player_state.is_looping_playlist = loop;
}
virtual void set_loaded_filename(StringView& filename) { m_player_state.loaded_filename = filename; }
void play_file_path(StringView path);
Audio::ClientConnection& client_connection() { return m_player_state.connection; }
PlaybackManager& manager() { return m_player_state.manager; }
Playlist& playlist() { return m_playlist; }
StringView loaded_filename() const { return m_loaded_filename; }
PlayState play_state() const { return m_play_state; }
void set_play_state(PlayState state);
LoopMode loop_mode() const { return m_loop_mode; }
void set_loop_mode(LoopMode mode);
double volume() const { return m_volume; }
void set_volume(double value);
void play();
void pause();
void toggle_pause();
void stop();
void seek(int sample);
virtual void play_state_changed(PlayState) = 0;
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) { }
protected:
virtual ~Player() = default;
void done_initializing()
{
set_play_state(PlayState::NoFileLoaded);
set_loop_mode(LoopMode::None);
time_elapsed(0);
set_volume(1.);
}
PlayerState m_player_state;
RefPtr<PlaylistModel> m_playlist_model;
private:
Playlist m_playlist;
PlayState m_play_state;
LoopMode m_loop_mode;
Audio::ClientConnection& m_audio_client_connection;
PlaybackManager m_playback_manager;
StringView m_loaded_filename;
double m_volume { 0 };
};

View File

@ -0,0 +1,85 @@
/*
* 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 <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;
m_next_index_to_play++;
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;
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2021, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "M3UParser.h"
#include "PlaylistWidget.h"
#include <AK/StringView.h>
#include <AK/Vector.h>
class Playlist {
public:
Playlist()
: m_model(adopt_ref(*new PlaylistModel()))
{
}
bool load(StringView);
RefPtr<PlaylistModel> model() { return m_model; }
int size() { return m_model->items().size(); }
StringView next();
StringView previous();
void set_looping(bool looping) { m_looping = looping; }
bool looping() const { return m_looping; }
private:
void try_fill_missing_info(Vector<M3UEntry>&, StringView);
RefPtr<PlaylistModel> m_model;
bool m_looping { false };
int m_next_index_to_play { 0 };
};

View File

@ -24,7 +24,7 @@ PlaylistWidget::PlaylistWidget()
auto index = m_table_view->index_at_event_position(point);
if (!index.is_valid())
return;
player->open_file(m_table_view->model()->data(index, static_cast<GUI::ModelRole>(PlaylistModelCustomRole::FilePath)).as_string());
player->play_file_path(m_table_view->model()->data(index, static_cast<GUI::ModelRole>(PlaylistModelCustomRole::FilePath)).as_string());
};
}

View File

@ -6,15 +6,14 @@
#pragma once
#include "VisualizationBase.h"
#include "VisualizationWidget.h"
#include <LibGUI/Frame.h>
namespace Audio {
class Buffer;
}
class SampleWidget final : public GUI::Frame
, public Visualization {
class SampleWidget final : public VisualizationWidget {
C_OBJECT(SampleWidget)
public:
virtual ~SampleWidget() override;

View File

@ -24,8 +24,8 @@
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window, PlayerState& state)
: Player(state)
SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window, Audio::ClientConnection& connection)
: Player(connection)
, m_window(window)
{
window.resize(455, 350);
@ -36,10 +36,9 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
set_layout<GUI::VerticalBoxLayout>();
m_splitter = add<GUI::HorizontalSplitter>();
m_player_view = m_splitter->add<GUI::Widget>();
m_playlist_model = adopt_ref(*new PlaylistModel());
m_playlist_widget = PlaylistWidget::construct();
m_playlist_widget->set_data_model(m_playlist_model);
m_playlist_widget->set_data_model(playlist().model());
m_playlist_widget->set_fixed_width(150);
m_player_view->set_layout<GUI::VerticalBoxLayout>();
@ -52,84 +51,56 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
m_visualization = m_player_view->add<BarsVisualizationWidget>();
// Set a temporary value for total samples.
// This value will be set properly when we load a new file.
const int total_samples = this->manager().total_length() * this->manager().device_sample_rate();
m_playback_progress_slider = m_player_view->add<AutoSlider>(Orientation::Horizontal);
m_playback_progress_slider->set_fixed_height(20);
m_playback_progress_slider->set_jump_to_cursor(true);
m_playback_progress_slider->set_min(0);
m_playback_progress_slider->set_max(total_samples);
m_playback_progress_slider->set_page_step(total_samples / 10);
m_playback_progress_slider->on_knob_released = [&](int value) {
this->manager().seek(value);
seek(value);
};
auto& toolbar_container = m_player_view->add<GUI::ToolbarContainer>();
auto& menubar = toolbar_container.add<GUI::Toolbar>();
m_play_button = menubar.add<GUI::Button>();
m_play_button->set_icon(is_paused() ? (!has_loaded_file() ? *m_play_icon : *m_pause_icon) : *m_pause_icon);
m_play_button->set_icon(*m_play_icon);
m_play_button->set_fixed_width(50);
m_play_button->set_enabled(has_loaded_file());
m_play_button->set_enabled(false);
m_play_button->on_click = [&](unsigned) {
bool paused = this->manager().toggle_pause();
set_paused(paused);
m_play_button->set_icon(paused ? *m_play_icon : *m_pause_icon);
m_stop_button->set_enabled(has_loaded_file());
toggle_pause();
};
m_stop_button = menubar.add<GUI::Button>();
m_stop_button->set_icon(*m_stop_icon);
m_stop_button->set_fixed_width(50);
m_stop_button->set_enabled(has_loaded_file());
m_stop_button->set_enabled(false);
m_stop_button->on_click = [&](unsigned) {
this->manager().stop();
set_stopped(true);
m_play_button->set_icon(*m_play_icon);
m_stop_button->set_enabled(false);
stop();
};
auto& timestamp_label = menubar.add<GUI::Label>();
timestamp_label.set_fixed_width(110);
timestamp_label.set_text("Elapsed: 00:00:00");
m_timestamp_label = menubar.add<GUI::Label>();
m_timestamp_label->set_fixed_width(110);
// filler_label
menubar.add<GUI::Label>();
m_back_button = menubar.add<GUI::Button>();
m_back_button->set_fixed_width(50);
m_back_button->set_icon(*m_back_icon);
m_back_button->set_enabled(has_loaded_file());
m_back_button->set_enabled(false);
m_back_button->on_click = [&](unsigned) {
if (!m_playlist_model.is_null()) {
auto it = m_playlist_model->items().find_if([&](const M3UEntry& e) { return e.path == loaded_filename(); });
if (it.index() == 0) {
open_file(m_playlist_model->items().at(m_playlist_model->items().size() - 1).path);
} else
open_file((it - 1)->path);
play();
}
play_file_path(playlist().previous());
};
m_next_button = menubar.add<GUI::Button>();
m_next_button->set_fixed_width(50);
m_next_button->set_icon(*m_next_icon);
m_next_button->set_enabled(has_loaded_file());
m_next_button->set_enabled(false);
m_next_button->on_click = [&](unsigned) {
if (!m_playlist_model.is_null()) {
auto it = m_playlist_model->items().find_if([&](const M3UEntry& e) { return e.path == loaded_filename(); });
if (it.index() + 1 == m_playlist_model->items().size()) {
open_file(m_playlist_model->items().at(0).path);
} else
open_file((it + 1)->path);
play();
}
play_file_path(playlist().next());
};
m_volume_label = &menubar.add<GUI::Label>();
m_volume_label->set_fixed_width(30);
m_volume_label->set_text("100%");
auto& volume_slider = menubar.add<GUI::HorizontalSlider>();
volume_slider.set_fixed_width(95);
@ -139,88 +110,12 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
volume_slider.on_change = [&](int value) {
double volume = m_nonlinear_volume_slider ? (double)(value * value) / (100 * 100) : value / 100.;
m_volume_label->set_text(String::formatted("{}%", (int)(volume * 100)));
set_volume(volume);
};
set_volume(1.);
set_nonlinear_volume_slider(false);
manager().on_update = [&]() {
// Determine how many of the source file samples have played.
int samples_played = client_connection().get_played_samples();
float source_to_dest_ratio = static_cast<float>(loaded_file_samplerate()) / manager().device_sample_rate();
samples_played *= source_to_dest_ratio;
samples_played += this->manager().last_seek();
int current_second = samples_played / loaded_file_samplerate();
timestamp_label.set_text(String::formatted("Elapsed: {:02}:{:02}:{:02}", current_second / 3600, current_second / 60, current_second % 60));
if (!m_playback_progress_slider->mouse_is_down()) {
m_playback_progress_slider->set_value(samples_played);
}
dynamic_cast<Visualization*>(m_visualization.ptr())->set_buffer(this->manager().current_buffer());
dynamic_cast<Visualization*>(m_visualization.ptr())->set_samplerate(manager().device_sample_rate());
};
manager().on_load_sample_buffer = [&](Audio::Buffer&) {
//TODO: Implement an equalizer
return;
};
manager().on_finished_playing = [&] {
m_play_button->set_icon(*m_play_icon);
if (looping()) {
open_file(loaded_filename());
return;
}
if (!m_playlist_model.is_null() && !m_playlist_model->items().is_empty()) {
auto it = m_playlist_model->items().find_if([&](const M3UEntry& e) { return e.path == loaded_filename(); });
if (it.index() + 1 == m_playlist_model->items().size()) {
if (looping_playlist()) {
open_file(m_playlist_model->items().at(0).path);
return;
}
} else
open_file((it + 1)->path);
}
m_stop_button->set_enabled(false);
};
}
void SoundPlayerWidgetAdvancedView::open_file(StringView path)
{
if (!Core::File::exists(path)) {
GUI::MessageBox::show(window(), String::formatted("File \"{}\" does not exist", path), "Error opening file", GUI::MessageBox::Type::Error);
return;
}
if (path.ends_with(".m3u", AK::CaseSensitivity::CaseInsensitive) || path.ends_with(".m3u8", AK::CaseSensitivity::CaseInsensitive)) {
read_playlist(path);
return;
}
NonnullRefPtr<Audio::Loader> loader = Audio::Loader::create(path);
if (loader->has_error() || !loader->sample_rate()) {
const String error_string = loader->error_string();
GUI::MessageBox::show(&m_window, String::formatted("Failed to load audio file: {} ({})", path, error_string.is_null() ? "Unknown error" : error_string),
"Filetype error", GUI::MessageBox::Type::Error);
return;
}
m_window.set_title(String::formatted("{} - Sound Player", loader->file()->filename()));
m_playback_progress_slider->set_max(loader->total_samples());
m_playback_progress_slider->set_page_step(loader->total_samples() / 10);
m_playback_progress_slider->set_enabled(true);
m_play_button->set_enabled(true);
m_play_button->set_icon(*m_pause_icon);
m_stop_button->set_enabled(true);
manager().set_loader(move(loader));
set_has_loaded_file(true);
set_loaded_file_samplerate(loader->sample_rate());
set_loaded_filename(path);
play();
done_initializing();
}
void SoundPlayerWidgetAdvancedView::set_nonlinear_volume_slider(bool nonlinear)
@ -237,7 +132,8 @@ void SoundPlayerWidgetAdvancedView::drop_event(GUI::DropEvent& event)
if (urls.is_empty())
return;
window()->move_to_front();
open_file(urls.first().path());
// FIXME: Add all paths from drop event to the playlist
play_file_path(urls.first().path());
}
}
@ -249,85 +145,73 @@ void SoundPlayerWidgetAdvancedView::keydown_event(GUI::KeyEvent& event)
GUI::Widget::keydown_event(event);
}
SoundPlayerWidgetAdvancedView::~SoundPlayerWidgetAdvancedView()
{
manager().on_load_sample_buffer = nullptr;
manager().on_update = nullptr;
}
void SoundPlayerWidgetAdvancedView::play()
{
manager().play();
set_paused(false);
set_stopped(false);
}
void SoundPlayerWidgetAdvancedView::read_playlist(StringView path)
{
auto parser = M3UParser::from_file(path);
auto items = parser->parse(true);
VERIFY(items->size() > 0);
try_fill_missing_info(*items, path);
for (auto& item : *items)
m_playlist_model->items().append(item);
set_playlist_visible(true);
m_playlist_model->invalidate();
open_file(items->at(0).path);
if (items->size() > 1) {
m_back_button->set_enabled(true);
m_next_button->set_enabled(true);
} else {
m_back_button->set_enabled(false);
m_next_button->set_enabled(false);
}
}
void SoundPlayerWidgetAdvancedView::set_playlist_visible(bool visible)
{
if (visible) {
if (!m_playlist_widget->parent()) {
m_player_view->parent_widget()->add_child(*m_playlist_widget);
}
} else {
if (!visible) {
m_playlist_widget->remove_from_parent();
m_player_view->set_max_width(window()->width());
} else if (!m_playlist_widget->parent()) {
m_player_view->parent_widget()->add_child(*m_playlist_widget);
}
}
void SoundPlayerWidgetAdvancedView::try_fill_missing_info(Vector<M3UEntry>& entries, StringView playlist_p)
void SoundPlayerWidgetAdvancedView::play_state_changed(Player::PlayState state)
{
LexicalPath playlist_path(playlist_p);
Vector<M3UEntry*> to_delete;
for (auto& entry : entries) {
if (!LexicalPath(entry.path).is_absolute()) {
entry.path = String::formatted("{}/{}", playlist_path.dirname(), entry.path);
}
m_back_button->set_enabled(playlist().size() > 1);
m_next_button->set_enabled(playlist().size() > 1);
if (!Core::File::exists(entry.path)) {
GUI::MessageBox::show(window(), String::formatted("The file \"{}\" present in the playlist does not exist or was not found. This file will be ignored.", entry.path), "Error reading playlist", GUI::MessageBox::Type::Warning);
to_delete.append(&entry);
continue;
}
m_play_button->set_enabled(state != PlayState::NoFileLoaded);
m_play_button->set_icon(state == PlayState::Playing ? *m_pause_icon : *m_play_icon);
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()) {
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 embedded metadata extractor for other audio formats
}
//TODO: Implement a metadata parser for the uncomfortably numerous popular embedded metadata formats
m_stop_button->set_enabled(state != PlayState::Stopped && state != PlayState::NoFileLoaded);
if (!entry.extended_info->file_size_in_bytes.has_value()) {
FILE* f = fopen(entry.path.characters(), "r");
VERIFY(f != nullptr);
fseek(f, 0, SEEK_END);
entry.extended_info->file_size_in_bytes = ftell(f);
fclose(f);
}
}
for (M3UEntry* entry : to_delete)
entries.remove_first_matching([&](M3UEntry& e) { return &e == entry; });
m_playback_progress_slider->set_enabled(state != PlayState::NoFileLoaded);
}
void SoundPlayerWidgetAdvancedView::loop_mode_changed(Player::LoopMode)
{
}
void SoundPlayerWidgetAdvancedView::time_elapsed(int seconds)
{
m_timestamp_label->set_text(String::formatted("Elapsed: {:02}:{:02}:{:02}", seconds / 3600, seconds / 60, seconds % 60));
}
void SoundPlayerWidgetAdvancedView::file_name_changed(StringView name)
{
m_window.set_title(String::formatted("{} - Sound Player", name));
}
void SoundPlayerWidgetAdvancedView::total_samples_changed(int total_samples)
{
m_playback_progress_slider->set_max(total_samples);
m_playback_progress_slider->set_page_step(total_samples / 10);
}
void SoundPlayerWidgetAdvancedView::sound_buffer_played(RefPtr<Audio::Buffer> buffer, int sample_rate, int samples_played)
{
m_visualization->set_buffer(buffer);
m_visualization->set_samplerate(sample_rate);
m_playback_progress_slider->set_value(samples_played);
}
void SoundPlayerWidgetAdvancedView::volume_changed(double volume)
{
m_volume_label->set_text(String::formatted("{}%", static_cast<int>(volume * 100)));
}
void SoundPlayerWidgetAdvancedView::playlist_loaded(StringView path, bool loaded)
{
if (!loaded) {
GUI::MessageBox::show(&m_window, String::formatted("Could not load playlist at \"{}\".", path), "Error opening playlist", GUI::MessageBox::Type::Error);
return;
}
set_playlist_visible(true);
play_file_path(playlist().next());
}
void SoundPlayerWidgetAdvancedView::audio_load_error(StringView path, StringView error_string)
{
GUI::MessageBox::show(&m_window, String::formatted("Failed to load audio file: {} ({})", path, error_string.is_null() ? "Unknown error" : error_string),
"Filetype error", GUI::MessageBox::Type::Error);
}

View File

@ -7,10 +7,10 @@
#pragma once
#include "BarsVisualizationWidget.h"
#include "Common.h"
#include "PlaybackManager.h"
#include "Player.h"
#include "VisualizationWidget.h"
#include <AK/NonnullRefPtr.h>
#include <LibAudio/ClientConnection.h>
#include <LibGUI/Splitter.h>
@ -21,15 +21,10 @@ class SoundPlayerWidgetAdvancedView final : public GUI::Widget
C_OBJECT(SoundPlayerWidgetAdvancedView)
public:
explicit SoundPlayerWidgetAdvancedView(GUI::Window& window, PlayerState& state);
~SoundPlayerWidgetAdvancedView() override;
explicit SoundPlayerWidgetAdvancedView(GUI::Window&, Audio::ClientConnection&);
void open_file(StringView path) override;
void read_playlist(StringView path);
void play() override;
void set_nonlinear_volume_slider(bool nonlinear);
void set_playlist_visible(bool visible);
void try_fill_missing_info(Vector<M3UEntry>& entries, StringView playlist_p);
template<typename T>
void set_visualization()
@ -41,6 +36,16 @@ public:
m_visualization = new_visualization;
}
virtual void play_state_changed(PlayState) override;
virtual void loop_mode_changed(LoopMode) override;
virtual void time_elapsed(int) override;
virtual void file_name_changed(StringView) override;
virtual void playlist_loaded(StringView, bool) override;
virtual void audio_load_error(StringView path, StringView error_reason) override;
virtual void volume_changed(double) override;
virtual void total_samples_changed(int) override;
virtual void sound_buffer_played(RefPtr<Audio::Buffer>, int sample_rate, int samples_played) override;
protected:
void keydown_event(GUI::KeyEvent&) override;
@ -51,7 +56,7 @@ private:
RefPtr<GUI::HorizontalSplitter> m_splitter;
RefPtr<GUI::Widget> m_player_view;
RefPtr<PlaylistWidget> m_playlist_widget;
RefPtr<GUI::Widget> m_visualization;
RefPtr<VisualizationWidget> m_visualization;
RefPtr<Gfx::Bitmap> m_play_icon;
RefPtr<Gfx::Bitmap> m_pause_icon;
@ -65,7 +70,7 @@ private:
RefPtr<GUI::Button> m_next_button;
RefPtr<AutoSlider> m_playback_progress_slider;
RefPtr<GUI::Label> m_volume_label;
RefPtr<GUI::Label> m_timestamp_label;
bool m_nonlinear_volume_slider;
size_t m_device_sample_rate { 44100 };
};

View File

@ -7,12 +7,15 @@
#pragma once
#include <LibAudio/Buffer.h>
#include <LibGUI/Frame.h>
class VisualizationWidget : public GUI::Frame {
C_OBJECT(VisualizationWidget)
class Visualization {
public:
virtual void set_buffer(RefPtr<Audio::Buffer> buffer) = 0;
virtual void set_samplerate(int) { }
protected:
virtual ~Visualization() = default;
virtual ~VisualizationWidget() = default;
};

View File

@ -5,6 +5,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "BarsVisualizationWidget.h"
#include "NoVisualizationWidget.h"
#include "Player.h"
#include "SampleWidget.h"
@ -35,72 +36,67 @@ int main(int argc, char** argv)
return 1;
}
PlaybackManager playback_manager(audio_client);
PlayerState initial_player_state { true,
true,
false,
false,
false,
44100,
1.0,
audio_client,
playback_manager,
"" };
auto app_icon = GUI::Icon::default_icon("app-sound-player");
auto window = GUI::Window::construct();
window->set_title("Sound Player");
window->set_icon(app_icon.bitmap_for_size(16));
auto& file_menu = window->add_menu("&File");
auto& playlist_menu = window->add_menu("Play&list");
String path = argv[1];
// start in advanced view by default
Player* player = &window->set_main_widget<SoundPlayerWidgetAdvancedView>(window, initial_player_state);
Player* player = &window->set_main_widget<SoundPlayerWidgetAdvancedView>(window, audio_client);
if (argc > 1) {
player->open_file(path);
player->play_file_path(path);
}
auto& file_menu = window->add_menu("&File");
file_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
Optional<String> path = GUI::FilePicker::get_open_filepath(window, "Open sound file...");
if (path.has_value()) {
player->open_file(path.value());
player->play_file_path(path.value());
}
}));
auto linear_volume_slider = GUI::Action::create_checkable("&Nonlinear Volume Slider", [&](auto& action) {
static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_nonlinear_volume_slider(action.is_checked());
});
file_menu.add_action(linear_volume_slider);
auto playlist_toggle = GUI::Action::create_checkable("&Show Playlist", [&](auto& action) {
static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_playlist_visible(action.is_checked());
});
playlist_menu.add_action(playlist_toggle);
if (path.ends_with(".m3u") || path.ends_with(".m3u8"))
playlist_toggle->set_checked(true);
playlist_menu.add_separator();
auto playlist_loop_toggle = GUI::Action::create_checkable("&Loop Playlist", [&](auto& action) {
static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_looping_playlist(action.is_checked());
});
playlist_menu.add_action(playlist_loop_toggle);
file_menu.add_separator();
file_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
app->quit();
}));
auto& playback_menu = window->add_menu("&Playback");
auto loop = GUI::Action::create_checkable("&Loop", { Mod_Ctrl, Key_R }, [&](auto& action) {
player->set_looping_file(action.is_checked());
GUI::ActionGroup loop_actions;
loop_actions.set_exclusive(true);
auto loop_none = GUI::Action::create_checkable("&No Loop", [&](auto&) {
player->set_loop_mode(Player::LoopMode::None);
});
loop_none->set_checked(true);
loop_actions.add_action(loop_none);
playback_menu.add_action(loop_none);
playback_menu.add_action(move(loop));
auto loop_file = GUI::Action::create_checkable("Loop &File", { Mod_Ctrl, Key_F }, [&](auto&) {
player->set_loop_mode(Player::LoopMode::File);
});
loop_actions.add_action(loop_file);
playback_menu.add_action(loop_file);
auto loop_playlist = GUI::Action::create_checkable("Loop &Playlist", { Mod_Ctrl, Key_P }, [&](auto&) {
player->set_loop_mode(Player::LoopMode::Playlist);
});
loop_actions.add_action(loop_playlist);
playback_menu.add_action(loop_playlist);
auto linear_volume_slider = GUI::Action::create_checkable("&Nonlinear Volume Slider", [&](auto& action) {
static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_nonlinear_volume_slider(action.is_checked());
});
playback_menu.add_separator();
playback_menu.add_action(linear_volume_slider);
playback_menu.add_separator();
auto playlist_toggle = GUI::Action::create_checkable("&Show Playlist", [&](auto& action) {
static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_playlist_visible(action.is_checked());
});
if (path.ends_with(".m3u") || path.ends_with(".m3u8"))
playlist_toggle->set_checked(true);
playback_menu.add_action(playlist_toggle);
auto& visualization_menu = window->add_menu("&Visualization");
GUI::ActionGroup visualization_actions;