SoundPlayer: Add playlist supprt

And a M3U(8) parser
This commit is contained in:
Cesar Torres 2021-03-26 01:26:12 +01:00 committed by Andreas Kling
parent e4d6a56a28
commit 2e28b8ebcc
Notes: sideshowbarker 2024-07-18 21:02:41 +09:00
10 changed files with 586 additions and 57 deletions

View File

@ -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

View File

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

View 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;
}

View 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;
};

View File

@ -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;
};

View 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());
}
}

View 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;
};

View File

@ -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; });
}

View File

@ -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;

View File

@ -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>();
});