mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-05-14 07:02:54 +00:00
This commit addresses two issues: 1. If you play a 96 KHz Wave file, the slider position is incorrect, because it is assumed all files are 44.1 KHz. 2. For high-bitrate files, there are audio dropouts due to not buffering enough audio data. Issue 1 is addressed by scaling the number of played samples by the ratio between the source and destination sample rates. Issue 2 is addressed by buffering a certain number of milliseconds worth of audio data (instead of a fixed number of bytes). This makes the the buffer size independent of the source sample rate. Some of the code is redesigned to be simpler. The code that did the book-keeping of which buffers need to be loaded and which have been already played has been removed. Instead, we enqueue a new buffer based on a low watermark of samples remaining in the audio server queue. Other small fixes include: 1. Disable the stop button when playback is finished. 2. Remove hard-coded instances of 44100. 3. Update the GUI every 50 ms (was 100), which improves visualizations.
323 lines
12 KiB
C++
323 lines
12 KiB
C++
/*
|
|
* Copyright (c) 2021, Cesar Torres <shortanemoia@protonmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "SoundPlayerWidgetAdvancedView.h"
|
|
#include "BarsVisualizationWidget.h"
|
|
#include "Common.h"
|
|
#include "M3UParser.h"
|
|
#include "PlaybackManager.h"
|
|
#include <AK/LexicalPath.h>
|
|
#include <AK/SIMD.h>
|
|
#include <LibGUI/Action.h>
|
|
#include <LibGUI/BoxLayout.h>
|
|
#include <LibGUI/Button.h>
|
|
#include <LibGUI/Label.h>
|
|
#include <LibGUI/MessageBox.h>
|
|
#include <LibGUI/Slider.h>
|
|
#include <LibGUI/Splitter.h>
|
|
#include <LibGUI/Toolbar.h>
|
|
#include <LibGUI/ToolbarContainer.h>
|
|
#include <LibGUI/Window.h>
|
|
#include <LibGfx/Bitmap.h>
|
|
|
|
SoundPlayerWidgetAdvancedView::SoundPlayerWidgetAdvancedView(GUI::Window& window, PlayerState& state)
|
|
: Player(state)
|
|
, m_window(window)
|
|
{
|
|
window.resize(455, 350);
|
|
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_ref(*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");
|
|
m_stop_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/stop.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_visualization = m_player_view->add<BarsVisualizationWidget>();
|
|
|
|
// Set a temporary value for total samples.
|
|
// This value will be set properly when we load a new file.
|
|
const int total_samples = this->manager().total_length() * this->manager().device_sample_rate();
|
|
|
|
m_playback_progress_slider = m_player_view->add<AutoSlider>(Orientation::Horizontal);
|
|
m_playback_progress_slider->set_fixed_height(20);
|
|
m_playback_progress_slider->set_jump_to_cursor(true);
|
|
m_playback_progress_slider->set_min(0);
|
|
m_playback_progress_slider->set_max(total_samples);
|
|
m_playback_progress_slider->set_page_step(total_samples / 10);
|
|
m_playback_progress_slider->on_knob_released = [&](int value) {
|
|
this->manager().seek(value);
|
|
};
|
|
|
|
auto& toolbar_container = m_player_view->add<GUI::ToolbarContainer>();
|
|
auto& menubar = toolbar_container.add<GUI::Toolbar>();
|
|
|
|
m_play_button = menubar.add<GUI::Button>();
|
|
m_play_button->set_icon(is_paused() ? (!has_loaded_file() ? *m_play_icon : *m_pause_icon) : *m_pause_icon);
|
|
m_play_button->set_fixed_width(50);
|
|
m_play_button->set_enabled(has_loaded_file());
|
|
m_play_button->on_click = [&](unsigned) {
|
|
bool paused = this->manager().toggle_pause();
|
|
set_paused(paused);
|
|
m_play_button->set_icon(paused ? *m_play_icon : *m_pause_icon);
|
|
m_stop_button->set_enabled(has_loaded_file());
|
|
};
|
|
|
|
m_stop_button = menubar.add<GUI::Button>();
|
|
m_stop_button->set_icon(*m_stop_icon);
|
|
m_stop_button->set_fixed_width(50);
|
|
m_stop_button->set_enabled(has_loaded_file());
|
|
m_stop_button->on_click = [&](unsigned) {
|
|
this->manager().stop();
|
|
set_stopped(true);
|
|
m_play_button->set_icon(*m_play_icon);
|
|
m_stop_button->set_enabled(false);
|
|
};
|
|
|
|
auto& timestamp_label = menubar.add<GUI::Label>();
|
|
timestamp_label.set_fixed_width(110);
|
|
timestamp_label.set_text("Elapsed: 00:00:00");
|
|
|
|
// 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);
|
|
m_volume_label->set_text("100%");
|
|
|
|
auto& volume_slider = menubar.add<GUI::HorizontalSlider>();
|
|
volume_slider.set_fixed_width(95);
|
|
volume_slider.set_min(0);
|
|
volume_slider.set_max(150);
|
|
volume_slider.set_value(100);
|
|
|
|
volume_slider.on_change = [&](int value) {
|
|
double volume = m_nonlinear_volume_slider ? (double)(value * value) / (100 * 100) : value / 100.;
|
|
m_volume_label->set_text(String::formatted("{}%", (int)(volume * 100)));
|
|
set_volume(volume);
|
|
};
|
|
|
|
set_volume(1.);
|
|
set_nonlinear_volume_slider(false);
|
|
|
|
manager().on_update = [&]() {
|
|
// Determine how many of the source file samples have played.
|
|
int samples_played = client_connection().get_played_samples();
|
|
float source_to_dest_ratio = static_cast<float>(loaded_file_samplerate()) / manager().device_sample_rate();
|
|
samples_played *= source_to_dest_ratio;
|
|
samples_played += this->manager().last_seek();
|
|
|
|
int current_second = samples_played / loaded_file_samplerate();
|
|
timestamp_label.set_text(String::formatted("Elapsed: {:02}:{:02}:{:02}", current_second / 3600, current_second / 60, current_second % 60));
|
|
if (!m_playback_progress_slider->mouse_is_down()) {
|
|
m_playback_progress_slider->set_value(samples_played);
|
|
}
|
|
|
|
dynamic_cast<Visualization*>(m_visualization.ptr())->set_buffer(this->manager().current_buffer());
|
|
dynamic_cast<Visualization*>(m_visualization.ptr())->set_samplerate(manager().device_sample_rate());
|
|
};
|
|
|
|
manager().on_load_sample_buffer = [&](Audio::Buffer&) {
|
|
//TODO: Implement an equalizer
|
|
return;
|
|
};
|
|
|
|
manager().on_finished_playing = [&] {
|
|
m_play_button->set_icon(*m_play_icon);
|
|
if (looping()) {
|
|
open_file(loaded_filename());
|
|
return;
|
|
}
|
|
|
|
if (!m_playlist_model.is_null() && !m_playlist_model->items().is_empty()) {
|
|
auto it = m_playlist_model->items().find_if([&](const M3UEntry& e) { return e.path == loaded_filename(); });
|
|
if (it.index() + 1 == m_playlist_model->items().size()) {
|
|
if (looping_playlist()) {
|
|
open_file(m_playlist_model->items().at(0).path);
|
|
return;
|
|
}
|
|
} else
|
|
open_file((it + 1)->path);
|
|
}
|
|
|
|
m_stop_button->set_enabled(false);
|
|
};
|
|
}
|
|
|
|
void SoundPlayerWidgetAdvancedView::open_file(StringView path)
|
|
{
|
|
if (!Core::File::exists(path)) {
|
|
GUI::MessageBox::show(window(), String::formatted("File \"{}\" does not exist", path), "Error opening file", GUI::MessageBox::Type::Error);
|
|
return;
|
|
}
|
|
|
|
if (path.ends_with(".m3u", AK::CaseSensitivity::CaseInsensitive) || path.ends_with(".m3u8", AK::CaseSensitivity::CaseInsensitive)) {
|
|
read_playlist(path);
|
|
return;
|
|
}
|
|
|
|
NonnullRefPtr<Audio::Loader> loader = Audio::Loader::create(path);
|
|
if (loader->has_error() || !loader->sample_rate()) {
|
|
const String error_string = loader->error_string();
|
|
GUI::MessageBox::show(&m_window, String::formatted("Failed to load audio file: {} ({})", path, error_string.is_null() ? "Unknown error" : error_string),
|
|
"Filetype error", GUI::MessageBox::Type::Error);
|
|
return;
|
|
}
|
|
m_window.set_title(String::formatted("{} - Sound Player", loader->file()->filename()));
|
|
m_playback_progress_slider->set_max(loader->total_samples());
|
|
m_playback_progress_slider->set_page_step(loader->total_samples() / 10);
|
|
m_playback_progress_slider->set_enabled(true);
|
|
m_play_button->set_enabled(true);
|
|
m_play_button->set_icon(*m_pause_icon);
|
|
m_stop_button->set_enabled(true);
|
|
manager().set_loader(move(loader));
|
|
set_has_loaded_file(true);
|
|
set_loaded_file_samplerate(loader->sample_rate());
|
|
set_loaded_filename(path);
|
|
play();
|
|
}
|
|
|
|
void SoundPlayerWidgetAdvancedView::set_nonlinear_volume_slider(bool nonlinear)
|
|
{
|
|
m_nonlinear_volume_slider = nonlinear;
|
|
}
|
|
|
|
void SoundPlayerWidgetAdvancedView::drop_event(GUI::DropEvent& event)
|
|
{
|
|
event.accept();
|
|
window()->move_to_front();
|
|
|
|
if (event.mime_data().has_urls()) {
|
|
auto urls = event.mime_data().urls();
|
|
if (urls.is_empty())
|
|
return;
|
|
open_file(urls.first().path());
|
|
}
|
|
}
|
|
|
|
SoundPlayerWidgetAdvancedView::~SoundPlayerWidgetAdvancedView()
|
|
{
|
|
manager().on_load_sample_buffer = nullptr;
|
|
manager().on_update = nullptr;
|
|
}
|
|
|
|
void SoundPlayerWidgetAdvancedView::play()
|
|
{
|
|
manager().play();
|
|
set_paused(false);
|
|
set_stopped(false);
|
|
}
|
|
|
|
void SoundPlayerWidgetAdvancedView::read_playlist(StringView path)
|
|
{
|
|
auto parser = M3UParser::from_file(path);
|
|
auto items = parser->parse(true);
|
|
VERIFY(items->size() > 0);
|
|
try_fill_missing_info(*items, path);
|
|
for (auto& item : *items)
|
|
m_playlist_model->items().append(item);
|
|
set_playlist_visible(true);
|
|
m_playlist_model->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; });
|
|
}
|