mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-13 01:59:14 +03:00
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:
parent
73924f9416
commit
3126b78903
Notes:
sideshowbarker
2024-07-18 01:52:49 +09:00
Author: https://github.com/lpereira Commit: https://github.com/SerenityOS/serenity/commit/3126b78903a Pull-request: https://github.com/SerenityOS/serenity/pull/10278 Reviewed-by: https://github.com/kleinesfilmroellchen ✅
@ -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:
|
||||
|
@ -7,6 +7,8 @@ serenity_component(
|
||||
|
||||
set(SOURCES
|
||||
main.cpp
|
||||
Player.cpp
|
||||
Playlist.cpp
|
||||
PlaybackManager.cpp
|
||||
SampleWidget.cpp
|
||||
SoundPlayerWidgetAdvancedView.cpp
|
||||
|
@ -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:
|
||||
|
@ -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; }
|
||||
|
122
Userland/Applications/SoundPlayer/Player.cpp
Normal file
122
Userland/Applications/SoundPlayer/Player.cpp
Normal 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);
|
||||
}
|
@ -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 };
|
||||
};
|
||||
|
85
Userland/Applications/SoundPlayer/Playlist.cpp
Normal file
85
Userland/Applications/SoundPlayer/Playlist.cpp
Normal 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;
|
||||
}
|
39
Userland/Applications/SoundPlayer/Playlist.h
Normal file
39
Userland/Applications/SoundPlayer/Playlist.h
Normal 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 };
|
||||
};
|
@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user