mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-12-29 06:02:07 +03:00
SoundPlayer: Add playlist supprt
And a M3U(8) parser
This commit is contained in:
parent
e4d6a56a28
commit
2e28b8ebcc
Notes:
sideshowbarker
2024-07-18 21:02:41 +09:00
Author: https://github.com/ngc6302h Commit: https://github.com/SerenityOS/serenity/commit/2e28b8ebccd Pull-request: https://github.com/SerenityOS/serenity/pull/5878 Reviewed-by: https://github.com/Dexesttp Reviewed-by: https://github.com/RealKC Reviewed-by: https://github.com/alimpfard
@ -9,6 +9,8 @@ jpg=/bin/QuickShow
|
||||
jpeg=/bin/QuickShow
|
||||
html=/bin/Browser
|
||||
wav=/bin/SoundPlayer
|
||||
m3u=/bin/SoundPlayer
|
||||
m3u8=/bin/SoundPlayer
|
||||
txt=/bin/TextEditor
|
||||
font=/bin/FontEditor
|
||||
sheets=/bin/Spreadsheet
|
||||
|
@ -7,6 +7,8 @@ set(SOURCES
|
||||
BarsVisualizationWidget.cpp
|
||||
AudioAlgorithms.cpp
|
||||
NoVisualizationWidget.cpp
|
||||
M3UParser.cpp
|
||||
PlaylistWidget.cpp
|
||||
)
|
||||
|
||||
serenity_app(SoundPlayer ICON app-sound-player)
|
||||
|
130
Userland/Applications/SoundPlayer/M3UParser.cpp
Normal file
130
Userland/Applications/SoundPlayer/M3UParser.cpp
Normal file
@ -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
Userland/Applications/SoundPlayer/M3UParser.h
Normal file
69
Userland/Applications/SoundPlayer/M3UParser.h
Normal file
@ -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;
|
||||
};
|
@ -27,6 +27,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "PlaybackManager.h"
|
||||
#include "PlaylistWidget.h"
|
||||
#include "VisualizationBase.h"
|
||||
#include <AK/RefPtr.h>
|
||||
|
||||
@ -34,11 +35,13 @@ struct PlayerState {
|
||||
bool is_paused;
|
||||
bool is_stopped;
|
||||
bool has_loaded_file;
|
||||
bool is_looping;
|
||||
bool is_looping_file;
|
||||
bool is_looping_playlist;
|
||||
int loaded_file_samplerate;
|
||||
double volume;
|
||||
Audio::ClientConnection& connection;
|
||||
PlaybackManager& manager;
|
||||
StringView loaded_filename;
|
||||
String loaded_filename;
|
||||
};
|
||||
|
||||
class Player {
|
||||
@ -53,17 +56,27 @@ public:
|
||||
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; }
|
||||
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_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; }
|
||||
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; }
|
||||
|
||||
@ -72,4 +85,5 @@ public:
|
||||
|
||||
protected:
|
||||
PlayerState m_player_state;
|
||||
RefPtr<PlaylistModel> m_playlist_model;
|
||||
};
|
||||
|
127
Userland/Applications/SoundPlayer/PlaylistWidget.cpp
Normal file
127
Userland/Applications/SoundPlayer/PlaylistWidget.cpp
Normal file
@ -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
Userland/Applications/SoundPlayer/PlaylistWidget.h
Normal file
79
Userland/Applications/SoundPlayer/PlaylistWidget.h
Normal file
@ -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;
|
||||
};
|
@ -26,13 +26,14 @@
|
||||
|
||||
#include "SoundPlayerWidgetAdvancedView.h"
|
||||
#include "BarsVisualizationWidget.h"
|
||||
#include "Common.h"
|
||||
#include "M3UParser.h"
|
||||
#include "PlaybackManager.h"
|
||||
#include "SoundPlayerWidget.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/SIMD.h>
|
||||
#include <LibGUI/Action.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Button.h>
|
||||
#include <LibGUI/DragOperation.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/Slider.h>
|
||||
@ -46,11 +47,16 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
|
||||
, m_window(window)
|
||||
{
|
||||
window.resize(455, 350);
|
||||
window.set_minimum_size(440, 130);
|
||||
window.set_minimum_size(600, 130);
|
||||
window.set_resizable(true);
|
||||
|
||||
set_fill_with_background_color(true);
|
||||
|
||||
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_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_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_min(0);
|
||||
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);
|
||||
};
|
||||
|
||||
auto& toolbar_container = add<GUI::ToolBarContainer>();
|
||||
|
||||
auto& toolbar_container = m_player_view->add<GUI::ToolBarContainer>();
|
||||
auto& menubar = toolbar_container.add<GUI::ToolBar>();
|
||||
|
||||
m_play_button = menubar.add<GUI::Button>();
|
||||
@ -99,16 +104,35 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
|
||||
|
||||
// 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->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->set_fixed_width(50);
|
||||
m_next_button->set_icon(*m_next_icon);
|
||||
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->set_fixed_width(30);
|
||||
@ -137,30 +161,46 @@ SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window
|
||||
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(loaded_file_samplerate());
|
||||
};
|
||||
|
||||
this->manager().on_load_sample_buffer = [&](Audio::Buffer& buffer) {
|
||||
if (volume() == 1.)
|
||||
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;
|
||||
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)
|
||||
{
|
||||
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();
|
||||
@ -208,3 +248,76 @@ void SoundPlayerWidgetAdvancedView::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->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; });
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ public:
|
||||
~SoundPlayerWidgetAdvancedView() override;
|
||||
|
||||
void open_file(StringView path) override;
|
||||
void read_playlist(StringView path);
|
||||
void play() override;
|
||||
|
||||
template<typename T>
|
||||
@ -61,6 +62,9 @@ private:
|
||||
void drop_event(GUI::DropEvent& event) override;
|
||||
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<Gfx::Bitmap> m_play_icon;
|
||||
|
@ -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();
|
||||
});
|
||||
app_menu.add_action(advanced_view_check);
|
||||
|
||||
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 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());
|
||||
static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_nonlinear_volume_slider(action.is_checked());
|
||||
});
|
||||
app_menu.add_action(linear_volume_slider);
|
||||
|
||||
auto ptr_copy = hide_scope;
|
||||
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);
|
||||
|
||||
app_menu.add_action(ptr_copy.release_nonnull());
|
||||
app_menu.add_separator();
|
||||
app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
|
||||
app->quit();
|
||||
@ -174,6 +162,7 @@ int main(int argc, char** argv)
|
||||
action.set_checked(true);
|
||||
return;
|
||||
}
|
||||
checked_vis = &action;
|
||||
static_cast<SoundPlayerWidgetAdvancedView*>(player)->set_visualization<NoVisualizationWidget>();
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user