浏览代码

SoundPlayer: Add playlist supprt

And a M3U(8) parser
Cesar Torres 4 年之前
父节点
当前提交
2e28b8ebcc

+ 2 - 0
Base/home/anon/.config/LaunchServer.ini

@@ -9,6 +9,8 @@ jpg=/bin/QuickShow
 jpeg=/bin/QuickShow
 jpeg=/bin/QuickShow
 html=/bin/Browser
 html=/bin/Browser
 wav=/bin/SoundPlayer
 wav=/bin/SoundPlayer
+m3u=/bin/SoundPlayer
+m3u8=/bin/SoundPlayer
 txt=/bin/TextEditor
 txt=/bin/TextEditor
 font=/bin/FontEditor
 font=/bin/FontEditor
 sheets=/bin/Spreadsheet
 sheets=/bin/Spreadsheet

+ 2 - 0
Userland/Applications/SoundPlayer/CMakeLists.txt

@@ -7,6 +7,8 @@ set(SOURCES
     BarsVisualizationWidget.cpp
     BarsVisualizationWidget.cpp
     AudioAlgorithms.cpp
     AudioAlgorithms.cpp
     NoVisualizationWidget.cpp
     NoVisualizationWidget.cpp
+    M3UParser.cpp
+    PlaylistWidget.cpp
 )
 )
 
 
 serenity_app(SoundPlayer ICON app-sound-player)
 serenity_app(SoundPlayer ICON app-sound-player)

+ 130 - 0
Userland/Applications/SoundPlayer/M3UParser.cpp

@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "M3UParser.h"
+#include <AK/NonnullOwnPtrVector.h>
+#include <AK/OwnPtr.h>
+#include <AK/RefPtr.h>
+#include <AK/Utf8View.h>
+#include <LibCore/FileStream.h>
+#include <ctype.h>
+
+M3UParser::M3UParser()
+{
+}
+
+NonnullOwnPtr<M3UParser> M3UParser::from_file(const String path)
+{
+
+    auto parser = make<M3UParser>();
+    VERIFY(!path.is_null() && !path.is_empty() && !path.is_whitespace());
+    parser->m_use_utf8 = path.ends_with(".m3u8", AK::CaseSensitivity::CaseInsensitive);
+    FILE* file = fopen(path.characters(), "r");
+    VERIFY(file != nullptr);
+    fseek(file, 0, SEEK_END);
+    size_t file_size = ftell(file);
+    fseek(file, 0, SEEK_SET);
+    VERIFY(file_size > 0);
+    Vector<u8> temp_buffer;
+    temp_buffer.resize(file_size);
+    auto bytes_read = fread(temp_buffer.data(), 1, file_size, file);
+    VERIFY(bytes_read == file_size);
+    parser->m_m3u_raw_data = *new String(temp_buffer.span(), NoChomp);
+    return parser;
+}
+
+NonnullOwnPtr<M3UParser> M3UParser::from_memory(const String& m3u_contents, bool utf8)
+{
+    auto parser = make<M3UParser>();
+    VERIFY(!m3u_contents.is_null() && !m3u_contents.is_empty() && !m3u_contents.is_whitespace());
+    parser->m_m3u_raw_data = m3u_contents;
+    parser->m_use_utf8 = utf8;
+    return parser;
+}
+
+NonnullOwnPtr<Vector<M3UEntry>> M3UParser::parse(bool include_extended_info)
+{
+    auto vec = make<Vector<M3UEntry>>();
+
+    bool has_exteded_info_tag = false;
+    if (!m_use_utf8) {
+        auto lines = m_m3u_raw_data.split_view('\n');
+
+        if (include_extended_info) {
+            if (lines[0] == "#EXTM3U")
+                has_exteded_info_tag = true;
+        }
+
+        M3UExtendedInfo metadata_for_next_file {};
+        for (auto& line : lines) {
+            line = line.trim_whitespace();
+            M3UEntry entry {};
+            if (line.starts_with('#') && has_exteded_info_tag) {
+                if (line.starts_with("#EXTINF")) {
+                    auto data = line.substring_view(8);
+                    auto separator = data.find_first_of(',');
+                    VERIFY(separator.has_value());
+                    auto seconds = data.substring_view(0, separator.value());
+                    VERIFY(!seconds.is_whitespace() && !seconds.is_null() && !seconds.is_empty());
+                    metadata_for_next_file.track_length_in_seconds = seconds.to_uint();
+                    auto display_name = data.substring_view(seconds.length() + 1);
+                    VERIFY(!display_name.is_empty() && !display_name.is_null() && !display_name.is_empty());
+                    metadata_for_next_file.track_display_title = display_name;
+                    //TODO: support the alternative, non-standard #EXTINF value of a key=value dictionary
+                } else if (line.starts_with("#PLAYLIST")) {
+                    auto name = line.substring_view(10);
+                    VERIFY(!name.is_empty());
+                    m_parsed_playlist_title = name;
+                } else if (line.starts_with("#EXTGRP")) {
+                    auto name = line.substring_view(8);
+                    VERIFY(!name.is_empty());
+                    metadata_for_next_file.group_name = name;
+                } else if (line.starts_with("#EXTALB")) {
+                    auto name = line.substring_view(8);
+                    VERIFY(!name.is_empty());
+                    metadata_for_next_file.album_title = name;
+                } else if (line.starts_with("#EXTART")) {
+                    auto name = line.substring_view(8);
+                    VERIFY(!name.is_empty());
+                    metadata_for_next_file.album_artist = name;
+                } else if (line.starts_with("#EXTGENRE")) {
+                    auto name = line.substring_view(10);
+                    VERIFY(!name.is_empty());
+                    metadata_for_next_file.album_genre = name;
+                }
+                //TODO: Support M3A files (M3U files with embedded mp3 files)
+            } else {
+                entry.path = line;
+                entry.extended_info = metadata_for_next_file;
+                vec->append(entry);
+            }
+        }
+    } else {
+        //TODO: Implement M3U8 parsing
+        TODO();
+    }
+    return vec;
+}

+ 69 - 0
Userland/Applications/SoundPlayer/M3UParser.h

@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Optional.h>
+#include <AK/RefPtr.h>
+#include <AK/String.h>
+#include <AK/StringView.h>
+#include <AK/Vector.h>
+
+// Extended M3U fields (de facto standard)
+struct M3UExtendedInfo {
+    Optional<u32> track_length_in_seconds;
+    Optional<String> track_display_title;
+    Optional<String> group_name;
+    Optional<String> album_title;
+    Optional<String> album_artist;
+    Optional<String> album_genre;
+    Optional<u64> file_size_in_bytes;
+    Optional<ReadonlyBytes> embedded_mp3;
+    Optional<String> cover_path;
+};
+
+struct M3UEntry {
+    String path;
+    Optional<M3UExtendedInfo> extended_info;
+};
+
+class M3UParser {
+public:
+    static NonnullOwnPtr<M3UParser> from_file(String path);
+    static NonnullOwnPtr<M3UParser> from_memory(const String& m3u_contents, bool utf8);
+
+    NonnullOwnPtr<Vector<M3UEntry>> parse(bool include_extended_info);
+
+    Optional<String>& get_playlist_title_metadata() { return m_parsed_playlist_title; }
+
+    M3UParser();
+
+private:
+    String m_m3u_raw_data;
+    String m_playlist_path;
+    bool m_use_utf8;
+    Optional<String> m_parsed_playlist_title;
+};

+ 22 - 8
Userland/Applications/SoundPlayer/Player.h

@@ -27,6 +27,7 @@
 #pragma once
 #pragma once
 
 
 #include "PlaybackManager.h"
 #include "PlaybackManager.h"
+#include "PlaylistWidget.h"
 #include "VisualizationBase.h"
 #include "VisualizationBase.h"
 #include <AK/RefPtr.h>
 #include <AK/RefPtr.h>
 
 
@@ -34,11 +35,13 @@ struct PlayerState {
     bool is_paused;
     bool is_paused;
     bool is_stopped;
     bool is_stopped;
     bool has_loaded_file;
     bool has_loaded_file;
-    bool is_looping;
+    bool is_looping_file;
+    bool is_looping_playlist;
+    int loaded_file_samplerate;
     double volume;
     double volume;
     Audio::ClientConnection& connection;
     Audio::ClientConnection& connection;
     PlaybackManager& manager;
     PlaybackManager& manager;
-    StringView loaded_filename;
+    String loaded_filename;
 };
 };
 
 
 class Player {
 class Player {
@@ -53,17 +56,27 @@ public:
     bool is_paused() const { return m_player_state.is_paused; }
     bool is_paused() const { return m_player_state.is_paused; }
     bool has_loaded_file() const { return m_player_state.has_loaded_file; }
     bool has_loaded_file() const { return m_player_state.has_loaded_file; }
     double volume() const { return m_player_state.volume; }
     double volume() const { return m_player_state.volume; }
-    bool looping() const { return m_player_state.is_looping; }
-    StringView& loaded_filename() { return m_player_state.loaded_filename; }
+    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; }
 
 
     virtual void set_stopped(bool stopped) { m_player_state.is_stopped = stopped; }
     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_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_has_loaded_file(bool loaded) { m_player_state.has_loaded_file = loaded; }
-    virtual void set_volume(double volume) { m_player_state.volume = volume; }
-    virtual void set_looping(bool loop)
+    virtual void set_volume(double volume)
     {
     {
-        m_player_state.is_looping = loop;
-        manager().loop(loop);
+        m_player_state.volume = volume;
+        client_connection().set_main_mix_volume((double)(volume * 100));
+    }
+    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; }
     virtual void set_loaded_filename(StringView& filename) { m_player_state.loaded_filename = filename; }
 
 
@@ -72,4 +85,5 @@ public:
 
 
 protected:
 protected:
     PlayerState m_player_state;
     PlayerState m_player_state;
+    RefPtr<PlaylistModel> m_playlist_model;
 };
 };

+ 127 - 0
Userland/Applications/SoundPlayer/PlaylistWidget.cpp

@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PlaylistWidget.h"
+#include "Player.h"
+#include "SoundPlayerWidgetAdvancedView.h"
+#include <AK/LexicalPath.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/HeaderView.h>
+#include <LibGUI/Model.h>
+#include <LibGUI/Window.h>
+
+PlaylistWidget::PlaylistWidget()
+{
+    set_layout<GUI::VerticalBoxLayout>();
+    m_table_view = add<PlaylistTableView>();
+    m_table_view->set_selection_mode(GUI::AbstractView::SelectionMode::SingleSelection);
+    m_table_view->set_selection_behavior(GUI::AbstractView::SelectionBehavior::SelectRows);
+    m_table_view->set_highlight_selected_rows(true);
+    m_table_view->on_doubleclick = [&](const Gfx::Point<int>& point) {
+        auto player = dynamic_cast<Player*>(window()->main_widget());
+        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());
+    };
+}
+
+GUI::Variant PlaylistModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+    if (role == GUI::ModelRole::TextAlignment)
+        return "CenterLeft";
+    if (role == GUI::ModelRole::Display) {
+        switch (index.column()) {
+        case 0:
+            return m_playlist_items[index.row()].extended_info->track_display_title.value_or(LexicalPath(m_playlist_items[index.row()].path).title());
+        case 1:
+            return format_duration(m_playlist_items[index.row()].extended_info->track_length_in_seconds.value_or(0));
+        case 2:
+            return m_playlist_items[index.row()].extended_info->group_name.value_or("");
+        case 3:
+            return m_playlist_items[index.row()].extended_info->album_title.value_or("");
+        case 4:
+            return m_playlist_items[index.row()].extended_info->album_artist.value_or("");
+        case 5:
+            return format_filesize(m_playlist_items[index.row()].extended_info->file_size_in_bytes.value_or(0));
+        }
+    }
+    if (role == GUI::ModelRole::Sort)
+        return data(index, GUI::ModelRole::Display);
+    if (role == static_cast<GUI::ModelRole>(PlaylistModelCustomRole::FilePath)) //path
+        return m_playlist_items[index.row()].path;
+
+    return {};
+}
+
+String PlaylistModel::format_filesize(u64 size_in_bytes)
+{
+    if (size_in_bytes > GiB)
+        return String::formatted("{:.2f} GiB", (double)size_in_bytes / GiB);
+    else if (size_in_bytes > MiB)
+        return String::formatted("{:.2f} MiB", (double)size_in_bytes / MiB);
+    else if (size_in_bytes > KiB)
+        return String::formatted("{:.2f} KiB", (double)size_in_bytes / KiB);
+    else
+        return String::formatted("{} B", size_in_bytes);
+}
+
+String PlaylistModel::format_duration(u32 duration_in_seconds)
+{
+    return String::formatted("{:02}:{:02}:{:02}", duration_in_seconds / 3600, duration_in_seconds / 60, duration_in_seconds % 60);
+}
+
+String PlaylistModel::column_name(int column) const
+{
+    switch (column) {
+    case 0:
+        return "Title";
+    case 1:
+        return "Duration";
+    case 2:
+        return "Group";
+    case 3:
+        return "Album";
+    case 4:
+        return "Artist";
+    case 5:
+        return "Filesize";
+    }
+    VERIFY_NOT_REACHED();
+}
+
+void PlaylistModel::update()
+{
+}
+
+void PlaylistTableView::doubleclick_event(GUI::MouseEvent& event)
+{
+    AbstractView::doubleclick_event(event);
+    if (event.button() == GUI::Left) {
+        if (on_doubleclick)
+            on_doubleclick(event.position());
+    }
+}

+ 79 - 0
Userland/Applications/SoundPlayer/PlaylistWidget.h

@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "M3UParser.h"
+#include <LibGUI/Model.h>
+#include <LibGUI/TableView.h>
+#include <LibGUI/Variant.h>
+#include <LibGUI/Widget.h>
+
+enum class PlaylistModelCustomRole {
+    _DONOTUSE = (int)GUI::ModelRole::Custom,
+    FilePath
+};
+
+class PlaylistModel : public GUI::Model {
+public:
+    ~PlaylistModel() override = default;
+
+    int row_count(const GUI::ModelIndex&) const override { return m_playlist_items.size(); }
+    int column_count(const GUI::ModelIndex&) const override { return 6; }
+    GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+    void update() override;
+    String column_name(int column) const override;
+    Vector<M3UEntry>& items() { return m_playlist_items; }
+
+private:
+    Vector<M3UEntry> m_playlist_items;
+
+    static String format_filesize(u64 size_in_bytes);
+    static String format_duration(u32 duration_in_seconds);
+};
+
+class PlaylistTableView : public GUI::TableView {
+    C_OBJECT(PlaylistTableView)
+public:
+    void doubleclick_event(GUI::MouseEvent& event) override;
+
+    Function<void(const Gfx::Point<int>&)> on_doubleclick;
+};
+
+class PlaylistWidget : public GUI::Widget {
+    C_OBJECT(PlaylistWidget)
+public:
+    PlaylistWidget();
+    void set_data_model(RefPtr<PlaylistModel> model)
+    {
+        m_table_view->set_model(model);
+        m_table_view->update();
+    }
+
+protected:
+private:
+    RefPtr<PlaylistTableView> m_table_view;
+};

+ 137 - 24
Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.cpp

@@ -26,13 +26,14 @@
 
 
 #include "SoundPlayerWidgetAdvancedView.h"
 #include "SoundPlayerWidgetAdvancedView.h"
 #include "BarsVisualizationWidget.h"
 #include "BarsVisualizationWidget.h"
+#include "Common.h"
+#include "M3UParser.h"
 #include "PlaybackManager.h"
 #include "PlaybackManager.h"
-#include "SoundPlayerWidget.h"
+#include <AK/LexicalPath.h>
 #include <AK/SIMD.h>
 #include <AK/SIMD.h>
 #include <LibGUI/Action.h>
 #include <LibGUI/Action.h>
 #include <LibGUI/BoxLayout.h>
 #include <LibGUI/BoxLayout.h>
 #include <LibGUI/Button.h>
 #include <LibGUI/Button.h>
-#include <LibGUI/DragOperation.h>
 #include <LibGUI/Label.h>
 #include <LibGUI/Label.h>
 #include <LibGUI/MessageBox.h>
 #include <LibGUI/MessageBox.h>
 #include <LibGUI/Slider.h>
 #include <LibGUI/Slider.h>
@@ -46,11 +47,16 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
     , m_window(window)
     , m_window(window)
 {
 {
     window.resize(455, 350);
     window.resize(455, 350);
-    window.set_minimum_size(440, 130);
+    window.set_minimum_size(600, 130);
     window.set_resizable(true);
     window.set_resizable(true);
-
     set_fill_with_background_color(true);
     set_fill_with_background_color(true);
+
     set_layout<GUI::VerticalBoxLayout>();
     set_layout<GUI::VerticalBoxLayout>();
+    m_splitter = add<GUI::HorizontalSplitter>();
+    m_player_view = m_splitter->add<GUI::Widget>();
+    m_playlist_model = adopt(*new PlaylistModel());
+
+    m_player_view->set_layout<GUI::VerticalBoxLayout>();
 
 
     m_play_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png");
     m_play_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png");
     m_pause_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/pause.png");
     m_pause_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/pause.png");
@@ -58,9 +64,9 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
     m_back_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png");
     m_back_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png");
     m_next_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png");
     m_next_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png");
 
 
-    m_visualization = add<BarsVisualizationWidget>();
+    m_visualization = m_player_view->add<BarsVisualizationWidget>();
 
 
-    m_playback_progress_slider = add<Slider>(Orientation::Horizontal);
+    m_playback_progress_slider = m_player_view->add<AutoSlider>(Orientation::Horizontal);
     m_playback_progress_slider->set_fixed_height(20);
     m_playback_progress_slider->set_fixed_height(20);
     m_playback_progress_slider->set_min(0);
     m_playback_progress_slider->set_min(0);
     m_playback_progress_slider->set_max(this->manager().total_length() * 44100); //this value should be set when we load a new file
     m_playback_progress_slider->set_max(this->manager().total_length() * 44100); //this value should be set when we load a new file
@@ -68,8 +74,7 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
         this->manager().seek(value);
         this->manager().seek(value);
     };
     };
 
 
-    auto& toolbar_container = add<GUI::ToolBarContainer>();
-
+    auto& toolbar_container = m_player_view->add<GUI::ToolBarContainer>();
     auto& menubar = toolbar_container.add<GUI::ToolBar>();
     auto& menubar = toolbar_container.add<GUI::ToolBar>();
 
 
     m_play_button = menubar.add<GUI::Button>();
     m_play_button = menubar.add<GUI::Button>();
@@ -99,16 +104,35 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
 
 
     // filler_label
     // filler_label
     menubar.add<GUI::Label>();
     menubar.add<GUI::Label>();
-
     m_back_button = menubar.add<GUI::Button>();
     m_back_button = menubar.add<GUI::Button>();
     m_back_button->set_fixed_width(50);
     m_back_button->set_fixed_width(50);
     m_back_button->set_icon(*m_back_icon);
     m_back_button->set_icon(*m_back_icon);
     m_back_button->set_enabled(has_loaded_file());
     m_back_button->set_enabled(has_loaded_file());
+    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();
+        }
+    };
 
 
     m_next_button = menubar.add<GUI::Button>();
     m_next_button = menubar.add<GUI::Button>();
     m_next_button->set_fixed_width(50);
     m_next_button->set_fixed_width(50);
     m_next_button->set_icon(*m_next_icon);
     m_next_button->set_icon(*m_next_icon);
     m_next_button->set_enabled(has_loaded_file());
     m_next_button->set_enabled(has_loaded_file());
+    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();
+        }
+    };
 
 
     m_volume_label = &menubar.add<GUI::Label>();
     m_volume_label = &menubar.add<GUI::Label>();
     m_volume_label->set_fixed_width(30);
     m_volume_label->set_fixed_width(30);
@@ -137,30 +161,46 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
         m_playback_progress_slider->set_value(samples_played);
         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_buffer(this->manager().current_buffer());
+        dynamic_cast<Visualization*>(m_visualization.ptr())->set_samplerate(loaded_file_samplerate());
+    };
+
+    manager().on_load_sample_buffer = [&](Audio::Buffer&) {
+        //TODO: Implement an equalizer
+        return;
     };
     };
 
 
-    this->manager().on_load_sample_buffer = [&](Audio::Buffer& buffer) {
-        if (volume() == 1.)
+    manager().on_finished_playing = [&] {
+        m_play_button->set_icon(*m_play_icon);
+        if (looping()) {
+            open_file(loaded_filename());
             return;
             return;
-        auto sample_count = buffer.sample_count();
-        if (sample_count % 4 == 0) {
-            const int total_iter = sample_count / (sizeof(AK::SIMD::f64x4) / sizeof(double) / 2);
-            AK::SIMD::f64x4* sample_ptr = const_cast<AK::SIMD::f64x4*>(reinterpret_cast<const AK::SIMD::f64x4*>((buffer.data())));
-            for (int i = 0; i < total_iter; ++i) {
-                sample_ptr[i] = sample_ptr[i] * volume();
-            }
-        } else {
-            const int total_iter = sample_count / (sizeof(AK::SIMD::f64x2) / sizeof(double) / 2);
-            AK::SIMD::f64x2* sample_ptr = const_cast<AK::SIMD::f64x2*>(reinterpret_cast<const AK::SIMD::f64x2*>((buffer.data())));
-            for (int i = 0; i < total_iter; ++i) {
-                sample_ptr[i] = sample_ptr[i] * volume();
-            }
+        }
+
+        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);
         }
         }
     };
     };
 }
 }
 
 
 void SoundPlayerWidgetAdvancedView::open_file(StringView path)
 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);
     NonnullRefPtr<Audio::Loader> loader = Audio::Loader::create(path);
     if (loader->has_error() || !loader->sample_rate()) {
     if (loader->has_error() || !loader->sample_rate()) {
         const String error_string = loader->error_string();
         const String error_string = loader->error_string();
@@ -208,3 +248,76 @@ void SoundPlayerWidgetAdvancedView::play()
     set_paused(false);
     set_paused(false);
     set_stopped(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->update();
+
+    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) {
+        m_playlist_widget = m_player_view->parent_widget()->add<PlaylistWidget>();
+        m_playlist_widget->set_data_model(m_playlist_model);
+        m_playlist_widget->set_fixed_width(150);
+    } else {
+        m_playlist_widget->remove_from_parent();
+        m_player_view->set_max_width(window()->width());
+    }
+}
+
+void SoundPlayerWidgetAdvancedView::try_fill_missing_info(Vector<M3UEntry>& entries, StringView playlist_p)
+{
+    LexicalPath playlist_path(playlist_p);
+    Vector<M3UEntry*> to_delete;
+    for (auto& entry : entries) {
+        LexicalPath entry_path(entry.path);
+        if (!entry_path.is_absolute()) {
+            entry.path = String::formatted("{}/{}", playlist_path.dirname(), entry_path.basename());
+        }
+
+        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.basename()), "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(entry.path).title();
+        if (!entry.extended_info->track_length_in_seconds.has_value()) {
+            if (entry_path.has_extension("wav")) {
+                auto wav_reader = Audio::Loader::create(entry.path);
+                entry.extended_info->track_length_in_seconds = wav_reader->total_samples() / wav_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);
+        }
+    }
+    for (M3UEntry* entry : to_delete)
+        entries.remove_first_matching([&](M3UEntry& e) { return &e == entry; });
+}

+ 4 - 0
Userland/Applications/SoundPlayer/SoundPlayerWidgetAdvancedView.h

@@ -43,6 +43,7 @@ public:
     ~SoundPlayerWidgetAdvancedView() override;
     ~SoundPlayerWidgetAdvancedView() override;
 
 
     void open_file(StringView path) override;
     void open_file(StringView path) override;
+    void read_playlist(StringView path);
     void play() override;
     void play() override;
 
 
     template<typename T>
     template<typename T>
@@ -61,6 +62,9 @@ private:
     void drop_event(GUI::DropEvent& event) override;
     void drop_event(GUI::DropEvent& event) override;
     GUI::Window& m_window;
     GUI::Window& m_window;
 
 
+    RefPtr<GUI::HorizontalSplitter> m_splitter;
+    RefPtr<GUI::Widget> m_player_view;
+    RefPtr<PlaylistWidget> m_playlist_widget;
     RefPtr<GUI::Widget> m_visualization;
     RefPtr<GUI::Widget> m_visualization;
 
 
     RefPtr<Gfx::Bitmap> m_play_icon;
     RefPtr<Gfx::Bitmap> m_play_icon;

+ 13 - 24
Userland/Applications/SoundPlayer/main.cpp

@@ -93,36 +93,24 @@ int main(int argc, char** argv)
         }
         }
     }));
     }));
 
 
-    RefPtr<GUI::Action> hide_scope;
-
-    auto advanced_view_check = GUI::Action::create_checkable("Advanced view", { Mod_Ctrl, Key_A }, [&](auto& action) {
-        PlayerState state = player->get_player_state();
-        window->close();
-        if (action.is_checked()) {
-            player = &window->set_main_widget<SoundPlayerWidgetAdvancedView>(window, state);
-            hide_scope->set_checkable(false);
-        } else {
-            player = &window->set_main_widget<SoundPlayerWidget>(window, state);
-            hide_scope->set_checkable(true);
-        }
-        window->show();
+    auto linear_volume_slider = GUI::Action::create_checkable("Nonlinear volume slider", [&](auto& action) {
+        static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_nonlinear_volume_slider(action.is_checked());
     });
     });
-    app_menu.add_action(advanced_view_check);
+    app_menu.add_action(linear_volume_slider);
 
 
-    hide_scope = GUI::Action::create_checkable("Hide visualization (legacy view)", { Mod_Ctrl, Key_H }, [&](auto& action) {
-        if (!advanced_view_check->is_checked())
-            static_cast<SoundPlayerWidget*>(player)->hide_scope(action.is_checked());
+    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 linear_volume_slider = GUI::Action::create_checkable("Nonlinear volume slider", [&](auto& action) {
-        if (advanced_view_check->is_checked())
-            static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_nonlinear_volume_slider(action.is_checked());
+    auto playlist_loop_toggle = GUI::Action::create_checkable("Loop playlist", [&](auto& action) {
+        static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_looping_playlist(action.is_checked());
     });
     });
-    app_menu.add_action(linear_volume_slider);
-
-    auto ptr_copy = hide_scope;
+    playlist_menu.add_action(playlist_loop_toggle);
 
 
-    app_menu.add_action(ptr_copy.release_nonnull());
     app_menu.add_separator();
     app_menu.add_separator();
     app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
     app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
         app->quit();
         app->quit();
@@ -174,6 +162,7 @@ int main(int argc, char** argv)
             action.set_checked(true);
             action.set_checked(true);
             return;
             return;
         }
         }
+        checked_vis = &action;
         static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_visualization<NoVisualizationWidget>();
         static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_visualization<NoVisualizationWidget>();
     });
     });