forked from SerenityOS/serenity
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SoundPlayer: Added playback controls
The playback of a file can now be paused, stopped, continued and the user can seek to any part of the file.
- Loading branch information
1 parent
2f13517
commit 77f3c12
Showing
7 changed files
with
404 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
#include "PlaybackManager.h" | ||
|
||
PlaybackManager::PlaybackManager(NonnullRefPtr<AClientConnection> connection, AWavLoader& loader) | ||
: m_loader(loader) | ||
, m_connection(connection) | ||
{ | ||
m_total_length = loader.total_samples() / static_cast<float>(loader.sample_rate()); | ||
m_timer = CTimer::construct(100, [&]() { next_buffer(); }); | ||
pause(); | ||
} | ||
|
||
PlaybackManager::~PlaybackManager() | ||
{ | ||
} | ||
|
||
void PlaybackManager::stop() | ||
{ | ||
set_paused(true); | ||
m_connection->clear_buffer(true); | ||
m_buffers.clear(); | ||
m_loader.reset(); | ||
m_last_seek = 0; | ||
m_next_buffer = nullptr; | ||
m_current_buffer = nullptr; | ||
m_next_ptr = 0; | ||
} | ||
|
||
void PlaybackManager::play() | ||
{ | ||
set_paused(false); | ||
} | ||
|
||
void PlaybackManager::seek(const int position) | ||
{ | ||
m_last_seek = position; | ||
bool paused_state = m_paused; | ||
set_paused(true); | ||
|
||
m_connection->clear_buffer(true); | ||
m_next_buffer = nullptr; | ||
m_current_buffer = nullptr; | ||
m_next_ptr = 0; | ||
m_buffers.clear(); | ||
m_loader.seek(position); | ||
|
||
if (!paused_state) | ||
set_paused(false); | ||
} | ||
|
||
void PlaybackManager::pause() | ||
{ | ||
set_paused(true); | ||
} | ||
|
||
void PlaybackManager::remove_dead_buffers() | ||
{ | ||
int id = m_connection->get_playing_buffer(); | ||
int current_id = -1; | ||
if (m_current_buffer) | ||
current_id = m_current_buffer->shared_buffer_id(); | ||
|
||
if (id >= 0 && id != current_id) { | ||
while (!m_buffers.is_empty()) { | ||
--m_next_ptr; | ||
auto buffer = m_buffers.take_first(); | ||
|
||
if (buffer->shared_buffer_id() == id) { | ||
m_current_buffer = buffer; | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
void PlaybackManager::load_next_buffer() | ||
{ | ||
if (m_buffers.size() < 10) { | ||
for (int i = 0; i < 20 && m_loader.loaded_samples() < m_loader.total_samples(); i++) { | ||
auto buffer = m_loader.get_more_samples(PLAYBACK_MANAGER_BUFFER_SIZE); | ||
if (buffer) | ||
m_buffers.append(buffer); | ||
} | ||
} | ||
|
||
if (m_next_ptr < m_buffers.size()) { | ||
m_next_buffer = m_buffers.at(m_next_ptr++); | ||
} else { | ||
m_next_buffer = nullptr; | ||
} | ||
} | ||
|
||
void PlaybackManager::set_paused(bool paused) | ||
{ | ||
if (!m_next_buffer) | ||
load_next_buffer(); | ||
|
||
m_paused = paused; | ||
m_connection->set_paused(paused); | ||
} | ||
|
||
bool PlaybackManager::toggle_pause() | ||
{ | ||
if (m_paused) { | ||
play(); | ||
} else { | ||
pause(); | ||
} | ||
return m_paused; | ||
} | ||
|
||
void PlaybackManager::next_buffer() | ||
{ | ||
if (on_update) | ||
on_update(); | ||
|
||
if (m_paused) | ||
return; | ||
|
||
remove_dead_buffers(); | ||
if (!m_next_buffer) { | ||
if (!m_connection->get_remaining_samples() && !m_paused) { | ||
dbg() << "Exhausted samples :^)"; | ||
stop(); | ||
} | ||
|
||
return; | ||
} | ||
|
||
bool enqueued = m_connection->try_enqueue(*m_next_buffer); | ||
if (!enqueued) | ||
return; | ||
|
||
load_next_buffer(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
#pragma once | ||
|
||
#include <AK/Vector.h> | ||
#include <LibAudio/AClientConnection.h> | ||
#include <LibAudio/AWavLoader.h> | ||
#include <LibCore/CTimer.h> | ||
|
||
#define PLAYBACK_MANAGER_BUFFER_SIZE 64 * KB | ||
#define PLAYBACK_MANAGER_RATE 44100 | ||
|
||
class PlaybackManager final { | ||
public: | ||
PlaybackManager(NonnullRefPtr<AClientConnection>, AWavLoader&); | ||
~PlaybackManager(); | ||
|
||
void play(); | ||
void stop(); | ||
void pause(); | ||
void seek(const int position); | ||
bool toggle_pause(); | ||
|
||
int last_seek() const { return m_last_seek; } | ||
bool is_paused() const { return m_paused; } | ||
float total_length() const { return m_total_length; } | ||
RefPtr<ABuffer> current_buffer() const { return m_current_buffer; } | ||
|
||
NonnullRefPtr<AClientConnection> connection() const { return m_connection; } | ||
AWavLoader& loader() const { return m_loader; } | ||
|
||
Function<void()> on_update; | ||
|
||
private: | ||
void next_buffer(); | ||
void set_paused(bool); | ||
void load_next_buffer(); | ||
void remove_dead_buffers(); | ||
|
||
bool m_paused { true }; | ||
int m_next_ptr { 0 }; | ||
int m_last_seek { 0 }; | ||
float m_total_length; | ||
AWavLoader& m_loader; | ||
NonnullRefPtr<AClientConnection> m_connection; | ||
RefPtr<ABuffer> m_next_buffer; | ||
RefPtr<ABuffer> m_current_buffer; | ||
Vector<RefPtr<ABuffer>> m_buffers; | ||
RefPtr<CTimer> m_timer; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
#include "SoundPlayerWidget.h" | ||
#include <AK/StringBuilder.h> | ||
#include <LibGUI/GBoxLayout.h> | ||
#include <LibGUI/GButton.h> | ||
#include <LibGUI/GLabel.h> | ||
#include <LibM/math.h> | ||
|
||
SoundPlayerWidget::SoundPlayerWidget(NonnullRefPtr<AClientConnection> connection, AWavLoader& loader) | ||
: m_manager(PlaybackManager(connection, loader)) | ||
{ | ||
set_fill_with_background_color(true); | ||
set_layout(make<GBoxLayout>(Orientation::Vertical)); | ||
layout()->set_margins({ 2, 2, 2, 2 }); | ||
|
||
m_sample_ratio = PLAYBACK_MANAGER_RATE / static_cast<float>(loader.sample_rate()); | ||
|
||
auto status_widget = GWidget::construct(this); | ||
status_widget->set_fill_with_background_color(true); | ||
status_widget->set_layout(make<GBoxLayout>(Orientation::Horizontal)); | ||
|
||
m_elapsed = GLabel::construct(status_widget); | ||
m_elapsed->set_frame_shape(FrameShape::Container); | ||
m_elapsed->set_frame_shadow(FrameShadow::Sunken); | ||
m_elapsed->set_frame_thickness(2); | ||
m_elapsed->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); | ||
m_elapsed->set_preferred_size(80, 0); | ||
|
||
m_sample_widget = SampleWidget::construct(status_widget); | ||
|
||
m_remaining = GLabel::construct(status_widget); | ||
m_remaining->set_frame_shape(FrameShape::Container); | ||
m_remaining->set_frame_shadow(FrameShadow::Sunken); | ||
m_remaining->set_frame_thickness(2); | ||
m_remaining->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); | ||
m_remaining->set_preferred_size(80, 0); | ||
|
||
m_slider = Slider::construct(Orientation::Horizontal, this); | ||
m_slider->set_min(0); | ||
m_slider->set_max(normalize_rate(static_cast<int>(loader.total_samples()))); | ||
m_slider->on_knob_released = [&](int value) { m_manager.seek(denormalize_rate(value)); }; | ||
|
||
auto control_widget = GWidget::construct(this); | ||
control_widget->set_fill_with_background_color(true); | ||
control_widget->set_layout(make<GBoxLayout>(Orientation::Horizontal)); | ||
control_widget->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); | ||
control_widget->set_preferred_size(0, 30); | ||
control_widget->layout()->set_margins({ 10, 2, 10, 2 }); | ||
control_widget->layout()->set_spacing(10); | ||
|
||
m_play = GButton::construct(control_widget); | ||
m_play->set_icon(*m_pause_icon); | ||
m_play->on_click = [this](GButton& button) { | ||
button.set_icon(m_manager.toggle_pause() ? *m_play_icon : *m_pause_icon); | ||
}; | ||
|
||
auto stop = GButton::construct(control_widget); | ||
stop->set_icon(GraphicsBitmap::load_from_file("/res/icons/16x16/stop.png")); | ||
stop->on_click = [&](GButton&) { m_manager.stop(); }; | ||
|
||
m_status = GLabel::construct(this); | ||
m_status->set_frame_shape(FrameShape::Box); | ||
m_status->set_frame_shadow(FrameShadow::Raised); | ||
m_status->set_frame_thickness(4); | ||
m_status->set_text_alignment(TextAlignment::CenterLeft); | ||
m_status->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); | ||
m_status->set_preferred_size(0, 18); | ||
|
||
m_status->set_text(String::format( | ||
"Sample rate %uHz, %u channels, %u bits per sample", | ||
loader.sample_rate(), | ||
loader.num_channels(), | ||
loader.bits_per_sample())); | ||
|
||
update_position(0); | ||
|
||
m_manager.on_update = [&]() { update_ui(); }; | ||
m_manager.play(); | ||
} | ||
|
||
SoundPlayerWidget::~SoundPlayerWidget() | ||
{ | ||
} | ||
|
||
SoundPlayerWidget::Slider::~Slider() | ||
{ | ||
} | ||
|
||
int SoundPlayerWidget::normalize_rate(int rate) const | ||
{ | ||
return static_cast<int>(rate * m_sample_ratio); | ||
} | ||
|
||
int SoundPlayerWidget::denormalize_rate(int rate) const | ||
{ | ||
return static_cast<int>(rate / m_sample_ratio); | ||
} | ||
|
||
void SoundPlayerWidget::update_ui() | ||
{ | ||
m_sample_widget->set_buffer(m_manager.current_buffer()); | ||
m_play->set_icon(m_manager.is_paused() ? *m_play_icon : *m_pause_icon); | ||
update_position(m_manager.connection()->get_played_samples()); | ||
} | ||
|
||
void SoundPlayerWidget::update_position(const int position) | ||
{ | ||
int total_norm_samples = position + normalize_rate(m_manager.last_seek()); | ||
float seconds = (total_norm_samples / static_cast<float>(PLAYBACK_MANAGER_RATE)); | ||
float remaining_seconds = m_manager.total_length() - seconds; | ||
|
||
m_elapsed->set_text(String::format( | ||
"Position:\n%u:%02u.%02u", | ||
static_cast<int>(seconds / 60), | ||
static_cast<int>(seconds) % 60, | ||
static_cast<int>(seconds * 100) % 100)); | ||
|
||
m_remaining->set_text(String::format( | ||
"Remaining:\n%u:%02u.%02u", | ||
static_cast<int>(remaining_seconds / 60), | ||
static_cast<int>(remaining_seconds) % 60, | ||
static_cast<int>(remaining_seconds * 100) % 100)); | ||
|
||
m_slider->set_value(total_norm_samples); | ||
} |
Oops, something went wrong.