diff --git a/Applications/SoundPlayer/Makefile b/Applications/SoundPlayer/Makefile index 930f535dbc..55f6d77d87 100644 --- a/Applications/SoundPlayer/Makefile +++ b/Applications/SoundPlayer/Makefile @@ -1,7 +1,9 @@ include ../../Makefile.common OBJS = \ + PlaybackManager.o \ SampleWidget.o \ + SoundPlayerWidget.o \ main.o APP = SoundPlayer diff --git a/Applications/SoundPlayer/PlaybackManager.cpp b/Applications/SoundPlayer/PlaybackManager.cpp new file mode 100644 index 0000000000..c83263ca8b --- /dev/null +++ b/Applications/SoundPlayer/PlaybackManager.cpp @@ -0,0 +1,134 @@ +#include "PlaybackManager.h" + +PlaybackManager::PlaybackManager(NonnullRefPtr connection, AWavLoader& loader) + : m_loader(loader) + , m_connection(connection) +{ + m_total_length = loader.total_samples() / static_cast(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(); +} diff --git a/Applications/SoundPlayer/PlaybackManager.h b/Applications/SoundPlayer/PlaybackManager.h new file mode 100644 index 0000000000..c58334b6a2 --- /dev/null +++ b/Applications/SoundPlayer/PlaybackManager.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +#define PLAYBACK_MANAGER_BUFFER_SIZE 64 * KB +#define PLAYBACK_MANAGER_RATE 44100 + +class PlaybackManager final { +public: + PlaybackManager(NonnullRefPtr, 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 current_buffer() const { return m_current_buffer; } + + NonnullRefPtr connection() const { return m_connection; } + AWavLoader& loader() const { return m_loader; } + + Function 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 m_connection; + RefPtr m_next_buffer; + RefPtr m_current_buffer; + Vector> m_buffers; + RefPtr m_timer; +}; diff --git a/Applications/SoundPlayer/SampleWidget.cpp b/Applications/SoundPlayer/SampleWidget.cpp index b8cdc93151..873427464b 100644 --- a/Applications/SoundPlayer/SampleWidget.cpp +++ b/Applications/SoundPlayer/SampleWidget.cpp @@ -23,30 +23,31 @@ void SampleWidget::paint_event(GPaintEvent& event) painter.add_clip_rect(event.rect()); painter.fill_rect(frame_inner_rect(), Color::Black); - if (!m_buffer) - return; - - int samples_per_pixel = m_buffer->sample_count() / frame_inner_rect().width(); float sample_max = 0; int count = 0; int x_offset = frame_inner_rect().x(); int x = x_offset; int y_offset = frame_inner_rect().center().y(); - for (int sample_index = 0; sample_index < m_buffer->sample_count() && (x - x_offset) < frame_inner_rect().width(); ++sample_index) { - float sample = fabsf(m_buffer->samples()[sample_index].left); + if (m_buffer) { + int samples_per_pixel = m_buffer->sample_count() / frame_inner_rect().width(); + for (int sample_index = 0; sample_index < m_buffer->sample_count() && (x - x_offset) < frame_inner_rect().width(); ++sample_index) { + float sample = fabsf(m_buffer->samples()[sample_index].left); - sample_max = max(sample, sample_max); - ++count; + sample_max = max(sample, sample_max); + ++count; - if (count >= samples_per_pixel) { - Point min_point = { x, y_offset + static_cast(-sample_max * frame_inner_rect().height() / 2) }; - Point max_point = { x++, y_offset + static_cast(sample_max * frame_inner_rect().height() / 2) }; - painter.draw_line(min_point, max_point, Color::Green); + if (count >= samples_per_pixel) { + Point min_point = { x, y_offset + static_cast(-sample_max * frame_inner_rect().height() / 2) }; + Point max_point = { x++, y_offset + static_cast(sample_max * frame_inner_rect().height() / 2) }; + painter.draw_line(min_point, max_point, Color::Green); - count = 0; - sample_max = 0; + count = 0; + sample_max = 0; + } } + } else { + painter.draw_line({ x, y_offset }, { frame_inner_rect().width(), y_offset }, Color::Green); } } diff --git a/Applications/SoundPlayer/SoundPlayerWidget.cpp b/Applications/SoundPlayer/SoundPlayerWidget.cpp new file mode 100644 index 0000000000..83eadd3860 --- /dev/null +++ b/Applications/SoundPlayer/SoundPlayerWidget.cpp @@ -0,0 +1,124 @@ +#include "SoundPlayerWidget.h" +#include +#include +#include +#include +#include + +SoundPlayerWidget::SoundPlayerWidget(NonnullRefPtr connection, AWavLoader& loader) + : m_manager(PlaybackManager(connection, loader)) +{ + set_fill_with_background_color(true); + set_layout(make(Orientation::Vertical)); + layout()->set_margins({ 2, 2, 2, 2 }); + + m_sample_ratio = PLAYBACK_MANAGER_RATE / static_cast(loader.sample_rate()); + + auto status_widget = GWidget::construct(this); + status_widget->set_fill_with_background_color(true); + status_widget->set_layout(make(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(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(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(rate * m_sample_ratio); +} + +int SoundPlayerWidget::denormalize_rate(int rate) const +{ + return static_cast(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(PLAYBACK_MANAGER_RATE)); + float remaining_seconds = m_manager.total_length() - seconds; + + m_elapsed->set_text(String::format( + "Position:\n%u:%02u.%02u", + static_cast(seconds / 60), + static_cast(seconds) % 60, + static_cast(seconds * 100) % 100)); + + m_remaining->set_text(String::format( + "Remaining:\n%u:%02u.%02u", + static_cast(remaining_seconds / 60), + static_cast(remaining_seconds) % 60, + static_cast(remaining_seconds * 100) % 100)); + + m_slider->set_value(total_norm_samples); +} diff --git a/Applications/SoundPlayer/SoundPlayerWidget.h b/Applications/SoundPlayer/SoundPlayerWidget.h new file mode 100644 index 0000000000..65a6dc36e4 --- /dev/null +++ b/Applications/SoundPlayer/SoundPlayerWidget.h @@ -0,0 +1,59 @@ +#pragma once + +#include "PlaybackManager.h" +#include "SampleWidget.h" +#include +#include +#include +#include + +class SoundPlayerWidget final : public GWidget { + C_OBJECT(SoundPlayerWidget) +public: + virtual ~SoundPlayerWidget() override; + +private: + explicit SoundPlayerWidget(NonnullRefPtr, AWavLoader&); + + void update_position(const int position); + void update_ui(); + int normalize_rate(int) const; + int denormalize_rate(int) const; + + class Slider final : public GSlider { + C_OBJECT(Slider) + public: + virtual ~Slider() override; + Function on_knob_released; + void set_value(int value) + { + if (!knob_dragging()) + GSlider::set_value(value); + } + + protected: + Slider(Orientation orientation, GWidget* parent) + : GSlider(orientation, parent) + { + } + + virtual void mouseup_event(GMouseEvent& event) override + { + if (on_knob_released && is_enabled()) + on_knob_released(value()); + + GSlider::mouseup_event(event); + } + }; + + PlaybackManager m_manager; + float m_sample_ratio; + RefPtr m_status; + RefPtr m_elapsed; + RefPtr m_remaining; + RefPtr m_slider; + RefPtr m_sample_widget; + RefPtr m_play_icon { GraphicsBitmap::load_from_file("/res/icons/16x16/play.png") }; + RefPtr m_pause_icon { GraphicsBitmap::load_from_file("/res/icons/16x16/pause.png") }; + RefPtr m_play; +}; diff --git a/Applications/SoundPlayer/main.cpp b/Applications/SoundPlayer/main.cpp index ef1445cb26..7f925e83af 100644 --- a/Applications/SoundPlayer/main.cpp +++ b/Applications/SoundPlayer/main.cpp @@ -1,11 +1,18 @@ -#include "SampleWidget.h" +#include "SoundPlayerWidget.h" +#include #include #include #include #include +#include +#include #include #include #include +#include +#include +#include +#include #include #include #include @@ -30,43 +37,24 @@ int main(int argc, char** argv) auto audio_client = AClientConnection::construct(); audio_client->handshake(); + auto app_menu = make("SoundPlayer"); + app_menu->add_action(GCommonActions::make_quit_action([&](auto&) { + app.quit(); + })); + + auto menubar = make(); + menubar->add_menu(move(app_menu)); + app.set_menubar(move(menubar)); + auto window = GWindow::construct(); window->set_title("SoundPlayer"); - window->set_rect(300, 300, 300, 200); - - auto widget = GWidget::construct(); - window->set_main_widget(widget); - - widget->set_fill_with_background_color(true); - widget->set_layout(make(Orientation::Vertical)); - widget->layout()->set_margins({ 2, 2, 2, 2 }); - - auto sample_widget = SampleWidget::construct(widget); - - auto button = GButton::construct("Quit", widget); - button->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); - button->set_preferred_size(0, 20); - button->on_click = [&](auto&) { - app.quit(); - }; - - auto next_sample_buffer = loader.get_more_samples(); - - auto timer = CTimer::construct(100, [&] { - if (!next_sample_buffer) { - sample_widget->set_buffer(nullptr); - return; - } - bool enqueued = audio_client->try_enqueue(*next_sample_buffer); - if (!enqueued) - return; - sample_widget->set_buffer(next_sample_buffer); - next_sample_buffer = loader.get_more_samples(16 * KB); - if (!next_sample_buffer) { - dbg() << "Exhausted samples :^)"; - } - }); + window->set_resizable(false); + window->set_rect(300, 300, 350, 140); + window->set_icon(GraphicsBitmap::load_from_file("/res/icons/16x16/app-sound-player.png")); + auto player = SoundPlayerWidget::construct(audio_client, loader); + window->set_main_widget(player); window->show(); + return app.exec(); }