mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 10:42:45 +00:00 
			
		
		
		
	SoundPlayer: Add playlist supprt
And a M3U(8) parser
This commit is contained in:
		
							parent
							
								
									e4d6a56a28
								
							
						
					
					
						commit
						2e28b8ebcc
					
				
					 10 changed files with 586 additions and 57 deletions
				
			
		|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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
									
								
								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 | #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; } |     bool looping() const { return m_player_state.is_looping_file; } | ||||||
|     StringView& loaded_filename() { return m_player_state.loaded_filename; } |     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_volume(double volume) | ||||||
|     virtual void set_looping(bool loop) |  | ||||||
|     { |     { | ||||||
|         m_player_state.is_looping = loop; |         m_player_state.volume = volume; | ||||||
|         manager().loop(loop); |         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
									
								
								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 "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()); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     this->manager().on_load_sample_buffer = [&](Audio::Buffer& buffer) { |     manager().on_load_sample_buffer = [&](Audio::Buffer&) { | ||||||
|         if (volume() == 1.) |         //TODO: Implement an equalizer
 | ||||||
|  |         return; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     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); |         if (!m_playlist_model.is_null() && !m_playlist_model->items().is_empty()) { | ||||||
|             AK::SIMD::f64x4* sample_ptr = const_cast<AK::SIMD::f64x4*>(reinterpret_cast<const AK::SIMD::f64x4*>((buffer.data()))); |             auto it = m_playlist_model->items().find_if([&](const M3UEntry& e) { return e.path == loaded_filename(); }); | ||||||
|             for (int i = 0; i < total_iter; ++i) { |             if (it.index() + 1 == m_playlist_model->items().size()) { | ||||||
|                 sample_ptr[i] = sample_ptr[i] * volume(); |                 if (looping_playlist()) { | ||||||
|             } |                     open_file(m_playlist_model->items().at(0).path); | ||||||
|         } else { |                     return; | ||||||
|             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()))); |             } else | ||||||
|             for (int i = 0; i < total_iter; ++i) { |                 open_file((it + 1)->path); | ||||||
|                 sample_ptr[i] = sample_ptr[i] * volume(); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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; }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  |  | ||||||
|  | @ -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) { |     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); |     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_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>(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Cesar Torres
						Cesar Torres