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 #include #include -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 #include -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&&); + RefPtr 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 + * Copyright (c) 2021, Leandro A. F. Pereira + * + * 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(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 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 -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, [[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 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 }; }; 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 + * Copyright (c) 2021, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Playlist.h" + +#include +#include +#include + +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& entries, StringView path) +{ + LexicalPath playlist_path(path); + Vector 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 +#include + +class Playlist { +public: + Playlist() + : m_model(adopt_ref(*new PlaylistModel())) + { + } + + bool load(StringView); + + RefPtr 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&, StringView); + + RefPtr 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(PlaylistModelCustomRole::FilePath)).as_string()); + player->play_file_path(m_table_view->model()->data(index, static_cast(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 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 #include -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(); m_splitter = add(); m_player_view = m_splitter->add(); - 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(); @@ -52,84 +51,56 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window m_visualization = m_player_view->add(); - // 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(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(); auto& menubar = toolbar_container.add(); m_play_button = menubar.add(); - 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(); 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(); - timestamp_label.set_fixed_width(110); - timestamp_label.set_text("Elapsed: 00:00:00"); + m_timestamp_label = menubar.add(); + m_timestamp_label->set_fixed_width(110); // filler_label menubar.add(); m_back_button = menubar.add(); 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(); 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(); m_volume_label->set_fixed_width(30); - m_volume_label->set_text("100%"); auto& volume_slider = menubar.add(); 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(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(m_visualization.ptr())->set_buffer(this->manager().current_buffer()); - dynamic_cast(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 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& entries, StringView playlist_p) +void SoundPlayerWidgetAdvancedView::play_state_changed(Player::PlayState state) { - LexicalPath playlist_path(playlist_p); - Vector 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 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(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); } 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 #include #include @@ -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& entries, StringView playlist_p); template 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, int sample_rate, int samples_played) override; + protected: void keydown_event(GUI::KeyEvent&) override; @@ -51,7 +56,7 @@ private: RefPtr m_splitter; RefPtr m_player_view; RefPtr m_playlist_widget; - RefPtr m_visualization; + RefPtr m_visualization; RefPtr m_play_icon; RefPtr m_pause_icon; @@ -65,7 +70,7 @@ private: RefPtr m_next_button; RefPtr m_playback_progress_slider; RefPtr m_volume_label; + RefPtr 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 similarity index 65% rename from Userland/Applications/SoundPlayer/VisualizationBase.h rename to 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 +#include + +class VisualizationWidget : public GUI::Frame { + C_OBJECT(VisualizationWidget) -class Visualization { public: virtual void set_buffer(RefPtr 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(window, initial_player_state); + Player* player = &window->set_main_widget(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 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(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(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(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(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(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;