summaryrefslogtreecommitdiff
path: root/Userland/Applications/SoundPlayer
diff options
context:
space:
mode:
authorLeandro Pereira <leandro@hardinfo.org>2021-09-29 21:55:42 -0700
committerAndreas Kling <kling@serenityos.org>2021-10-25 23:37:18 +0200
commit3126b78903a4cf8bdb91a0d4a2551558bf1cb77f (patch)
tree6a7e40356d875c5658b28df6c5037557099f6b23 /Userland/Applications/SoundPlayer
parent73924f94164c2bccf5097d68d1597abb5621f036 (diff)
downloadserenity-3126b78903a4cf8bdb91a0d4a2551558bf1cb77f.zip
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.
Diffstat (limited to 'Userland/Applications/SoundPlayer')
-rw-r--r--Userland/Applications/SoundPlayer/BarsVisualizationWidget.h5
-rw-r--r--Userland/Applications/SoundPlayer/CMakeLists.txt2
-rw-r--r--Userland/Applications/SoundPlayer/NoVisualizationWidget.h5
-rw-r--r--Userland/Applications/SoundPlayer/PlaybackManager.h1
-rw-r--r--Userland/Applications/SoundPlayer/Player.cpp122
-rw-r--r--Userland/Applications/SoundPlayer/Player.h107
-rw-r--r--Userland/Applications/SoundPlayer/Playlist.cpp85
-rw-r--r--Userland/Applications/SoundPlayer/Playlist.h39
-rw-r--r--Userland/Applications/SoundPlayer/PlaylistWidget.cpp2
-rw-r--r--Userland/Applications/SoundPlayer/SampleWidget.h5
-rw-r--r--Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp258
-rw-r--r--Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h23
-rw-r--r--Userland/Applications/SoundPlayer/VisualizationWidget.h (renamed from Userland/Applications/SoundPlayer/VisualizationBase.h)7
-rw-r--r--Userland/Applications/SoundPlayer/main.cpp76
14 files changed, 440 insertions, 297 deletions
diff --git a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h
index 679928e60c..262229537e 100644
--- a/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h
+++ b/Userland/Applications/SoundPlayer/BarsVisualizationWidget.h
@@ -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:
diff --git a/Userland/Applications/SoundPlayer/CMakeLists.txt b/Userland/Applications/SoundPlayer/CMakeLists.txt
index 9fe6266b34..578078938e 100644
--- a/Userland/Applications/SoundPlayer/CMakeLists.txt
+++ b/Userland/Applications/SoundPlayer/CMakeLists.txt
@@ -7,6 +7,8 @@ serenity_component(
set(SOURCES
main.cpp
+ Player.cpp
+ Playlist.cpp
PlaybackManager.cpp
SampleWidget.cpp
SoundPlayerWidgetAdvancedView.cpp
diff --git a/Userland/Applications/SoundPlayer/NoVisualizationWidget.h b/Userland/Applications/SoundPlayer/NoVisualizationWidget.h
index 8132225689..ae1153cede 100644
--- a/Userland/Applications/SoundPlayer/NoVisualizationWidget.h
+++ b/Userland/Applications/SoundPlayer/NoVisualizationWidget.h
@@ -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:
diff --git a/Userland/Applications/SoundPlayer/PlaybackManager.h b/Userland/Applications/SoundPlayer/PlaybackManager.h
index 9d051d2cf8..d791e2ea25 100644
--- a/Userland/Applications/SoundPlayer/PlaybackManager.h
+++ b/Userland/Applications/SoundPlayer/PlaybackManager.h
@@ -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; }
diff --git a/Userland/Applications/SoundPlayer/Player.cpp b/Userland/Applications/SoundPlayer/Player.cpp
new file mode 100644
index 0000000000..e0cb980bac
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/Player.cpp
@@ -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);
+}
diff --git a/Userland/Applications/SoundPlayer/Player.h b/Userland/Applications/SoundPlayer/Player.h
index 23b4fdfebe..9846fc2177 100644
--- a/Userland/Applications/SoundPlayer/Player.h
+++ b/Userland/Applications/SoundPlayer/Player.h
@@ -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)
+ void play_file_path(StringView path);
+
+ 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:
+ void done_initializing()
{
- m_player_state.is_looping_playlist = loop;
+ set_play_state(PlayState::NoFileLoaded);
+ set_loop_mode(LoopMode::None);
+ time_elapsed(0);
+ set_volume(1.);
}
- virtual void set_loaded_filename(StringView& filename) { m_player_state.loaded_filename = filename; }
- Audio::ClientConnection& client_connection() { return m_player_state.connection; }
- PlaybackManager& manager() { return m_player_state.manager; }
+private:
+ Playlist m_playlist;
+ PlayState m_play_state;
+ LoopMode m_loop_mode;
-protected:
- virtual ~Player() = default;
+ Audio::ClientConnection& m_audio_client_connection;
+ PlaybackManager m_playback_manager;
- PlayerState m_player_state;
- RefPtr<PlaylistModel> m_playlist_model;
+ StringView m_loaded_filename;
+ double m_volume { 0 };
};
diff --git a/Userland/Applications/SoundPlayer/Playlist.cpp b/Userland/Applications/SoundPlayer/Playlist.cpp
new file mode 100644
index 0000000000..5faf3a10cd
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/Playlist.cpp
@@ -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;
+}
diff --git a/Userland/Applications/SoundPlayer/Playlist.h b/Userland/Applications/SoundPlayer/Playlist.h
new file mode 100644
index 0000000000..d41d38e208
--- /dev/null
+++ b/Userland/Applications/SoundPlayer/Playlist.h
@@ -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 };
+};
diff --git a/Userland/Applications/SoundPlayer/PlaylistWidget.cpp b/Userland/Applications/SoundPlayer/PlaylistWidget.cpp
index fb7e737b27..95780659ab 100644
--- a/Userland/Applications/SoundPlayer/PlaylistWidget.cpp
+++ b/Userland/Applications/SoundPlayer/PlaylistWidget.cpp
@@ -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());
};
}
diff --git a/Userland/Applications/SoundPlayer/SampleWidget.h b/Userland/Applications/SoundPlayer/SampleWidget.h
index 5f8dbf51de..63f3b8dc66 100644
--- a/Userland/Applications/SoundPlayer/SampleWidget.h
+++ b/Userland/Applications/SoundPlayer/SampleWidget.h
@@ -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;
diff --git a/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp b/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp
index dfe25e1507..8259a4041e 100644
--- a/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp
+++ b/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp
@@ -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()
+void SoundPlayerWidgetAdvancedView::set_playlist_visible(bool visible)
{
- manager().on_load_sample_buffer = nullptr;
- manager().on_update = nullptr;
+ 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::play()
+void SoundPlayerWidgetAdvancedView::play_state_changed(Player::PlayState state)
{
- manager().play();
- set_paused(false);
- set_stopped(false);
+ m_back_button->set_enabled(playlist().size() > 1);
+ m_next_button->set_enabled(playlist().size() > 1);
+
+ m_play_button->set_enabled(state != PlayState::NoFileLoaded);
+ m_play_button->set_icon(state == PlayState::Playing ? *m_pause_icon : *m_play_icon);
+
+ m_stop_button->set_enabled(state != PlayState::Stopped && state != PlayState::NoFileLoaded);
+
+ m_playback_progress_slider->set_enabled(state != PlayState::NoFileLoaded);
}
-void SoundPlayerWidgetAdvancedView::read_playlist(StringView path)
+void SoundPlayerWidgetAdvancedView::loop_mode_changed(Player::LoopMode)
{
- 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);
+void SoundPlayerWidgetAdvancedView::time_elapsed(int seconds)
+{
+ m_timestamp_label->set_text(String::formatted("Elapsed: {:02}:{:02}:{:02}", seconds / 3600, seconds / 60, seconds % 60));
+}
- 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::file_name_changed(StringView name)
+{
+ m_window.set_title(String::formatted("{} - Sound Player", name));
}
-void SoundPlayerWidgetAdvancedView::set_playlist_visible(bool visible)
+void SoundPlayerWidgetAdvancedView::total_samples_changed(int total_samples)
{
- if (visible) {
- if (!m_playlist_widget->parent()) {
- m_player_view->parent_widget()->add_child(*m_playlist_widget);
- }
- } else {
- m_playlist_widget->remove_from_parent();
- m_player_view->set_max_width(window()->width());
- }
+ 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::try_fill_missing_info(Vector<M3UEntry>& entries, StringView playlist_p)
+void SoundPlayerWidgetAdvancedView::playlist_loaded(StringView path, bool loaded)
{
- 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);
- }
-
- 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;
- }
-
- 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
-
- 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);
- }
+ if (!loaded) {
+ GUI::MessageBox::show(&m_window, String::formatted("Could not load playlist at \"{}\".", path), "Error opening playlist", GUI::MessageBox::Type::Error);
+ return;
}
- for (M3UEntry* entry : to_delete)
- entries.remove_first_matching([&](M3UEntry& e) { return &e == entry; });
+ 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);
}
diff --git a/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h b/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h
index 5f2b91d2d8..44437e1aa5 100644
--- a/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h
+++ b/Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h
@@ -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 };
};
diff --git a/Userland/Applications/SoundPlayer/VisualizationBase.h b/Userland/Applications/SoundPlayer/VisualizationWidget.h
index 1ee6557f78..99a64ae711 100644
--- a/Userland/Applications/SoundPlayer/VisualizationBase.h
+++ b/Userland/Applications/SoundPlayer/VisualizationWidget.h
@@ -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;
};
diff --git a/Userland/Applications/SoundPlayer/main.cpp b/Userland/Applications/SoundPlayer/main.cpp
index a95d63c7c7..889208a983 100644
--- a/Userland/Applications/SoundPlayer/main.cpp
+++ b/Userland/Applications/SoundPlayer/main.cpp
@@ -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");
+ 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);
+
+ 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 loop = GUI::Action::create_checkable("&Loop", { Mod_Ctrl, Key_R }, [&](auto& action) {
- player->set_looping_file(action.is_checked());
+ 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();
- playback_menu.add_action(move(loop));
+ 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;